v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)

This commit is contained in:
John Dvorak
2025-08-15 10:00:00 -07:00
commit 92deb689cd
321 changed files with 79170 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
{
"name": "imhotep-core",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/anomalyco/imhotep.git"
},
"engines": {
"node": ">=18.0.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "tsc -p tsconfig.test.json && node --test dist-test/**/*.test.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./scene-target": {
"types": "./dist/scene-target.d.ts",
"default": "./dist/scene-target.js"
},
"./property-contracts": {
"types": "./dist/property-contracts.d.ts",
"default": "./dist/property-contracts.js"
},
"./property-results": {
"types": "./dist/property-results.d.ts",
"default": "./dist/property-results.js"
},
"./world": {
"types": "./dist/world.d.ts",
"default": "./dist/world.js"
}
}
}
+309
View File
@@ -0,0 +1,309 @@
/**
* AST node definitions for Imhotep authoring syntax.
*
* All AST nodes follow a unist-style shape:
* {
* type: string,
* position?: Position,
* data?: Record<string, unknown>,
* children?: AstNode[]
* }
*
* This structure is intentionally rich for tooling, transforms, and diagnostics.
*/
import type { Position, LiteralValue } from './types.js'
import type {
FormulaNode,
PropertyRunBlock,
SampledRunBlock,
} from './logic-ast.js'
// ---------------------------------------------------------------------------
// Base Node
// ---------------------------------------------------------------------------
export interface AstNode {
type: string
position?: Position
data?: Record<string, unknown>
}
export interface ParentNode extends AstNode {
children: AstNode[]
}
// ---------------------------------------------------------------------------
// Program
// ---------------------------------------------------------------------------
export interface Program extends ParentNode {
type: 'Program'
children: BlockNode[]
}
// ---------------------------------------------------------------------------
// Block-Level Nodes
// ---------------------------------------------------------------------------
export type BlockNode =
| FrameBlock
| AcrossBlock
| AlwaysBlock
| WhenBlock
| FormulaNode
| PropertyRunBlock
| SampledRunBlock
export interface FrameBlock extends ParentNode {
type: 'FrameBlock'
frame: FrameRef
children: AssertionNode[]
}
export interface AcrossBlock extends ParentNode {
type: 'AcrossBlock'
axes: AcrossAxis[]
children: BlockNode[]
}
export interface AcrossAxis {
name: string
values: (string | number)[]
}
export interface AlwaysBlock extends ParentNode {
type: 'AlwaysBlock'
children: AssertionNode[]
}
export interface WhenBlock extends ParentNode {
type: 'WhenBlock'
guard: GuardExpression
children: AssertionNode[]
}
export interface GuardExpression extends AstNode {
type: 'GuardExpression'
op: string
left: string | number
right?: string | number
}
// ---------------------------------------------------------------------------
// Assertion Nodes
// ---------------------------------------------------------------------------
export type AssertionNode =
| RelationAssertion
| TopologyAssertion
| SizeAssertion
| StateAssertion
| TemporalAssertion
| CompoundAssertion
| FormulaNode
export interface RelationAssertion extends AstNode {
type: 'RelationAssertion'
relation: string
subject: SelectorRef
reference: SelectorRef
options: RelationOptions
}
export interface RelationOptions extends AstNode {
type: 'RelationOptions'
minGap?: LiteralValue
maxGap?: LiteralValue
tolerance?: LiteralValue
inStackingContext?: boolean
}
export interface TopologyAssertion extends AstNode {
type: 'TopologyAssertion'
predicate: string
subject: SelectorRef
reference?: SelectorRef
options?: TopologyOptions
}
export interface TopologyOptions extends AstNode {
type: 'TopologyOptions'
tolerance?: LiteralValue
}
export interface SizeAssertion extends AstNode {
type: 'SizeAssertion'
property: string
subject: SelectorRef
bounds: SizeBounds
}
export interface SizeBounds extends AstNode {
type: 'SizeBounds'
min?: LiteralValue
max?: LiteralValue
exact?: LiteralValue
}
export interface StateAssertion extends AstNode {
type: 'StateAssertion'
stateSelector: StateSelector
assertion: AssertionNode
}
export interface TemporalAssertion extends AstNode {
type: 'TemporalAssertion'
timelineSelector: TimelineSelector
assertion: AssertionNode
}
export interface CompoundAssertion extends ParentNode {
type: 'CompoundAssertion'
quantifier?: QuantifierNode
operator?: 'and' | 'or'
children: AssertionNode[]
}
// ---------------------------------------------------------------------------
// Selector and Reference Nodes
// ---------------------------------------------------------------------------
export interface SelectorRef extends AstNode {
type: 'SelectorRef'
value: string
}
// ---------------------------------------------------------------------------
// Semantic Subject Reference (Stream 7)
// ---------------------------------------------------------------------------
export type SubjectReference =
| { kind: 'css'; selector: string }
| { kind: 'role'; role: string; name?: string }
| { kind: 'text'; text: string }
| { kind: 'labelText'; label: string }
| { kind: 'testId'; testId: string }
export function subjectReferenceToString(ref: SubjectReference): string {
switch (ref.kind) {
case 'css':
return `css='${ref.selector}'`
case 'role':
return ref.name
? `role='${ref.role}' name='${ref.name}'`
: `role='${ref.role}'`
case 'text':
return `text='${ref.text}'`
case 'labelText':
return `labelText='${ref.label}'`
case 'testId':
return `testId='${ref.testId}'`
default:
return 'unknown'
}
}
export interface StateSelector extends AstNode {
type: 'StateSelector'
kind: string
name?: string
}
export interface TimelineSelector extends AstNode {
type: 'TimelineSelector'
mode: string
samples?: number[]
from?: number
to?: number
}
// ---------------------------------------------------------------------------
// Literal Nodes
// ---------------------------------------------------------------------------
export interface LiteralNode extends AstNode {
type: 'Literal'
kind: 'length' | 'range' | 'tolerance' | 'string' | 'number' | 'boolean'
value: unknown
unit?: string
}
export interface RangeLiteralNode extends AstNode {
type: 'RangeLiteral'
min: LiteralNode
max: LiteralNode
}
export interface ToleranceLiteralNode extends AstNode {
type: 'ToleranceLiteral'
value: number
unit: 'jnd' | 'px'
}
// ---------------------------------------------------------------------------
// Quantifier Nodes
// ---------------------------------------------------------------------------
export interface QuantifierNode extends AstNode {
type: 'Quantifier'
kind: string
count?: number
}
// ---------------------------------------------------------------------------
// Frame Reference
// ---------------------------------------------------------------------------
export interface FrameRef extends AstNode {
type: 'FrameRef'
kind: string
selector?: string
name?: string
}
// ---------------------------------------------------------------------------
// AST Type Guards
// ---------------------------------------------------------------------------
export function isAstNode(value: unknown): value is AstNode {
return (
typeof value === 'object' &&
value !== null &&
'type' in value &&
typeof (value as Record<string, unknown>).type === 'string'
)
}
export function isParentNode(node: AstNode): node is ParentNode {
return 'children' in node && Array.isArray((node as ParentNode).children)
}
export function isProgram(node: AstNode): node is Program {
return node.type === 'Program'
}
export function isFrameBlock(node: AstNode): node is FrameBlock {
return node.type === 'FrameBlock'
}
export function isRelationAssertion(node: AstNode): node is RelationAssertion {
return node.type === 'RelationAssertion'
}
export function isCompoundAssertion(node: AstNode): node is CompoundAssertion {
return node.type === 'CompoundAssertion'
}
export function isFormulaNode(node: AstNode): node is FormulaNode {
return node.type === 'FormulaNode'
}
export function isPropertyRunBlock(node: AstNode): node is PropertyRunBlock {
return node.type === 'PropertyRunBlock'
}
export function isSampledRunBlock(node: AstNode): node is SampledRunBlock {
return node.type === 'SampledRunBlock'
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
/**
* Determinism proof tests — RED phase.
*
* These tests assert that Imhotep execution is deterministic when a
* seeded context is provided. They currently FAIL because the codebase
* relies on global mutable state (module-global id counter, Date.now,
* Math.random).
*
* Run: npm test -w packages/imhotep-core
*/
import { describe, it, beforeEach } from 'node:test'
import assert from 'node:assert'
import {
createDeterministicContext,
createVolatileContext,
getDefaultContext,
setDefaultContext,
resetDefaultContext,
} from './context.js'
import { compile } from 'imhotep-dsl'
import { createSnapshotId } from 'imhotep-state'
// ---------------------------------------------------------------------------
// Context unit tests
// ---------------------------------------------------------------------------
describe('ExecutionContext', () => {
beforeEach(() => {
resetDefaultContext()
})
it('deterministic context produces identical ids across calls', () => {
const ctx = createDeterministicContext(42)
const a = ctx.idGenerator()
const b = ctx.idGenerator()
const c = ctx.idGenerator()
assert.deepStrictEqual([a, b, c], ['id-1', 'id-2', 'id-3'])
})
it('deterministic context with same seed produces same rng sequence', () => {
const ctx1 = createDeterministicContext(123)
const ctx2 = createDeterministicContext(123)
const seq1 = Array.from({ length: 5 }, () => ctx1.rng())
const seq2 = Array.from({ length: 5 }, () => ctx2.rng())
assert.deepStrictEqual(seq1, seq2)
})
it('deterministic context with different seeds produces different rng sequence', () => {
const ctx1 = createDeterministicContext(123)
const ctx2 = createDeterministicContext(456)
const seq1 = Array.from({ length: 5 }, () => ctx1.rng())
const seq2 = Array.from({ length: 5 }, () => ctx2.rng())
assert.notDeepStrictEqual(seq1, seq2)
})
it('deterministic context clock is stable', () => {
const ctx = createDeterministicContext()
assert.strictEqual(ctx.clock(), 0)
assert.strictEqual(ctx.clock(), 0)
})
it('volatile context clock returns real time', () => {
const before = Date.now()
const ctx = createVolatileContext()
const t = ctx.clock()
const after = Date.now()
assert.ok(t >= before)
assert.ok(t <= after)
})
it('default context is deterministic in test mode', () => {
const ctx1 = getDefaultContext()
const ctx2 = getDefaultContext()
// In test mode, each call returns a fresh deterministic context.
assert.notStrictEqual(ctx1, ctx2)
assert.strictEqual(typeof ctx1.seed, 'number')
assert.strictEqual(ctx1.clock(), 0)
assert.strictEqual(ctx2.clock(), 0)
})
it('setDefaultContext overrides the global default', () => {
const det = createDeterministicContext(99)
setDefaultContext(det)
assert.strictEqual(getDefaultContext(), det)
assert.strictEqual(getDefaultContext().seed, 99)
})
it('parallel deterministic contexts do not interfere', () => {
const ctxA = createDeterministicContext(1)
const ctxB = createDeterministicContext(1)
const idsA = [ctxA.idGenerator(), ctxA.idGenerator()]
const idsB = [ctxB.idGenerator(), ctxB.idGenerator(), ctxB.idGenerator()]
assert.deepStrictEqual(idsA, ['id-1', 'id-2'])
assert.deepStrictEqual(idsB, ['id-1', 'id-2', 'id-3'])
})
})
// ---------------------------------------------------------------------------
// Cross-package determinism proof tests (currently RED)
// ---------------------------------------------------------------------------
describe('Determinism across packages', () => {
it('default compiler ids are non-deterministic across runs', () => {
// RED: defaultNextId uses a module-global counter.
// Two compile() calls without options produce different ids.
const ast = [
{
type: 'RelationAssertion' as const,
subject: { type: 'SelectorRef' as const, value: '.a' },
relation: 'leftOf' as const,
reference: { type: 'SelectorRef' as const, value: '.b' },
options: { type: 'RelationOptions' as const },
position: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 10, offset: 10 } },
},
] as const
const result1 = compile(ast as any)
const result2 = compile(ast as any)
const ids1 = Array.from(result1.semanticIr.clauses.values()).map((c: any) => c.id)
const ids2 = Array.from(result2.semanticIr.clauses.values()).map((c: any) => c.id)
// This assertion documents the desired deterministic behavior.
// It currently FAILS because the global counter advances between calls.
assert.deepStrictEqual(ids1, ids2, 'clause ids must be deterministic with same context')
})
it('injected compiler ids are deterministic across runs', () => {
// GREEN preview: when generateId is injected, ids are deterministic.
const ctx1 = createDeterministicContext(7)
const ctx2 = createDeterministicContext(7)
const ast = [
{
type: 'RelationAssertion' as const,
subject: { type: 'SelectorRef' as const, value: '.a' },
relation: 'leftOf' as const,
reference: { type: 'SelectorRef' as const, value: '.b' },
options: { type: 'RelationOptions' as const },
position: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 10, offset: 10 } },
},
] as const
const result1 = compile(ast as any, { generateId: () => ctx1.idGenerator() })
const result2 = compile(ast as any, { generateId: () => ctx2.idGenerator() })
const ids1 = Array.from(result1.semanticIr.clauses.values()).map((c: any) => c.id)
const ids2 = Array.from(result2.semanticIr.clauses.values()).map((c: any) => c.id)
assert.deepStrictEqual(ids1, ids2, 'injected ids must be deterministic with same seed')
})
it('default snapshot ids are deterministic and unique', () => {
// With the monotonic counter in createSnapshotId, consecutive calls
// produce predictable but unique ids in deterministic test contexts.
const id1 = createSnapshotId()
const id2 = createSnapshotId()
assert.strictEqual(id1, 'snap_0_id-1_1')
assert.strictEqual(id2, 'snap_0_id-1_2')
assert.notStrictEqual(id1, id2)
})
it('snapshot timestamps are stable with deterministic clock', () => {
const ctx = createDeterministicContext(7)
const t1 = ctx.clock()
const t2 = ctx.clock()
assert.strictEqual(t1, t2)
assert.strictEqual(t1, 0)
})
})
+130
View File
@@ -0,0 +1,130 @@
/**
* Execution context for deterministic and reproducible Imhotep runs.
*
* Every source of non-determinism (id generation, timestamps, randomness)
* is captured behind injectable functions so tests can control them.
*
* In production, use `createVolatileContext()`. In tests, use
* `createDeterministicContext(seed)`.
*/
export interface ExecutionContext {
/** Stable id generator (e.g. counter-based or UUID). */
idGenerator: () => string
/** Stable clock (e.g. fixed start time). */
clock: () => number
/** Seeded random number generator returning values in [0, 1). */
rng: () => number
/** The seed driving rng; stored in replay metadata. */
seed: number
}
// ---------------------------------------------------------------------------
// Deterministic default (test mode)
// ---------------------------------------------------------------------------
/** Simple linear congruential generator for deterministic random. */
function lcg(seed: number): () => number {
let s = seed >>> 0
return () => {
s = (1664525 * s + 1013904223) >>> 0
return s / 4294967296
}
}
/** Counter-based id generator. */
function counterIdGenerator(prefix = 'id'): () => string {
let n = 0
return () => `${prefix}-${++n}`
}
/**
* Create a fully deterministic execution context.
*
* IDs are counter-based, timestamps start at 0, and rng is a seeded LCG.
*/
export function createDeterministicContext(seed = 42): ExecutionContext {
return {
idGenerator: counterIdGenerator('id'),
clock: () => 0,
rng: lcg(seed),
seed,
}
}
// ---------------------------------------------------------------------------
// Volatile context (production)
// ---------------------------------------------------------------------------
/** Generate a random 32-bit integer seed. */
export function generateSeed(): number {
return Math.floor(Math.random() * 1_000_000_000)
}
/**
* Create a volatile execution context using real clocks and Math.random.
*
* This is the production default. The returned seed can be captured in
* replay metadata.
*/
export function createVolatileContext(seed?: number): ExecutionContext {
const actualSeed = seed ?? generateSeed()
// Use a fresh LCG even in volatile mode so that the seed is meaningful.
// If caller passes a seed we honour it; otherwise we use Math.random.
const rng = lcg(actualSeed)
return {
idGenerator: () => {
// Simple nanoid-like fallback (21 chars, base-64 alphabet)
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'
let id = ''
for (let i = 0; i < 21; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)]
}
return id
},
clock: () => Date.now(),
rng,
seed: actualSeed,
}
}
// ---------------------------------------------------------------------------
// Global default (backward compatible)
// ---------------------------------------------------------------------------
let _defaultContext: ExecutionContext | undefined
function isTestEnvironment(): boolean {
return (
typeof process !== 'undefined' &&
!!process.env.NODE_TEST_CONTEXT
)
}
/** Return the current default context.
*
* In production, a single volatile context is created and cached.
* In tests (detected via NODE_TEST_CONTEXT), a fresh deterministic
* context is returned on each call unless one has been explicitly set
* via setDefaultContext().
*/
export function getDefaultContext(): ExecutionContext {
if (_defaultContext) {
return _defaultContext
}
if (isTestEnvironment()) {
return createDeterministicContext()
}
_defaultContext = createVolatileContext()
return _defaultContext
}
/** Set the global default context (useful for test setup). */
export function setDefaultContext(ctx: ExecutionContext): void {
_defaultContext = ctx
}
/** Reset the global default context so the next call creates a fresh one. */
export function resetDefaultContext(): void {
_defaultContext = undefined
}
+211
View File
@@ -0,0 +1,211 @@
/**
* Extractor and solver contract interfaces for Imhotep.
*
* The extractor is a compiler target: it consumes a fact request plan and
* produces a geometry world. The solver operates on explicit clause families
* with declared fact requirements and deterministic outputs.
*/
import type {
ImhotepId,
Environment,
StateSnapshot,
TimelineSnapshot,
Subject,
ClauseResult,
GroupResult,
Proof,
} from './types.js'
import type { GeometryWorld, FactRequestPlan } from './world.js'
import type { ExtractionTrace, EvaluationTrace, Diagnostic } from './diagnostics.js'
import type { SceneTarget, RenderCase } from './scene-target.js'
export type { SceneTarget, RenderCase }
// ---------------------------------------------------------------------------
// Extractor Request
// ---------------------------------------------------------------------------
export interface ExtractorRequest {
requestId: ImhotepId
sceneTarget: SceneTarget
env: Environment
statePlan: {
snapshots: string[]
}
timelinePlan: {
mode: string
}
subjects: Array<{
id: ImhotepId
selector: string
}>
requiredFacts: FactRequestPlan
}
// ---------------------------------------------------------------------------
// Extractor Response
// ---------------------------------------------------------------------------
export interface ExtractorResponse {
requestId: ImhotepId
status: 'ok' | 'partial' | 'error'
snapshots: GeometryWorld[]
diagnostics: Diagnostic[]
extractionTrace: ExtractionTrace
}
// ---------------------------------------------------------------------------
// Extractor Contract
// ---------------------------------------------------------------------------
export interface ExtractorContract {
/**
* Consume a fact request plan and produce a geometry world.
*/
extract(request: ExtractorRequest, runtime: unknown): Promise<ExtractorResponse>
}
// ---------------------------------------------------------------------------
// Extractor Adapter Interface
// ---------------------------------------------------------------------------
export interface ExtractorAdapter {
name: string
version: string
supportedFacts: string[]
execute(request: ExtractorRequest): Promise<AdapterExtractionResult>
}
export interface AdapterExtractionResult {
status: 'ok' | 'partial' | 'error'
world: GeometryWorld
diagnostics: Diagnostic[]
trace: ExtractionTrace
}
// ---------------------------------------------------------------------------
// Solver Clause Contract
// ---------------------------------------------------------------------------
export interface SolverClauseContract {
/**
* Descriptor declaring what facts this clause family requires.
*/
descriptor: ClauseDescriptor
/**
* Evaluate a single clause against the geometry world.
*/
evaluate(world: GeometryWorld, clauseId: ImhotepId, traceBuilder: TraceBuilder): ClauseResult
}
export interface ClauseDescriptor {
clauseKind: string
version: number
requiredFacts: string[]
}
export interface TraceBuilder {
addStep(step: unknown): void
build(): unknown[]
}
// ---------------------------------------------------------------------------
// Evaluation Result
// ---------------------------------------------------------------------------
export interface EvaluationResult {
clauseResults: ClauseResult[]
groupResults: GroupResult[]
proofs: Proof[]
diagnostics: Diagnostic[]
trace: EvaluationTrace
}
// ---------------------------------------------------------------------------
// Solver Evaluation Contract
// ---------------------------------------------------------------------------
export interface SolverContract {
/**
* Evaluate compiled execution IR against a set of geometry worlds.
*/
evaluate(executionIr: unknown, worldSet: GeometryWorld[]): EvaluationResult
}
// ---------------------------------------------------------------------------
// Compiler Contract
// ---------------------------------------------------------------------------
export interface CompilerContract {
compile(input: string | unknown): CompilerOutput
}
export interface CompilerOutput {
ast: unknown
semanticIr: unknown
executionIr: unknown
diagnostics: Diagnostic[]
}
// ---------------------------------------------------------------------------
// Reporter Contract
// ---------------------------------------------------------------------------
export interface ReporterContract {
report(evaluationResult: EvaluationResult, options: ReporterOptions): ReporterOutput
}
export interface ReporterOptions {
format: 'text' | 'json' | 'html'
includeTrace: boolean
includeProofs: boolean
}
export interface ReporterOutput {
text: string
json: string
overlays: unknown[]
}
// ---------------------------------------------------------------------------
// Fact Requirement Analysis
// ---------------------------------------------------------------------------
export interface FactRequirementAnalysis {
requiredFacts: string[]
optionalFacts: string[]
unsupportedFacts: string[]
plan: FactRequestPlan
}
// ---------------------------------------------------------------------------
// Scene Closure
// ---------------------------------------------------------------------------
export interface SceneClosure {
subjects: Map<ImhotepId, Subject>
frames: Map<ImhotepId, unknown>
states: Map<ImhotepId, StateSnapshot>
timelines: Map<ImhotepId, TimelineSnapshot>
resolved: boolean
diagnostics: Diagnostic[]
}
// ---------------------------------------------------------------------------
// Validation Contract
// ---------------------------------------------------------------------------
export interface ValidationContract {
validateAst(ast: unknown): ValidationResult
validateSemanticIr(semanticIr: unknown): ValidationResult
}
export interface ValidationResult {
valid: boolean
diagnostics: Diagnostic[]
}
+524
View File
@@ -0,0 +1,524 @@
/**
* Diagnostic and trace types for Imhotep.
*
* Imhotep maintains two major error classes:
* 1. Contract failure diagnostics — the UI violated a declared law
* 2. System use diagnostics — the user or engine could not formulate
* or evaluate the request correctly
*
* The diagnostic model is inspired by vfile / vfile-message ecosystems.
*/
import type { ImhotepId, Position, ClauseResult, Proof } from './types.js'
// ---------------------------------------------------------------------------
// Diagnostic Categories
// ---------------------------------------------------------------------------
export type DiagnosticCategory =
| 'parse-error'
| 'validation-error'
| 'resolution-error'
| 'extraction-error'
| 'contract-failure'
| 'indeterminate-result'
| 'internal-error'
export type DiagnosticSeverity = 'error' | 'warning' | 'info'
// ---------------------------------------------------------------------------
// Diagnostic Error Codes (Canonical Registry)
// ---------------------------------------------------------------------------
// All packages MUST use codes from this union. No package may invent a
// code that is not listed here.
// ---------------------------------------------------------------------------
export type DiagnosticCode =
// -------------------------------------------------------------------------
// Parse errors (imhotep-dsl)
// -------------------------------------------------------------------------
| 'IMH_PARSE_UNEXPECTED_TOKEN'
| 'IMH_PARSE_INVALID_SYNTAX'
| 'IMH_PARSE_UNTERMINATED_BLOCK'
| 'IMH_PARSE_ERROR'
| 'IMH_PARSE_IS_KEYWORD'
| 'IMH_PARSE_MISSING_QUOTES'
| 'IMH_PARSE_WRONG_QUOTE_STYLE'
| 'IMH_PARSE_MISSING_UNIT'
// -------------------------------------------------------------------------
// Validation errors (imhotep-dsl)
// -------------------------------------------------------------------------
| 'IMH_VALID_ILLEGAL_RELATION_OPTION'
| 'IMH_VALID_INVALID_UNIT'
| 'IMH_VALID_INVALID_STATE_TIMELINE'
| 'IMH_VALID_INVALID_ENV_GUARD'
| 'IMH_VALID_INVALID_QUANTIFIER_NESTING'
| 'IMH_VALID_CONTRADICTION'
// -------------------------------------------------------------------------
// Resolution errors (imhotep-playwright, imhotep-extractor)
// -------------------------------------------------------------------------
| 'IMH_SELECTOR_ZERO_MATCHES'
| 'IMH_SELECTOR_AMBIGUOUS'
| 'IMH_SELECTOR_NO_MATCH'
| 'IMH_SELECTOR_RESOLUTION_FAILED'
| 'IMH_FRAME_AMBIGUOUS'
| 'IMH_FRAME_UNSUPPORTED'
| 'IMH_TOPOLOGY_UNSUPPORTED'
| 'IMH_STATE_MATERIALIZATION_FAILED'
// -------------------------------------------------------------------------
// Extraction errors (imhotep-cdp, imhotep-extractor)
// -------------------------------------------------------------------------
| 'IMH_EXTRACT_PARTIAL'
| 'IMH_EXTRACT_UNAVAILABLE_FACT'
| 'IMH_EXTRACT_PROTOCOL_ERROR'
| 'IMH_CDP_SESSION_ATTACH_FAILED'
| 'IMH_DOM_EXTRACTION_FAILED'
| 'IMH_BOX_MODEL_PARTIAL'
| 'IMH_BOX_MODEL_FAILED'
| 'IMH_VISUAL_BOX_PARTIAL'
| 'IMH_VISUAL_BOX_FAILED'
| 'IMH_FRAGMENT_PARTIAL'
| 'IMH_FRAGMENT_FAILED'
| 'IMH_TRANSFORM_PARTIAL'
| 'IMH_TRANSFORM_FAILED'
| 'IMH_STYLE_PARTIAL'
| 'IMH_STYLE_FAILED'
| 'IMH_TOPOLOGY_PARTIAL'
| 'IMH_TOPOLOGY_FAILED'
// -------------------------------------------------------------------------
// Extractor planner errors (imhotep-extractor)
// -------------------------------------------------------------------------
| 'IMH_EXTRACTOR_EMPTY_SELECTOR'
| 'IMH_EXTRACTOR_MISSING_ENV_GUARD'
| 'IMH_EXTRACTOR_UNRESOLVED_ENV_GUARD'
| 'IMH_EXTRACTOR_MISSING_STATE'
| 'IMH_EXTRACTOR_UNRESOLVED_STATE'
| 'IMH_EXTRACTOR_UNSUPPORTED_STATE'
| 'IMH_EXTRACTOR_NON_STATIC_TIMELINE'
| 'IMH_EXTRACTOR_UNSUPPORTED_CLAUSE_TYPE'
// -------------------------------------------------------------------------
// Contract failures (imhotep-solver, imhotep-playwright)
// -------------------------------------------------------------------------
| 'IMH_RELATION_LEFT_OF_FAILED'
| 'IMH_RELATION_RIGHT_OF_FAILED'
| 'IMH_RELATION_ABOVE_FAILED'
| 'IMH_RELATION_BELOW_FAILED'
| 'IMH_RELATION_ALIGNED_FAILED'
| 'IMH_RELATION_CENTERED_FAILED'
| 'IMH_RELATION_INSIDE_FAILED'
| 'IMH_RELATION_CONTAINS_FAILED'
| 'IMH_RELATION_OVERLAPS_FAILED'
| 'IMH_RELATION_BESIDE_FAILED'
| 'IMH_RELATION_ADJACENT_FAILED'
| 'IMH_RELATION_NEAR_FAILED'
| 'IMH_RELATION_FAILED'
| 'IMH_SIZE_AT_LEAST_FAILED'
| 'IMH_SIZE_AT_MOST_FAILED'
| 'IMH_SIZE_BETWEEN_FAILED'
| 'IMH_ALIGNMENT_FAILED'
| 'IMH_TOPOLOGY_CLIPPED_FAILED'
| 'IMH_TOPOLOGY_STACKING_FAILED'
| 'IMH_VISIBILITY_FAILED'
| 'IMH_PREDICATE_FAILED'
// -------------------------------------------------------------------------
// Cardinality failures (imhotep-playwright)
// -------------------------------------------------------------------------
| 'IMH_CARDINALITY_EXACTLYONE_FAILED'
| 'IMH_CARDINALITY_ATLEASTN_FAILED'
| 'IMH_CARDINALITY_ATMOSTN_FAILED'
// -------------------------------------------------------------------------
// Indeterminate results
// -------------------------------------------------------------------------
| 'IMH_INDETERMINATE_MISSING_FACT'
| 'IMH_INDETERMINATE_UNSTABLE_INPUT'
// -------------------------------------------------------------------------
// Internal errors
// -------------------------------------------------------------------------
| 'IMH_INTERNAL_UNKNOWN_CLAUSE_KIND'
| 'IMH_INTERNAL_EVALUATION_EXCEPTION'
| 'IMH_FEATURE_NOT_YET_IMPLEMENTED'
| 'IMH_UNKNOWN_FAILURE'
// -------------------------------------------------------------------------
// System use / fact observation diagnostics
// -------------------------------------------------------------------------
| 'IMH_FACT_OBSERVED_GAP'
| 'IMH_FACT_OBSERVED_SIZE'
| 'IMH_FACT_OBSERVED_TOPOLOGY'
// -------------------------------------------------------------------------
// Logic engine errors (imhotep-solver)
// -------------------------------------------------------------------------
| 'IMH_LOGIC_UNKNOWN_FORMULA_KIND'
| 'IMH_LOGIC_DOMAIN_UNRESOLVED'
| 'IMH_LOGIC_VACUOUS_FORALL'
| 'IMH_LOGIC_EMPTY_DOMAIN_EXISTS'
| 'IMH_LOGIC_PREDICATE_MISSING'
| 'IMH_LOGIC_UNBOUND_VARIABLE'
| 'IMH_LOGIC_UNSUPPORTED_TERM'
| 'IMH_LOGIC_UNKNOWN_NODE'
| 'IMH_LOGIC_ARITY_MISMATCH'
| 'IMH_LOGIC_UNKNOWN_PREDICATE'
| 'IMH_LOGIC_FREE_VARIABLE'
| 'IMH_LOGIC_EMPTY_DOMAIN'
// -------------------------------------------------------------------------
// Solver engine errors (imhotep-solver)
// -------------------------------------------------------------------------
| 'IMH_EVALUATOR_MISSING'
| 'IMH_EVALUATOR_EXCEPTION'
// -------------------------------------------------------------------------
// Quantifier errors (imhotep-solver)
// -------------------------------------------------------------------------
| 'IMH_QUANTIFIER_NO_SUBCLAUSES'
| 'IMH_PAIRWISE_INSUFFICIENT'
| 'IMH_UNKNOWN_QUANTIFIER'
// -------------------------------------------------------------------------
// Property-run diagnostics (imhotep-playwright)
// -------------------------------------------------------------------------
| 'IMH_ENUMERATED_RUN_ERROR'
| 'IMH_SAMPLED_RUN_ERROR'
| 'IMH_PROPERTY_RUN_FAILED'
| 'IMH_PROPERTY_PASSED'
| 'IMH_PROPERTY_FAILED'
| 'IMH_PROPERTY_REPLAY'
| 'IMH_PROPERTY_SHRUNK'
| 'IMH_ENUMERATED_PASSED'
| 'IMH_ENUMERATED_FAILED'
// -------------------------------------------------------------------------
// Reporter internal diagnostics (imhotep-reporter)
// -------------------------------------------------------------------------
| 'IMH_FRAME_CONTEXT'
| 'IMH_WITNESS_ENV'
| 'IMH_WITNESS_SNAPSHOT'
// ---------------------------------------------------------------------------
// Related Diagnostic
// ---------------------------------------------------------------------------
export interface RelatedDiagnostic {
code?: DiagnosticCode
message: string
position?: Position
}
// ---------------------------------------------------------------------------
// V1 Diagnostic Schema Extensions
// ---------------------------------------------------------------------------
export interface SourceRef {
file?: string
line?: number
column?: number
selector?: string
}
export interface SuggestedFix {
action: 'remove' | 'replace' | 'insert' | 'wrap'
target?: string
value?: string
rationale: string
}
// ---------------------------------------------------------------------------
// Diagnostic Shape
// ---------------------------------------------------------------------------
export interface Diagnostic {
code: DiagnosticCode
severity: DiagnosticSeverity
category: DiagnosticCategory
message: string
position?: Position
source: string
clauseId?: ImhotepId
sceneId?: ImhotepId
snapshotId?: ImhotepId
envCaseId?: ImhotepId
/** Subject resolution origin for semantic selectors (Stream 7). */
subjectOrigin?: string
related: RelatedDiagnostic[]
traceRef?: ImhotepId
fixHints: string[]
/** V1 required: structured metrics associated with this diagnostic. */
metrics: Record<string, number>
/** V1 required: canonical source reference. */
sourceRef: SourceRef | import('./types.js').SourceReference
/** V1 optional: automated fix suggestion. */
suggestedFix?: SuggestedFix
/** Human-readable label derived from the normalized contract (workstream C). */
clauseLabel?: string
}
// ---------------------------------------------------------------------------
// Trace Events
// ---------------------------------------------------------------------------
export type TracePhase =
| 'parse-started'
| 'ast-created'
| 'ir-normalized'
| 'fact-requirements-computed'
| 'extraction-started'
| 'extraction-step-completed'
| 'world-normalized'
| 'clause-evaluated'
| 'proof-created'
| 'witness-shrunk'
| 'diagnostic-emitted'
export interface TraceEvent {
traceEventId: ImhotepId
phase: TracePhase
at: number
refs: {
clauseId?: ImhotepId
proofId?: ImhotepId
snapshotId?: ImhotepId
diagnosticId?: ImhotepId
}
payload: Record<string, unknown>
}
export interface EvaluationTrace {
events: TraceEvent[]
}
// ---------------------------------------------------------------------------
// Witness Shrinking
// ---------------------------------------------------------------------------
export interface ShrinkAxis {
name: 'environment' | 'state' | 'timeline' | 'subject' | 'facts' | 'clauseGroup'
reduced: boolean
remainingValue: unknown
}
export interface ShrinkResult {
originalWitness: Proof['witness']
shrunkWitness: Proof['witness']
axes: ShrinkAxis[]
preserved: boolean
explanation: string
}
// ---------------------------------------------------------------------------
// Extraction Trace
// ---------------------------------------------------------------------------
export interface ExtractionTrace {
steps: ExtractionStep[]
timings: TimingEntry[]
protocolCalls: ProtocolCall[]
}
export interface ExtractionStep {
stepId: ImhotepId
factKind: string
status: 'ok' | 'partial' | 'error'
}
export interface TimingEntry {
stepId: ImhotepId
startMs: number
endMs: number
}
export interface ProtocolCall {
stepId: ImhotepId
protocol: string
method: string
params: Record<string, unknown>
}
// ---------------------------------------------------------------------------
// Diagnostic Factory
// ---------------------------------------------------------------------------
export function createDiagnostic(
code: DiagnosticCode,
category: DiagnosticCategory,
severity: DiagnosticSeverity,
message: string,
opts?: Partial<Omit<Diagnostic, 'code' | 'category' | 'severity' | 'message'>>,
): Diagnostic {
return {
code,
severity,
category,
message,
source: opts?.source ?? 'imhotep-core',
related: opts?.related ?? [],
fixHints: opts?.fixHints ?? getDefaultFixHints(code),
metrics: opts?.metrics ?? {},
sourceRef: opts?.sourceRef ?? {},
...opts,
}
}
// ---------------------------------------------------------------------------
// Default Severity / Category / Fix-Hint Helpers
// ---------------------------------------------------------------------------
/**
* Return the default severity for a canonical diagnostic code.
* This is a fallback; callers should override when context demands it.
*/
export function getDefaultSeverity(code: DiagnosticCode): DiagnosticSeverity {
switch (code) {
// Warnings
case 'IMH_SELECTOR_AMBIGUOUS':
case 'IMH_FRAME_AMBIGUOUS':
case 'IMH_SELECTOR_NO_MATCH':
case 'IMH_EXTRACT_PARTIAL':
case 'IMH_BOX_MODEL_PARTIAL':
case 'IMH_VISUAL_BOX_PARTIAL':
case 'IMH_FRAGMENT_PARTIAL':
case 'IMH_TRANSFORM_PARTIAL':
case 'IMH_STYLE_PARTIAL':
case 'IMH_TOPOLOGY_PARTIAL':
case 'IMH_INDETERMINATE_MISSING_FACT':
case 'IMH_INDETERMINATE_UNSTABLE_INPUT':
return 'warning'
// Info
case 'IMH_LOGIC_VACUOUS_FORALL':
case 'IMH_LOGIC_EMPTY_DOMAIN_EXISTS':
case 'IMH_FACT_OBSERVED_GAP':
case 'IMH_FACT_OBSERVED_SIZE':
case 'IMH_FACT_OBSERVED_TOPOLOGY':
case 'IMH_PROPERTY_PASSED':
case 'IMH_PROPERTY_REPLAY':
case 'IMH_ENUMERATED_PASSED':
case 'IMH_FRAME_CONTEXT':
case 'IMH_WITNESS_ENV':
case 'IMH_WITNESS_SNAPSHOT':
return 'info'
// Everything else defaults to error
default:
return 'error'
}
}
/**
* Return the default category for a canonical diagnostic code.
*/
export function getDefaultCategory(code: DiagnosticCode): DiagnosticCategory {
if (code.startsWith('IMH_PARSE_')) return 'parse-error'
if (code.startsWith('IMH_VALID_')) return 'validation-error'
if (code.startsWith('IMH_SELECTOR_') || code.startsWith('IMH_FRAME_') || code.startsWith('IMH_STATE_MATERIALIZATION')) return 'resolution-error'
if (code.startsWith('IMH_EXTRACTOR_')) return 'resolution-error'
if (code.startsWith('IMH_EXTRACT_') || code.startsWith('IMH_CDP_') || code.startsWith('IMH_DOM_') || code.startsWith('IMH_BOX_MODEL') || code.startsWith('IMH_VISUAL_BOX') || code.startsWith('IMH_FRAGMENT') || code.startsWith('IMH_TRANSFORM') || code.startsWith('IMH_STYLE') || code.startsWith('IMH_TOPOLOGY_PARTIAL') || code.startsWith('IMH_TOPOLOGY_FAILED')) return 'extraction-error'
if (code.startsWith('IMH_RELATION_') || code.startsWith('IMH_SIZE_') || code.startsWith('IMH_ALIGNMENT') || code.startsWith('IMH_TOPOLOGY_CLIPPED') || code.startsWith('IMH_TOPOLOGY_STACKING') || code.startsWith('IMH_VISIBILITY') || code.startsWith('IMH_PREDICATE') || code.startsWith('IMH_CARDINALITY')) return 'contract-failure'
if (code.startsWith('IMH_FACT_OBSERVED_')) return 'contract-failure'
if (code.startsWith('IMH_PROPERTY_') || code.startsWith('IMH_ENUMERATED_') || code === 'IMH_PROPERTY_RUN_FAILED') return 'contract-failure'
if (code.startsWith('IMH_INDETERMINATE_')) return 'indeterminate-result'
if (code.startsWith('IMH_INTERNAL_') || code.startsWith('IMH_FEATURE_') || code === 'IMH_UNKNOWN_FAILURE') return 'internal-error'
if (code.startsWith('IMH_LOGIC_') || code.startsWith('IMH_EVALUATOR_') || code.startsWith('IMH_QUANTIFIER_') || code === 'IMH_UNKNOWN_QUANTIFIER') return 'internal-error'
if (code.startsWith('IMH_FRAME_CONTEXT') || code.startsWith('IMH_WITNESS_')) return 'internal-error'
return 'internal-error'
}
/**
* Return default fix-hints for a canonical diagnostic code.
* Callers should augment or override these with context-specific hints.
*/
export function getDefaultFixHints(code: DiagnosticCode): string[] {
const hints: string[] = []
if (code.startsWith('IMH_PARSE_')) {
hints.push('Selectors must be single-quoted strings, e.g. \'[data-testid="x"]\'.')
hints.push('Use relation keywords directly without "is": e.g. \'a\' leftOf \'b\'.')
hints.push('Gap values require units: e.g. gap 8px.')
}
if (code === 'IMH_SELECTOR_ZERO_MATCHES' || code === 'IMH_SELECTOR_NO_MATCH') {
hints.push('Check that the selector is valid and matches an element in the DOM.')
hints.push('Use ui.extract(selector) to verify the selector resolves to at least one element.')
}
if (code === 'IMH_SELECTOR_AMBIGUOUS') {
hints.push('The selector matches more than one element. Use a more specific selector or add a quantifier.')
}
if (code === 'IMH_EXTRACT_PROTOCOL_ERROR' || code === 'IMH_CDP_SESSION_ATTACH_FAILED') {
hints.push('Verify the page is fully loaded before running assertions.')
hints.push('Check that selectors are valid CSS selectors or semantic references.')
hints.push('Use ui.extract(selector) to debug selector resolution.')
}
if (code === 'IMH_FEATURE_NOT_YET_IMPLEMENTED') {
hints.push('This assertion uses a relation that is documented but not yet implemented.')
hints.push('Use a supported relation such as leftOf, rightOf, above, below, inside, overlaps, alignedWith, or centeredWithin.')
}
if (code.startsWith('IMH_RELATION_') || code === 'IMH_ALIGNMENT_FAILED' || code === 'IMH_PREDICATE_FAILED') {
hints.push('Verify the expected layout and consider adjusting thresholds.')
hints.push('Check element positions using ui.extract(selector) to inspect actual geometry.')
}
if (code.startsWith('IMH_SIZE_')) {
hints.push('Check the expected size threshold and the actual element dimensions using ui.extract(selector).')
}
if (code.startsWith('IMH_CARDINALITY_')) {
hints.push('Verify the selector matches the expected number of elements.')
hints.push('Use a more specific selector or adjust the cardinality bound.')
}
if (code.startsWith('IMH_LOGIC_')) {
hints.push('Check formula syntax and ensure all referenced predicates and domains are defined.')
}
if (code.startsWith('IMH_TOPOLOGY_')) {
hints.push('Check CSS properties that affect topology (position, overflow, contain, z-index).')
}
if (code === 'IMH_PROPERTY_RUN_FAILED') {
hints.push('Review the failing counterexample and replay with the same seed.')
}
if (code.startsWith('IMH_ENUMERATED_RUN_ERROR') || code.startsWith('IMH_SAMPLED_RUN_ERROR')) {
hints.push('Check that the property callback does not throw for the reported input.')
}
return hints
}
// ---------------------------------------------------------------------------
// Category Classification Helpers
// ---------------------------------------------------------------------------
export function isParseError(diagnostic: Diagnostic): boolean {
return diagnostic.category === 'parse-error'
}
export function isContractFailure(diagnostic: Diagnostic): boolean {
return diagnostic.category === 'contract-failure'
}
export function isSystemUseError(diagnostic: Diagnostic): boolean {
return (
diagnostic.category === 'parse-error' ||
diagnostic.category === 'validation-error' ||
diagnostic.category === 'resolution-error' ||
diagnostic.category === 'extraction-error' ||
diagnostic.category === 'internal-error'
)
}
export function isIndeterminate(diagnostic: Diagnostic): boolean {
return diagnostic.category === 'indeterminate-result'
}
+223
View File
@@ -0,0 +1,223 @@
/**
* Domain definitions for deterministic scene logic.
*
* A domain is a finite set of geometry subjects extracted from a scene.
* Domains are the ground over which quantifiers range.
*
* Invariant: every domain is enumerable and closed within a single
* materialized GeometryWorld. No domain may silently default to an
* empty set; empty domains must be reported explicitly.
*/
import type { ImhotepId, SubjectKind } from './types.js'
// ---------------------------------------------------------------------------
// Domain Descriptor
// ---------------------------------------------------------------------------
export interface DomainDescriptor {
domainId: ImhotepId
kind: DomainKind
selector?: string
parentVar?: string
subjectKind?: SubjectKind
}
export type DomainKind =
| 'elements'
| 'descendants'
| 'lineBoxes'
| 'textRuns'
| 'fragments'
| 'frames'
| 'custom'
// ---------------------------------------------------------------------------
// Domain Value (materialized)
// ---------------------------------------------------------------------------
export interface DomainValue {
domainId: ImhotepId
/** Stable numeric subject ids, stored as a flat array for fast iteration. */
subjectIds: Uint32Array
/** Source selector or domain expression that produced this set. */
provenance: string
/** If true, the domain is known to be complete for the scene. */
closed: boolean
}
// ---------------------------------------------------------------------------
// Built-in Domain Functions
// ---------------------------------------------------------------------------
export interface DomainFunctions {
/** Select elements matching a CSS selector. */
elements(selector: string): DomainDescriptor
/** Select descendants of a bound parent variable matching a selector. */
descendants(parentVar: string, selector: string): DomainDescriptor
/** Select line boxes for a given text node subject. */
lineBoxes(textNodeId: ImhotepId): DomainDescriptor
/** Select text runs for a given text node subject. */
textRuns(textNodeId: ImhotepId): DomainDescriptor
/** Select fragment boxes for a given subject. */
fragments(subjectId: ImhotepId): DomainDescriptor
/** Select frames of a given kind. */
frames(frameKind: string): DomainDescriptor
}
// ---------------------------------------------------------------------------
// Domain Enumeration Result
// ---------------------------------------------------------------------------
export interface DomainEnumeration {
domainId: ImhotepId
subjectIds: Uint32Array
empty: boolean
diagnostic?: DomainDiagnostic
}
export interface DomainDiagnostic {
code: string
message: string
domainId: ImhotepId
}
// ---------------------------------------------------------------------------
// Default Domain Registry Implementation
// ---------------------------------------------------------------------------
export class DomainRegistry {
private registry = new Map<ImhotepId, DomainDescriptor>()
register(descriptor: DomainDescriptor): void {
this.registry.set(descriptor.domainId, descriptor)
}
lookup(domainId: ImhotepId): DomainDescriptor | undefined {
return this.registry.get(domainId)
}
list(): DomainDescriptor[] {
return Array.from(this.registry.values())
}
clear(): void {
this.registry.clear()
}
}
// Global default instance for backward compatibility.
const globalDomainRegistry = new DomainRegistry()
export function registerDomain(descriptor: DomainDescriptor): void {
globalDomainRegistry.register(descriptor)
}
export function lookupDomain(domainId: ImhotepId): DomainDescriptor | undefined {
return globalDomainRegistry.lookup(domainId)
}
export function listDomains(): DomainDescriptor[] {
return globalDomainRegistry.list()
}
export function clearDomainRegistry(): void {
globalDomainRegistry.clear()
}
// ---------------------------------------------------------------------------
// Domain Factory
// ---------------------------------------------------------------------------
export function createElementsDomain(
domainId: ImhotepId,
selector: string,
): DomainDescriptor {
return {
domainId,
kind: 'elements',
selector,
}
}
export function createDescendantsDomain(
domainId: ImhotepId,
parentVar: string,
selector: string,
): DomainDescriptor {
return {
domainId,
kind: 'descendants',
parentVar,
selector,
}
}
export function createLineBoxesDomain(
domainId: ImhotepId,
textNodeId: ImhotepId,
): DomainDescriptor {
return {
domainId,
kind: 'lineBoxes',
selector: textNodeId,
}
}
export function createTextRunsDomain(
domainId: ImhotepId,
textNodeId: ImhotepId,
): DomainDescriptor {
return {
domainId,
kind: 'textRuns',
selector: textNodeId,
}
}
export function createFragmentsDomain(
domainId: ImhotepId,
subjectId: ImhotepId,
): DomainDescriptor {
return {
domainId,
kind: 'fragments',
selector: subjectId,
}
}
export function createFramesDomain(
domainId: ImhotepId,
frameKind: string,
): DomainDescriptor {
return {
domainId,
kind: 'frames',
selector: frameKind,
}
}
// ---------------------------------------------------------------------------
// Domain Value Factory
// ---------------------------------------------------------------------------
export function createDomainValue(
domainId: ImhotepId,
subjectIds: Uint32Array,
provenance: string,
closed = true,
): DomainValue {
return {
domainId,
subjectIds,
provenance,
closed,
}
}
@@ -0,0 +1,322 @@
/**
* Tests for geometry cache serialization and deserialization.
*/
import { test, describe } from 'node:test'
import assert from 'node:assert'
import {
serializeGeometryWorld,
deserializeGeometryWorld,
computeGeometryCacheKey,
readCachedWorld,
writeCachedWorld,
clearGeometryCache,
readCachedExtractionResult,
writeCachedExtractionResult,
} from './geometry-cache.js'
import { createEmptyWorld } from './world.js'
import type { GeometryWorld } from './world.js'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
describe('geometry cache', () => {
function createTestWorld(): GeometryWorld {
const world = createEmptyWorld('scene_1', 'snap_1')
world.env.viewportWidth = 1280
world.env.viewportHeight = 720
world.source.url = 'https://example.com'
world.strings.values = ['hello', 'world']
// Populate subjects table with some data
world.subjects.ids = new Uint32Array([1, 2, 3])
world.subjects.domNodeId = new Uint32Array([10, 20, 30])
world.subjects.subjectKind = new Uint16Array([1, 1, 2])
world.subjects.primaryBoxId = new Uint32Array([100, 200, 300])
// Populate boxes with geometry
world.boxes.boxId = new Uint32Array([100, 200])
world.boxes.subjectId = new Uint32Array([1, 2])
world.boxes.frameId = new Uint32Array([1, 1])
world.boxes.borderLeft = new Float64Array([0, 110])
world.boxes.borderTop = new Float64Array([0, 0])
world.boxes.borderRight = new Float64Array([100, 210])
world.boxes.borderBottom = new Float64Array([40, 40])
world.boxes.paddingLeft = new Float64Array([0, 0])
world.boxes.paddingTop = new Float64Array([0, 0])
world.boxes.paddingRight = new Float64Array([0, 0])
world.boxes.paddingBottom = new Float64Array([0, 0])
world.boxes.contentLeft = new Float64Array([0, 0])
world.boxes.contentTop = new Float64Array([0, 0])
world.boxes.contentRight = new Float64Array([0, 0])
world.boxes.contentBottom = new Float64Array([0, 0])
// Populate topology
world.topology.containingBlockOf = new Uint32Array([0, 1, 2])
world.topology.nearestPositionedAncestorOf = new Uint32Array([0, 0, 0])
world.topology.scrollContainerOf = new Uint32Array([0, 0, 0])
world.topology.stackingContextOf = new Uint32Array([1, 1, 1])
world.topology.formattingContextOf = new Uint32Array([1, 1, 1])
world.topology.clippingRootOf = new Uint32Array([0, 0, 0])
world.topology.paintOrderBucket = new Uint16Array([1, 1, 1])
world.topology.paintOrderIndex = new Uint32Array([0, 1, 2])
return world
}
test('round-trip serialization preserves primitive fields', () => {
const world = createTestWorld()
const json = serializeGeometryWorld(world)
const restored = deserializeGeometryWorld(json)
assert.strictEqual(restored.sceneId, 'scene_1')
assert.strictEqual(restored.snapshotId, 'snap_1')
assert.strictEqual(restored.env.viewportWidth, 1280)
assert.strictEqual(restored.env.viewportHeight, 720)
assert.strictEqual(restored.source.url, 'https://example.com')
assert.deepStrictEqual(restored.strings.values, ['hello', 'world'])
})
test('round-trip preserves TypedArray data and types', () => {
const world = createTestWorld()
const json = serializeGeometryWorld(world)
const restored = deserializeGeometryWorld(json)
// Verify subjects table
assert.ok(restored.subjects.ids instanceof Uint32Array)
assert.deepStrictEqual(
Array.from(restored.subjects.ids),
[1, 2, 3],
)
assert.ok(restored.subjects.subjectKind instanceof Uint16Array)
assert.deepStrictEqual(
Array.from(restored.subjects.subjectKind),
[1, 1, 2],
)
// Verify boxes table
assert.ok(restored.boxes.borderLeft instanceof Float64Array)
assert.deepStrictEqual(
Array.from(restored.boxes.borderLeft),
[0, 110],
)
assert.ok(restored.boxes.borderRight instanceof Float64Array)
assert.deepStrictEqual(
Array.from(restored.boxes.borderRight),
[100, 210],
)
})
test('round-trip preserves topology data', () => {
const world = createTestWorld()
const json = serializeGeometryWorld(world)
const restored = deserializeGeometryWorld(json)
assert.deepStrictEqual(
Array.from(restored.topology.stackingContextOf),
[1, 1, 1],
)
assert.deepStrictEqual(
Array.from(restored.topology.paintOrderIndex),
[0, 1, 2],
)
})
test('empty world round-trips', () => {
const world = createEmptyWorld('empty_scene', 'empty_snap')
const json = serializeGeometryWorld(world)
const restored = deserializeGeometryWorld(json)
assert.strictEqual(restored.sceneId, 'empty_scene')
assert.strictEqual(restored.snapshotId, 'empty_snap')
assert.strictEqual(restored.subjects.ids.length, 0)
assert.strictEqual(restored.boxes.boxId.length, 0)
})
test('world with undefined tables round-trips without crash', () => {
const world = createEmptyWorld('partial', 'snap') as any
// Simulate a world where some tables are undefined (common after extraction)
world.frames = undefined
world.fragments = undefined
world.styles = undefined
world.text = undefined
world.paint = undefined
world.provenance = undefined
world.confidence = undefined
const json = serializeGeometryWorld(world)
const restored = deserializeGeometryWorld(json)
assert.strictEqual(restored.sceneId, 'partial')
assert.ok(restored.frames)
assert.ok(restored.fragments)
assert.ok(restored.styles)
assert.ok(restored.text)
assert.ok(restored.paint)
assert.ok(restored.provenance)
assert.ok(restored.confidence)
})
test('cache key is stable for identical inputs', () => {
const key1 = computeGeometryCacheKey('https://example.com', ['.a', '.b'], { viewportWidth: 1280 })
const key2 = computeGeometryCacheKey('https://example.com', ['.a', '.b'], { viewportWidth: 1280 })
assert.strictEqual(key1, key2)
})
test('cache key differs for different selectors', () => {
const key1 = computeGeometryCacheKey('https://example.com', ['.a', '.b'], {})
const key2 = computeGeometryCacheKey('https://example.com', ['.a', '.c'], {})
assert.notStrictEqual(key1, key2)
})
test('cache key is order-independent for selectors', () => {
const key1 = computeGeometryCacheKey('https://example.com', ['.a', '.b', '.c'], {})
const key2 = computeGeometryCacheKey('https://example.com', ['.c', '.a', '.b'], {})
assert.strictEqual(key1, key2)
})
test('cache key differs for different environments', () => {
const key1 = computeGeometryCacheKey('https://example.com', ['.a'], { viewportWidth: 1280 })
const key2 = computeGeometryCacheKey('https://example.com', ['.a'], { viewportWidth: 1920 })
assert.notStrictEqual(key1, key2)
})
test('write and read cached world', async () => {
const cacheDir = mkdtempSync(join(tmpdir(), 'imhotep-cache-test-'))
try {
const world = createTestWorld()
const cacheKey = 'test-key-123'
await writeCachedWorld(cacheDir, cacheKey, world)
const restored = await readCachedWorld(cacheDir, cacheKey)
assert.ok(restored, 'should read back cached world')
assert.strictEqual(restored.sceneId, 'scene_1')
assert.deepStrictEqual(
Array.from(restored.subjects.ids),
[1, 2, 3],
)
// Cache miss for unknown key
const miss = await readCachedWorld(cacheDir, 'unknown-key')
assert.strictEqual(miss, null)
} finally {
rmSync(cacheDir, { recursive: true, force: true })
}
})
test('clear cache removes all entries', async () => {
const cacheDir = mkdtempSync(join(tmpdir(), 'imhotep-cache-test-'))
try {
const world = createEmptyWorld('s', 'sn')
await writeCachedWorld(cacheDir, 'key1', world)
await writeCachedWorld(cacheDir, 'key2', world)
assert.ok(await readCachedWorld(cacheDir, 'key1'))
assert.ok(await readCachedWorld(cacheDir, 'key2'))
clearGeometryCache(cacheDir)
assert.strictEqual(await readCachedWorld(cacheDir, 'key1'), null)
assert.strictEqual(await readCachedWorld(cacheDir, 'key2'), null)
} finally {
rmSync(cacheDir, { recursive: true, force: true })
}
})
test('write and read cached extraction result with selector mappings', async () => {
const cacheDir = mkdtempSync(join(tmpdir(), 'imhotep-cache-test-'))
try {
const world = createTestWorld()
const selectorToIds = new Map<string, number[]>([
['.button', [1, 2]],
['.label', [3]],
])
const errors = [
{
code: 'IMH_TEST_ERROR',
severity: 'error' as const,
category: 'test-error' as const,
message: 'Test error',
source: 'test',
related: [] as string[],
fixHints: [] as string[],
metrics: {} as Record<string, unknown>,
sourceRef: {} as Record<string, unknown>,
},
]
await writeCachedExtractionResult(cacheDir, 'extract-1', world, selectorToIds, errors)
const cached = await readCachedExtractionResult(cacheDir, 'extract-1')
assert.ok(cached, 'should read back cached extraction result')
assert.strictEqual(cached.world.sceneId, 'scene_1')
assert.deepStrictEqual(
Array.from(cached.selectorToIds.entries()),
[['.button', [1, 2]], ['.label', [3]]],
)
assert.strictEqual(cached.errors.length, 1)
assert.strictEqual(cached.errors[0].code, 'IMH_TEST_ERROR')
// Cache miss for unknown key
assert.strictEqual(await readCachedExtractionResult(cacheDir, 'unknown-key'), null)
} finally {
rmSync(cacheDir, { recursive: true, force: true })
}
})
test('cached extraction result round-trips empty maps and arrays', async () => {
const cacheDir = mkdtempSync(join(tmpdir(), 'imhotep-cache-test-'))
try {
const world = createEmptyWorld('empty', 'snap')
const selectorToIds = new Map<string, number[]>()
const errors: any[] = []
await writeCachedExtractionResult(cacheDir, 'empty-extract', world, selectorToIds, errors)
const cached = await readCachedExtractionResult(cacheDir, 'empty-extract')
assert.ok(cached)
assert.strictEqual(cached.selectorToIds.size, 0)
assert.strictEqual(cached.errors.length, 0)
} finally {
rmSync(cacheDir, { recursive: true, force: true })
}
})
test('cache read is fast for large worlds', async () => {
const cacheDir = mkdtempSync(join(tmpdir(), 'imhotep-cache-test-'))
try {
// Build a world with 1000 subjects and boxes
const world = createEmptyWorld('bench', 'snap')
const n = 1000
world.subjects.ids = new Uint32Array(Array.from({ length: n }, (_, i) => i + 1))
world.subjects.domNodeId = new Uint32Array(Array.from({ length: n }, (_, i) => i + 10))
world.subjects.subjectKind = new Uint16Array(Array.from({ length: n }, () => 1))
world.subjects.primaryBoxId = new Uint32Array(Array.from({ length: n }, (_, i) => i + 100))
world.boxes.boxId = new Uint32Array(Array.from({ length: n }, (_, i) => i + 100))
world.boxes.subjectId = new Uint32Array(Array.from({ length: n }, (_, i) => i + 1))
world.boxes.frameId = new Uint32Array(Array.from({ length: n }, () => 1))
world.boxes.borderLeft = new Float64Array(Array.from({ length: n }, (_, i) => i * 10))
world.boxes.borderTop = new Float64Array(Array.from({ length: n }, () => 0))
world.boxes.borderRight = new Float64Array(Array.from({ length: n }, (_, i) => i * 10 + 100))
world.boxes.borderBottom = new Float64Array(Array.from({ length: n }, () => 50))
const selectorToIds = new Map<string, number[]>([['.item', Array.from({ length: n }, (_, i) => i + 1)]])
const errors: any[] = []
await writeCachedExtractionResult(cacheDir, 'bench-extract', world, selectorToIds, errors)
const start = performance.now()
const cached = await readCachedExtractionResult(cacheDir, 'bench-extract')
const duration = performance.now() - start
assert.ok(cached, 'should read cached large world')
assert.strictEqual(cached.world.subjects.ids.length, n)
assert.ok(duration < 100, `cache read should be under 100ms, took ${duration.toFixed(2)}ms`)
} finally {
rmSync(cacheDir, { recursive: true, force: true })
}
})
})
+419
View File
@@ -0,0 +1,419 @@
/**
* Geometry snapshot caching for Imhotep.
*
* Serializes and deserializes GeometryWorld instances so extracted
* browser geometry can be cached to disk and reused across assertion
* batches without re-extraction.
*/
import type { GeometryWorld, WorldEnvironment, WorldSource } from './world.js'
import type { ImhotepId } from './types.js'
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
interface SerializedTypedArray {
type: 'Uint32Array' | 'Uint16Array' | 'Uint8Array' | 'Int32Array' | 'Int16Array' | 'Float64Array' | 'Float32Array'
data: number[]
}
interface SerializedTable {
[column: string]: SerializedTypedArray | string[] | number[]
}
interface SerializedWorld {
version: number
sceneId: ImhotepId
snapshotId: ImhotepId
env: WorldEnvironment
source: WorldSource
strings: { values: string[] }
subjects: SerializedTable
dom: SerializedTable
frames: SerializedTable
matrices: SerializedTable
rects: SerializedTable
boxes: SerializedTable
visualBoxes: SerializedTable
fragments: SerializedTable
transforms: SerializedTable
styles: SerializedTable
text: SerializedTable
topology: SerializedTable
scroll: SerializedTable
clipping: SerializedTable
paint: SerializedTable
visibility: SerializedTable
provenance: SerializedTable
confidence: SerializedTable
}
function serializeTypedArray(arr: TypedArray): SerializedTypedArray {
const ctor = arr.constructor.name as SerializedTypedArray['type']
return { type: ctor, data: Array.from(arr) }
}
function deserializeTypedArray(serialized: SerializedTypedArray): TypedArray {
const ctor = globalThis[serialized.type] as new (data: number[]) => TypedArray
return new ctor(serialized.data)
}
type TypedArray =
| Uint32Array
| Uint16Array
| Uint8Array
| Int32Array
| Int16Array
| Float64Array
| Float32Array
function isTypedArray(value: unknown): value is TypedArray {
return (
value instanceof Uint32Array ||
value instanceof Uint16Array ||
value instanceof Uint8Array ||
value instanceof Int32Array ||
value instanceof Int16Array ||
value instanceof Float64Array ||
value instanceof Float32Array
)
}
function serializeTable(table: Record<string, unknown>): SerializedTable {
const result: SerializedTable = {}
if (!table) return result
for (const [key, value] of Object.entries(table)) {
if (isTypedArray(value)) {
result[key] = serializeTypedArray(value)
} else if (Array.isArray(value)) {
result[key] = value as number[] | string[]
} else if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
result[key] = [value] as number[] | string[]
} else {
result[key] = value as SerializedTable[string]
}
}
return result
}
function deserializeTable(serialized: SerializedTable): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(serialized)) {
if (value && typeof value === 'object' && 'type' in value && 'data' in value) {
result[key] = deserializeTypedArray(value as SerializedTypedArray)
} else if (Array.isArray(value)) {
result[key] = value
} else {
result[key] = value
}
}
return result
}
/**
* Serialize a GeometryWorld to a JSON string.
*/
export function serializeGeometryWorld(world: GeometryWorld): string {
const serialized: SerializedWorld = {
version: 1,
sceneId: world.sceneId,
snapshotId: world.snapshotId,
env: world.env,
source: world.source,
strings: world.strings,
subjects: serializeTable(world.subjects as unknown as Record<string, unknown>),
dom: serializeTable(world.dom as unknown as Record<string, unknown>),
frames: serializeTable(world.frames as unknown as Record<string, unknown>),
matrices: serializeTable(world.matrices as unknown as Record<string, unknown>),
rects: serializeTable(world.rects as unknown as Record<string, unknown>),
boxes: serializeTable(world.boxes as unknown as Record<string, unknown>),
visualBoxes: serializeTable(world.visualBoxes as unknown as Record<string, unknown>),
fragments: serializeTable(world.fragments as unknown as Record<string, unknown>),
transforms: serializeTable(world.transforms as unknown as Record<string, unknown>),
styles: serializeTable(world.styles as unknown as Record<string, unknown>),
text: serializeTable(world.text as unknown as Record<string, unknown>),
topology: serializeTable(world.topology as unknown as Record<string, unknown>),
scroll: serializeTable(world.scroll as unknown as Record<string, unknown>),
clipping: serializeTable(world.clipping as unknown as Record<string, unknown>),
paint: serializeTable(world.paint as unknown as Record<string, unknown>),
visibility: serializeTable(world.visibility as unknown as Record<string, unknown>),
provenance: serializeTable(world.provenance as unknown as Record<string, unknown>),
confidence: serializeTable(world.confidence as unknown as Record<string, unknown>),
}
return JSON.stringify(serialized)
}
/**
* Deserialize a JSON string back into a GeometryWorld.
*/
export function deserializeGeometryWorld(json: string): GeometryWorld {
const serialized = JSON.parse(json) as SerializedWorld
if (serialized.version !== 1) {
throw new Error(`Unsupported geometry cache version: ${serialized.version}`)
}
return {
sceneId: serialized.sceneId,
snapshotId: serialized.snapshotId,
env: serialized.env,
source: serialized.source,
strings: serialized.strings,
subjects: deserializeTable(serialized.subjects) as unknown as GeometryWorld['subjects'],
dom: deserializeTable(serialized.dom) as unknown as GeometryWorld['dom'],
frames: deserializeTable(serialized.frames) as unknown as GeometryWorld['frames'],
matrices: deserializeTable(serialized.matrices) as unknown as GeometryWorld['matrices'],
rects: deserializeTable(serialized.rects) as unknown as GeometryWorld['rects'],
boxes: deserializeTable(serialized.boxes) as unknown as GeometryWorld['boxes'],
visualBoxes: deserializeTable(serialized.visualBoxes) as unknown as GeometryWorld['visualBoxes'],
fragments: deserializeTable(serialized.fragments) as unknown as GeometryWorld['fragments'],
transforms: deserializeTable(serialized.transforms) as unknown as GeometryWorld['transforms'],
styles: deserializeTable(serialized.styles) as unknown as GeometryWorld['styles'],
text: deserializeTable(serialized.text) as unknown as GeometryWorld['text'],
topology: deserializeTable(serialized.topology) as unknown as GeometryWorld['topology'],
scroll: deserializeTable(serialized.scroll) as unknown as GeometryWorld['scroll'],
clipping: deserializeTable(serialized.clipping) as unknown as GeometryWorld['clipping'],
paint: deserializeTable(serialized.paint) as unknown as GeometryWorld['paint'],
visibility: deserializeTable(serialized.visibility) as unknown as GeometryWorld['visibility'],
provenance: deserializeTable(serialized.provenance) as unknown as GeometryWorld['provenance'],
confidence: deserializeTable(serialized.confidence) as unknown as GeometryWorld['confidence'],
}
}
// ---------------------------------------------------------------------------
// Cache key computation
// ---------------------------------------------------------------------------
function djb2Hash(str: string): string {
let hash = 5381
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff
}
return hash.toString(16).padStart(8, '0')
}
/**
* Compute a stable cache key for a geometry extraction.
*
* The key incorporates:
* - page URL
* - sorted selectors (so order doesn't matter)
* - environment (viewport dimensions, color scheme, etc.)
*/
export function computeGeometryCacheKey(
pageUrl: string,
selectors: string[],
env: Partial<WorldEnvironment>,
): string {
const selectorsHash = djb2Hash([...selectors].sort().join('\x00'))
const envHash = djb2Hash(
JSON.stringify({
viewportWidth: env.viewportWidth ?? 0,
viewportHeight: env.viewportHeight ?? 0,
deviceScaleFactor: env.deviceScaleFactor ?? 1,
colorScheme: env.colorScheme ?? 'light',
pointer: env.pointer ?? 'fine',
hover: env.hover ?? false,
reducedMotion: env.reducedMotion ?? 'no-preference',
locale: env.locale ?? 'en',
writingMode: env.writingMode ?? 'horizontal-tb',
}),
)
const urlHash = djb2Hash(pageUrl)
return `${urlHash}-${selectorsHash}-${envHash}`
}
// ---------------------------------------------------------------------------
// File-based cache storage
// ---------------------------------------------------------------------------
import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from 'node:fs'
import { readFile, writeFile, unlink } from 'node:fs/promises'
import { join } from 'node:path'
const DEFAULT_CACHE_DIR = join(process.cwd(), '.imhotep-cache')
const DEFAULT_MAX_CACHE_ENTRIES = 100
function cacheFilePath(cacheDir: string, cacheKey: string): string {
return join(cacheDir, `${cacheKey}.json`)
}
/**
* Evict oldest cache entries when exceeding maxEntries.
*/
function evictOldestEntries(cacheDir: string, maxEntries: number): void {
if (!existsSync(cacheDir)) return
const files = readdirSync(cacheDir)
.filter((f) => f.endsWith('.json'))
.map((f) => {
const fullPath = join(cacheDir, f)
try {
return { name: f, path: fullPath, mtime: statSync(fullPath).mtimeMs }
} catch {
return null
}
})
.filter((f): f is NonNullable<typeof f> => f !== null)
.sort((a, b) => a.mtime - b.mtime)
if (files.length > maxEntries) {
const toRemove = files.length - maxEntries
for (let i = 0; i < toRemove; i++) {
try {
rmSync(files[i].path)
} catch {
// ignore deletion errors
}
}
}
}
/**
* Read a cached GeometryWorld from disk (async, non-blocking).
*/
export async function readCachedWorld(cacheDir: string, cacheKey: string): Promise<GeometryWorld | null> {
const path = cacheFilePath(cacheDir, cacheKey)
try {
const json = await readFile(path, 'utf-8')
return deserializeGeometryWorld(json)
} catch {
return null
}
}
/**
* Write a GeometryWorld to the cache on disk (async, non-blocking).
*/
export async function writeCachedWorld(cacheDir: string, cacheKey: string, world: GeometryWorld): Promise<void> {
mkdirSync(cacheDir, { recursive: true })
evictOldestEntries(cacheDir, DEFAULT_MAX_CACHE_ENTRIES)
const path = cacheFilePath(cacheDir, cacheKey)
await writeFile(path, serializeGeometryWorld(world))
}
/**
* Clear all cached geometry snapshots.
*/
export function clearGeometryCache(cacheDir: string = DEFAULT_CACHE_DIR): void {
if (!existsSync(cacheDir)) {
return
}
for (const file of readdirSync(cacheDir)) {
if (file.endsWith('.json')) {
try {
rmSync(join(cacheDir, file))
} catch {
// ignore deletion errors
}
}
}
}
// ---------------------------------------------------------------------------
// Full extraction result caching (world + selector mapping + errors)
// ---------------------------------------------------------------------------
interface CachedExtractionResult {
version: number
world: string
selectorToIds: [string, number[]][]
errors: Array<{
code: string
severity: string
category: string
message: string
source: string
related: string[]
fixHints: string[]
metrics: Record<string, unknown>
sourceRef: Record<string, unknown>
clauseLabel?: string
}>
}
/**
* Serialize a full extraction result (world + selector mappings + errors).
*/
export function serializeExtractionResult(
world: GeometryWorld,
selectorToIds: Map<string, number[]>,
errors: Array<{
code: string
severity: string
category: string
message: string
source: string
related: string[]
fixHints: string[]
metrics: Record<string, unknown>
sourceRef: Record<string, unknown>
clauseLabel?: string
}>,
): string {
const cached: CachedExtractionResult = {
version: 1,
world: serializeGeometryWorld(world),
selectorToIds: Array.from(selectorToIds.entries()),
errors,
}
return JSON.stringify(cached)
}
/**
* Deserialize a full extraction result from JSON.
*/
export function deserializeExtractionResult(json: string): {
world: GeometryWorld
selectorToIds: Map<string, number[]>
errors: CachedExtractionResult['errors']
} {
const cached = JSON.parse(json) as CachedExtractionResult
if (cached.version !== 1) {
throw new Error(`Unsupported extraction cache version: ${cached.version}`)
}
return {
world: deserializeGeometryWorld(cached.world),
selectorToIds: new Map(cached.selectorToIds),
errors: cached.errors,
}
}
/**
* Read a cached extraction result from disk (async, non-blocking).
*/
export async function readCachedExtractionResult(
cacheDir: string,
cacheKey: string,
): Promise<{ world: GeometryWorld; selectorToIds: Map<string, number[]>; errors: CachedExtractionResult['errors'] } | null> {
const path = cacheFilePath(cacheDir, cacheKey)
try {
const json = await readFile(path, 'utf-8')
return deserializeExtractionResult(json)
} catch {
return null
}
}
/**
* Write an extraction result to the cache on disk (async, non-blocking).
*/
export async function writeCachedExtractionResult(
cacheDir: string,
cacheKey: string,
world: GeometryWorld,
selectorToIds: Map<string, number[]>,
errors: CachedExtractionResult['errors'],
): Promise<void> {
mkdirSync(cacheDir, { recursive: true })
evictOldestEntries(cacheDir, DEFAULT_MAX_CACHE_ENTRIES)
const path = cacheFilePath(cacheDir, cacheKey)
await writeFile(path, serializeExtractionResult(world, selectorToIds, errors))
}
/**
* Return the default cache directory path.
*/
export function getDefaultCacheDir(): string {
return DEFAULT_CACHE_DIR
}
+86
View File
@@ -0,0 +1,86 @@
/**
* Imhotep Core — foundational contracts and type system.
*
* Exported modules:
* types — Core type definitions (positions, IDs, results, proofs)
* ast — Unist-style AST node definitions
* ir — Semantic IR and Execution IR definitions
* world — Geometry world schema types
* contracts — Extractor, solver, compiler, reporter interfaces
* diagnostics — Diagnostic categories, error codes, traces, shrinking
*/
export * from './types.js'
export * from './ast.js'
export * from './ir.js'
export { FragmentKind, createEmptyWorld } from './world.js'
export type {
GeometryWorld,
WorldEnvironment,
WorldSource,
StringTable,
SubjectsTable,
DomTable,
FramesTable,
MatricesTable,
RectsTable,
BoxesTable,
FragmentsTable,
TransformsTable,
StylesTable,
TextTable,
TopologyTable,
ScrollTable,
ClippingTable,
PaintTable,
VisibilityTable,
ProvenanceTable,
ConfidenceTable,
FactRequestPlan,
} from './world.js'
export * from './contracts.js'
export * from './diagnostics.js'
export * from './pipeline.js'
export * from './logic-ast.js'
export * from './logic-ir.js'
export * from './domains.js'
export * from './scene-target.js'
export * from './property-contracts.js'
export * from './property-results.js'
export * from './canonical.js'
export * from './context.js'
export {
serializeGeometryWorld,
deserializeGeometryWorld,
computeGeometryCacheKey,
readCachedWorld,
writeCachedWorld,
clearGeometryCache,
getDefaultCacheDir,
readCachedExtractionResult,
writeCachedExtractionResult,
} from './geometry-cache.js'
// Public API type aliases for high-level evaluation results
import type { ClauseResult, ImhotepId } from './types.js'
import type { Diagnostic } from './diagnostics.js'
export interface NormalizedContract {
clauseId: ImhotepId
clauseLabel: string
relation: string
subject: string
reference: string
options: Record<string, unknown>
}
export type ImhotepResult = {
schemaVersion: string
passed: boolean
summary: string
clauseResults: ClauseResult[]
diagnostics: ImhotepDiagnostic[]
normalizedContracts?: NormalizedContract[]
}
export type ImhotepDiagnostic = Diagnostic
@@ -0,0 +1,253 @@
/**
* DSL -> Geometry integration tests.
*
* Verifies that fluent assertions compile to IR that geometry worlds can satisfy,
* that frame references resolve correctly, and that tolerance values propagate.
*/
import { describe, it, before } from 'node:test'
import assert from 'node:assert'
import { expect, frame, compile as dslCompile } from 'imhotep-dsl'
import { evaluate, registerDefaultClauses } from 'imhotep-solver'
import { createEmptySemanticIr } from './ir.js'
import type { ExecutionIr } from './ir.js'
import type { ExecutionContext } from 'imhotep-extractor'
import { ExtractionPlanner, FactRequirementRegistry, FACT_FAMILY } from 'imhotep-extractor'
import {
buildMockGeometryWorld,
mapSemanticIrToClauses,
} from './integration-mocks.js'
describe('DSL -> Geometry', () => {
before(() => {
registerDefaultClauses()
})
it('fluent assertions compile to IR that geometry world can satisfy', () => {
// Arrange: build DSL assertion via fluent API
const fluent = expect('.tooltip').to.be.above('.trigger')
const ast = fluent.toAst()
// Arrange: compile to semantic + execution IR with deterministic ids
let idCounter = 0
const result = dslCompile([ast], {
generateId: (prefix) => `${prefix}_${++idCounter}`,
})
// Assert compilation succeeded
assert.strictEqual(result.diagnostics.length, 0)
assert.strictEqual(result.executionIr.clauseCount, 1)
// Arrange: build geometry world with matching subjects in compiler order
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.tooltip',
box: { top: 0, bottom: 10, left: 0, right: 50 },
},
{
id: 1,
selector: '.trigger',
box: { top: 20, bottom: 30, left: 0, right: 50 },
},
],
})
// Map semantic IR subject ids to world subject ids (compiler order)
const subjectIdMap = new Map<string, number>()
let worldIndex = 0
for (const [id] of result.semanticIr.subjects) {
subjectIdMap.set(id, worldIndex++)
}
// Act: build clause descriptors and evaluate
const clauses = mapSemanticIrToClauses(result.semanticIr, subjectIdMap)
const evalResult = evaluate(world, clauses)
// Assert
assert.strictEqual(evalResult.clauseResults.length, 1)
assert.strictEqual(evalResult.clauseResults[0].status, 'pass')
assert.strictEqual(evalResult.clauseResults[0].truth, 'determinate')
})
it('frame references in DSL resolve correctly in geometry world', () => {
// Arrange: build DSL assertion with nearestPositionedAncestor frame
const fluent = expect('.tooltip')
.in(frame.nearestPositionedAncestor('.trigger'))
.to.be.above('.trigger')
const ast = fluent.toAst()
// Arrange: compile
let idCounter = 0
const result = dslCompile([ast], {
generateId: (prefix) => `${prefix}_${++idCounter}`,
})
// Assert: semantic IR has a non-default frame
const nonDefaultFrames: Array<[string, { kind: string }]> = []
for (const [, frame] of result.semanticIr.frames.entries()) {
if (frame.kind !== 'viewport') {
nonDefaultFrames.push(['', frame])
}
}
assert.strictEqual(nonDefaultFrames.length, 1)
assert.strictEqual(nonDefaultFrames[0][1].kind, 'nearestPositionedAncestor')
// Arrange: build world with frame topology
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.tooltip',
box: { top: 0, bottom: 10, left: 0, right: 50 },
},
{
id: 1,
selector: '.trigger',
box: { top: 20, bottom: 30, left: 0, right: 50 },
},
],
topology: {
nearestPositionedAncestorOf: [1],
},
})
// Map subjects
const subjectIdMap = new Map<string, number>()
let worldIndex = 0
for (const [id] of result.semanticIr.subjects) {
subjectIdMap.set(id, worldIndex++)
}
// Act
const clauses = mapSemanticIrToClauses(result.semanticIr, subjectIdMap)
const evalResult = evaluate(world, clauses)
// Assert
assert.strictEqual(evalResult.clauseResults.length, 1)
assert.strictEqual(evalResult.clauseResults[0].status, 'pass')
assert.strictEqual(evalResult.clauseResults[0].truth, 'determinate')
})
it('tolerance values propagate to solver correctly', () => {
// Arrange: build DSL assertion with tolerance
const fluent = expect('.tooltip').to.be.inside('.container', {
tolerance: 5,
})
const ast = fluent.toAst()
// Arrange: compile
let idCounter = 0
const result = dslCompile([ast], {
generateId: (prefix) => `${prefix}_${++idCounter}`,
})
// Assert: compiler produced a tolerance entry
const nonDefaultTolerances: Array<[string, { value: number; unit: string }]> = []
for (const [, tol] of result.semanticIr.tolerances.entries()) {
if (!(tol.value === 0 && tol.unit === 'px')) {
nonDefaultTolerances.push(['', tol])
}
}
assert.strictEqual(nonDefaultTolerances.length, 1)
assert.strictEqual(nonDefaultTolerances[0][1].value, 5)
assert.strictEqual(nonDefaultTolerances[0][1].unit, 'px')
// Arrange: build world where tooltip is almost inside container
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.tooltip',
box: { top: 2, bottom: 8, left: 2, right: 48 },
},
{
id: 1,
selector: '.container',
box: { top: 0, bottom: 10, left: 0, right: 50 },
},
],
})
// Map subjects
const subjectIdMap = new Map<string, number>()
let worldIndex = 0
for (const [id] of result.semanticIr.subjects) {
subjectIdMap.set(id, worldIndex++)
}
// Act
const clauses = mapSemanticIrToClauses(result.semanticIr, subjectIdMap)
const evalResult = evaluate(world, clauses)
// Assert
assert.strictEqual(evalResult.clauseResults.length, 1)
assert.strictEqual(evalResult.clauseResults[0].status, 'pass')
assert.ok(evalResult.clauseResults[0].metrics)
})
it('execution IR produces extractable fact requirements', () => {
// Arrange: build DSL assertion
const fluent = expect('.a').to.be.leftOf('.b')
const ast = fluent.toAst()
// Arrange: compile
let idCounter = 0
const result = dslCompile([ast], {
generateId: (prefix) => `${prefix}_${++idCounter}`,
})
// Arrange: build execution context for planner
const semanticIr = result.semanticIr
const executionIr = result.executionIr
const subjectIds: string[] = []
for (const [id] of semanticIr.subjects) {
subjectIds.push(id)
}
const frameIds: string[] = []
for (const [id] of semanticIr.frames) {
frameIds.push(id)
}
const stateIds: string[] = []
for (const [id] of semanticIr.states) {
stateIds.push(id)
}
const timelineIds: string[] = []
for (const [id] of semanticIr.timelines) {
timelineIds.push(id)
}
const envGuardIds: string[] = []
for (const [id] of semanticIr.envGuards) {
envGuardIds.push(id)
}
const toleranceIds: string[] = []
for (const [id] of semanticIr.tolerances) {
toleranceIds.push(id)
}
const context: ExecutionContext = {
executionIr,
semanticIr,
subjectIds,
frameIds,
stateIds,
timelineIds,
envGuardIds,
toleranceIds,
}
// Arrange: register fact requirements for leftOf
const registry = new FactRequirementRegistry()
registry.register(1, [{ family: FACT_FAMILY.GEOMETRY }])
// Act: create extraction plan
const planner = new ExtractionPlanner({ factRegistry: registry })
const plan = planner.createPlan(context)
// Assert: plan requests geometry facts
assert.strictEqual(plan.requests.length >= 1, true)
assert.strictEqual(plan.requests[0].requiredFacts.geometry, true)
})
})
@@ -0,0 +1,282 @@
/**
* Extractor -> Solver integration tests.
*
* Verifies that raw extraction results normalize into worlds solvers can
* evaluate, that topology facts enable topology assertions, and that missing
* facts produce indeterminate results.
*/
import { describe, it, before } from 'node:test'
import assert from 'node:assert'
import {
ExtractionPlanner,
FactRequirementRegistry,
FACT_FAMILY,
} from 'imhotep-extractor'
import type { ExecutionContext } from 'imhotep-extractor'
import { evaluate, registerDefaultClauses } from 'imhotep-solver'
import { createEmptySemanticIr } from './ir.js'
import type { ExecutionIr } from './ir.js'
import {
buildMockGeometryWorld,
buildClauseDescriptor,
} from './integration-mocks.js'
describe('Extractor -> Solver', () => {
before(() => {
registerDefaultClauses()
})
it('raw extraction results normalize into worlds solvers can evaluate', () => {
// Arrange: create semantic IR with two subjects and an above relation
const semanticIr = createEmptySemanticIr()
const subjectId = 'subject_1'
const refId = 'subject_2'
semanticIr.subjects.set(subjectId, {
id: subjectId,
selector: '.a',
kind: 'element',
origin: { astNodeIds: [], positions: [] },
})
semanticIr.subjects.set(refId, {
id: refId,
selector: '.b',
kind: 'element',
origin: { astNodeIds: [], positions: [] },
})
const clauseId = 'clause_1'
semanticIr.clauses.set(clauseId, {
id: clauseId,
origin: { astNodeIds: [], positions: [] },
subjectRef: subjectId,
referenceRef: refId,
relation: 'above',
frameRef: 'frame_default',
stateRef: 'state_default',
timelineRef: 'timeline_default',
envGuardRef: 'guard_default',
toleranceRef: 'tolerance_default',
bounds: {},
})
const executionIr: ExecutionIr = {
clauseCount: 1,
clauseType: new Uint16Array([3]),
clauseSubject: new Uint32Array([0]),
clauseReference: new Uint32Array([1]),
clauseFrame: new Uint32Array([0]),
clauseState: new Uint32Array([0]),
clauseTimeline: new Uint32Array([0]),
clauseTolerance: new Uint32Array([0]),
clauseEnvGuard: new Uint32Array([0]),
clauseArg0: new Float64Array([Number.NEGATIVE_INFINITY]),
clauseArg1: new Float64Array([Number.POSITIVE_INFINITY]),
clauseFlags: new Uint32Array([0]),
clauseOrigin: new Uint32Array([0]),
}
const context: ExecutionContext = {
executionIr,
semanticIr,
subjectIds: [subjectId, refId],
frameIds: ['frame_default'],
stateIds: ['state_default'],
timelineIds: ['timeline_default'],
envGuardIds: ['guard_default'],
toleranceIds: ['tolerance_default'],
}
// Arrange: create fact registry and planner
const registry = new FactRequirementRegistry()
registry.register(3, [{ family: FACT_FAMILY.GEOMETRY }])
const planner = new ExtractionPlanner({ factRegistry: registry })
// Act: create extraction plan
const plan = planner.createPlan(context)
// Assert: plan has one request requiring geometry
assert.strictEqual(plan.requests.length, 1)
assert.strictEqual(plan.requests[0].requiredFacts.geometry, true)
// Act: build mock extracted world and evaluate
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.a',
box: { top: 0, bottom: 10, left: 0, right: 50 },
},
{
id: 1,
selector: '.b',
box: { top: 20, bottom: 30, left: 0, right: 50 },
},
],
})
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'relation.above',
subjectRef: 0,
referenceRef: 1,
}),
]
const result = evaluate(world, clauses)
assert.strictEqual(result.clauseResults[0].status, 'pass')
})
it('topology facts from extractor enable topology assertions', () => {
// Arrange: build world with topology data
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.child',
box: { top: 0, bottom: 10, left: 0, right: 10 },
},
{
id: 1,
selector: '.scroll',
box: { top: 0, bottom: 100, left: 0, right: 100 },
},
],
topology: {
scrollContainerOf: [1],
},
})
// Act: evaluate topology clause
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'topology.attachedToScrollContainer',
subjectRef: 0,
referenceRef: 1,
}),
]
const result = evaluate(world, clauses)
// Assert
assert.strictEqual(result.clauseResults[0].status, 'pass')
assert.strictEqual(result.clauseResults[0].truth, 'determinate')
assert.strictEqual(
result.clauseResults[0].metrics?.attached,
1,
)
})
it('stacking context topology facts enable stacking assertions', () => {
// Arrange: build world with shared stacking context
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.layer-a',
box: { top: 0, bottom: 10, left: 0, right: 10 },
},
{
id: 1,
selector: '.layer-b',
box: { top: 5, bottom: 15, left: 5, right: 15 },
},
],
topology: {
stackingContextOf: [42, 42],
},
})
// Act: evaluate stacking context clause
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'topology.inStackingContext',
subjectRef: 0,
referenceRef: 1,
}),
]
const result = evaluate(world, clauses)
// Assert: same stacking context id means pass
assert.strictEqual(result.clauseResults[0].status, 'pass')
assert.strictEqual(result.clauseResults[0].truth, 'determinate')
})
it('missing facts produce indeterminate results', () => {
// Arrange: build world with only one subject
const world = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.a',
box: { top: 0, bottom: 10, left: 0, right: 10 },
},
],
})
// Act: evaluate clause referencing non-existent subject
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'relation.above',
subjectRef: 0,
referenceRef: 99,
}),
]
const result = evaluate(world, clauses)
// Assert: indeterminate because reference box is missing
assert.strictEqual(result.clauseResults[0].status, 'error')
assert.strictEqual(result.clauseResults[0].truth, 'indeterminate')
})
it('unsupported clause types emit extraction diagnostics', () => {
// Arrange: create execution IR with unregistered clause type
const semanticIr = createEmptySemanticIr()
const executionIr: ExecutionIr = {
clauseCount: 1,
clauseType: new Uint16Array([999]),
clauseSubject: new Uint32Array([0]),
clauseReference: new Uint32Array([0]),
clauseFrame: new Uint32Array([0]),
clauseState: new Uint32Array([0]),
clauseTimeline: new Uint32Array([0]),
clauseTolerance: new Uint32Array([0]),
clauseEnvGuard: new Uint32Array([0]),
clauseArg0: new Float64Array([0]),
clauseArg1: new Float64Array([0]),
clauseFlags: new Uint32Array([0]),
clauseOrigin: new Uint32Array([0]),
}
const context: ExecutionContext = {
executionIr,
semanticIr,
subjectIds: [],
frameIds: [],
stateIds: [],
timelineIds: [],
envGuardIds: [],
toleranceIds: [],
}
const registry = new FactRequirementRegistry()
const planner = new ExtractionPlanner({ factRegistry: registry })
// Act: create plan
const plan = planner.createPlan(context)
// Assert: diagnostic emitted for unsupported clause
assert.ok(
plan.diagnostics.some(
(d) => d.code === 'IMH_EXTRACTOR_UNSUPPORTED_CLAUSE_TYPE',
),
)
})
})
@@ -0,0 +1,357 @@
/**
* Mock helpers for cross-package integration tests.
*
* Provides dependency-injected builders for geometry worlds,
* extraction results, solver results, and complete mock scenes.
*/
import type {
GeometryWorld,
ClauseDescriptor,
ClauseResult,
} from 'imhotep-solver'
import type { EvaluationOutput } from 'imhotep-solver'
import type { GeometrySnapshot, SnapshotMetadata } from 'imhotep-state'
import type { Diagnostic as CoreDiagnostic } from './diagnostics.js'
import type { ImhotepId } from './types.js'
import type { SemanticIr } from './ir.js'
// ---------------------------------------------------------------------------
// Geometry World Builder
// ---------------------------------------------------------------------------
export interface MockSubject {
id: number
selector: string
box: {
top: number
bottom: number
left: number
right: number
}
}
export interface MockFrame {
id: number
kind: string
subjectId: number
}
export interface MockWorldOptions {
sceneId?: string
snapshotId?: string
subjects: MockSubject[]
frames?: MockFrame[]
topology?: {
nearestPositionedAncestorOf?: number[]
scrollContainerOf?: number[]
stackingContextOf?: number[]
containingBlockOf?: number[]
}
}
/**
* Build a solver-compatible geometry world from a declarative description.
*/
export function buildMockGeometryWorld(opts: MockWorldOptions): GeometryWorld {
const subjectCount = opts.subjects.length
const world: GeometryWorld = {
sceneId: opts.sceneId ?? 'scene_test',
snapshotId: opts.snapshotId ?? 'snap_test',
env: {
viewportWidth: 1280,
viewportHeight: 720,
deviceScaleFactor: 1,
colorScheme: 'light',
pointer: 'fine',
hover: false,
reducedMotion: false,
locale: 'en',
writingMode: 'horizontal-tb',
},
strings: { values: [] },
subjects: {
ids: opts.subjects.map((s) => s.id),
domNodeId: new Array(subjectCount).fill(0),
subjectKind: new Array(subjectCount).fill(0),
primaryBoxId: opts.subjects.map((_, i) => i),
firstFragmentId: new Array(subjectCount).fill(0),
fragmentCount: new Array(subjectCount).fill(0),
},
dom: {
nodeId: opts.subjects.map((s) => s.id),
parentNodeId: new Array(subjectCount).fill(0),
childCount: new Array(subjectCount).fill(0),
tagNameStringId: new Array(subjectCount).fill(0),
},
boxes: {
boxId: opts.subjects.map((_, i) => i),
subjectId: opts.subjects.map((s) => s.id),
frameId: new Array(subjectCount).fill(0),
borderLeft: opts.subjects.map((s) => s.box.left),
borderTop: opts.subjects.map((s) => s.box.top),
borderRight: opts.subjects.map((s) => s.box.right),
borderBottom: opts.subjects.map((s) => s.box.bottom),
paddingLeft: opts.subjects.map((s) => s.box.left),
paddingTop: opts.subjects.map((s) => s.box.top),
paddingRight: opts.subjects.map((s) => s.box.right),
paddingBottom: opts.subjects.map((s) => s.box.bottom),
contentLeft: opts.subjects.map((s) => s.box.left),
contentTop: opts.subjects.map((s) => s.box.top),
contentRight: opts.subjects.map((s) => s.box.right),
contentBottom: opts.subjects.map((s) => s.box.bottom),
},
rects: {
rectId: opts.subjects.map((_, i) => i),
left: opts.subjects.map((s) => s.box.left),
top: opts.subjects.map((s) => s.box.top),
right: opts.subjects.map((s) => s.box.right),
bottom: opts.subjects.map((s) => s.box.bottom),
},
topology: {
containingBlockOf: opts.topology?.containingBlockOf ?? [],
nearestPositionedAncestorOf:
opts.topology?.nearestPositionedAncestorOf ?? [],
scrollContainerOf: opts.topology?.scrollContainerOf ?? [],
stackingContextOf: opts.topology?.stackingContextOf ?? [],
formattingContextOf: [],
clippingRootOf: [],
paintOrderBucket: [],
paintOrderIndex: [],
},
scroll: {
containerId: [],
scrollLeft: [],
scrollTop: [],
scrollWidth: [],
scrollHeight: [],
clientWidth: [],
clientHeight: [],
},
visualBoxes: {
boxId: opts.subjects.map((_, i) => i),
subjectId: opts.subjects.map((s) => s.id),
frameId: new Array(subjectCount).fill(0),
borderLeft: opts.subjects.map((s) => s.box.left),
borderTop: opts.subjects.map((s) => s.box.top),
borderRight: opts.subjects.map((s) => s.box.right),
borderBottom: opts.subjects.map((s) => s.box.bottom),
paddingLeft: opts.subjects.map((s) => s.box.left),
paddingTop: opts.subjects.map((s) => s.box.top),
paddingRight: opts.subjects.map((s) => s.box.right),
paddingBottom: opts.subjects.map((s) => s.box.bottom),
contentLeft: opts.subjects.map((s) => s.box.left),
contentTop: opts.subjects.map((s) => s.box.top),
contentRight: opts.subjects.map((s) => s.box.right),
contentBottom: opts.subjects.map((s) => s.box.bottom),
},
transforms: {
transformId: [],
subjectId: [],
matrixStart: [],
matrixLength: [],
originX: [],
originY: [],
},
matrices: {
values: [],
},
clipping: {
clipNodeId: [],
subjectId: [],
clipKind: [],
clipLeft: [],
clipTop: [],
clipRight: [],
clipBottom: [],
parentClipNodeId: [],
},
visibility: {
subjectId: [],
isRendered: [],
isVisible: [],
visibleArea: [],
clippedArea: [],
},
}
return world
}
// ---------------------------------------------------------------------------
// Clause Descriptor Builder
// ---------------------------------------------------------------------------
export function buildClauseDescriptor(opts: {
clauseId: string
clauseKind: string
subjectRef: number
referenceRef?: number
frameRef?: number
bounds?: { minGap?: number; maxGap?: number; tolerance?: number; min?: number; max?: number }
options?: Record<string, unknown>
}): ClauseDescriptor {
return {
clauseId: opts.clauseId,
clauseKind: opts.clauseKind,
version: 1,
subjectRef: opts.subjectRef,
referenceRef: opts.referenceRef,
frameRef: opts.frameRef,
bounds: opts.bounds ?? {},
options: opts.options ?? {},
}
}
// ---------------------------------------------------------------------------
// Solver Result Builder
// ---------------------------------------------------------------------------
export function buildMockSolverResult(opts: {
clauseResults?: ClauseResult[]
diagnostics?: Array<{
code: string
severity: 'error' | 'warning' | 'info'
category: string
message: string
clauseId?: string
}>
}): EvaluationOutput {
return {
clauseResults: opts.clauseResults ?? [],
groupResults: [],
proofs: [],
diagnostics: opts.diagnostics ?? [],
trace: [],
}
}
// ---------------------------------------------------------------------------
// Extraction Result Builder
// ---------------------------------------------------------------------------
export function buildMockExtractionResult(opts: {
requestId?: string
worlds: GeometryWorld[]
diagnostics?: CoreDiagnostic[]
}): {
requestId: string
status: 'ok' | 'partial' | 'error'
worlds: GeometryWorld[]
diagnostics: CoreDiagnostic[]
} {
return {
requestId: opts.requestId ?? 'req_1',
status: 'ok',
worlds: opts.worlds,
diagnostics: opts.diagnostics ?? [],
}
}
// ---------------------------------------------------------------------------
// Scene Builder
// ---------------------------------------------------------------------------
export interface MockScene {
world: GeometryWorld
clauses: ClauseDescriptor[]
}
export function createMockScene(opts: {
subjects: MockSubject[]
relations: Array<{
kind: string
subjectId: number
referenceId: number
bounds?: { minGap?: number; maxGap?: number }
}>
}): MockScene {
const world = buildMockGeometryWorld({ subjects: opts.subjects })
const clauses = opts.relations.map((r, i) =>
buildClauseDescriptor({
clauseId: `clause_${i + 1}`,
clauseKind: r.kind,
subjectRef: r.subjectId,
referenceRef: r.referenceId,
bounds: r.bounds,
}),
)
return { world, clauses }
}
// ---------------------------------------------------------------------------
// Semantic IR to Solver Clause Mapping
// ---------------------------------------------------------------------------
/**
* Map a compiled Semantic IR to solver ClauseDescriptors.
*
* This bridges the DSL compiler output to the solver evaluation input.
* The `subjectIdMap` translates semantic IR subject ids to world subject ids.
*/
export function mapSemanticIrToClauses(
semanticIr: SemanticIr,
subjectIdMap: Map<ImhotepId, number>,
): ClauseDescriptor[] {
const clauses: ClauseDescriptor[] = []
for (const [, clause] of semanticIr.clauses) {
const subjectRef = subjectIdMap.get(clause.subjectRef)
const referenceRef = clause.referenceRef
? subjectIdMap.get(clause.referenceRef)
: undefined
if (subjectRef === undefined) continue
const tolerance = semanticIr.tolerances.get(clause.toleranceRef)
const bounds: Record<string, number> = {}
if (clause.bounds.minGap) {
bounds.minGap = clause.bounds.minGap.value
}
if (clause.bounds.maxGap) {
bounds.maxGap = clause.bounds.maxGap.value
}
if (tolerance) {
bounds.tolerance = tolerance.value
}
clauses.push({
clauseId: clause.id,
clauseKind: `relation.${clause.relation}`,
version: 1,
subjectRef,
referenceRef,
bounds,
})
}
return clauses
}
// ---------------------------------------------------------------------------
// Snapshot Builder
// ---------------------------------------------------------------------------
export function buildMockSnapshot(opts: {
id?: string
stateKind?: string
stateSource?: string
selector?: string
world: GeometryWorld
}): GeometrySnapshot {
const metadata: SnapshotMetadata = {
snapshotId: opts.id ?? 'snap_1',
stateKind: (opts.stateKind as any) ?? 'default',
stateSource: (opts.stateSource as any) ?? 'synthetic',
selector: opts.selector,
timestamp: Date.now(),
}
return {
id: opts.id ?? 'snap_1',
metadata,
world: opts.world,
}
}
@@ -0,0 +1,328 @@
/**
* Reporter -> Diagnostics integration tests.
*
* Verifies that solver results produce correct diagnostics, that proof traces
* map to human-readable output, and that witness shrinking works end-to-end.
*/
import { describe, it } from 'node:test'
import assert from 'node:assert'
import {
makeDiagnostic,
diagnosticFromProof,
renderHumanReport,
renderJsonReport,
buildJsonReport,
shrinkWitness,
createTraceBuilder,
findClauseTraces,
formatDiagnosticCompact,
} from 'imhotep-reporter'
import type {
ProofLike,
Witness,
ShrinkResult,
} from 'imhotep-reporter'
import { buildMockSolverResult } from './integration-mocks.js'
describe('Reporter -> Diagnostics', () => {
it('solver results produce correct diagnostics from failing proofs', () => {
// Arrange: create a failing proof with predicate and witness
const proof: ProofLike = {
proofId: 'p1',
clauseId: 'c1',
outcome: 'fail',
truth: 'determinate',
failedPredicate: { op: '>=', left: 10, right: 24 },
witness: {
subjectId: 1,
referenceId: 2,
envCaseId: 'env_1',
snapshotId: 'default',
},
}
// Act: convert proof to diagnostic
const diagnostic = diagnosticFromProof(proof, {
idGen: () => 'd1',
codeForClause: () => 'IMH_RELATION_ABOVE_FAILED',
messageForClause: () => 'Expected .tooltip to be above .trigger',
fixHintsForClause: () => ['Increase vertical gap to at least 24px'],
})
// Assert
assert.ok(diagnostic)
assert.strictEqual(diagnostic!.code, 'IMH_RELATION_ABOVE_FAILED')
assert.strictEqual(diagnostic!.category, 'contract-failure')
assert.strictEqual(diagnostic!.severity, 'error')
assert.ok(
diagnostic!.related.some((rel) =>
rel.message.includes('Predicate'),
),
)
assert.ok(
diagnostic!.fixHints.some((hint) =>
hint.includes('gap'),
),
)
})
it('indeterminate proofs produce indeterminate-result diagnostics', () => {
// Arrange: create an indeterminate failing proof
const proof: ProofLike = {
proofId: 'p1',
clauseId: 'c1',
outcome: 'fail',
truth: 'indeterminate',
witness: { envCaseId: 'env_1' },
}
// Act
const diagnostic = diagnosticFromProof(proof, {
idGen: () => 'd1',
codeForClause: () => 'IMH_RELATION_ABOVE_FAILED',
messageForClause: () => 'Could not determine relation',
fixHintsForClause: () => [],
})
// Assert
assert.ok(diagnostic)
assert.strictEqual(diagnostic!.category, 'indeterminate-result')
})
it('passing proofs produce no diagnostics', () => {
// Arrange: create a passing proof
const proof: ProofLike = {
proofId: 'p1',
clauseId: 'c1',
outcome: 'pass',
truth: 'determinate',
}
// Act
const diagnostic = diagnosticFromProof(proof, {
idGen: () => 'd1',
codeForClause: () => 'IMH_RELATION_ABOVE_FAILED',
messageForClause: () => 'Should not be called',
fixHintsForClause: () => [],
})
// Assert
assert.strictEqual(diagnostic, null)
})
it('proof traces map to human-readable output', () => {
// Arrange: create diagnostic linked to a trace
const diagnostic = makeDiagnostic(
{
code: 'IMH_RELATION_LEFT_OF_FAILED',
category: 'contract-failure',
message: 'Button is not left of field',
traceRef: 't1',
position: {
start: { line: 3, column: 5, offset: 20 },
end: { line: 3, column: 40, offset: 55 },
},
},
{ idGen: () => 'd1' },
)
const traces = [
{
traceEventId: 't1',
phase: 'clause-evaluated' as const,
at: 1000,
refs: { clauseId: 'c1', diagnosticId: 'd1' },
payload: { status: 'fail' },
},
{
traceEventId: 't2',
phase: 'proof-created' as const,
at: 1001,
refs: { clauseId: 'c1', proofId: 'p1' },
payload: {},
},
]
// Act: render human report with traces
const report = renderHumanReport(
[diagnostic],
traces,
new Map(),
{ showTraces: true },
)
// Assert
assert.ok(report.includes('Button is not left of field'))
assert.ok(report.includes('clause-evaluated'))
assert.ok(report.includes('IMH_RELATION_LEFT_OF_FAILED'))
assert.ok(report.includes('line 3'))
})
it('trace builder emits events findable by clause id', () => {
// Arrange: create trace builder
const builder = createTraceBuilder({
idGen: () => 't1',
now: () => Date.now(),
})
// Act: emit events
builder.emit({
phase: 'clause-evaluated',
refs: { clauseId: 'c1' },
payload: { status: 'pass' },
})
builder.emit({
phase: 'clause-evaluated',
refs: { clauseId: 'c2' },
payload: { status: 'fail' },
})
builder.emit({
phase: 'proof-created',
refs: { clauseId: 'c1', proofId: 'p1' },
payload: {},
})
// Assert: findClauseTraces returns correct subset
const found = findClauseTraces(builder.events(), 'c1')
assert.strictEqual(found.length, 2)
assert.ok(
found.every((ev) => ev.refs.clauseId === 'c1'),
)
})
it('witness shrinking works end-to-end', () => {
// Arrange: create a witness with redundant env cases
const witness: Witness = {
proof: {
proofId: 'p1',
clauseId: 'c1',
outcome: 'fail',
truth: 'determinate',
},
envCases: ['env_1', 'env_2', 'env_3'],
snapshots: ['default', 'hover'],
subjects: [1, 2, 3],
facts: [100, 101, 102],
}
// Act: shrink witness — only env_2 and default snapshot are needed
const stillFails = (w: Witness) =>
w.envCases.includes('env_2') && w.snapshots.includes('default')
const result = shrinkWitness(witness, stillFails)
// Assert: witness was reduced
assert.strictEqual(result.reduced, true)
assert.deepStrictEqual(result.witness.envCases, ['env_2'])
assert.deepStrictEqual(result.witness.snapshots, ['default'])
assert.ok(result.axes.includes('env-case'))
assert.ok(result.axes.includes('snapshot'))
assert.ok(result.steps > 0)
})
it('shrink results render in JSON report', () => {
// Arrange: create a shrunk witness
const witness: Witness = {
proof: {
proofId: 'p1',
clauseId: 'c1',
outcome: 'fail',
truth: 'determinate',
},
envCases: ['env_1'],
snapshots: ['default'],
subjects: [1],
facts: [100],
}
const shrinkResult: ShrinkResult = {
reduced: true,
witness,
axes: ['env-case', 'subject'],
steps: 3,
}
const shrinkMap = new Map<string, ShrinkResult>()
shrinkMap.set('c1', shrinkResult)
const diagnostic = makeDiagnostic(
{
code: 'IMH_TEST',
category: 'contract-failure',
message: 'Fail',
clauseId: 'c1',
},
{ idGen: () => 'd1' },
)
// Act: build JSON report with shrink
const report = buildJsonReport(
[diagnostic],
[],
shrinkMap,
{ includeShrink: true },
)
// Assert
assert.strictEqual(report.summary.errorCount, 1)
assert.ok(report.shrinkResults)
assert.ok(report.shrinkResults!.c1)
assert.strictEqual(report.shrinkResults!.c1.reduced, true)
assert.ok(report.shrinkResults!.c1.axes.includes('env-case'))
// Act: stringify
const json = renderJsonReport(
[diagnostic],
[],
shrinkMap,
{ includeShrink: true, indent: 2 },
)
assert.ok(json.includes('shrinkResults'))
assert.ok(json.includes('env-case'))
})
it('solver diagnostics flow through to reporter formatting', () => {
// Arrange: create solver result with diagnostics
const solverResult = buildMockSolverResult({
clauseResults: [
{
clauseId: 'c1',
status: 'fail',
truth: 'determinate',
metrics: { observedGap: 4, minGap: 16 },
witness: { subjectId: 1, referenceId: 2 },
},
],
diagnostics: [
{
code: 'IMH_INTERNAL_UNKNOWN_CLAUSE_KIND',
severity: 'error',
category: 'internal-error',
message: 'Evaluation error',
},
],
})
// Act: convert solver diagnostics to reporter diagnostics
const diagnostics = solverResult.diagnostics.map((d) =>
makeDiagnostic(
{
code: d.code,
category: d.category as any,
message: d.message,
severity: d.severity as any,
},
{ idGen: () => 'd1' },
),
)
// Assert
assert.strictEqual(diagnostics.length, 1)
assert.strictEqual(diagnostics[0].code, 'IMH_INTERNAL_UNKNOWN_CLAUSE_KIND')
// Act: render compact
const compact = formatDiagnosticCompact(diagnostics[0])
assert.ok(compact.includes('IMH_INTERNAL_UNKNOWN_CLAUSE_KIND'))
})
})
@@ -0,0 +1,264 @@
/**
* State -> Solver integration tests.
*
* Verifies that state snapshots create distinct geometry worlds,
* that state comparison assertions evaluate correctly, and that
* transition sampling produces valid worlds.
*/
import { describe, it, before } from 'node:test'
import assert from 'node:assert'
import { evaluate, registerDefaultClauses } from 'imhotep-solver'
import type { GeometryWorld } from 'imhotep-solver'
import {
compareSnapshots,
createSnapshotStore,
} from 'imhotep-state'
import type { GeometrySnapshot } from 'imhotep-state'
import {
buildMockGeometryWorld,
buildMockSnapshot,
buildClauseDescriptor,
} from './integration-mocks.js'
describe('State -> Solver', () => {
before(() => {
registerDefaultClauses()
})
it('state snapshots create distinct geometry worlds', () => {
// Arrange: build two worlds that differ only in box height
const defaultWorld = buildMockGeometryWorld({
snapshotId: 'snap_default',
subjects: [
{
id: 0,
selector: '.btn',
box: { top: 0, bottom: 30, left: 0, right: 100 },
},
],
})
const hoverWorld = buildMockGeometryWorld({
snapshotId: 'snap_hover',
subjects: [
{
id: 0,
selector: '.btn',
box: { top: 0, bottom: 32, left: 0, right: 100 },
},
],
})
// Act: wrap as snapshots
const defaultSnap = buildMockSnapshot({
id: 'snap_default',
stateKind: 'default',
world: defaultWorld,
})
const hoverSnap = buildMockSnapshot({
id: 'snap_hover',
stateKind: 'hover',
world: hoverWorld,
})
// Assert: snapshots are distinct
const diff = compareSnapshots(defaultSnap, hoverSnap)
assert.strictEqual(diff.identical, false)
assert.ok(
diff.differences.some((d) => d.includes('world content differs')),
)
})
it('state comparison assertions evaluate correctly across snapshots', () => {
// Arrange: default and hover worlds with different sizes
const defaultWorld = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.btn',
box: { top: 0, bottom: 30, left: 0, right: 100 },
},
],
})
const hoverWorld = buildMockGeometryWorld({
subjects: [
{
id: 0,
selector: '.btn',
box: { top: 0, bottom: 32, left: 0, right: 100 },
},
],
})
// Act: evaluate size assertion (height >= 31) on both worlds
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'size.atLeast',
subjectRef: 0,
bounds: { min: 31 },
options: { dimension: 'height' },
}),
]
const defaultResult = evaluate(defaultWorld, clauses)
const hoverResult = evaluate(hoverWorld, clauses)
// Assert: default fails (30 < 31), hover passes (32 >= 31)
assert.strictEqual(defaultResult.clauseResults[0].status, 'fail')
assert.strictEqual(hoverResult.clauseResults[0].status, 'pass')
})
it('snapshot store maintains distinct worlds per state', () => {
// Arrange: create store and add multiple state snapshots
const store = createSnapshotStore()
const defaultWorld = buildMockGeometryWorld({
snapshotId: 'snap_default',
subjects: [
{
id: 0,
selector: '.input',
box: { top: 0, bottom: 20, left: 0, right: 200 },
},
],
})
const focusWorld = buildMockGeometryWorld({
snapshotId: 'snap_focus',
subjects: [
{
id: 0,
selector: '.input',
box: { top: 0, bottom: 22, left: 0, right: 200 },
},
],
})
store.add(
buildMockSnapshot({
id: 'snap_default',
stateKind: 'default',
world: defaultWorld,
}),
)
store.add(
buildMockSnapshot({
id: 'snap_focus',
stateKind: 'focus',
world: focusWorld,
}),
)
// Act: retrieve snapshots
const snapshots = store.list()
// Assert
assert.strictEqual(snapshots.length, 2)
assert.strictEqual(snapshots[0].metadata.stateKind, 'default')
assert.strictEqual(snapshots[1].metadata.stateKind, 'focus')
// Assert: worlds are distinct
const diff = compareSnapshots(snapshots[0], snapshots[1])
assert.strictEqual(diff.identical, false)
})
it('transition sampling produces valid worlds for solver', () => {
// Arrange: create a store to hold transition samples
const store = createSnapshotStore()
// Simulate 3 transition samples with increasing vertical offset
for (let i = 0; i < 3; i++) {
const world = buildMockGeometryWorld({
snapshotId: `snap_transition_${i}`,
subjects: [
{
id: 0,
selector: '.box',
box: {
top: i * 10,
bottom: i * 10 + 20,
left: 0,
right: 50,
},
},
],
})
const snap = buildMockSnapshot({
id: `snap_transition_${i}`,
stateKind: 'default',
stateSource: 'synthetic',
world,
})
store.add(snap)
}
// Act: retrieve and evaluate each snapshot
const snapshots = store.list()
assert.strictEqual(snapshots.length, 3)
for (const snap of snapshots) {
const world = snap.world as GeometryWorld
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'size.atLeast',
subjectRef: 0,
bounds: { min: 20 },
options: { dimension: 'height' },
}),
]
const result = evaluate(world, clauses)
assert.strictEqual(
result.clauseResults[0].status,
'pass',
`Sample ${snap.id} should pass height >= 20`,
)
}
})
it('state worlds carry correct metadata for diagnostics', () => {
// Arrange: build snapshot with full metadata
const world = buildMockGeometryWorld({
snapshotId: 'snap_hover',
subjects: [
{
id: 0,
selector: '.tooltip',
box: { top: 0, bottom: 10, left: 0, right: 50 },
},
],
})
const snap = buildMockSnapshot({
id: 'snap_hover',
stateKind: 'hover',
stateSource: 'interaction',
selector: '.tooltip',
world,
})
// Assert: metadata is preserved
assert.strictEqual(snap.metadata.stateKind, 'hover')
assert.strictEqual(snap.metadata.stateSource, 'interaction')
assert.strictEqual(snap.metadata.selector, '.tooltip')
assert.strictEqual(snap.id, 'snap_hover')
// Act: evaluate a relation on the state world
const clauses = [
buildClauseDescriptor({
clauseId: 'c1',
clauseKind: 'size.atLeast',
subjectRef: 0,
bounds: { min: 5 },
options: { dimension: 'height' },
}),
]
const result = evaluate(snap.world as GeometryWorld, clauses)
assert.strictEqual(result.clauseResults[0].status, 'pass')
})
})
+221
View File
@@ -0,0 +1,221 @@
/**
* Semantic IR and Execution IR definitions for Imhotep.
*
* Semantic IR:
* Normalized graph of explicit obligations, frames, selectors, states,
* environments, and tolerances. Stored as tables keyed by stable id.
*
* Execution IR:
* Flattened, data-oriented arrays optimized for bulk evaluation against
* a geometry world. This is the hot-path representation.
*/
import type {
ImhotepId,
SourceOrigin,
LengthLiteral,
Environment,
StateSnapshot,
TimelineSnapshot,
Frame,
Subject,
} from './types.js'
// ---------------------------------------------------------------------------
// Semantic IR Tables
// ---------------------------------------------------------------------------
export interface SemanticIr {
subjects: Map<ImhotepId, SemanticSubject>
frames: Map<ImhotepId, SemanticFrame>
states: Map<ImhotepId, SemanticState>
timelines: Map<ImhotepId, SemanticTimeline>
tolerances: Map<ImhotepId, SemanticTolerance>
envGuards: Map<ImhotepId, SemanticEnvGuard>
clauses: Map<ImhotepId, SemanticClause>
groups: Map<ImhotepId, SemanticGroup>
diagnosticMetadata: Map<ImhotepId, SemanticDiagnosticMeta>
}
export interface SemanticSubject {
id: ImhotepId
selector: string
kind: Subject['kind']
origin: SourceOrigin
}
export interface SemanticFrame {
id: ImhotepId
kind: Frame['kind']
selector?: string
name?: string
originX: number
originY: number
writingMode: Frame['writingMode']
origin: SourceOrigin
}
export interface SemanticState {
id: ImhotepId
kind: StateSnapshot['kind']
name?: string
origin: SourceOrigin
}
export interface SemanticTimeline {
id: ImhotepId
mode: TimelineSnapshot['mode']
samples?: number[]
origin: SourceOrigin
}
export interface SemanticTolerance {
id: ImhotepId
value: number
unit: 'jnd' | 'px'
origin: SourceOrigin
}
export interface SemanticEnvGuard {
id: ImhotepId
expression: string
normalizedCases: Environment[]
origin: SourceOrigin
}
export interface SemanticClause {
id: ImhotepId
origin: SourceOrigin
subjectRef: ImhotepId
referenceRef?: ImhotepId
relation: string
frameRef: ImhotepId
stateRef: ImhotepId
timelineRef: ImhotepId
envGuardRef: ImhotepId
toleranceRef: ImhotepId
bounds: {
minGap?: LengthLiteral
maxGap?: LengthLiteral
}
}
export interface SemanticGroup {
id: ImhotepId
origin: SourceOrigin
operator: 'and' | 'or' | 'not' | 'quantifier'
quantifier?: {
kind: string
count?: number
}
clauseRefs: ImhotepId[]
groupRefs?: ImhotepId[]
}
export interface SemanticDiagnosticMeta {
id: ImhotepId
clauseRef?: ImhotepId
messageTemplate: string
origin: SourceOrigin
}
// ---------------------------------------------------------------------------
// Execution IR Tables
// ---------------------------------------------------------------------------
/**
* Execution IR stores clause data in parallel typed arrays for bulk
* evaluation. Every array is indexed by clauseIndex.
*/
export interface ExecutionIr {
clauseCount: number
// Clause classification and cross-references (all Uint32Array)
clauseType: Uint16Array
clauseSubject: Uint32Array
clauseReference: Uint32Array
clauseFrame: Uint32Array
clauseState: Uint32Array
clauseTimeline: Uint32Array
clauseTolerance: Uint32Array
clauseEnvGuard: Uint32Array
// Numeric bounds (Float64Array)
clauseArg0: Float64Array
clauseArg1: Float64Array
// Flags and origin (Uint32Array)
clauseFlags: Uint32Array
clauseOrigin: Uint32Array
}
export interface ExecutionIrBuilder {
addClause(descriptor: ExecutionClauseDescriptor): number
build(): ExecutionIr
}
export interface ExecutionClauseDescriptor {
clauseType: number
subjectIndex: number
referenceIndex: number
frameIndex: number
stateIndex: number
timelineIndex: number
toleranceIndex: number
envGuardIndex: number
arg0: number
arg1: number
flags: number
originIndex: number
}
// ---------------------------------------------------------------------------
// IR Lowering Pipeline Stages
// ---------------------------------------------------------------------------
export type LoweringStage =
| 'parse'
| 'validate'
| 'normalize'
| 'resolve-defaults'
| 'compile'
| 'derive-facts'
export interface LoweringPipeline {
stages: LoweringStage[]
currentStage: LoweringStage
diagnostics: unknown[]
}
// ---------------------------------------------------------------------------
// Clause Family Registry
// ---------------------------------------------------------------------------
export interface ClauseFamilyDescriptor {
clauseKind: string
version: number
requiredFacts: string[]
}
export interface ClauseFamilyRegistry {
register(descriptor: ClauseFamilyDescriptor): void
lookup(clauseKind: string): ClauseFamilyDescriptor | undefined
}
// ---------------------------------------------------------------------------
// Helper: Create empty Semantic IR
// ---------------------------------------------------------------------------
export function createEmptySemanticIr(): SemanticIr {
return {
subjects: new Map(),
frames: new Map(),
states: new Map(),
timelines: new Map(),
tolerances: new Map(),
envGuards: new Map(),
clauses: new Map(),
groups: new Map(),
diagnosticMetadata: new Map(),
}
}
+185
View File
@@ -0,0 +1,185 @@
/**
* Pure first-order logic AST types for Imhotep V1.1.
*
* These nodes represent the deterministic scene logic layer:
* quantifiers, boolean connectives, predicate calls, and terms.
*
* All nodes follow the unist-style shape with source spans.
*/
import type { AstNode } from './ast.js'
import type { Position } from './types.js'
// ---------------------------------------------------------------------------
// Formula Union
// ---------------------------------------------------------------------------
export type FormulaNode =
| ForAllFormula
| ExistsFormula
| AndFormula
| OrFormula
| NotFormula
| ImpliesFormula
| PredicateCall
// ---------------------------------------------------------------------------
// Quantifier Formulas
// ---------------------------------------------------------------------------
export interface ForAllFormula extends AstNode {
type: 'FormulaNode'
kind: 'forall'
bindings: TupleBinding[]
body: FormulaNode
}
export interface ExistsFormula extends AstNode {
type: 'FormulaNode'
kind: 'exists'
bindings: TupleBinding[]
body: FormulaNode
}
// ---------------------------------------------------------------------------
// Boolean Connective Formulas
// ---------------------------------------------------------------------------
export interface AndFormula extends AstNode {
type: 'FormulaNode'
kind: 'and'
left: FormulaNode
right: FormulaNode
}
export interface OrFormula extends AstNode {
type: 'FormulaNode'
kind: 'or'
left: FormulaNode
right: FormulaNode
}
export interface NotFormula extends AstNode {
type: 'FormulaNode'
kind: 'not'
operand: FormulaNode
}
export interface ImpliesFormula extends AstNode {
type: 'FormulaNode'
kind: 'implies'
antecedent: FormulaNode
consequent: FormulaNode
}
// ---------------------------------------------------------------------------
// Atomic Formula: Predicate Call
// ---------------------------------------------------------------------------
export interface PredicateCall extends AstNode {
type: 'FormulaNode'
kind: 'predicate'
predicate: string
args: TermNode[]
}
// ---------------------------------------------------------------------------
// Terms
// ---------------------------------------------------------------------------
export type TermNode = VariableRef | DomainRef | AccessorTerm
export interface VariableRef extends AstNode {
type: 'VariableRef'
name: string
}
export interface DomainRef extends AstNode {
type: 'DomainRef'
domain: string
selector?: string
parentVar?: string
}
export interface AccessorTerm extends AstNode {
type: 'AccessorTerm'
variable: string
property: string
}
// ---------------------------------------------------------------------------
// Tuple Binding (for multi-variable quantification)
// ---------------------------------------------------------------------------
export interface TupleBinding extends AstNode {
type: 'TupleBinding'
variables: string[]
domain: DomainRef
}
// ---------------------------------------------------------------------------
// Property Run Blocks
// ---------------------------------------------------------------------------
export interface PropertyRunBlock extends AstNode {
type: 'PropertyRunBlock'
mode: 'sampled' | 'enumerated'
inputDomain: unknown
body: AstNode[]
}
export interface SampledRunBlock extends AstNode {
type: 'SampledRunBlock'
arbitrary: unknown
numRuns?: number
seed?: number
body: AstNode[]
}
// ---------------------------------------------------------------------------
// Type Guards
// ---------------------------------------------------------------------------
export function isForAllFormula(node: FormulaNode): node is ForAllFormula {
return node.kind === 'forall'
}
export function isExistsFormula(node: FormulaNode): node is ExistsFormula {
return node.kind === 'exists'
}
export function isAndFormula(node: FormulaNode): node is AndFormula {
return node.kind === 'and'
}
export function isOrFormula(node: FormulaNode): node is OrFormula {
return node.kind === 'or'
}
export function isNotFormula(node: FormulaNode): node is NotFormula {
return node.kind === 'not'
}
export function isImpliesFormula(node: FormulaNode): node is ImpliesFormula {
return node.kind === 'implies'
}
export function isPredicateCall(node: FormulaNode): node is PredicateCall {
return node.kind === 'predicate'
}
export function isVariableRef(node: AstNode): node is VariableRef {
return node.type === 'VariableRef'
}
export function isDomainRef(node: AstNode): node is DomainRef {
return node.type === 'DomainRef'
}
export function isAccessorTerm(node: AstNode): node is AccessorTerm {
return node.type === 'AccessorTerm'
}
export function isTupleBinding(node: AstNode): node is TupleBinding {
return node.type === 'TupleBinding'
}
+232
View File
@@ -0,0 +1,232 @@
/**
* Flattened, data-oriented execution IR for first-order logic formulas.
*
* The hot-path representation stores formulas in parallel typed arrays
* indexed by formulaId. No deep nesting exists at runtime; child
* relationships are expressed through numeric offsets into the same
* flat tables.
*
* Invariant: every formula referenced by a child pointer is stored in
* the same LogicIr instance. The root formula always has id 0.
*/
import type { ImhotepId, SourceOrigin } from './types.js'
// ---------------------------------------------------------------------------
// Formula Kinds (must fit in Uint8)
// ---------------------------------------------------------------------------
export const FORMULA_KIND_FORALL = 1
export const FORMULA_KIND_EXISTS = 2
export const FORMULA_KIND_AND = 3
export const FORMULA_KIND_OR = 4
export const FORMULA_KIND_NOT = 5
export const FORMULA_KIND_IMPLIES = 6
export const FORMULA_KIND_PREDICATE = 7
export type FormulaKind =
| typeof FORMULA_KIND_FORALL
| typeof FORMULA_KIND_EXISTS
| typeof FORMULA_KIND_AND
| typeof FORMULA_KIND_OR
| typeof FORMULA_KIND_NOT
| typeof FORMULA_KIND_IMPLIES
| typeof FORMULA_KIND_PREDICATE
// ---------------------------------------------------------------------------
// Logic IR Tables
// ---------------------------------------------------------------------------
export interface LogicIr {
/** Total number of formulas in this IR. */
formulaCount: number
// Formula classification (all Uint8Array)
formulaKind: Uint8Array
// Child pointers (Uint32Array, 0xFFFFFFFF means "none")
formulaLeft: Uint32Array
formulaRight: Uint32Array
// Binding region (for quantifiers)
// Each quantifier formula references a slice [bindingStart, bindingStart + bindingCount)
// in the flat binding table below.
bindingStart: Uint32Array
bindingCount: Uint8Array
// Predicate call region (for atomic formulas)
// Each predicate formula references a slice [argStart, argStart + argCount)
predicateId: Uint32Array
argStart: Uint32Array
argCount: Uint8Array
// Origin and proof tracking (Uint32Array)
originIndex: Uint32Array
}
// ---------------------------------------------------------------------------
// Flat Binding Table
// ---------------------------------------------------------------------------
export interface BindingTable {
/** Number of bindings. */
count: number
// Variable names (string table index)
variableNameId: Uint32Array
// Domain descriptor index
domainId: Uint32Array
}
// ---------------------------------------------------------------------------
// Flat Argument Table (for predicate calls)
// ---------------------------------------------------------------------------
export interface ArgTable {
/** Number of argument terms. */
count: number
// Term kind: 1 = variable, 2 = accessor, 3 = literal number
termKind: Uint8Array
// For variables: binding table index
// For accessors: variable binding index
// For literals: 0xFFFFFFFF (value stored in literalValue)
termRef: Uint32Array
// For accessors: string table index of property name
// For literals: the literal numeric value (reinterpreted as Uint32)
termAux: Uint32Array
}
// ---------------------------------------------------------------------------
// Predicate Registry Index
// ---------------------------------------------------------------------------
export interface PredicateIndexEntry {
predicateId: number
name: string
arity: number
domainSignature: number[]
requiredFacts: string[]
}
export interface PredicateIndex {
entries: PredicateIndexEntry[]
byName: Map<string, number>
}
// ---------------------------------------------------------------------------
// String Table (shared across IR tables)
// ---------------------------------------------------------------------------
export interface IrStringTable {
values: string[]
byValue: Map<string, number>
}
// ---------------------------------------------------------------------------
// Evaluation Request
// ---------------------------------------------------------------------------
export interface LogicEvaluationRequest {
logicIr: LogicIr
bindings: BindingTable
args: ArgTable
predicates: PredicateIndex
strings: IrStringTable
rootFormulaId: number
}
// ---------------------------------------------------------------------------
// Builder
// ---------------------------------------------------------------------------
export interface LogicIrBuilder {
addFormula(descriptor: LogicFormulaDescriptor): number
addBinding(descriptor: LogicBindingDescriptor): number
addArg(descriptor: LogicArgDescriptor): number
build(): LogicIr
}
export interface LogicFormulaDescriptor {
kind: FormulaKind
left?: number
right?: number
bindingStart?: number
bindingCount?: number
predicateId?: number
argStart?: number
argCount?: number
originIndex?: number
}
export interface LogicBindingDescriptor {
variableNameId: number
domainId: number
}
export interface LogicArgDescriptor {
termKind: number
termRef: number
termAux: number
}
// ---------------------------------------------------------------------------
// Helper: Create empty Logic IR
// ---------------------------------------------------------------------------
export function createEmptyLogicIr(): LogicIr {
return {
formulaCount: 0,
formulaKind: new Uint8Array(0),
formulaLeft: new Uint32Array(0),
formulaRight: new Uint32Array(0),
bindingStart: new Uint32Array(0),
bindingCount: new Uint8Array(0),
predicateId: new Uint32Array(0),
argStart: new Uint32Array(0),
argCount: new Uint8Array(0),
originIndex: new Uint32Array(0),
}
}
export function createEmptyBindingTable(): BindingTable {
return {
count: 0,
variableNameId: new Uint32Array(0),
domainId: new Uint32Array(0),
}
}
export function createEmptyArgTable(): ArgTable {
return {
count: 0,
termKind: new Uint8Array(0),
termRef: new Uint32Array(0),
termAux: new Uint32Array(0),
}
}
export function createEmptyStringTable(): IrStringTable {
return {
values: [],
byValue: new Map(),
}
}
// ---------------------------------------------------------------------------
// Helper: Intern a string into the string table
// ---------------------------------------------------------------------------
export function internString(table: IrStringTable, value: string): number {
const existing = table.byValue.get(value)
if (existing !== undefined) {
return existing
}
const id = table.values.length
table.values.push(value)
table.byValue.set(value, id)
return id
}
+331
View File
@@ -0,0 +1,331 @@
/**
* Unit tests for Imhotep logic AST nodes, IR structure, and domain
* definitions.
*
* Covers Stream 1 contracts:
* - Formula node construction and type guards
* - Logic IR flat array layout
* - Domain descriptor and value factories
*/
import { describe, it } from 'node:test'
import assert from 'node:assert'
import type {
FormulaNode,
ForAllFormula,
ExistsFormula,
AndFormula,
OrFormula,
NotFormula,
ImpliesFormula,
PredicateCall,
VariableRef,
DomainRef,
AccessorTerm,
TupleBinding,
PropertyRunBlock,
SampledRunBlock,
} from './logic-ast.js'
import {
isForAllFormula,
isExistsFormula,
isAndFormula,
isOrFormula,
isNotFormula,
isImpliesFormula,
isPredicateCall,
isVariableRef,
isDomainRef,
isAccessorTerm,
isTupleBinding,
} from './logic-ast.js'
import {
createEmptyLogicIr,
createEmptyBindingTable,
createEmptyArgTable,
createEmptyStringTable,
internString,
FORMULA_KIND_FORALL,
FORMULA_KIND_PREDICATE,
} from './logic-ir.js'
import {
createElementsDomain,
createDescendantsDomain,
createLineBoxesDomain,
createTextRunsDomain,
createFragmentsDomain,
createFramesDomain,
createDomainValue,
registerDomain,
lookupDomain,
listDomains,
clearDomainRegistry,
} from './domains.js'
// ---------------------------------------------------------------------------
// Formula AST
// ---------------------------------------------------------------------------
describe('Logic AST', () => {
it('constructs a ForAllFormula', () => {
const formula: ForAllFormula = {
type: 'FormulaNode',
kind: 'forall',
bindings: [
{
type: 'TupleBinding',
variables: ['btn'],
domain: {
type: 'DomainRef',
domain: 'elements',
selector: '.button',
},
},
],
body: {
type: 'FormulaNode',
kind: 'predicate',
predicate: 'atLeast',
args: [
{ type: 'VariableRef', name: 'btn' },
],
},
}
assert.strictEqual(formula.kind, 'forall')
assert.strictEqual(formula.bindings.length, 1)
assert.strictEqual(formula.bindings[0].variables[0], 'btn')
assert.ok(isForAllFormula(formula))
assert.strictEqual(isExistsFormula(formula), false)
})
it('constructs an ExistsFormula', () => {
const formula: ExistsFormula = {
type: 'FormulaNode',
kind: 'exists',
bindings: [
{
type: 'TupleBinding',
variables: ['icon'],
domain: {
type: 'DomainRef',
domain: 'descendants',
parentVar: 'card',
selector: '.icon',
},
},
],
body: {
type: 'FormulaNode',
kind: 'predicate',
predicate: 'above',
args: [
{ type: 'VariableRef', name: 'icon' },
{ type: 'VariableRef', name: 'content' },
],
},
}
assert.ok(isExistsFormula(formula))
assert.strictEqual(formula.bindings[0].domain.domain, 'descendants')
})
it('constructs boolean connectives', () => {
const p: PredicateCall = {
type: 'FormulaNode',
kind: 'predicate',
predicate: 'width',
args: [{ type: 'VariableRef', name: 'x' }],
}
const q: PredicateCall = {
type: 'FormulaNode',
kind: 'predicate',
predicate: 'height',
args: [{ type: 'VariableRef', name: 'x' }],
}
const andF: AndFormula = {
type: 'FormulaNode',
kind: 'and',
left: p,
right: q,
}
const orF: OrFormula = {
type: 'FormulaNode',
kind: 'or',
left: p,
right: q,
}
const notF: NotFormula = {
type: 'FormulaNode',
kind: 'not',
operand: p,
}
const impliesF: ImpliesFormula = {
type: 'FormulaNode',
kind: 'implies',
antecedent: p,
consequent: q,
}
assert.ok(isAndFormula(andF))
assert.ok(isOrFormula(orF))
assert.ok(isNotFormula(notF))
assert.ok(isImpliesFormula(impliesF))
assert.ok(isPredicateCall(p))
})
it('type guards narrow correctly', () => {
const varRef: VariableRef = { type: 'VariableRef', name: 'btn' }
const domRef: DomainRef = { type: 'DomainRef', domain: 'elements', selector: '.btn' }
const accTerm: AccessorTerm = { type: 'AccessorTerm', variable: 'btn', property: 'width' }
assert.ok(isVariableRef(varRef))
assert.ok(isDomainRef(domRef))
assert.ok(isAccessorTerm(accTerm))
assert.strictEqual(isVariableRef(domRef), false)
})
it('constructs PropertyRunBlock and SampledRunBlock', () => {
const propBlock: PropertyRunBlock = {
type: 'PropertyRunBlock',
mode: 'enumerated',
inputDomain: { size: ['sm', 'md', 'lg'] },
body: [],
}
const sampledBlock: SampledRunBlock = {
type: 'SampledRunBlock',
arbitrary: {},
numRuns: 100,
seed: 42,
body: [],
}
assert.strictEqual(propBlock.mode, 'enumerated')
assert.strictEqual(sampledBlock.numRuns, 100)
})
})
// ---------------------------------------------------------------------------
// Logic IR
// ---------------------------------------------------------------------------
describe('Logic IR', () => {
it('creates empty Logic IR with zero-length arrays', () => {
const ir = createEmptyLogicIr()
assert.strictEqual(ir.formulaCount, 0)
assert.strictEqual(ir.formulaKind.length, 0)
assert.strictEqual(ir.formulaLeft.length, 0)
assert.strictEqual(ir.formulaRight.length, 0)
assert.strictEqual(ir.bindingStart.length, 0)
assert.strictEqual(ir.bindingCount.length, 0)
assert.strictEqual(ir.predicateId.length, 0)
assert.strictEqual(ir.argStart.length, 0)
assert.strictEqual(ir.argCount.length, 0)
assert.strictEqual(ir.originIndex.length, 0)
})
it('creates empty binding and arg tables', () => {
const bindings = createEmptyBindingTable()
assert.strictEqual(bindings.count, 0)
assert.strictEqual(bindings.variableNameId.length, 0)
const args = createEmptyArgTable()
assert.strictEqual(args.count, 0)
assert.strictEqual(args.termKind.length, 0)
})
it('interns strings into the string table', () => {
const table = createEmptyStringTable()
const id1 = internString(table, 'width')
const id2 = internString(table, 'height')
const id3 = internString(table, 'width')
assert.strictEqual(id1, 0)
assert.strictEqual(id2, 1)
assert.strictEqual(id3, 0)
assert.strictEqual(table.values.length, 2)
assert.strictEqual(table.values[id1], 'width')
})
it('exposes formula kind constants', () => {
assert.strictEqual(FORMULA_KIND_FORALL, 1)
assert.strictEqual(FORMULA_KIND_PREDICATE, 7)
})
})
// ---------------------------------------------------------------------------
// Domains
// ---------------------------------------------------------------------------
describe('Domains', () => {
it('creates elements domain descriptor', () => {
const d = createElementsDomain('dom_1', '.button')
assert.strictEqual(d.domainId, 'dom_1')
assert.strictEqual(d.kind, 'elements')
assert.strictEqual(d.selector, '.button')
})
it('creates descendants domain descriptor', () => {
const d = createDescendantsDomain('dom_2', '$card', '.title')
assert.strictEqual(d.kind, 'descendants')
assert.strictEqual(d.parentVar, '$card')
assert.strictEqual(d.selector, '.title')
})
it('creates line boxes domain descriptor', () => {
const d = createLineBoxesDomain('dom_3', 'text_1')
assert.strictEqual(d.kind, 'lineBoxes')
assert.strictEqual(d.selector, 'text_1')
})
it('creates text runs domain descriptor', () => {
const d = createTextRunsDomain('dom_4', 'text_1')
assert.strictEqual(d.kind, 'textRuns')
})
it('creates fragments domain descriptor', () => {
const d = createFragmentsDomain('dom_5', 'subj_1')
assert.strictEqual(d.kind, 'fragments')
})
it('creates frames domain descriptor', () => {
const d = createFramesDomain('dom_6', 'viewport')
assert.strictEqual(d.kind, 'frames')
assert.strictEqual(d.selector, 'viewport')
})
it('creates domain value with subject ids', () => {
const ids = new Uint32Array([10, 20, 30])
const dv = createDomainValue('dom_1', ids, 'elements(.button)', true)
assert.strictEqual(dv.domainId, 'dom_1')
assert.strictEqual(dv.subjectIds.length, 3)
assert.strictEqual(dv.closed, true)
assert.strictEqual(dv.provenance, 'elements(.button)')
})
it('registers and looks up domains', () => {
clearDomainRegistry()
const d = createElementsDomain('dom_test', '.test')
registerDomain(d)
const lookedUp = lookupDomain('dom_test')
assert.ok(lookedUp)
assert.strictEqual(lookedUp?.selector, '.test')
const all = listDomains()
assert.strictEqual(all.length, 1)
clearDomainRegistry()
assert.strictEqual(listDomains().length, 0)
})
})
+379
View File
@@ -0,0 +1,379 @@
// perf.test.ts - Performance regression tests for Imhotep core hot paths
// Uses Node.js built-in test runner and performance hooks.
// Targets (V1 developer guide):
// Unit tests < 10ms each
// Integration tests < 100ms each
// Compilation < 5ms per assertion
// Extraction < 50ms per page
// Solving < 1ms per clause
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { performance } from 'node:perf_hooks'
import { compile } from 'imhotep-dsl'
import {
normalizeSubjects,
normalizeBoxes,
normalizeTopology,
normalizeFrames,
buildTransformChain,
composeTransform2D,
clearTransformCaches,
} from 'imhotep-geometry'
import { evaluate, registerDefaultClauses } from 'imhotep-solver'
describe('Performance regression — compilation', () => {
it('compiles 100 assertions in under 500ms', () => {
const assertions = generateAssertions(100)
const start = performance.now()
const ir = compile(assertions)
const duration = performance.now() - start
assert.strictEqual(
duration < 500,
true,
`Compilation took ${duration.toFixed(2)}ms`
)
})
it('compiles 10 assertions in under 50ms (5ms per assertion)', () => {
const assertions = generateAssertions(10)
const start = performance.now()
compile(assertions)
const duration = performance.now() - start
assert.strictEqual(
duration < 50,
true,
`Compilation took ${duration.toFixed(2)}ms`
)
})
})
describe('Performance regression — world creation', () => {
it('normalizes 1000 subjects in under 50ms', () => {
const raw = generateRawSubjects(1000)
const start = performance.now()
normalizeSubjects(raw)
const duration = performance.now() - start
assert.strictEqual(
duration < 50,
true,
`Subject normalization took ${duration.toFixed(2)}ms`
)
})
it('normalizes 1000 boxes in under 50ms', () => {
const raw = generateRawBoxes(1000)
const start = performance.now()
normalizeBoxes(raw)
const duration = performance.now() - start
assert.strictEqual(
duration < 50,
true,
`Box normalization took ${duration.toFixed(2)}ms`
)
})
it('normalizes topology in under 25ms', () => {
const raw = generateRawTopology(1000)
const start = performance.now()
normalizeTopology(raw, 1000)
const duration = performance.now() - start
assert.strictEqual(
duration < 25,
true,
`Topology normalization took ${duration.toFixed(2)}ms`
)
})
})
describe('Performance regression — frame resolution', () => {
it('resolves 100 transform chains in under 50ms', () => {
const matrices = generateTransformMatrices()
const start = performance.now()
for (let i = 0; i < 100; i++) {
buildTransformChain(matrices)
}
const duration = performance.now() - start
assert.strictEqual(
duration < 50,
true,
`Frame resolution took ${duration.toFixed(2)}ms`
)
})
it('composes 100 2D transforms in under 100ms', () => {
const start = performance.now()
for (let i = 0; i < 100; i++) {
composeTransform2D(0, 0, i, i, 0, 1, 1)
}
const duration = performance.now() - start
assert.strictEqual(
duration < 100,
true,
`Transform composition took ${duration.toFixed(2)}ms`
)
})
it('caches repeated transform chains (second pass under 1ms)', () => {
const matrices = generateTransformMatrices()
clearTransformCaches()
buildTransformChain(matrices) // warm cache
const start = performance.now()
for (let i = 0; i < 100; i++) {
buildTransformChain(matrices)
}
const duration = performance.now() - start
assert.strictEqual(
duration < 1,
true,
`Cached frame resolution took ${duration.toFixed(2)}ms`
)
})
})
describe('Performance regression — solving', () => {
it('evaluates 100 clauses in under 100ms (1ms per clause)', () => {
registerDefaultClauses()
const world = generateMinimalWorld()
const clauses = generateClauses(100)
const start = performance.now()
evaluate(world, clauses, { skipMissingEvaluators: true })
const duration = performance.now() - start
assert.strictEqual(
duration < 100,
true,
`Solving took ${duration.toFixed(2)}ms`
)
})
it('evaluates 10 clauses in under 10ms', () => {
registerDefaultClauses()
const world = generateMinimalWorld()
const clauses = generateClauses(10)
const start = performance.now()
evaluate(world, clauses, { skipMissingEvaluators: true })
const duration = performance.now() - start
assert.strictEqual(
duration < 10,
true,
`Solving took ${duration.toFixed(2)}ms`
)
})
})
// ---------------------------------------------------------------------------
// Mock data generators
// ---------------------------------------------------------------------------
function generateAssertions(n: number): Array<{
type: 'RelationAssertion'
relation: string
subject: { type: 'SelectorRef'; value: string }
reference: { type: 'SelectorRef'; value: string }
options: { type: 'RelationOptions' }
}> {
return Array.from({ length: n }, (_, i) => ({
type: 'RelationAssertion' as const,
relation: i % 2 === 0 ? 'leftOf' : 'above',
subject: { type: 'SelectorRef' as const, value: `.s-${i}` },
reference: { type: 'SelectorRef' as const, value: `.r-${i}` },
options: { type: 'RelationOptions' as const },
}))
}
function generateRawSubjects(n: number): Array<{
id: number
domNodeId: number
kind: string
primaryBoxId: number
firstFragmentId: number
fragmentCount: number
firstTextRunId: number
textRunCount: number
}> {
return Array.from({ length: n }, (_, i) => ({
id: i,
domNodeId: i,
kind: 'element',
primaryBoxId: i,
firstFragmentId: 0,
fragmentCount: 0,
firstTextRunId: 0,
textRunCount: 0,
}))
}
function generateRawBoxes(n: number): Array<{
id: number
subjectId: number
frameId: number
borderLeft: number
borderTop: number
borderRight: number
borderBottom: number
paddingLeft: number
paddingTop: number
paddingRight: number
paddingBottom: number
contentLeft: number
contentTop: number
contentRight: number
contentBottom: number
}> {
return Array.from({ length: n }, (_, i) => ({
id: i,
subjectId: i,
frameId: 0,
borderLeft: 0,
borderTop: 0,
borderRight: 10,
borderBottom: 10,
paddingLeft: 0,
paddingTop: 0,
paddingRight: 0,
paddingBottom: 0,
contentLeft: 0,
contentTop: 0,
contentRight: 10,
contentBottom: 10,
}))
}
function generateRawTopology(n: number): {
containingBlockOf: number[]
nearestPositionedAncestorOf: number[]
scrollContainerOf: number[]
stackingContextOf: number[]
formattingContextOf: number[]
clippingRootOf: number[]
paintOrderBucket: number[]
paintOrderIndex: number[]
} {
return {
containingBlockOf: Array.from({ length: n }, () => 0),
nearestPositionedAncestorOf: Array.from({ length: n }, () => 0),
scrollContainerOf: Array.from({ length: n }, () => 0),
stackingContextOf: Array.from({ length: n }, () => 0),
formattingContextOf: Array.from({ length: n }, () => 0),
clippingRootOf: Array.from({ length: n }, () => 0),
paintOrderBucket: Array.from({ length: n }, () => 0),
paintOrderIndex: Array.from({ length: n }, () => 0),
}
}
function generateTransformMatrices(): Float64Array[] {
return [
new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 10, 0, 1]),
]
}
function generateMinimalWorld(): any {
return {
sceneId: 'scene_1',
snapshotId: 'snapshot_1',
env: {
viewportWidth: 1280,
viewportHeight: 720,
deviceScaleFactor: 1,
colorScheme: 'light',
pointer: 'fine',
hover: false,
reducedMotion: false,
locale: 'en',
writingMode: 'horizontal-tb',
},
strings: { values: [] },
subjects: {
ids: new Uint32Array([0, 1]),
domNodeId: new Uint32Array([0, 1]),
subjectKind: new Uint16Array([1, 1]),
primaryBoxId: new Uint32Array([0, 1]),
firstFragmentId: new Uint32Array([0, 0]),
fragmentCount: new Uint16Array([0, 0]),
},
boxes: {
boxId: new Uint32Array([0, 1]),
subjectId: new Uint32Array([0, 1]),
frameId: new Uint32Array([0, 0]),
borderLeft: new Float64Array([0, 0]),
borderTop: new Float64Array([0, 0]),
borderRight: new Float64Array([10, 10]),
borderBottom: new Float64Array([10, 10]),
paddingLeft: new Float64Array([0, 0]),
paddingTop: new Float64Array([0, 0]),
paddingRight: new Float64Array([0, 0]),
paddingBottom: new Float64Array([0, 0]),
contentLeft: new Float64Array([0, 0]),
contentTop: new Float64Array([0, 0]),
contentRight: new Float64Array([10, 10]),
contentBottom: new Float64Array([10, 10]),
},
rects: {
rectId: new Uint32Array([0, 1]),
left: new Float64Array([0, 5]),
top: new Float64Array([0, 5]),
right: new Float64Array([10, 15]),
bottom: new Float64Array([10, 15]),
},
topology: {
containingBlockOf: new Uint32Array([0, 0]),
nearestPositionedAncestorOf: new Uint32Array([0, 0]),
scrollContainerOf: new Uint32Array([0, 0]),
stackingContextOf: new Uint32Array([0, 0]),
formattingContextOf: new Uint32Array([0, 0]),
clippingRootOf: new Uint32Array([0, 0]),
paintOrderBucket: new Uint16Array([0, 0]),
paintOrderIndex: new Uint32Array([0, 1]),
},
scroll: {
containerId: new Uint32Array([0]),
scrollLeft: new Float64Array([0]),
scrollTop: new Float64Array([0]),
scrollWidth: new Float64Array([1280]),
scrollHeight: new Float64Array([720]),
clientWidth: new Float64Array([1280]),
clientHeight: new Float64Array([720]),
},
clipping: {
clipNodeId: new Uint32Array([0]),
subjectId: new Uint32Array([0]),
clipKind: new Uint16Array([1]),
clipLeft: new Float64Array([0]),
clipTop: new Float64Array([0]),
clipRight: new Float64Array([1280]),
clipBottom: new Float64Array([720]),
parentClipNodeId: new Uint32Array([0]),
},
visibility: {
subjectId: new Uint32Array([0, 1]),
isRendered: new Uint8Array([1, 1]),
isVisible: new Uint8Array([1, 1]),
visibleArea: new Float64Array([100, 100]),
clippedArea: new Float64Array([0, 0]),
},
}
}
function generateClauses(n: number): Array<{
clauseId: string
clauseKind: string
version: number
subjectRef: number
referenceRef?: number
}> {
const kinds = [
'relation.leftOf',
'relation.above',
'relation.overlaps',
'relation.inside',
]
return Array.from({ length: n }, (_, i) => ({
clauseId: `clause-${i}`,
clauseKind: kinds[i % kinds.length],
version: 1,
subjectRef: 0,
referenceRef: 1,
}))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
/**
* Property runner input domain contracts.
*
* These types define how render inputs are supplied to a property run:
* either as an explicit finite enumeration (determinate) or as a
* generated sampled domain (sampled). The mode is always explicit.
*
* Invariant 6: every InputDomain carries its mode at the type level so
* downstream code cannot silently switch from enumerated to generated.
*/
// ---------------------------------------------------------------------------
// InputDomain
// ---------------------------------------------------------------------------
export type InputDomain =
| { mode: 'enumerated'; values: unknown[] }
| { mode: 'generated'; arbitrary: unknown; seed?: number; numRuns?: number }
// ---------------------------------------------------------------------------
// Type Guards
// ---------------------------------------------------------------------------
export function isEnumeratedDomain(domain: InputDomain): domain is Extract<InputDomain, { mode: 'enumerated' }> {
return domain.mode === 'enumerated'
}
export function isGeneratedDomain(domain: InputDomain): domain is Extract<InputDomain, { mode: 'generated' }> {
return domain.mode === 'generated'
}
// ---------------------------------------------------------------------------
// Factory Helpers
// ---------------------------------------------------------------------------
export function enumeratedDomain(values: unknown[]): InputDomain {
return { mode: 'enumerated', values }
}
export function generatedDomain(arbitrary: unknown, options?: { seed?: number; numRuns?: number }): InputDomain {
return { mode: 'generated', arbitrary, seed: options?.seed, numRuns: options?.numRuns }
}
@@ -0,0 +1,85 @@
/**
* Property run result contracts.
*
* These interfaces materialize Invariant 6: every result must clearly say
* whether it is scene-determinate, scene-indeterminate, enumerated-determinate,
* or sampled. The mode field is a literal discriminant so switch-exhaustiveness
* checking works.
*/
import type { ImhotepId } from './types.js'
// ---------------------------------------------------------------------------
// SampledRunResult
// ---------------------------------------------------------------------------
export interface SampledRunResult {
mode: 'sampled'
seed: number
numRuns: number
passed: boolean
counterexample?: unknown
shrunkCounterexample?: unknown
failingScene?: unknown
diagnostics: unknown[]
// P1.3 Property-Run Reporting Upgrade
minimalFailingCase?: unknown
caseIndex?: number
durationMs?: number
replayPayload?: { props: unknown; seed: number; caseIndex: number }
}
// ---------------------------------------------------------------------------
// EnumeratedRunResult
// ---------------------------------------------------------------------------
export interface EnumeratedRunResult {
mode: 'enumerated-determinate'
totalCases: number
passed: boolean
failingCase?: unknown
diagnostics: unknown[]
// P1.3 Property-Run Reporting Upgrade
minimalFailingCase?: unknown
caseIndex?: number
seed?: number
durationMs?: number
replayPayload?: { props: unknown; seed: number; caseIndex: number }
}
// ---------------------------------------------------------------------------
// Scene Evaluation Results (for completeness in the taxonomy)
// ---------------------------------------------------------------------------
export interface DeterministicSceneResult {
mode: 'scene-determinate' | 'scene-indeterminate'
sceneId: ImhotepId
results: unknown[]
proofs: unknown[]
diagnostics: unknown[]
}
// ---------------------------------------------------------------------------
// Union type for any property or scene run result
// ---------------------------------------------------------------------------
export type PropertyRunResult =
| SampledRunResult
| EnumeratedRunResult
| DeterministicSceneResult
// ---------------------------------------------------------------------------
// Type Guards
// ---------------------------------------------------------------------------
export function isSampledRunResult(result: PropertyRunResult): result is SampledRunResult {
return result.mode === 'sampled'
}
export function isEnumeratedRunResult(result: PropertyRunResult): result is EnumeratedRunResult {
return result.mode === 'enumerated-determinate'
}
export function isDeterministicSceneResult(result: PropertyRunResult): result is DeterministicSceneResult {
return result.mode === 'scene-determinate' || result.mode === 'scene-indeterminate'
}
@@ -0,0 +1,223 @@
/**
* Unit tests for SceneTarget discriminated union, type guards, and matchers.
*/
import { describe, it } from 'node:test'
import assert from 'node:assert'
import type { SceneTarget, RenderCase } from './scene-target.js'
import {
isSceneTarget,
isPageTarget,
isPlaywrightPageTarget,
isStorybookStoryTarget,
isReactComponentTarget,
isVueComponentTarget,
isCustomRendererTarget,
isFixtureTarget,
isComponentTarget,
isRendererTarget,
matchSceneTarget,
pageTarget,
playwrightPageTarget,
storybookStoryTarget,
reactComponentTarget,
vueComponentTarget,
customRendererTarget,
fixtureTarget,
createRenderCase,
} from './scene-target.js'
describe('SceneTarget type guards', () => {
it('accepts valid page target', () => {
const target = pageTarget('https://example.com')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isPageTarget(target), true)
assert.strictEqual(isPlaywrightPageTarget(target), false)
assert.strictEqual(isComponentTarget(target), false)
assert.strictEqual(isRendererTarget(target), false)
})
it('accepts valid playwright-page target', () => {
const target = playwrightPageTarget('page-1', 'https://example.com')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isPlaywrightPageTarget(target), true)
assert.strictEqual(isPageTarget(target), false)
})
it('accepts valid storybook-story target', () => {
const target = storybookStoryTarget('button--primary', 'http://localhost:6006')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isStorybookStoryTarget(target), true)
assert.strictEqual(isRendererTarget(target), true)
assert.strictEqual(isComponentTarget(target), false)
})
it('accepts valid react-component target', () => {
const target = reactComponentTarget('react-1', 'Button')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isReactComponentTarget(target), true)
assert.strictEqual(isComponentTarget(target), true)
assert.strictEqual(isRendererTarget(target), true)
})
it('accepts valid vue-component target', () => {
const target = vueComponentTarget('vue-1', 'Button')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isVueComponentTarget(target), true)
assert.strictEqual(isComponentTarget(target), true)
})
it('accepts valid custom-renderer target', () => {
const target = customRendererTarget('custom-1', 'my-widget')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isCustomRendererTarget(target), true)
assert.strictEqual(isComponentTarget(target), true)
})
it('accepts valid fixture target', () => {
const target = fixtureTarget('fixtures/button.html')
assert.strictEqual(isSceneTarget(target), true)
assert.strictEqual(isFixtureTarget(target), true)
assert.strictEqual(isRendererTarget(target), false)
})
it('rejects non-objects', () => {
assert.strictEqual(isSceneTarget(null), false)
assert.strictEqual(isSceneTarget(undefined), false)
assert.strictEqual(isSceneTarget('string'), false)
assert.strictEqual(isSceneTarget(42), false)
})
it('rejects objects without valid kind', () => {
assert.strictEqual(isSceneTarget({}), false)
assert.strictEqual(isSceneTarget({ kind: 'unknown' }), false)
})
it('rejects page target with missing url', () => {
assert.strictEqual(isSceneTarget({ kind: 'page' }), false)
})
})
describe('SceneTarget matchers', () => {
it('matches page target to correct branch', () => {
const target = pageTarget('https://example.com')
const result = matchSceneTarget<string>(target, {
page: (url) => `page:${url}`,
'playwright-page': (pageRef) => `playwright-page:${pageRef}`,
'storybook-story': (storyId) => `storybook-story:${storyId}`,
'react-component': (rendererId, componentId) => `react-component:${rendererId}:${componentId}`,
'vue-component': (rendererId, componentId) => `vue-component:${rendererId}:${componentId}`,
'custom-renderer': (rendererId, targetId) => `custom-renderer:${rendererId}:${targetId}`,
fixture: (fixtureId) => `fixture:${fixtureId}`,
})
assert.strictEqual(result, 'page:https://example.com')
})
it('matches react-component target to correct branch', () => {
const target = reactComponentTarget('react-1', 'Button')
const result = matchSceneTarget(target, {
page: () => 'page',
'playwright-page': () => 'playwright-page',
'storybook-story': () => 'storybook-story',
'react-component': (rendererId, componentId) => `react:${rendererId}:${componentId}`,
'vue-component': () => 'vue',
'custom-renderer': () => 'custom',
fixture: () => 'fixture',
})
assert.strictEqual(result, 'react:react-1:Button')
})
it('matches all target kinds exhaustively', () => {
const targets: SceneTarget[] = [
pageTarget('https://a.com'),
playwrightPageTarget('p1'),
storybookStoryTarget('s1', 'http://sb'),
reactComponentTarget('r1', 'C1'),
vueComponentTarget('v1', 'C2'),
customRendererTarget('c1', 't1'),
fixtureTarget('f1'),
]
for (const target of targets) {
const result = matchSceneTarget(target, {
page: () => 'page',
'playwright-page': () => 'playwright-page',
'storybook-story': () => 'storybook-story',
'react-component': () => 'react-component',
'vue-component': () => 'vue-component',
'custom-renderer': () => 'custom-renderer',
fixture: () => 'fixture',
})
assert.strictEqual(result, target.kind)
}
})
})
describe('SceneTarget factory functions', () => {
it('pageTarget creates correct shape', () => {
const target = pageTarget('https://example.com')
assert.deepStrictEqual(target, { kind: 'page', url: 'https://example.com' })
})
it('playwrightPageTarget creates correct shape with optional url', () => {
const withUrl = playwrightPageTarget('page-1', 'https://example.com')
assert.deepStrictEqual(withUrl, { kind: 'playwright-page', pageRef: 'page-1', url: 'https://example.com' })
const withoutUrl = playwrightPageTarget('page-1')
assert.deepStrictEqual(withoutUrl, { kind: 'playwright-page', pageRef: 'page-1', url: undefined })
})
it('storybookStoryTarget creates correct shape', () => {
const target = storybookStoryTarget('button--primary', 'http://localhost:6006')
assert.deepStrictEqual(target, {
kind: 'storybook-story',
storyId: 'button--primary',
storybookUrl: 'http://localhost:6006',
})
})
it('reactComponentTarget creates correct shape', () => {
const target = reactComponentTarget('react-dom', 'Button')
assert.deepStrictEqual(target, { kind: 'react-component', rendererId: 'react-dom', componentId: 'Button' })
})
it('vueComponentTarget creates correct shape', () => {
const target = vueComponentTarget('vue-3', 'Button')
assert.deepStrictEqual(target, { kind: 'vue-component', rendererId: 'vue-3', componentId: 'Button' })
})
it('customRendererTarget creates correct shape', () => {
const target = customRendererTarget('svelte', 'Widget')
assert.deepStrictEqual(target, { kind: 'custom-renderer', rendererId: 'svelte', targetId: 'Widget' })
})
it('fixtureTarget creates correct shape', () => {
const target = fixtureTarget('fixtures/card.html')
assert.deepStrictEqual(target, { kind: 'fixture', fixtureId: 'fixtures/card.html' })
})
})
describe('RenderCase', () => {
it('createRenderCase builds minimal case', () => {
const rc = createRenderCase('case-1', { size: 'md' })
assert.strictEqual(rc.caseId, 'case-1')
assert.deepStrictEqual(rc.input, { size: 'md' })
assert.strictEqual(rc.env, undefined)
assert.strictEqual(rc.metadata, undefined)
})
it('createRenderCase includes env and metadata when provided', () => {
const rc = createRenderCase('case-2', { disabled: true }, { viewportWidth: 800 }, { run: 3 })
assert.strictEqual(rc.caseId, 'case-2')
assert.deepStrictEqual(rc.input, { disabled: true })
assert.deepStrictEqual(rc.env, { viewportWidth: 800 })
assert.deepStrictEqual(rc.metadata, { run: 3 })
})
it('isSceneTarget rejects RenderCase', () => {
const rc = createRenderCase('case-1', {})
assert.strictEqual(isSceneTarget(rc), false)
})
})
+202
View File
@@ -0,0 +1,202 @@
/**
* SceneTarget discriminated union and type guards.
*
* The scene target is the stage. It must be possible to mount any component,
* any story, any page, and treat it as a deterministic scene. The adapter
* boundary is the moat that keeps renderer concerns out of the core.
*/
import type { Environment } from './types.js'
// ---------------------------------------------------------------------------
// SceneTarget Union
// ---------------------------------------------------------------------------
export type SceneTarget =
| { kind: 'page'; url: string }
| { kind: 'playwright-page'; pageRef: string; url?: string }
| { kind: 'storybook-story'; storyId: string; storybookUrl: string }
| { kind: 'react-component'; rendererId: string; componentId: string }
| { kind: 'vue-component'; rendererId: string; componentId: string }
| { kind: 'custom-renderer'; rendererId: string; targetId: string }
| { kind: 'fixture'; fixtureId: string }
// ---------------------------------------------------------------------------
// RenderCase Contract
// ---------------------------------------------------------------------------
export interface RenderCase {
caseId: string
input: unknown
env?: Partial<Environment>
metadata?: Record<string, unknown>
}
// ---------------------------------------------------------------------------
// Type Guards
// ---------------------------------------------------------------------------
export function isSceneTarget(value: unknown): value is SceneTarget {
if (typeof value !== 'object' || value === null) return false
const obj = value as Record<string, unknown>
if (typeof obj.kind !== 'string') return false
switch (obj.kind) {
case 'page':
return typeof obj.url === 'string'
case 'playwright-page':
return typeof obj.pageRef === 'string' && (obj.url === undefined || typeof obj.url === 'string')
case 'storybook-story':
return typeof obj.storyId === 'string' && typeof obj.storybookUrl === 'string'
case 'react-component':
return typeof obj.rendererId === 'string' && typeof obj.componentId === 'string'
case 'vue-component':
return typeof obj.rendererId === 'string' && typeof obj.componentId === 'string'
case 'custom-renderer':
return typeof obj.rendererId === 'string' && typeof obj.targetId === 'string'
case 'fixture':
return typeof obj.fixtureId === 'string'
default:
return false
}
}
export function isPageTarget(target: SceneTarget): target is Extract<SceneTarget, { kind: 'page' }> {
return target.kind === 'page'
}
export function isPlaywrightPageTarget(
target: SceneTarget
): target is Extract<SceneTarget, { kind: 'playwright-page' }> {
return target.kind === 'playwright-page'
}
export function isStorybookStoryTarget(
target: SceneTarget
): target is Extract<SceneTarget, { kind: 'storybook-story' }> {
return target.kind === 'storybook-story'
}
export function isReactComponentTarget(
target: SceneTarget
): target is Extract<SceneTarget, { kind: 'react-component' }> {
return target.kind === 'react-component'
}
export function isVueComponentTarget(
target: SceneTarget
): target is Extract<SceneTarget, { kind: 'vue-component' }> {
return target.kind === 'vue-component'
}
export function isCustomRendererTarget(
target: SceneTarget
): target is Extract<SceneTarget, { kind: 'custom-renderer' }> {
return target.kind === 'custom-renderer'
}
export function isFixtureTarget(target: SceneTarget): target is Extract<SceneTarget, { kind: 'fixture' }> {
return target.kind === 'fixture'
}
export function isComponentTarget(
target: SceneTarget
): target is
| Extract<SceneTarget, { kind: 'react-component' }>
| Extract<SceneTarget, { kind: 'vue-component' }>
| Extract<SceneTarget, { kind: 'custom-renderer' }> {
return target.kind === 'react-component' || target.kind === 'vue-component' || target.kind === 'custom-renderer'
}
export function isRendererTarget(
target: SceneTarget
): target is
| Extract<SceneTarget, { kind: 'react-component' }>
| Extract<SceneTarget, { kind: 'vue-component' }>
| Extract<SceneTarget, { kind: 'storybook-story' }>
| Extract<SceneTarget, { kind: 'custom-renderer' }> {
return (
target.kind === 'react-component' ||
target.kind === 'vue-component' ||
target.kind === 'storybook-story' ||
target.kind === 'custom-renderer'
)
}
// ---------------------------------------------------------------------------
// Matchers
// ---------------------------------------------------------------------------
export function matchSceneTarget<R>(
target: SceneTarget,
cases: {
page: (url: string) => R
'playwright-page': (pageRef: string, url?: string) => R
'storybook-story': (storyId: string, storybookUrl: string) => R
'react-component': (rendererId: string, componentId: string) => R
'vue-component': (rendererId: string, componentId: string) => R
'custom-renderer': (rendererId: string, targetId: string) => R
fixture: (fixtureId: string) => R
}
): R {
switch (target.kind) {
case 'page':
return cases.page(target.url)
case 'playwright-page':
return cases['playwright-page'](target.pageRef, target.url)
case 'storybook-story':
return cases['storybook-story'](target.storyId, target.storybookUrl)
case 'react-component':
return cases['react-component'](target.rendererId, target.componentId)
case 'vue-component':
return cases['vue-component'](target.rendererId, target.componentId)
case 'custom-renderer':
return cases['custom-renderer'](target.rendererId, target.targetId)
case 'fixture':
return cases.fixture(target.fixtureId)
}
}
// ---------------------------------------------------------------------------
// Factory Functions
// ---------------------------------------------------------------------------
export function pageTarget(url: string): SceneTarget {
return { kind: 'page', url }
}
export function playwrightPageTarget(pageRef: string, url?: string): SceneTarget {
return { kind: 'playwright-page', pageRef, url }
}
export function storybookStoryTarget(storyId: string, storybookUrl: string): SceneTarget {
return { kind: 'storybook-story', storyId, storybookUrl }
}
export function reactComponentTarget(rendererId: string, componentId: string): SceneTarget {
return { kind: 'react-component', rendererId, componentId }
}
export function vueComponentTarget(rendererId: string, componentId: string): SceneTarget {
return { kind: 'vue-component', rendererId, componentId }
}
export function customRendererTarget(rendererId: string, targetId: string): SceneTarget {
return { kind: 'custom-renderer', rendererId, targetId }
}
export function fixtureTarget(fixtureId: string): SceneTarget {
return { kind: 'fixture', fixtureId }
}
// ---------------------------------------------------------------------------
// RenderCase Factory
// ---------------------------------------------------------------------------
export function createRenderCase(
caseId: string,
input: unknown,
env?: Partial<Environment>,
metadata?: Record<string, unknown>
): RenderCase {
return { caseId, input, env, metadata }
}
+216
View File
@@ -0,0 +1,216 @@
import { describe, it } from 'node:test'
import assert from 'node:assert'
import type {
Point,
Position,
LengthLiteral,
ClauseStatus,
TruthValue,
ClauseResult,
Proof,
} from './types.js'
import type {
DiagnosticCategory,
DiagnosticCode,
} from './diagnostics.js'
import {
isAstNode,
isParentNode,
isProgram,
isRelationAssertion,
isCompoundAssertion,
type Program,
type RelationAssertion,
type CompoundAssertion,
} from './ast.js'
import { createEmptySemanticIr } from './ir.js'
import { createEmptyWorld } from './world.js'
import { createDiagnostic, isContractFailure, isSystemUseError } from './diagnostics.js'
describe('Core Types', () => {
it('constructs a valid Point', () => {
const p: Point = { line: 1, column: 1, offset: 0 }
assert.strictEqual(p.line, 1)
assert.strictEqual(p.column, 1)
assert.strictEqual(p.offset, 0)
})
it('constructs a valid Position', () => {
const pos: Position = {
start: { line: 2, column: 5, offset: 17 },
end: { line: 2, column: 51, offset: 63 },
}
assert.strictEqual(pos.start.line, 2)
assert.strictEqual(pos.end.offset, 63)
})
it('constructs a valid LengthLiteral', () => {
const len: LengthLiteral = { value: 16, unit: 'px' }
assert.strictEqual(len.value, 16)
assert.strictEqual(len.unit, 'px')
})
it('narrows ClauseStatus discriminated union', () => {
const statuses: ClauseStatus[] = ['pass', 'fail', 'error', 'skipped']
for (const s of statuses) {
assert.ok(['pass', 'fail', 'error', 'skipped'].includes(s))
}
})
it('narrows TruthValue discriminated union', () => {
const truths: TruthValue[] = ['determinate', 'indeterminate', 'approximate']
for (const t of truths) {
assert.ok(['determinate', 'indeterminate', 'approximate'].includes(t))
}
})
it('constructs a valid ClauseResult', () => {
const result: ClauseResult = {
clauseId: 'clause_42',
status: 'fail',
truth: 'determinate',
metrics: { observedGap: 11, minGap: 16, maxGap: 20 },
witness: { subjectId: 2, referenceId: 9, frameId: 1 },
diagnostics: [],
}
assert.strictEqual(result.status, 'fail')
assert.strictEqual(result.truth, 'determinate')
assert.strictEqual(result.metrics.observedGap, 11)
})
it('constructs a valid Proof', () => {
const proof: Proof = {
proofId: 'proof_99',
clauseId: 'clause_42',
outcome: 'fail',
truth: 'determinate',
usedFacts: [102, 103, 220],
derivedFacts: [501, 502],
failedPredicate: { op: '>=', left: 11, right: 24 },
witness: {
subjectId: 5,
referenceId: 8,
envCaseId: 'env_3',
snapshotId: 'hover',
},
}
assert.strictEqual(proof.usedFacts.length, 3)
assert.strictEqual(proof.failedPredicate?.op, '>=')
})
})
describe('AST Type Guards', () => {
it('identifies valid AstNode', () => {
assert.strictEqual(isAstNode({ type: 'Program' }), true)
assert.strictEqual(isAstNode(null), false)
assert.strictEqual(isAstNode({}), false)
})
it('identifies ParentNode via children property', () => {
const program: Program = {
type: 'Program',
children: [],
}
assert.strictEqual(isParentNode(program), true)
const relation: RelationAssertion = {
type: 'RelationAssertion',
relation: 'leftOf',
subject: { type: 'SelectorRef', value: '.a' },
reference: { type: 'SelectorRef', value: '.b' },
options: { type: 'RelationOptions' },
}
assert.strictEqual(isParentNode(relation), false)
})
it('narrows Program type', () => {
const node = { type: 'Program', children: [] } as const
assert.strictEqual(isProgram(node), true)
assert.strictEqual(isRelationAssertion(node), false)
})
it('narrows RelationAssertion type', () => {
const node: RelationAssertion = {
type: 'RelationAssertion',
relation: 'leftOf',
subject: { type: 'SelectorRef', value: '.a' },
reference: { type: 'SelectorRef', value: '.b' },
options: { type: 'RelationOptions' },
}
assert.strictEqual(isRelationAssertion(node), true)
assert.strictEqual(isProgram(node), false)
})
it('narrows CompoundAssertion type', () => {
const node: CompoundAssertion = {
type: 'CompoundAssertion',
quantifier: { type: 'Quantifier', kind: 'all' },
children: [],
}
assert.strictEqual(isCompoundAssertion(node), true)
})
})
describe('Semantic IR', () => {
it('creates empty Semantic IR with all tables', () => {
const ir = createEmptySemanticIr()
assert.ok(ir.subjects instanceof Map)
assert.ok(ir.frames instanceof Map)
assert.ok(ir.clauses instanceof Map)
assert.ok(ir.groups instanceof Map)
assert.ok(ir.tolerances instanceof Map)
assert.ok(ir.envGuards instanceof Map)
assert.ok(ir.states instanceof Map)
assert.ok(ir.timelines instanceof Map)
assert.ok(ir.diagnosticMetadata instanceof Map)
})
})
describe('Geometry World', () => {
it('creates empty world with correct scene/snapshot ids', () => {
const world = createEmptyWorld('scene_1', 'snapshot_default')
assert.strictEqual(world.sceneId, 'scene_1')
assert.strictEqual(world.snapshotId, 'snapshot_default')
assert.strictEqual(world.env.viewportWidth, 0)
assert.ok(world.subjects.ids instanceof Uint32Array)
assert.ok(world.boxes.boxId instanceof Uint32Array)
})
})
describe('Diagnostics', () => {
it('creates a diagnostic with correct category', () => {
const d = createDiagnostic(
'IMH_RELATION_LEFT_OF_FAILED',
'contract-failure',
'error',
'Expected .gallery to be left of .details.',
)
assert.strictEqual(d.code, 'IMH_RELATION_LEFT_OF_FAILED')
assert.strictEqual(d.category, 'contract-failure')
assert.strictEqual(d.severity, 'error')
assert.strictEqual(isContractFailure(d), true)
assert.strictEqual(isSystemUseError(d), false)
})
it('classifies system use errors correctly', () => {
const parseErr = createDiagnostic(
'IMH_PARSE_INVALID_SYNTAX',
'parse-error',
'error',
'Unexpected token',
)
assert.strictEqual(isSystemUseError(parseErr), true)
assert.strictEqual(isContractFailure(parseErr), false)
const resolutionErr = createDiagnostic(
'IMH_SELECTOR_ZERO_MATCHES',
'resolution-error',
'error',
'Selector matched zero elements.',
)
assert.strictEqual(isSystemUseError(resolutionErr), true)
})
})
+308
View File
@@ -0,0 +1,308 @@
/**
* Core type definitions for Imhotep.
*
* Imhotep uses three internal representations:
* 1. Authoring AST — rich tree, unist-style, source-positioned
* 2. Semantic IR — normalized graph with explicit obligations
* 3. Execution IR — flattened, data-oriented arrays for speed
*
* This file holds the foundational types shared across all stages.
*/
// ---------------------------------------------------------------------------
// Position and Source Origin
// ---------------------------------------------------------------------------
export interface Point {
/** 1-based line number in the source text */
line: number
/** 1-based column number in the source text */
column: number
/** 0-based character offset in the source text */
offset: number
}
export interface Position {
start: Point
end: Point
}
export interface SourceOrigin {
/** AST node ids that contributed to this IR node */
astNodeIds: string[]
/** Source spans traceable back to authoring syntax */
positions: Position[]
}
// ---------------------------------------------------------------------------
// Identifier and Reference Types
// ---------------------------------------------------------------------------
export type ImhotepId = string
export interface IdRef {
id: ImhotepId
}
// ---------------------------------------------------------------------------
// Literal Value Types
// ---------------------------------------------------------------------------
export type LengthUnit = 'px' | 'em' | 'rem' | 'ch' | 'jnd'
export interface LengthLiteral {
value: number
unit: LengthUnit
}
export interface RangeLiteral {
min: LengthLiteral
max: LengthLiteral
}
export interface ToleranceLiteral {
value: number
unit: 'jnd' | 'px'
}
export type LiteralValue =
| { kind: 'length'; value: number; unit: LengthUnit }
| { kind: 'range'; min: LengthLiteral; max: LengthLiteral }
| { kind: 'tolerance'; value: number; unit: 'jnd' | 'px' }
| { kind: 'string'; value: string }
| { kind: 'number'; value: number }
| { kind: 'boolean'; value: boolean }
// ---------------------------------------------------------------------------
// Environment and Snapshot
// ---------------------------------------------------------------------------
export type ColorScheme = 'light' | 'dark'
export type PointerType = 'coarse' | 'fine' | 'none'
export type ReducedMotion = 'reduce' | 'no-preference'
export type WritingMode = 'horizontal-tb' | 'vertical-rl' | 'vertical-lr'
export interface Environment {
viewportWidth: number
viewportHeight: number
deviceScaleFactor: number
colorScheme: ColorScheme
pointer: PointerType
hover: boolean
reducedMotion: ReducedMotion
locale: string
writingMode: WritingMode
}
export interface EnvCase {
id: ImhotepId
env: Environment
}
// ---------------------------------------------------------------------------
// State and Timeline
// ---------------------------------------------------------------------------
export type StateKind =
| 'default'
| 'hover'
| 'focus'
| 'focusVisible'
| 'active'
| 'disabled'
| 'checked'
| 'expanded'
| 'collapsed'
| 'selected'
| 'pressed'
| 'visited'
| 'custom'
export interface StateSnapshot {
id: ImhotepId
kind: StateKind
name?: string
}
export type TimelineMode = 'static' | 'keyframes' | 'range' | 'transition'
export interface TimelineSnapshot {
id: ImhotepId
mode: TimelineMode
samples?: number[]
}
// ---------------------------------------------------------------------------
// Frame Types
// ---------------------------------------------------------------------------
export type FrameKind =
| 'viewport'
| 'document'
| 'element'
| 'nearestPositionedAncestor'
| 'containingBlock'
| 'scrollContainer'
| 'namedGridArea'
| 'landmark'
| 'stackingContextRoot'
| 'custom'
export interface Frame {
id: ImhotepId
kind: FrameKind
selector?: string
name?: string
originX: number
originY: number
writingMode: WritingMode
}
// ---------------------------------------------------------------------------
// Subject Types
// ---------------------------------------------------------------------------
export type SubjectKind =
| 'element'
| 'pseudoElement'
| 'textRange'
| 'lineBox'
| 'fragmentBox'
| 'landmark'
| 'gridArea'
| 'scrollContainer'
| 'syntheticFrame'
export interface Subject {
id: ImhotepId
selector: string
kind: SubjectKind
}
// ---------------------------------------------------------------------------
// Assertion and Quantifier Types
// ---------------------------------------------------------------------------
export type RelationName =
| 'leftOf'
| 'rightOf'
| 'above'
| 'below'
| 'alignedWith'
| 'leftAlignedWith'
| 'rightAlignedWith'
| 'topAlignedWith'
| 'bottomAlignedWith'
| 'centeredWithin'
| 'inside'
| 'contains'
| 'overlaps'
| 'intersects'
| 'touches'
| 'separatedFrom'
| 'hasGap'
export type SizeProperty =
| 'width'
| 'height'
| 'minWidth'
| 'minHeight'
| 'maxWidth'
| 'maxHeight'
export type QuantifierKind =
| 'all'
| 'any'
| 'none'
| 'exactly'
| 'atLeast'
| 'atMost'
| 'ordered'
| 'pairwise'
export interface Quantifier {
kind: QuantifierKind
count?: number
}
// ---------------------------------------------------------------------------
// Clause Result and Truth
// ---------------------------------------------------------------------------
export type ClauseStatus = 'pass' | 'fail' | 'error' | 'skipped'
export type TruthValue = 'determinate' | 'indeterminate' | 'approximate'
export interface ClauseMetrics {
[key: string]: number | LengthLiteral | undefined
}
export interface ClauseWitness {
subjectId: number
referenceId?: number
frameId: number
envCaseId?: ImhotepId
snapshotId?: ImhotepId
}
export interface SourceReference {
/** 0-based index of the assertion in a fluent batch */
fluentIndex?: number
/** 1-based line number in the dense spec string */
specLine?: number
/** 1-based column number in the dense spec string */
specColumn?: number
}
export interface ClauseResult {
clauseId: ImhotepId
status: ClauseStatus
truth: TruthValue
metrics: ClauseMetrics
witness: ClauseWitness
diagnostics: ImhotepId[]
/** Traceability metadata linking this result to its authored source */
sourceRef?: SourceReference
/** Human-readable label derived from the normalized contract */
clauseLabel?: string
}
// ---------------------------------------------------------------------------
// Group Result
// ---------------------------------------------------------------------------
export type GroupOperator = 'and' | 'or' | 'not' | 'quantifier'
export interface GroupResult {
groupId: ImhotepId
operator: GroupOperator
clauseResults: ImhotepId[]
groupResults?: ImhotepId[]
status: ClauseStatus
truth: TruthValue
}
// ---------------------------------------------------------------------------
// Proof and Determination
// ---------------------------------------------------------------------------
export interface FailedPredicate {
op: string
left: number
right: number
}
export interface Proof {
proofId: ImhotepId
clauseId: ImhotepId
outcome: ClauseStatus
truth: TruthValue
usedFacts: number[]
derivedFacts: number[]
failedPredicate?: FailedPredicate
witness: {
subjectId: number
referenceId?: number
envCaseId: ImhotepId
snapshotId: ImhotepId
}
}
+574
View File
@@ -0,0 +1,574 @@
/**
* Geometry world schema types for Imhotep.
*
* The geometry world is the extracted and normalized scene model.
* It is immutable per snapshot, indexed by stable numeric ids for speed,
* and explicit about confidence and provenance.
*/
import type { ImhotepId, Environment } from './types.js'
// ---------------------------------------------------------------------------
// Coordinate Space
// ---------------------------------------------------------------------------
/**
* Coordinate space distinguishes between layout-local and visual-viewport
* coordinates. Layout coordinates are pre-transform (the local box model
* as computed by the browser). Visual coordinates are post-transform
* (the effective position after all CSS transforms are applied).
*
* Playwright boundingBox() returns visual coordinates.
* CDP DOM.getBoxModel returns layout coordinates.
*/
export enum CoordinateSpace {
LAYOUT = 0, // Local layout coordinates (pre-transform)
VISUAL = 1, // Visual viewport coordinates (post-transform)
}
// ---------------------------------------------------------------------------
// Top-Level World
// ---------------------------------------------------------------------------
export interface GeometryWorld {
sceneId: ImhotepId
snapshotId: ImhotepId
env: WorldEnvironment
source: WorldSource
strings: StringTable
subjects: SubjectsTable
dom: DomTable
frames: FramesTable
matrices: MatricesTable
rects: RectsTable
boxes: BoxesTable // Layout-space box geometry (CDP box model, pre-transform)
visualBoxes: BoxesTable // Visual-space box geometry (post-transform, viewport-relative)
fragments: FragmentsTable
transforms: TransformsTable
styles: StylesTable
text: TextTable
topology: TopologyTable
scroll: ScrollTable
clipping: ClippingTable
paint: PaintTable
visibility: VisibilityTable
provenance: ProvenanceTable
confidence: ConfidenceTable
}
// ---------------------------------------------------------------------------
// Environment and Source
// ---------------------------------------------------------------------------
export interface WorldEnvironment {
viewportWidth: number
viewportHeight: number
deviceScaleFactor: number
colorScheme: string
pointer: string
hover: boolean
reducedMotion: string
locale: string
writingMode: string
}
export interface WorldSource {
url: string
browserName: string
browserVersion: string
engine: 'chromium-cdp'
extractedAt: number
}
// ---------------------------------------------------------------------------
// String Table
// ---------------------------------------------------------------------------
export interface StringTable {
values: string[]
}
// ---------------------------------------------------------------------------
// Subjects
// ---------------------------------------------------------------------------
export interface SubjectsTable {
ids: Uint32Array
domNodeId: Uint32Array
subjectKind: Uint16Array
primaryBoxId: Uint32Array
firstFragmentId: Uint32Array
fragmentCount: Uint16Array
firstTextRunId: Uint32Array
textRunCount: Uint16Array
}
// ---------------------------------------------------------------------------
// DOM
// ---------------------------------------------------------------------------
export interface DomTable {
nodeId: Uint32Array
backendNodeId: Uint32Array
parentNodeId: Uint32Array
firstChildIndex: Uint32Array
childCount: Uint16Array
shadowRootKind: Uint8Array
tagNameStringId: Uint32Array
roleStringId: Uint32Array
ariaNameStringId: Uint32Array
}
// ---------------------------------------------------------------------------
// Frames
// ---------------------------------------------------------------------------
export enum FrameKind {
Viewport = 1,
ContainingBlock = 2,
NearestPositionedAncestor = 3,
ScrollContainer = 4,
Synthetic = 5,
}
export interface FramesTable {
frameId: Uint32Array
frameKind: Uint16Array
ownerSubjectId: Uint32Array
parentFrameId: Uint32Array
originX: Float64Array
originY: Float64Array
axisMatrixStart: Uint32Array
clipRectId: Uint32Array
scrollContainerId: Uint32Array
writingMode: Uint16Array
}
// ---------------------------------------------------------------------------
// Matrices
// ---------------------------------------------------------------------------
export interface MatricesTable {
values: Float64Array
}
// ---------------------------------------------------------------------------
// Rects
// ---------------------------------------------------------------------------
export interface RectsTable {
rectId: Uint32Array
left: Float64Array
top: Float64Array
right: Float64Array
bottom: Float64Array
}
// ---------------------------------------------------------------------------
// Boxes
// ---------------------------------------------------------------------------
export interface BoxesTable {
boxId: Uint32Array
subjectId: Uint32Array
frameId: Uint32Array
borderLeft: Float64Array
borderTop: Float64Array
borderRight: Float64Array
borderBottom: Float64Array
paddingLeft: Float64Array
paddingTop: Float64Array
paddingRight: Float64Array
paddingBottom: Float64Array
contentLeft: Float64Array
contentTop: Float64Array
contentRight: Float64Array
contentBottom: Float64Array
}
// ---------------------------------------------------------------------------
// Fragments
// ---------------------------------------------------------------------------
export enum FragmentKind {
Line = 1,
Column = 2,
Region = 3,
Page = 4,
}
export interface FragmentsTable {
fragmentId: Uint32Array
subjectId: Uint32Array
fragmentKind: Uint16Array
boxLeft: Float64Array
boxTop: Float64Array
boxRight: Float64Array
boxBottom: Float64Array
lineIndex: Int32Array
flowIndex: Int32Array
parentFragmentId: Uint32Array
}
// ---------------------------------------------------------------------------
// Transforms
// ---------------------------------------------------------------------------
export interface TransformsTable {
transformId: Uint32Array
subjectId: Uint32Array
matrixStart: Uint32Array
matrixLength: Uint16Array
originX: Float64Array
originY: Float64Array
}
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
export interface StylesTable {
subjectId: Uint32Array
display: Uint16Array
position: Uint16Array
zIndexKind: Uint8Array
zIndexValue: Int32Array
overflowX: Uint16Array
overflowY: Uint16Array
opacity: Float32Array
visibility: Uint16Array
containFlags: Uint32Array
pointerEvents: Uint16Array
lineHeight: Float64Array
fontFamilyStringId: Uint32Array
fontSize: Float64Array
fontWeight: Uint16Array
}
// ---------------------------------------------------------------------------
// Text
// ---------------------------------------------------------------------------
export interface TextTable {
runId: Uint32Array
subjectId: Uint32Array
contentStringId: Uint32Array
lineBoxId: Uint32Array
inkLeft: Float64Array
inkTop: Float64Array
inkRight: Float64Array
inkBottom: Float64Array
baselineY: Float64Array
capHeight: Float64Array
computedLineHeight: Float64Array
}
// ---------------------------------------------------------------------------
// Topology
// ---------------------------------------------------------------------------
export interface TopologyTable {
containingBlockOf: Uint32Array
nearestPositionedAncestorOf: Uint32Array
scrollContainerOf: Uint32Array
stackingContextOf: Uint32Array
formattingContextOf: Uint32Array
clippingRootOf: Uint32Array
paintOrderBucket: Uint16Array
paintOrderIndex: Uint32Array
}
// ---------------------------------------------------------------------------
// Scroll
// ---------------------------------------------------------------------------
export interface ScrollTable {
containerId: Uint32Array
scrollLeft: Float64Array
scrollTop: Float64Array
scrollWidth: Float64Array
scrollHeight: Float64Array
clientWidth: Float64Array
clientHeight: Float64Array
}
// ---------------------------------------------------------------------------
// Clipping
// ---------------------------------------------------------------------------
export interface ClippingTable {
clipNodeId: Uint32Array
subjectId: Uint32Array
clipKind: Uint16Array
clipLeft: Float64Array
clipTop: Float64Array
clipRight: Float64Array
clipBottom: Float64Array
parentClipNodeId: Uint32Array
}
// ---------------------------------------------------------------------------
// Paint
// ---------------------------------------------------------------------------
export interface PaintTable {
paintNodeId: Uint32Array
subjectId: Uint32Array
stackingContextId: Uint32Array
bucket: Uint16Array
localPaintIndex: Uint32Array
}
// ---------------------------------------------------------------------------
// Visibility
// ---------------------------------------------------------------------------
export interface VisibilityTable {
subjectId: Uint32Array
isRendered: Uint8Array
isVisible: Uint8Array
visibleArea: Float64Array
clippedArea: Float64Array
}
// ---------------------------------------------------------------------------
// Provenance
// ---------------------------------------------------------------------------
export interface ProvenanceTable {
factId: Uint32Array
extractionStepId: Uint32Array
sourceKind: Uint16Array
sourceRef: Uint32Array
}
// ---------------------------------------------------------------------------
// Confidence
// ---------------------------------------------------------------------------
export interface ConfidenceTable {
factId: Uint32Array
confidence: Float32Array
reasonCode: Uint16Array
}
// ---------------------------------------------------------------------------
// Fact Request Plan (fed to extractors)
// ---------------------------------------------------------------------------
export interface FactRequestPlan {
geometry: boolean
fragments: boolean
styles: string[] | boolean
topology: string[] | boolean
text: boolean
}
// ---------------------------------------------------------------------------
// Helper: Create empty world shell
// ---------------------------------------------------------------------------
export function createEmptyWorld(sceneId: ImhotepId, snapshotId: ImhotepId): GeometryWorld {
return {
sceneId,
snapshotId,
env: {
viewportWidth: 0,
viewportHeight: 0,
deviceScaleFactor: 1,
colorScheme: 'light',
pointer: 'fine',
hover: false,
reducedMotion: 'no-preference',
locale: 'en',
writingMode: 'horizontal-tb',
},
source: {
url: '',
browserName: '',
browserVersion: '',
engine: 'chromium-cdp',
extractedAt: 0,
},
strings: { values: [] },
subjects: {
ids: new Uint32Array(0),
domNodeId: new Uint32Array(0),
subjectKind: new Uint16Array(0),
primaryBoxId: new Uint32Array(0),
firstFragmentId: new Uint32Array(0),
fragmentCount: new Uint16Array(0),
firstTextRunId: new Uint32Array(0),
textRunCount: new Uint16Array(0),
},
dom: {
nodeId: new Uint32Array(0),
backendNodeId: new Uint32Array(0),
parentNodeId: new Uint32Array(0),
firstChildIndex: new Uint32Array(0),
childCount: new Uint16Array(0),
shadowRootKind: new Uint8Array(0),
tagNameStringId: new Uint32Array(0),
roleStringId: new Uint32Array(0),
ariaNameStringId: new Uint32Array(0),
},
frames: {
frameId: new Uint32Array(0),
frameKind: new Uint16Array(0),
ownerSubjectId: new Uint32Array(0),
parentFrameId: new Uint32Array(0),
originX: new Float64Array(0),
originY: new Float64Array(0),
axisMatrixStart: new Uint32Array(0),
clipRectId: new Uint32Array(0),
scrollContainerId: new Uint32Array(0),
writingMode: new Uint16Array(0),
},
matrices: { values: new Float64Array(0) },
rects: {
rectId: new Uint32Array(0),
left: new Float64Array(0),
top: new Float64Array(0),
right: new Float64Array(0),
bottom: new Float64Array(0),
},
boxes: {
boxId: new Uint32Array(0),
subjectId: new Uint32Array(0),
frameId: new Uint32Array(0),
borderLeft: new Float64Array(0),
borderTop: new Float64Array(0),
borderRight: new Float64Array(0),
borderBottom: new Float64Array(0),
paddingLeft: new Float64Array(0),
paddingTop: new Float64Array(0),
paddingRight: new Float64Array(0),
paddingBottom: new Float64Array(0),
contentLeft: new Float64Array(0),
contentTop: new Float64Array(0),
contentRight: new Float64Array(0),
contentBottom: new Float64Array(0),
},
visualBoxes: {
boxId: new Uint32Array(0),
subjectId: new Uint32Array(0),
frameId: new Uint32Array(0),
borderLeft: new Float64Array(0),
borderTop: new Float64Array(0),
borderRight: new Float64Array(0),
borderBottom: new Float64Array(0),
paddingLeft: new Float64Array(0),
paddingTop: new Float64Array(0),
paddingRight: new Float64Array(0),
paddingBottom: new Float64Array(0),
contentLeft: new Float64Array(0),
contentTop: new Float64Array(0),
contentRight: new Float64Array(0),
contentBottom: new Float64Array(0),
},
fragments: {
fragmentId: new Uint32Array(0),
subjectId: new Uint32Array(0),
fragmentKind: new Uint16Array(0),
boxLeft: new Float64Array(0),
boxTop: new Float64Array(0),
boxRight: new Float64Array(0),
boxBottom: new Float64Array(0),
lineIndex: new Int32Array(0),
flowIndex: new Int32Array(0),
parentFragmentId: new Uint32Array(0),
},
transforms: {
transformId: new Uint32Array(0),
subjectId: new Uint32Array(0),
matrixStart: new Uint32Array(0),
matrixLength: new Uint16Array(0),
originX: new Float64Array(0),
originY: new Float64Array(0),
},
styles: {
subjectId: new Uint32Array(0),
display: new Uint16Array(0),
position: new Uint16Array(0),
zIndexKind: new Uint8Array(0),
zIndexValue: new Int32Array(0),
overflowX: new Uint16Array(0),
overflowY: new Uint16Array(0),
opacity: new Float32Array(0),
visibility: new Uint16Array(0),
containFlags: new Uint32Array(0),
pointerEvents: new Uint16Array(0),
lineHeight: new Float64Array(0),
fontFamilyStringId: new Uint32Array(0),
fontSize: new Float64Array(0),
fontWeight: new Uint16Array(0),
},
text: {
runId: new Uint32Array(0),
subjectId: new Uint32Array(0),
contentStringId: new Uint32Array(0),
lineBoxId: new Uint32Array(0),
inkLeft: new Float64Array(0),
inkTop: new Float64Array(0),
inkRight: new Float64Array(0),
inkBottom: new Float64Array(0),
baselineY: new Float64Array(0),
capHeight: new Float64Array(0),
computedLineHeight: new Float64Array(0),
},
topology: {
containingBlockOf: new Uint32Array(0),
nearestPositionedAncestorOf: new Uint32Array(0),
scrollContainerOf: new Uint32Array(0),
stackingContextOf: new Uint32Array(0),
formattingContextOf: new Uint32Array(0),
clippingRootOf: new Uint32Array(0),
paintOrderBucket: new Uint16Array(0),
paintOrderIndex: new Uint32Array(0),
},
scroll: {
containerId: new Uint32Array(0),
scrollLeft: new Float64Array(0),
scrollTop: new Float64Array(0),
scrollWidth: new Float64Array(0),
scrollHeight: new Float64Array(0),
clientWidth: new Float64Array(0),
clientHeight: new Float64Array(0),
},
clipping: {
clipNodeId: new Uint32Array(0),
subjectId: new Uint32Array(0),
clipKind: new Uint16Array(0),
clipLeft: new Float64Array(0),
clipTop: new Float64Array(0),
clipRight: new Float64Array(0),
clipBottom: new Float64Array(0),
parentClipNodeId: new Uint32Array(0),
},
paint: {
paintNodeId: new Uint32Array(0),
subjectId: new Uint32Array(0),
stackingContextId: new Uint32Array(0),
bucket: new Uint16Array(0),
localPaintIndex: new Uint32Array(0),
},
visibility: {
subjectId: new Uint32Array(0),
isRendered: new Uint8Array(0),
isVisible: new Uint8Array(0),
visibleArea: new Float64Array(0),
clippedArea: new Float64Array(0),
},
provenance: {
factId: new Uint32Array(0),
extractionStepId: new Uint32Array(0),
sourceKind: new Uint16Array(0),
sourceRef: new Uint32Array(0),
},
confidence: {
factId: new Uint32Array(0),
confidence: new Float32Array(0),
reasonCode: new Uint16Array(0),
},
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"paths": {},
"composite": false,
"noEmitOnError": false
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts",
"src/integration-*.ts"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": false,
"noEmit": true,
"noEmitOnError": false
},
"include": [
"src/**/*"
],
"exclude": []
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist-test",
"rootDir": "./src",
"paths": {},
"composite": false,
"noEmitOnError": false
},
"include": [
"src/**/*.ts"
]
}