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
+36 -9
View File
@@ -744,7 +744,28 @@ function remapTopologyIds(world: GeometryWorld): void {
backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i])
}
const remap = (src: ArrayLike<number>): number[] => {
const rawSubjectIds = (world as any)._topologySubjectIds as number[] | undefined
function reorderAndRemap(rawValues: ArrayLike<number>, targetLength: number): number[] {
if (!rawSubjectIds || rawSubjectIds.length === 0) {
return remapSimple(rawValues)
}
const rawToValue = new Map<number, number>()
for (let j = 0; j < rawSubjectIds.length; j++) {
rawToValue.set(rawSubjectIds[j], rawValues[j] ?? 0)
}
const out = new Array<number>(targetLength)
for (let i = 0; i < targetLength; i++) {
const backendId = world.subjects.domNodeId[i]
const rawVal = rawToValue.get(backendId) ?? 0
out[i] = rawVal > 0 ? (backendToSolver.get(rawVal) ?? 0) : 0
}
return out
}
function remapSimple(src: ArrayLike<number>): number[] {
const out = new Array<number>(src.length)
for (let i = 0; i < src.length; i++) {
const backendId = src[i]
@@ -754,19 +775,21 @@ function remapTopologyIds(world: GeometryWorld): void {
}
const t = world.topology
t.containingBlockOf = remap(t.containingBlockOf)
t.nearestPositionedAncestorOf = remap(t.nearestPositionedAncestorOf)
t.scrollContainerOf = remap(t.scrollContainerOf)
t.stackingContextOf = remap(t.stackingContextOf)
t.formattingContextOf = remap(t.formattingContextOf)
t.clippingRootOf = remap(t.clippingRootOf)
t.containingBlockOf = reorderAndRemap(t.containingBlockOf, nSubjects)
t.nearestPositionedAncestorOf = reorderAndRemap(t.nearestPositionedAncestorOf, nSubjects)
t.scrollContainerOf = reorderAndRemap(t.scrollContainerOf, nSubjects)
t.stackingContextOf = reorderAndRemap(t.stackingContextOf, nSubjects)
t.formattingContextOf = reorderAndRemap(t.formattingContextOf, nSubjects)
t.clippingRootOf = reorderAndRemap(t.clippingRootOf, nSubjects)
if (world.clipping) {
world.clipping.subjectId = remap(world.clipping.subjectId)
world.clipping.subjectId = remapSimple(world.clipping.subjectId)
}
if (world.scroll) {
world.scroll.containerId = remap(world.scroll.containerId)
world.scroll.containerId = remapSimple(world.scroll.containerId)
}
delete (world as any)._topologySubjectIds
}
export async function extractWorldCdp(
@@ -865,6 +888,8 @@ export async function extractWorldCdp(
fontWeight: Array.from(canonical.styles.fontWeight),
}
;(world as any)._topologySubjectIds = (canonical as any).topology?.subjectIds
remapTopologyIds(world)
const selectorToIds = new Map<string, number[]>()
@@ -1166,6 +1191,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
const unaryPredicate = clause.relation === 'atLeast'
|| clause.relation === 'atMost'
|| clause.relation === 'aspectRatio'
|| (clause.relation === 'inStackingContext' && !clause.reference)
body = {
type: 'FormulaNode',
@@ -1213,6 +1239,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto
|| clause.relation === 'atMost'
|| clause.relation === 'between'
|| clause.relation === 'aspectRatio'
|| (clause.relation === 'inStackingContext' && !clause.reference)
if (isUnary) {
return {