fix: remove design-debt shims — falsy ID bug, selector normalization, concurrency, exception swallowing

pipeline.ts: || undefined → ?? undefined (9 occurrences)
- || converts valid subject ID 0 to undefined because 0 is falsy in JS.
  This broke clause witnesses and topology references for the first subject.

domain-index.ts: remove .toLowerCase() on CSS selectors
- CSS selectors are case-sensitive (IDs, class names, attribute values).
  Lowercasing on lookup but not on storage (selectorIndex) meant case-
  sensitive selectors never matched — returning empty arrays silently.

canonical.ts: add warning when visualBoxes falls back to layout boxes
- visualBoxes ?? boxes silently substituted layout coordinates for visual
  space, producing incorrect results for transform-dependent assertions.
  Now emits console.warn so silent data corruption is visible.

extraction.ts: serialize materializeSemanticSelector calls (3 sites)
- Changed Promise.all over page.evaluate() to sequential for..of. While
  Playwright serializes CDP calls internally, concurrent DOM-modifying
  evaluate() calls create undefined execution order. Sequential resolution
  eliminates theoretical race conditions for semantic selector injection.

engine.ts: include stack trace in evaluator exception diagnostics
- Catch-all converted ALL exceptions (including TypeError from programming
  bugs) to IMH_EVALUATOR_EXCEPTION with just err.message. Now includes
  stack trace and logs to console.warn for visibility. Distinguishes
  TypeError (programming bug) from other evaluation errors.

648 SDK tests + 57 E2E hard tests pass, zero regressions.
This commit is contained in:
John Dvorak
2026-05-22 11:55:58 -07:00
parent 0a73063c76
commit a424d29ccc
5 changed files with 36 additions and 31 deletions
+3 -1
View File
@@ -778,7 +778,9 @@ export function adaptSolverWorldToCanonical(
matrices: solverWorld.matrices ?? { values: [] }, matrices: solverWorld.matrices ?? { values: [] },
rects: solverWorld.rects, rects: solverWorld.rects,
boxes: solverWorld.boxes, boxes: solverWorld.boxes,
visualBoxes: solverWorld.visualBoxes ?? solverWorld.boxes, visualBoxes: solverWorld.visualBoxes && solverWorld.visualBoxes.boxId.length > 0
? solverWorld.visualBoxes
: (console.warn('[imhotep-core] adaptSolverWorldToCanonical: visualBoxes missing, falling back to layout boxes. Visual-space assertions may produce incorrect results.'), solverWorld.boxes),
fragments: { fragments: {
fragmentId: [], fragmentId: [],
subjectId: [], subjectId: [],
+9 -9
View File
@@ -802,8 +802,8 @@ export function executionIrToClauseDescriptors(context: ExecutionContext): Claus
clauseKind: 'unknown', clauseKind: 'unknown',
version: 1, version: 1,
subjectRef: executionIr.clauseSubject[i], subjectRef: executionIr.clauseSubject[i],
referenceRef: executionIr.clauseReference[i] || undefined, referenceRef: executionIr.clauseReference[i] ?? undefined,
frameRef: executionIr.clauseFrame[i] || undefined, frameRef: executionIr.clauseFrame[i] ?? undefined,
options: { unsupported: true, rawType: clauseType }, options: { unsupported: true, rawType: clauseType },
}) })
continue continue
@@ -837,13 +837,13 @@ export function executionIrToClauseDescriptors(context: ExecutionContext): Claus
clauseId: `clause_${i}`, clauseId: `clause_${i}`,
clauseKind, clauseKind,
version: 1, version: 1,
subjectRef: subjectRef || undefined, subjectRef: subjectRef ?? undefined,
referenceRef: referenceRef || undefined, referenceRef: referenceRef ?? undefined,
frameRef: executionIr.clauseFrame[i] || undefined, frameRef: executionIr.clauseFrame[i] ?? undefined,
stateRef: context.stateIds[executionIr.clauseState[i]] || undefined, stateRef: context.stateIds[executionIr.clauseState[i]] ?? undefined,
timelineRef: context.timelineIds[executionIr.clauseTimeline[i]] || undefined, timelineRef: context.timelineIds[executionIr.clauseTimeline[i]] ?? undefined,
envGuardRef: context.envGuardIds[executionIr.clauseEnvGuard[i]] || undefined, envGuardRef: context.envGuardIds[executionIr.clauseEnvGuard[i]] ?? undefined,
toleranceRef: context.toleranceIds[executionIr.clauseTolerance[i]] || undefined, toleranceRef: context.toleranceIds[executionIr.clauseTolerance[i]] ?? undefined,
bounds: Object.keys(bounds).length > 0 ? bounds : undefined, bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
options: Object.keys(options).length > 0 ? options : undefined, options: Object.keys(options).length > 0 ? options : undefined,
}) })
@@ -16,7 +16,7 @@ import { GeometryWorld } from './world.js'
* If the selector is not indexed, returns an empty array. * If the selector is not indexed, returns an empty array.
*/ */
export function getElementsBySelector(world: GeometryWorld, selector: string): number[] { export function getElementsBySelector(world: GeometryWorld, selector: string): number[] {
const normalized = selector.trim().toLowerCase() const normalized = selector.trim()
return world.selectorIndex.get(normalized) ?? [] return world.selectorIndex.get(normalized) ?? []
} }
+15 -18
View File
@@ -409,12 +409,11 @@ export async function extractWorldFastGeometry(
selectorToIds: Array<[string, number[]]> selectorToIds: Array<[string, number[]]>
} }
const selectorPlans: SelectorPlan[] = await Promise.all( const selectorPlans: SelectorPlan[] = []
selectors.map(async (key, i) => { for (let i = 0; i < selectors.length; i++) {
const queries = await materializeSemanticSelector(playwrightPage, key, i) const queries = await materializeSemanticSelector(playwrightPage, selectors[i], i)
return { key, queries } selectorPlans.push({ key: selectors[i], queries })
}), }
)
try { try {
const extracted = await playwrightPage.evaluate(({ plans, needs }: any) => { const extracted = await playwrightPage.evaluate(({ plans, needs }: any) => {
@@ -800,12 +799,11 @@ export async function extractWorldCdp(
const errors: ImhotepDiagnostic[] = [] const errors: ImhotepDiagnostic[] = []
const selectorToNodeIds = new Map<string, number[]>() const selectorToNodeIds = new Map<string, number[]>()
const selectorPlans: SelectorPlan[] = await Promise.all( const selectorPlans: SelectorPlan[] = []
selectors.map(async (key, i) => { for (let i = 0; i < selectors.length; i++) {
const queries = await materializeSemanticSelector(playwrightPage, key, i) const queries = await materializeSemanticSelector(playwrightPage, selectors[i], i)
return { key, queries } selectorPlans.push({ key: selectors[i], queries })
}), }
)
const sessionManager = createSessionManager(playwrightPage) const sessionManager = createSessionManager(playwrightPage)
try { try {
@@ -993,12 +991,11 @@ export async function extractWorld(
if (requiredFacts?.styles) { if (requiredFacts?.styles) {
try { try {
const plans: SelectorPlan[] = await Promise.all( const plans: SelectorPlan[] = []
filteredSelectors.map(async (key, i) => { for (let i = 0; i < filteredSelectors.length; i++) {
const queries = await materializeSemanticSelector(playwrightPage, key, i) const queries = await materializeSemanticSelector(playwrightPage, filteredSelectors[i], i)
return { key, queries } plans.push({ key: filteredSelectors[i], queries })
}), }
)
const chWidthsBySelector = await measureChWidthsByPlan(playwrightPage, plans) const chWidthsBySelector = await measureChWidthsByPlan(playwrightPage, plans)
attachMeasuredChWidths(result.world, result.selectorToIds, chWidthsBySelector) attachMeasuredChWidths(result.world, result.selectorToIds, chWidthsBySelector)
} catch { } catch {
+8 -2
View File
@@ -272,7 +272,13 @@ export function evaluate(
diagnostics.push(...result.diagnostics); diagnostics.push(...result.diagnostics);
} }
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err)
const stack = err instanceof Error ? err.stack : undefined
console.warn(`[imhotep-solver] clause evaluator exception for kind "${clause.clauseKind}" (id ${clause.clauseId}): ${message}`)
if (stack) console.warn(stack)
const detail = err instanceof Error && err.name === 'TypeError'
? `UNCAUGHT PROGRAMMING BUG: ${message}\n${stack ?? ''}`
: `Evaluator error: ${message}`
const result: ClauseResult = { const result: ClauseResult = {
clauseId: clause.clauseId, clauseId: clause.clauseId,
status: 'error', status: 'error',
@@ -282,7 +288,7 @@ export function evaluate(
code: 'IMH_EVALUATOR_EXCEPTION', code: 'IMH_EVALUATOR_EXCEPTION',
severity: 'error', severity: 'error',
category: 'internal-error', category: 'internal-error',
message, message: detail,
clauseId: clause.clauseId, clauseId: clause.clauseId,
}, },
], ],