fix: topology array reordering by subject order + unary inStackingContext

Topology array order mismatch (critical bug):
- CDP browser script iterates elements in document order (querySelectorAll),
  but solver accesses topology arrays by selector-resolution order.
- Fix: add subjectIds array to TopologyRecord tracking the backendNodeId
  at each document-order position; remapTopologyIds now reorders all 6
  topology arrays before remapping backendNodeIds to solver IDs.
- Fallback: when subjectIds is missing (cached pre-fix data), falls back
  to simple remap without reordering.

Unary inStackingContext compilation:
- compileCanonicalClauseToFormula treated inStackingContext as always binary,
  creating referenceBinding with undefined selector for unary assertions.
- Fix: add (inStackingContext && !clause.reference) to unaryPredicate/isUnary.

Hard test fixture (fixtures package):
- 29-element geometric fixture with 10 scenarios
- 57 assertions (54 spatial + 3 topology) all pass end-to-end
This commit is contained in:
John Dvorak
2026-05-22 11:38:28 -07:00
parent 1ac30c6e18
commit 0a73063c76
4 changed files with 213 additions and 9 deletions
+5
View File
@@ -546,6 +546,7 @@ export class CDPExtractor {
scroll: [],
clipping: [],
topology: {
subjectIds: [],
containingBlockOf: [],
nearestPositionedAncestorOf: [],
scrollContainerOf: [],
@@ -557,6 +558,9 @@ export class CDPExtractor {
},
}
// ... etc until the next empty topology at line ~964
if (request.requiredFacts.topology !== false && backendNodeIds.length > 0) {
try {
const { result, errors } = await extractTopology(session, backendNodeIds, subjectIds)
@@ -956,6 +960,7 @@ function createEmptySnapshot(request: ExtractorRequest): GeometryWorldSnapshot {
fontWeight: [],
},
topology: {
subjectIds: [],
containingBlockOf: [],
nearestPositionedAncestorOf: [],
scrollContainerOf: [],
+5
View File
@@ -42,6 +42,7 @@ export interface ClippingRecord {
* Topology relation record.
*/
export interface TopologyRecord {
subjectIds: number[]
containingBlockOf: number[]
nearestPositionedAncestorOf: number[]
scrollContainerOf: number[]
@@ -83,6 +84,7 @@ export async function extractTopology(
scroll: [],
clipping: [],
topology: {
subjectIds: [],
containingBlockOf: [],
nearestPositionedAncestorOf: [],
scrollContainerOf: [],
@@ -138,6 +140,7 @@ export async function extractTopology(
scroll: [],
clipping: [],
topology: {
subjectIds: [],
containingBlockOf: [],
nearestPositionedAncestorOf: [],
scrollContainerOf: [],
@@ -269,6 +272,7 @@ export async function extractTopology(
})
}
results.topology.subjectIds.push(subjectId)
results.topology.containingBlockOf.push(getId(getContainingBlock(el)))
results.topology.nearestPositionedAncestorOf.push(getId(getNearestPositionedAncestor(el)))
results.topology.scrollContainerOf.push(getId(getScrollContainer(el)))
@@ -301,6 +305,7 @@ export async function extractTopology(
scroll: [],
clipping: [],
topology: {
subjectIds: [],
containingBlockOf: [],
nearestPositionedAncestorOf: [],
scrollContainerOf: [],