fix: CDP extraction concurrency + topology ID remapping + predicate completeness
CDP protocol fixes:
- session.ts: getDocumentRootNodeId() now pushes {depth:-1} full DOM tree
(was shallow default, causing querySelectorAll to find zero nodes)
- extraction.ts: serialize resolveSelector calls (was concurrent Promise.all,
CDP DOM.querySelectorAll is not safe for concurrent calls on same session)
Topology ID mismatch fixes (critical — backendNodeId vs solver subject ID):
- extraction.ts: add remapTopologyIds() — builds Map<backendNodeId,solverId>
from world.subjects.domNodeId/ids, remaps all 6 topology arrays +
clipping.subjectId + scroll.containerId
- topology.ts: createsStackingContext() now checks contain:paint|layout
(both create stacking contexts per CSS spec, was missing)
- topology.ts: evaluateInStackingContext() handles unary (no reference)
Predicate completeness:
- predicates.ts: 3 new evaluators — intersects, touches, hasGap (indices 31-33)
- predicates.ts: escapeClippingChainOf replaced indeterminate stub with
deterministic 2-ary evaluator (overflow + clip chain + ancestry checks)
- predicates.ts: attachedToScrollContainer arity 1→2, checks reference match
- predicates.ts: 7 aliases + 4 alignment aliases promoted to BUILTIN_PREDICATES
DSL grammar fixes:
- grammar.ts: isKeywordThatCanBePredicate() extended with all missing keywords
- lower-to-canonical.ts: add TopologyAssertion lowering (was silently dropped)
- compiler.ts: add intersects/touches/hasGap relation codes
- validator.ts: add option sets for new predicates
- lexer.ts: add intersects/touches/hasGap token kinds
Safety + infrastructure:
- joins.ts: MAX_CARTESIAN_TUPLES=100,000 guard with descriptive error
- extraction.ts: computeRequiredFacts() registers defaults + broadens topology gate
- package.json: add missing imhotep-state dependency to imhotep-playwright
Tests:
- 9 new predicate tests (intersects, touches, hasGap, escapeClippingChainOf)
- Convert stale separatedFrom/contains GAP tests to working assertions
- Add computeRequiredFacts topology gating tests (public.test.ts)
This commit is contained in:
@@ -188,6 +188,7 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
|
||||
fragments: boolean
|
||||
domAncestry: boolean
|
||||
} {
|
||||
registerDefaultPredicates()
|
||||
const facts = new Set<string>()
|
||||
for (const formula of formulas) {
|
||||
const predicates = collectPredicates(formula)
|
||||
@@ -222,9 +223,11 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
|
||||
return found
|
||||
})
|
||||
|
||||
const needsTopology = Array.from(facts).some((f) => f.startsWith('topology.') || f.endsWith('.clipChain'))
|
||||
|
||||
return {
|
||||
geometry: true, // Always required for subject resolution
|
||||
topology: facts.has('topology.stackingContextOf') || facts.has('subject.clipChain'),
|
||||
topology: needsTopology,
|
||||
styles: facts.has('styles') || facts.has('computedStyle') || needsCssLengthMetrics,
|
||||
fragments: facts.has('subject.fragmentCount') || facts.has('subject.firstFragmentId'),
|
||||
domAncestry: needsDomAncestry,
|
||||
@@ -732,6 +735,40 @@ export async function extractWorldFastGeometry(
|
||||
// CDP Extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function remapTopologyIds(world: GeometryWorld): void {
|
||||
const nSubjects = world.subjects.ids.length
|
||||
if (nSubjects === 0) return
|
||||
|
||||
const backendToSolver = new Map<number, number>()
|
||||
for (let i = 0; i < nSubjects; i++) {
|
||||
backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i])
|
||||
}
|
||||
|
||||
const remap = (src: ArrayLike<number>): number[] => {
|
||||
const out = new Array<number>(src.length)
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const backendId = src[i]
|
||||
out[i] = backendId > 0 ? (backendToSolver.get(backendId) ?? 0) : 0
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (world.clipping) {
|
||||
world.clipping.subjectId = remap(world.clipping.subjectId)
|
||||
}
|
||||
if (world.scroll) {
|
||||
world.scroll.containerId = remap(world.scroll.containerId)
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractWorldCdp(
|
||||
playwrightPage: Page,
|
||||
selectors: string[],
|
||||
@@ -751,15 +788,13 @@ export async function extractWorldCdp(
|
||||
try {
|
||||
await sessionManager.enableDomain('DOM')
|
||||
|
||||
const session = sessionManager.getSession()
|
||||
await session.send('DOM.getDocument', { depth: -1 })
|
||||
|
||||
const allQueries = selectorPlans.flatMap((plan) =>
|
||||
plan.queries.map((query) => ({ key: plan.key, query })),
|
||||
)
|
||||
const allMatches = await Promise.all(
|
||||
allQueries.map(({ query }) => resolveSelector(sessionManager, query)),
|
||||
)
|
||||
const allMatches: Awaited<ReturnType<typeof resolveSelector>>[] = []
|
||||
for (const { query } of allQueries) {
|
||||
allMatches.push(await resolveSelector(sessionManager, query))
|
||||
}
|
||||
const resolvedSubjects: Array<{ id: string; selector: string; nodeId: number; backendNodeId: number }> = []
|
||||
for (let i = 0; i < allQueries.length; i++) {
|
||||
const { key, query } = allQueries[i]
|
||||
@@ -830,6 +865,8 @@ export async function extractWorldCdp(
|
||||
fontWeight: Array.from(canonical.styles.fontWeight),
|
||||
}
|
||||
|
||||
remapTopologyIds(world)
|
||||
|
||||
const selectorToIds = new Map<string, number[]>()
|
||||
for (const [selectorKey, nodeIds] of selectorToNodeIds) {
|
||||
const idSet = new Set(nodeIds)
|
||||
|
||||
Reference in New Issue
Block a user