feat: implement variable-bound FOL domain resolution for descendants/children
- Extend DomainResolver.resolve() signature to accept optional BindingEnv so that parentVar domains can be resolved with runtime variable bindings - Pass BindingEnv through evaluateForAll/evaluateExists to resolver calls - Add buildAncestorIndex() to precompute DOM ancestor sets from CDP data - SelectorDomainResolver now filters descendant domains by the bound parent when domain.parentVar is present and ancestor index is available - Return undefined for parentVar domains when no ancestor index or env (prevents silent fallback to global domain resolution) - Update all test DomainResolver mocks for new resolve interface - Add 10 unit tests covering ancestor index construction, backward compat, descendant filtering, exclusion of non-descendants, empty descendants, missing parentVar/env, and no-ancestor-index safety
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
getPredicateEvaluator,
|
||||
getRequiredFactsForPredicate,
|
||||
type DomainResolver,
|
||||
BindingEnv,
|
||||
} from 'imhotep-solver'
|
||||
import { buildGeometryWorld } from './world-builder.js'
|
||||
import {
|
||||
@@ -943,8 +944,49 @@ export async function extractWorld(
|
||||
// Domain Resolver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildAncestorIndex(world: GeometryWorld): Map<number, Set<number>> {
|
||||
const ancestors = new Map<number, Set<number>>()
|
||||
const nodeIdToSubjectId = new Map<number, number>()
|
||||
|
||||
for (let i = 0; i < world.subjects.ids.length; i++) {
|
||||
const sid = world.subjects.ids[i]
|
||||
const nodeId = world.subjects.domNodeId[i]
|
||||
if (nodeId > 0) {
|
||||
nodeIdToSubjectId.set(nodeId, sid)
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeIdToSubjectId.size === 0) return ancestors
|
||||
|
||||
for (const [cid, sid] of nodeIdToSubjectId) {
|
||||
const chain = new Set<number>()
|
||||
let current = cid
|
||||
while (current > 0) {
|
||||
const pos = world.dom.nodeId.indexOf(current)
|
||||
if (pos === -1) break
|
||||
const parent = world.dom.parentNodeId[pos]
|
||||
if (parent <= 0) break
|
||||
const parentSid = nodeIdToSubjectId.get(parent)
|
||||
if (parentSid !== undefined) {
|
||||
chain.add(parentSid)
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
if (chain.size > 0) {
|
||||
ancestors.set(sid, chain)
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors
|
||||
}
|
||||
|
||||
export class SelectorDomainResolver implements DomainResolver {
|
||||
private domains = new Map<string, DomainValue>()
|
||||
private ancestorIndex?: Map<number, Set<number>>
|
||||
|
||||
setAncestorIndex(ancestors: Map<number, Set<number>>) {
|
||||
this.ancestorIndex = ancestors
|
||||
}
|
||||
|
||||
register(selector: string, subjectIds: number[]) {
|
||||
this.domains.set(selector, {
|
||||
@@ -955,7 +997,29 @@ export class SelectorDomainResolver implements DomainResolver {
|
||||
})
|
||||
}
|
||||
|
||||
resolve(domain: DomainRef): DomainValue | undefined {
|
||||
resolve(domain: DomainRef, env?: BindingEnv): DomainValue | undefined {
|
||||
if (domain.parentVar) {
|
||||
if (!env || !this.ancestorIndex) return undefined
|
||||
const parentId = env.lookup(domain.parentVar)
|
||||
if (parentId === undefined) return undefined
|
||||
const selector = domain.selector ?? domain.domain
|
||||
const globalDomain = this.domains.get(selector)
|
||||
if (!globalDomain) return undefined
|
||||
const filtered: number[] = []
|
||||
for (const id of globalDomain.subjectIds) {
|
||||
const ancestors = this.ancestorIndex.get(id)
|
||||
if (ancestors?.has(parentId)) {
|
||||
filtered.push(id)
|
||||
}
|
||||
}
|
||||
const key = `descendants_p${parentId}_${selector}`
|
||||
return {
|
||||
domainId: `dom_${key}`,
|
||||
subjectIds: new Uint32Array(filtered),
|
||||
provenance: `descendants(parentId=${parentId}, ${selector})`,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
return this.domains.get(domain.selector ?? domain.domain)
|
||||
}
|
||||
}
|
||||
@@ -1685,7 +1749,7 @@ export function buildCompatibilityReport(ui: ImhotepUi): CompatibilityReport {
|
||||
})
|
||||
|
||||
const emptyResolver: DomainResolver = {
|
||||
resolve(domain) {
|
||||
resolve(domain, _env?) {
|
||||
if (domain.selector === '.none') {
|
||||
return {
|
||||
domainId: 'dom_none',
|
||||
|
||||
Reference in New Issue
Block a user