diff --git a/packages/imhotep-cdp/src/extractor.ts b/packages/imhotep-cdp/src/extractor.ts
index be5abcf..addfcfb 100644
--- a/packages/imhotep-cdp/src/extractor.ts
+++ b/packages/imhotep-cdp/src/extractor.ts
@@ -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: [],
diff --git a/packages/imhotep-cdp/src/topology.ts b/packages/imhotep-cdp/src/topology.ts
index dbe2d79..949e424 100644
--- a/packages/imhotep-cdp/src/topology.ts
+++ b/packages/imhotep-cdp/src/topology.ts
@@ -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: [],
diff --git a/packages/imhotep-fixtures/src/pages/hard-fixture.html b/packages/imhotep-fixtures/src/pages/hard-fixture.html
new file mode 100644
index 0000000..3514ccb
--- /dev/null
+++ b/packages/imhotep-fixtures/src/pages/hard-fixture.html
@@ -0,0 +1,167 @@
+
+
+
+
+Imhotep Hard Geometric Fixture
+
+
+
+
+
+ Imhotep Hard Geometric Fixture
+ Open DevTools Console to see bounding rects of all data-testid elements.
+ All elements are absolutely positioned for deterministic layout.
+
+
+
+1. TRANSFORM (rotate 15deg + scale 1.5)
+
+
+
+2. overflown-childChild 50x250 in 200x200 container
+
+
+
+3. NESTED (relative > absolute > fixed, contain:paint)
+
+
+
+4. OVERLAPPING Z-INDEX (z:1,2,3)
+z=1
+z=2
+z=3
+
+
+5. ZERO-SIZE / HIDDEN
+
+none
+hidden
+opacity:0
+
+
+6. FLEXBOX GAP (gap:20px)
+
+
+
+7. BORDER (12px) + PADDING (20px) border-box
+border-box
+
+
+8. EDGE ALIGNMENT
+ref
+L
+R
+T
+B
+
+
+9. DIAGONAL SEPARATION
+TL
+BR
+
+
+10. ADJACENT pair (edge touch)
+A
+B
+CENTERED child
+
+
+
diff --git a/packages/imhotep-playwright/src/extraction.ts b/packages/imhotep-playwright/src/extraction.ts
index 17a200e..67d6301 100644
--- a/packages/imhotep-playwright/src/extraction.ts
+++ b/packages/imhotep-playwright/src/extraction.ts
@@ -744,7 +744,28 @@ function remapTopologyIds(world: GeometryWorld): void {
backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i])
}
- const remap = (src: ArrayLike): number[] => {
+ const rawSubjectIds = (world as any)._topologySubjectIds as number[] | undefined
+
+ function reorderAndRemap(rawValues: ArrayLike, targetLength: number): number[] {
+ if (!rawSubjectIds || rawSubjectIds.length === 0) {
+ return remapSimple(rawValues)
+ }
+
+ const rawToValue = new Map()
+ for (let j = 0; j < rawSubjectIds.length; j++) {
+ rawToValue.set(rawSubjectIds[j], rawValues[j] ?? 0)
+ }
+
+ const out = new Array(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[] {
const out = new Array(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()
@@ -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 {