From b57f280ea38c6de5f0ba45a85cf9ce97ce51606d Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sat, 1 Aug 2026 17:32:19 +0400 Subject: [PATCH 1/4] wip: checkpoint obligation dossier retrieval --- src/adapters/mcp/protocol.ts | 6 +- src/application/evidence-hydrator.ts | 375 +++ src/application/retrieve-context.ts | 875 ++++--- src/domain/query/plan.ts | 218 ++ src/domain/query/rank.ts | 1098 --------- src/domain/query/slice.ts | 394 --- src/domain/query/traverse.ts | 261 -- src/domain/query/types.ts | 313 ++- src/domain/query/workflow.ts | 1070 ++++++++ .../http/idea-generation.controller.ts | 10 +- .../pipeline/api/pipeline-trigger.service.ts | 6 +- .../pipeline/api/queue-registry.service.ts | 66 +- .../pipeline/assembly/assembly.worker.ts | 8 +- .../pipeline/workers/db-sync.worker.ts | 20 +- .../pipeline/workers/orchestrator.worker.ts | 8 +- .../src/modules/planning/planner.service.ts | 6 +- .../src/modules/reports/assembly.service.ts | 2 +- .../research/research-agent.service.ts | 2 +- .../workers/section-research.worker.ts | 8 +- tests/unit/benchmark-quality.test.ts | 60 +- tests/unit/benchmark-runtime-proof.test.ts | 12 +- tests/unit/evidence-hydrator.test.ts | 504 ++++ .../query-index-execution-validation.test.ts | 42 +- tests/unit/query-plan.test.ts | 322 +++ tests/unit/query-workflow.test.ts | 1910 +++++++++++++++ .../retrieve-context-proof-eviction.test.ts | 200 ++ tests/unit/retrieve-context.test.ts | 2158 ++++------------- tests/unit/sample-workspace.test.ts | 20 +- tests/unit/stdio-server.test.ts | 43 +- .../lib/infrastructure/benchmark/quality.ts | 47 +- .../lib/infrastructure/benchmark/questions.ts | 7 +- 31 files changed, 6072 insertions(+), 3999 deletions(-) create mode 100644 src/application/evidence-hydrator.ts create mode 100644 src/domain/query/plan.ts delete mode 100644 src/domain/query/rank.ts delete mode 100644 src/domain/query/slice.ts delete mode 100644 src/domain/query/traverse.ts create mode 100644 src/domain/query/workflow.ts create mode 100644 tests/unit/evidence-hydrator.test.ts create mode 100644 tests/unit/query-plan.test.ts create mode 100644 tests/unit/query-workflow.test.ts create mode 100644 tests/unit/retrieve-context-proof-eviction.test.ts diff --git a/src/adapters/mcp/protocol.ts b/src/adapters/mcp/protocol.ts index 3ef7a55c..26832674 100644 --- a/src/adapters/mcp/protocol.ts +++ b/src/adapters/mcp/protocol.ts @@ -52,7 +52,7 @@ export const MCP_TOOLS: readonly McpToolDefinition[] = Object.freeze([ Object.freeze({ name: 'retrieve', description: - 'Return the smallest deterministic evidence path for a TypeScript or JavaScript codebase question.', + 'Return one deterministic authenticated answer dossier, or exact missing requirements, for a TypeScript or JavaScript codebase question.', inputSchema: Object.freeze({ type: 'object', additionalProperties: false, @@ -62,7 +62,7 @@ export const MCP_TOOLS: readonly McpToolDefinition[] = Object.freeze([ type: 'string', minLength: 1, maxLength: MAX_RETRIEVE_QUESTION_LENGTH, - description: 'The codebase question to answer from authenticated graph evidence.', + description: 'A locate, explain, or workflow question to prove from the indexed graph.', }), budget: Object.freeze({ type: 'integer', @@ -135,7 +135,7 @@ export async function handleMcpProtocolRequest( version: context.version, }, instructions: - 'Call retrieve once with the codebase question. Madar returns one deterministic authenticated evidence path or a terminal evidence boundary.', + 'Call retrieve once with the codebase question. State ready contains a complete authenticated dossier; every other state names the exact missing or terminal condition. Do not infer omitted workflow steps.', }) case 'ping': return success(id, {}) diff --git a/src/application/evidence-hydrator.ts b/src/application/evidence-hydrator.ts new file mode 100644 index 00000000..72501162 --- /dev/null +++ b/src/application/evidence-hydrator.ts @@ -0,0 +1,375 @@ +import { createHash } from 'node:crypto' +import { isUtf8 } from 'node:buffer' +import { readFileSync, realpathSync } from 'node:fs' +import { isAbsolute, relative, resolve, sep } from 'node:path' +import type { GraphAttributes } from '../domain/graph/directed-multigraph.js' +import type { + IndexBodyFact, IndexChannelNode, IndexRange, IndexValue, +} from '../domain/index/model.js' +import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' +import { + type EvidenceHydrationTargets, type HydratedEntity, + type HydratedEvidenceResult, type HydratedExcerpt, type HydratedFile, + type HydratedProof, type SelectedEvidenceEdge, +} from '../domain/query/types.js' +type Failure = Extract +type ReadySource = [ + path: string, sha256: string, text: string, + starts: readonly number[], ends: readonly number[], file: string, +] +type FactProof = [owner: string, excerpt: string] +type CallFact = Extract +type EdgeRow = readonly [from: string, to: string, attrs: GraphAttributes, id: string] +const SHA = /^[a-f0-9]{64}$/ +const channelFields: readonly (keyof IndexChannelNode)[] = [ + 'channel_kind', 'transport', 'key', 'parent_channel_id', 'scope', +] +const compare = (left: string, right: string): number => + left < right ? -1 : left > right ? 1 : 0 +class Halt { constructor(readonly value: Failure) {} } +function halt(state: Failure['state'], subject: string): never { + throw new Halt({ state, subject }) +} +function corrupt(subject: string): never { halt('corrupt', subject) } +const nonEmpty = (value: unknown): value is string => + typeof value === 'string' && value.length > 0 && !value.includes('\0') +const populated = (value: unknown): boolean => + Array.isArray(value) && value.length > 0 +const orderPos = ( + left: IndexRange['start'], right: IndexRange['start'], +): number => left.line - right.line || left.column - right.column +function range(value: unknown): value is IndexRange { + if (!value || typeof value !== 'object') return false + const candidate = value as IndexRange + return [candidate.start, candidate.end].every((position) => + position && typeof position === 'object' + && Number.isSafeInteger(position.line) && position.line > 0 + && Number.isSafeInteger(position.column) && position.column > 0) + && orderPos(candidate.start, candidate.end) <= 0 +} +const contains = (outer: IndexRange, inner: IndexRange): boolean => + orderPos(outer.start, inner.start) <= 0 + && orderPos(inner.end, outer.end) <= 0 +const sameRange = (left: IndexRange, right: IndexRange): boolean => + orderPos(left.start, right.start) === 0 + && orderPos(left.end, right.end) === 0 +const location = (value: IndexRange): string => + `L${value.start.line}${value.start.line === value.end.line + ? '' : `-L${value.end.line}`}` +function lineOffsets(text: string): [number[], number[]] { + const starts = [0] + const ends: number[] = [] + for (const match of text.matchAll(/\r\n|[\n\r\u2028\u2029]/g)) { + ends.push(match.index) + starts.push(match.index + match[0].length) + } + ends.push(text.length) + return [starts, ends] +} +function offset(source: ReadySource, line: number, column: number): number | null { + const start = source[3][line - 1], end = source[4][line - 1], + result = start === undefined ? 0 : start + column - 1 + return start === undefined || end === undefined || result > end ? null : result +} +function excerpt(source: ReadySource, value: IndexRange): string | null { + const start = offset(source, value.start.line, value.start.column), + end = offset(source, value.end.line, value.end.column) + return start === null || end === null || end < start + ? null + : source[2].slice(start, end) +} +function ready(i: ReadyQueryIndex, input: EvidenceHydrationTargets): HydratedEvidenceResult { + const ids = (values: readonly string[], subject: string): string[] => { + if (!Array.isArray(values) || values.some((value) => !nonEmpty(value))) corrupt(subject) + return [...new Set(values)].sort(compare) + } + const symbols = ids(input.symbolIds, 'symbol targets') + const decls = ids(input.declarationSymbolIds, 'declaration targets') + const ops = ids(input.operationIds, 'operation targets') + const validations = ids(input.validationOperationIds ?? [], 'validation operation targets') + for (const id of decls) if (!symbols.includes(id)) corrupt(id) + if (!Array.isArray(input.edges)) corrupt('edge targets') + const edges = new Map() + for (const edge of input.edges) { + if (!edge || !nonEmpty(edge.id) || !nonEmpty(edge.fromId) || !nonEmpty(edge.toId) + || edge.relation !== undefined && !nonEmpty(edge.relation)) corrupt('edge targets') + const prior = edges.get(edge.id)?.[0] + if (prior && (prior.fromId !== edge.fromId || prior.toId !== edge.toId + || prior.relation !== edge.relation)) corrupt(edge.id) + if (!prior) edges.set(edge.id, [edge]) + } + const d = new Set(decls), o = new Set([...ops, ...validations]) + const s = new Map(), f = new Map() + const e = new Map(), x = new Map() + const p = new Map(), u = new Set() + function node(id: string): GraphAttributes { + if (!i.graph.hasNode(id)) corrupt(id) + return i.graph.nodeAttributes(id) + } + function src(path: string): ReadySource { + const cached = s.get(path) + if (cached) return cached + const expected = i.file_hashes.get(path) + if (expected === undefined) halt('stale', path) + if (!SHA.test(expected)) corrupt(path) + let root: string, candidate: string, bytes: Buffer + try { + root = realpathSync(i.root_path) + candidate = realpathSync(resolve(root, path)) + const rel = relative(root, candidate) + if (isAbsolute(path) || rel === '..' + || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { + halt('unavailable', path) + } + bytes = readFileSync(candidate) + } catch { + halt('unavailable', path) + } + const actual = createHash('sha256').update(bytes).digest('hex') + if (actual !== expected) halt('stale', path) + if (!isUtf8(bytes)) corrupt(path) + const text = bytes.toString('utf8') + const file = `f${f.size}` + const result: ReadySource = [path, expected, text, ...lineOffsets(text), file] + f.set(path, [file, expected]) + s.set(path, result) + return result + } + function proof( + source: ReadySource, value: IndexRange, + expected?: string, subject = source[0], store = true, + ): string { + const text = excerpt(source, value) + if (text === null) corrupt(subject) + const actual = createHash('sha256').update(text, 'utf8').digest('hex') + if (expected !== undefined && (!SHA.test(expected) || actual !== expected)) { + corrupt(subject) + } + if (!store) return '' + const key = `${source[1]}\0${value.start.line}:${value.start.column}:${value.end.line}:${value.end.column}\0${actual}` + const cached = x.get(key) + if (cached) return cached[0] + const alias = `x${x.size}` + x.set(key, [alias, source[5], value, actual, text]) + return alias + } + function entity(id: string, allowChannel = false): string { + const cached = e.get(id) + if (cached) { + if (!allowChannel && cached[1] === 'channel') corrupt(id) + return cached[0] + } + const attrs = node(id) + const alias = `e${e.size}` + if (attrs.node_kind === 'channel') { + if (!allowChannel) corrupt(id) + const channel = i.channels_by_id.get(id) + if (!channel || channelFields.some((field) => attrs[field] !== channel[field])) { + corrupt(id) + } + e.set(id, [ + alias, 'channel', channel.channel_kind, channel.transport, channel.key, + channel.parent_channel_id, channel.scope, + ]) + return alias + } + const { + node_kind: nodeKind, label, source_file: path, + definition_range: definition, declaration_range: declaration, + } = attrs + if (!nonEmpty(nodeKind) || nodeKind === 'file' + || !nonEmpty(label) || !nonEmpty(path) + || !range(definition) || !range(declaration) + || !contains(definition, declaration) + || !populated(attrs.provenance) + || attrs.line_number !== definition.start.line + || attrs.end_line_number !== definition.end.line + || attrs.source_location !== location(definition)) { + corrupt(id) + } + const source = src(path) + if (excerpt(source, definition) === null) halt('stale', path) + const declProof = d.has(id) ? proof(source, declaration) : undefined + e.set(id, [alias, 'symbol', label, nodeKind, source[5]]) + if (declProof) { + p.set(id, [`p${p.size}`, 'declaration', alias, declProof]) + u.add(alias) + } + return alias + } + function fact(value: IndexBodyFact, exactOnly: boolean, store = true): FactProof { + const { owner_symbol_id: ownerId, evidence } = value + const hadOwner = e.has(ownerId) + const ownerKey = entity(ownerId) + if ((!exactOnly || !store) && !hadOwner) e.delete(ownerId) + const targetId = value.kind === 'call' ? value.target_symbol_id : undefined + if (targetId && ['channel', 'file'].includes(String(node(targetId).node_kind))) { + corrupt(targetId) + } + if (targetId && e.has(targetId)) u.add(entity(targetId)) + const owner = node(ownerId) + const definition = owner.definition_range + if (!range(definition)) corrupt(value.id) + const file = i.graph.hasNode(evidence.file_id) + ? i.graph.nodeAttributes(evidence.file_id) : null + const path = owner.source_file + const owns = (id: string): boolean => { + const target = i.operation_by_id.get(id) + return (!exactOnly || o.has(id)) && target?.owner_symbol_id === ownerId + } + const invalidRefs = value.control.some((frame) => + frame.kind !== 'exception' && !owns(frame.controller_fact_id)) + || value.kind === 'parallel' && value.member_fact_ids.some((id) => !owns(id)) + || value.kind === 'persistence' && !owns(value.call_fact_id) + if (!file || file.node_kind !== 'file' + || !nonEmpty(path) || file.source_file !== path + || file.content_hash !== i.file_hashes.get(path) + || !range(evidence.range) || !range(evidence.statement_range) + || !contains(definition, evidence.statement_range) + || !contains(evidence.statement_range, evidence.range) + || invalidRefs) { + corrupt(value.id) + } + const source = src(path) + const controlled = ['condition', 'loop', 'parallel'].includes(value.kind) + const statement = proof( + source, evidence.statement_range, evidence.excerpt_sha256, value.id, + store && !controlled, + ) + return [ownerKey, controlled ? proof(source, evidence.range, undefined, value.id, store) + : statement] + } + function edge(target: SelectedEvidenceEdge, row: EdgeRow): void { + const [from, to, attrs, id] = row + const { + relation, source_file: path, evidence, execution_owner_id: ownerId, + source_location: sourceLocation, + } = attrs + if (id !== target.id || from !== target.fromId || to !== target.toId + || !nonEmpty(relation) + || target.relation !== undefined && relation !== target.relation) { + corrupt(target.id) + } + const fromKey = entity(from, true) + const toKey = entity(to, true) + if (!nonEmpty(path) || !populated(attrs.provenance) + || !evidence || typeof evidence !== 'object' || Array.isArray(evidence)) { + corrupt(id) + } + const raw = evidence as Record + const { + range: at, source: proofKind, statement_range: statement, + excerpt_sha256: hash, + } = raw + if (![ + 'typescript-semantic', 'typescript-syntactic', + 'framework-decorator', 'wrapper-summary', + ].includes(proofKind as string) || !range(at)) corrupt(id) + if (relation === 'calls') { + if (Object.keys(raw).length !== 2 || ownerId !== undefined + || sourceLocation !== location(at)) corrupt(id) + const excerptKey = selectedCall(id, from, (call) => + call.target_symbol_id === to && sameRange(call.evidence.range, at) + && proofKind === (call.source === 'framework' + ? 'framework-decorator' : call.source)) + if (path !== node(from).source_file) corrupt(id) + return record(id, fromKey, toKey, relation, excerptKey) + } + if (Object.keys(raw).length !== 4 || !range(statement) + || !contains(statement, at) || !nonEmpty(hash) + || sourceLocation !== location(statement)) corrupt(id) + if (!nonEmpty(ownerId)) corrupt(id) + const ownerAttrs = node(ownerId) + if (ownerAttrs.node_kind === 'channel' || ownerAttrs.node_kind === 'file' + || ownerAttrs.source_file !== path || !range(ownerAttrs.definition_range) + || !contains(ownerAttrs.definition_range, statement)) corrupt(id) + const fromChannel = i.channels_by_id.get(from) + const toChannel = i.channels_by_id.get(to) + const valid = relation === 'publishes_to' + ? ownerId === from && !fromChannel && !!toChannel + : relation === 'routes_through' + ? fromChannel?.channel_kind === 'job' + && toChannel?.channel_kind === 'queue' + && fromChannel.parent_channel_id === to + && fromChannel.transport === toChannel.transport + : relation === 'consumed_by' + && !!fromChannel && !toChannel + if (!valid) corrupt(id) + if (relation === 'consumed_by' && ownerId !== to) { + selectedCall(id, ownerId, (call) => + sameRange(statement, call.evidence.statement_range) + && call.evidence.excerpt_sha256 === hash) + } + const source = src(path) + record(id, fromKey, toKey, relation, proof(source, statement, hash, id)) + } + function selectedCall( + edgeId: string, ownerId: string, accepts: (fact: CallFact) => boolean, + ): string { + const accepted = (call: CallFact): boolean => + call.owner_symbol_id === ownerId && accepts(call) + const matches = (i.operations_by_owner.get(ownerId) ?? []) + .filter((value): value is CallFact => value.kind === 'call' + && accepted(value)) + if (matches.length !== 1) corrupt(edgeId) + const match = matches[0]! + const indexed = i.operation_by_id.get(match.id) + if (indexed?.kind !== 'call' || !accepted(indexed) + || !sameRange(indexed.evidence.range, match.evidence.range) + || !sameRange(indexed.evidence.statement_range, match.evidence.statement_range)) { + corrupt(edgeId) + } + return fact(indexed, false)[1] + } + function record( + id: string, fromKey: string, toKey: string, relation: string, excerptKey: string, + ): void { + p.set(id, [`p${p.size}`, 'edge', fromKey, toKey, relation, excerptKey]) + u.add(fromKey).add(toKey) + } + for (const id of symbols) entity(id) + for (const id of validations) { + const value = i.operation_by_id.get(id) + if (!value || value.id !== id) corrupt(id) + fact(value, true, false) + } + for (const id of ops) { + const value = i.operation_by_id.get(id) + if (!value || value.id !== id) corrupt(id) + const hydrated = fact(value, true) + const alias = `e${e.size}` + e.set(id, [alias, 'operation', hydrated[0], value]) + p.set(id, [`p${p.size}`, 'operation', alias, hydrated[1]]) + u.add(hydrated[0]) + } + try { + for (const row of i.graph.edgeEntries()) { + const selected = edges.get(row[3]) + if (selected) selected[1] = selected[1] === undefined ? row : null + } + } catch { + corrupt('selected edges') + } + for (const [, [target, row]] of [...edges].sort((a, b) => compare(a[0], b[0]))) { + if (!row) corrupt(target.id) + edge(target, row) + } + for (const [id, entry] of e) { + if (entry[1] === 'symbol' && !u.has(entry[0])) corrupt(id) + } + return { state: 'ready', files: f, excerpts: x, entities: e, proofs: p } +} +export function hydrateEvidence( + index: QueryIndex, + input: EvidenceHydrationTargets, +): HydratedEvidenceResult { + try { + if (index.state !== 'ready') return { state: index.state, subject: index.subject } + return ready(index, input) + } catch (error) { + return error instanceof Halt + ? error.value : { state: 'corrupt', subject: 'evidence hydration' } + } +} diff --git a/src/application/retrieve-context.ts b/src/application/retrieve-context.ts index 51021e1a..dd59be51 100644 --- a/src/application/retrieve-context.ts +++ b/src/application/retrieve-context.ts @@ -1,381 +1,618 @@ -import { createHash } from 'node:crypto' -import { readFileSync, realpathSync } from 'node:fs' -import { isAbsolute, relative, resolve, sep } from 'node:path' -import { TextDecoder } from 'node:util' +import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' -import { canonicalJsonString } from '../domain/graph/canonical-json.js' -import type { GraphAttributes } from '../domain/graph/directed-multigraph.js' -import { type QueryIndex, type ReadyQueryIndex } from '../domain/query/index-status.js' -import type { IndexRange } from '../domain/index/model.js' -import { rankQueryAnchors } from '../domain/query/rank.js' -import { sliceEvidence } from '../domain/query/slice.js' -import { traverseEvidencePaths } from '../domain/query/traverse.js' import { - normalizeRetrieveRequest, type EvidenceBoundary, type EvidenceNode, - type EvidenceRelationship, type NormalizedRetrieveRequest, type QueryPathEdge, - type RetrieveContextResult, type RetrieveOutcome, + hydrateEvidence, +} from './evidence-hydrator.js' +import { canonicalJsonString, compareCodeUnits as compare } from '../domain/graph/canonical-json.js' +import type { IndexBodyFact, IndexValue } from '../domain/index/model.js' +import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' +import { planQuestion } from '../domain/query/plan.js' +import { + selectWorkflow, +} from '../domain/query/workflow.js' +import { + MAX_RETRIEVE_EXCERPTS, + MAX_RETRIEVE_FILES, + RETRIEVE_RESULT_SCHEMA, + RETRIEVE_RESULT_VERSION, + normalizeRetrieveRequest, + valueHas, + type AnswerDossier, + type DossierEntity, + type DossierLink, + type DossierOrderGroup, + type DossierProof, + type HydratedEvidenceResult, + type MissingRequirement, + type NormalizedRetrieveRequest, + type QuerySummary, + type QueryPlan, + type RetrieveContextResult, + type RetrieveMetrics, + type WorkflowSelection, } from '../domain/query/types.js' -type AuthenticatedSource = { - state: 'ready' - text: string - lineStarts: readonly number[] - lineEnds: readonly number[] - proofHashes: Map +type ReadyHydration = Extract +type InternalHydration = HydratedEvidenceResult +type DossierBuild = { state: 'ready'; dossier: AnswerDossier } + | { state: 'incomplete'; missing: MissingRequirement } + | { state: 'corrupt'; subject: string } + +function missingBuild( + code: 'required_proof_missing' | 'required_reference_missing', + target: string, + obligationId?: string, +): DossierBuild { + return { state: 'incomplete', missing: { + code, target, ...(obligationId ? { obligation_id: obligationId } : {}), + } } } - | { state: 'stale' | 'unavailable'; subject: string } -type AuthenticatedNode = { state: 'ready'; node: EvidenceNode } - | { state: 'corrupt' | 'stale' | 'unavailable'; subject: string } -type ChannelProof = readonly [edgeId: string, attributes: GraphAttributes] - -const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }) -const proofCache = new WeakMap< -ReadyQueryIndex, -Map ->() -function validLine(value: unknown): value is number { - return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 +function limitMissing( + code: 'required_file_limit' | 'required_excerpt_limit' | 'required_token_budget', + required: number, + limit: number, +): readonly MissingRequirement[] { + return [{ code, required, limit }] } -function stringFact(attrs: GraphAttributes, key: string): string | null { - const value = attrs[key] - return typeof value === 'string' && value.length > 0 ? value : null +const VALID_LINKS = new Set([ + 'direct:calls', + 'channel:publishes_to,consumed_by', + 'channel:publishes_to,routes_through,consumed_by', +]) + +function metrics( + request: NormalizedRetrieveRequest, + selection?: WorkflowSelection, + hydration?: ReadyHydration, + failed = new Set(), +): RetrieveMetrics { + const required = selection?.obligations.filter(({ mandatory }) => mandatory) ?? [] + const source = selection?.metrics + return { + budget_tokens: request.budget, + serialized_tokens: 0, + selected_files: hydration?.files.size ?? 0, + authenticated_excerpts: hydration?.excerpts.size ?? 0, + required_obligations: required.length, + proven_obligations: required.filter(({ proven, id }) => proven && !failed.has(id)).length, + optional_bundles_omitted: 0, + root_candidates: source?.rootCandidateCount ?? 0, + initial_candidates: source?.candidateCount ?? 0, + explored_nodes: source?.actualNodeCount ?? 0, + causal_hops: source?.causalRelationHops ?? 0, + recovery_passes: source?.recoveryPasses ?? 0, + recovery_frontier_nodes: source?.recoveryFrontierCount ?? 0, + alternate_seeds: Math.max(0, (source?.rootCandidateCount ?? 0) - 1), + } } -function insideRoot(root: string, source: string): boolean { - const path = relative(root, source) - return path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path) +function stabilize(input: T): T { + input.metrics.serialized_tokens = 0 + const body = countTokens(canonicalJsonString(input)) - countTokens('0') + const estimate = body + countTokens(String(body)) + input.metrics.serialized_tokens = body + countTokens(String(estimate)) + return input } -function readSource( - index: ReadyQueryIndex, file: string, cache: Map, -): AuthenticatedSource { - const cached = cache.get(file) - if (cached) return cached - const remember = (result: AuthenticatedSource): AuthenticatedSource => { - cache.set(file, result) - return result - } - - const expected = index.file_hashes.get(file) - if (!expected) return remember({ state: 'stale', subject: file }) +function header( + state: S, + request: NormalizedRetrieveRequest, + selection?: WorkflowSelection, + hydration?: ReadyHydration, + failed?: Set, +): { schema: typeof RETRIEVE_RESULT_SCHEMA; version: typeof RETRIEVE_RESULT_VERSION + state: S; metrics: RetrieveMetrics } { + return { schema: RETRIEVE_RESULT_SCHEMA, version: RETRIEVE_RESULT_VERSION, + state, metrics: metrics(request, selection, hydration, failed) } +} - try { - const root = realpathSync(index.root_path) - const candidate = realpathSync(resolve(root, file)) - if (isAbsolute(file) || !insideRoot(root, candidate)) { - return remember({ state: 'unavailable', subject: file }) - } - const bytes = readFileSync(candidate) - const actual = createHash('sha256').update(bytes).digest('hex') - if (actual !== expected) { - return remember({ state: 'stale', subject: file }) - } - const text = utf8.decode(bytes) - const lines = lineOffsets(text) - return remember({ - state: 'ready', text, lineStarts: lines.starts, lineEnds: lines.ends, - proofHashes: new Map(), - }) - } catch { - return remember({ state: 'unavailable', subject: file }) - } +function query(plan: QueryPlan): QuerySummary { + return { intent: plan.intent, subject: plan.subject, terms: plan.terms } } -function lineOffsets(text: string): { - starts: readonly number[] - ends: readonly number[] -} { - const starts = [0], ends: number[] = [] - for (let index = 0; index < text.length; index += 1) { - const code = text.charCodeAt(index) - if (![10, 13, 0x2028, 0x2029].includes(code)) continue - ends.push(index) - if (code === 13 && text.charCodeAt(index + 1) === 10) index += 1 - starts.push(index + 1) +type TerminalResult = Exclude +function fitTerminal(input: TerminalResult, budget: number): TerminalResult { + const result = stabilize(input) + if (result.metrics.serialized_tokens <= budget) return result + if (result.state === 'incomplete') { + result.query.subject = result.query.subject.slice(0, 32) + result.query.terms = [] + result.missing = result.missing.map(({ + code, obligation_id, required, limit, + }) => ({ + code, + ...(obligation_id ? { obligation_id } : {}), + ...(required === undefined ? {} : { required }), + ...(limit === undefined ? {} : { limit }), + })) + } else if (result.state === 'unsupported') result.terms = [] + else result.failures = result.failures.map(({ state, subject }) => + ({ state, subject: subject.slice(0, 32) })) + stabilize(result) + if (result.metrics.serialized_tokens > budget && result.state === 'incomplete') { + result.query.subject = '' + stabilize(result) } - ends.push(text.length) - return { starts, ends } + if (result.metrics.serialized_tokens > budget) return stabilize({ + schema: result.schema, version: result.version, state: 'corrupt', + metrics: result.metrics, + failures: [{ state: 'corrupt', subject: 'terminal result budget' }], + }) + return result } -function offset( - source: Extract, - pos: IndexRange['start'], -): number | null { - if (!Number.isSafeInteger(pos.line) || pos.line < 1 - || !Number.isSafeInteger(pos.column) || pos.column < 1) return null - const start = source.lineStarts[pos.line - 1] - const end = source.lineEnds[pos.line - 1] - if (start === undefined || end === undefined) return null - const offset = start + pos.column - 1 - return offset <= end ? offset : null +function declarationTargets( + plan: QueryPlan, selection: WorkflowSelection, index: ReadyQueryIndex, +): string[] { + const required = plan.intent === 'locate' ? selection.symbolIds.slice(0, 1) : [] + const subject = selection.obligations.find(({ kind, proven }) => + kind === 'subject' && proven) + if (plan.intent === 'explain' && subject) required.push(...subject.symbolIds.slice(0, 1)) + const incident = new Set(selection.edges.flatMap(({ fromId, toId }) => [fromId, toId])) + for (const id of selection.operationIds) { + const owner = index.operation_by_id.get(id)?.owner_symbol_id + if (owner) incident.add(owner) + } + required.push(...selection.symbolIds.filter((id) => !incident.has(id))) + return [...new Set(required)].sort(compare) } -function validRange(value: unknown): value is IndexRange { - if (!value || typeof value !== 'object') return false - const range = value as IndexRange - return positionKey(range.start) <= positionKey(range.end) +function emittedOperations( + plan: QueryPlan, selection: WorkflowSelection, index: ReadyQueryIndex, +): string[] { + const path = new Set([ + ...selection.rootSymbolIds, ...selection.terminalSymbolIds, + ...selection.links.flatMap(({ fromId, toId }) => [fromId, toId]), + ]) + const result = new Set(selection.operationIds.filter((id) => { + const fact = index.operation_by_id.get(id) + return fact && (plan.intent !== 'workflow' + || !['condition', 'loop', 'parallel'].includes(fact.kind)) + && (fact.kind !== 'call' || path.has(fact.owner_symbol_id)) + })) + for (const id of result) { + const fact = index.operation_by_id.get(id) + if (fact?.kind === 'persistence') result.add(fact.call_fact_id) + if (fact?.kind === 'parallel') fact.member_fact_ids.forEach((member) => result.add(member)) + } + return [...result].sort(compare) } -function positionKey(pos: IndexRange['start'] | undefined): number { - return pos && Number.isSafeInteger(pos.line) && pos.line > 0 - && Number.isSafeInteger(pos.column) && pos.column > 0 - ? pos.line * 0x1_0000_0000 + pos.column - : Number.NaN +function incomplete( + request: NormalizedRetrieveRequest, + plan: QueryPlan, + missing: readonly MissingRequirement[], + selection?: WorkflowSelection, + hydration?: ReadyHydration, + packedFailure = false, +): RetrieveContextResult { + const failed = new Set(missing.flatMap((entry) => + entry.obligation_id ? [entry.obligation_id] : [])) + if (packedFailure && failed.size === 0) { + for (const entry of selection?.obligations ?? []) if (entry.mandatory) failed.add(entry.id) + } + return fitTerminal({ + ...header('incomplete', request, selection, hydration, failed), + query: query(plan), + missing, + }, request.budget) } -function excerpt( - source: Extract, - range: IndexRange, -): string | null { - const start = offset(source, range.start) - const end = offset(source, range.end) - return start === null || end === null || end < start - ? null - : source.text.slice(start, end) +function failure( + request: NormalizedRetrieveRequest, + state: 'stale' | 'unavailable' | 'corrupt', + subject: string, + selection?: WorkflowSelection, +): RetrieveContextResult { + return fitTerminal({ + ...header(state, request, selection), + failures: [{ state, subject: subject.slice(0, 96) }], + }, request.budget) } -function checkFactProofs( - index: ReadyQueryIndex, - ownerId: string, - source: Extract, -): boolean { - for (const fact of index.operations_by_owner.get(ownerId) ?? []) { - if (!proofMatches( - source, - fact.evidence.statement_range, - fact.evidence.excerpt_sha256, - )) return false +function projectValue(value: IndexValue, entity: (id: string) => string | undefined): object { + if (value.kind === 'symbol') { + const id = entity(value.symbol_id) + return id ? { kind: 'symbol', entity: id } : { kind: 'unknown', reason: 'outside_dossier' } } - return true + if (value.kind === 'array' || value.kind === 'template') { + const entries = value.kind === 'array' ? value.elements : value.parts + return { + kind: value.kind, + [value.kind === 'array' ? 'elements' : 'parts']: + entries.map((entry) => projectValue(entry, entity)), + } + } + if (value.kind === 'object') { + return { kind: 'object', entries: value.entries.map(({ key, value: entry }) => ({ + key, + value: projectValue(entry, entity), + })) } + } + return value } -function proofMatches( - source: Extract, - range: IndexRange, - expected: string, -): boolean { - const key = `${range.start.line}:${range.start.column}:${ - range.end.line}:${range.end.column}` - let actual = source.proofHashes.get(key) - if (!actual) { - const proofText = excerpt(source, range) - if (proofText === null) return false - actual = createHash('sha256').update(proofText, 'utf8').digest('hex') - source.proofHashes.set(key, actual) +type ReferenceKey = 'rootSymbolIds' | 'terminalSymbolIds' | 'symbolIds' + | 'operationIds' | 'edgeIds' | 'controllerOperationId' | 'fromId' | 'toId' +const DETAIL_FIELDS = { + literal: ['role'], condition: ['condition_kind'], loop: ['loop_kind'], + parallel: ['combinator', 'completion', 'lane_count'], + return: [], throw: [], mutation: ['operation', 'target'], + persistence: ['operation', 'receiver_type'], +} as const + +function operationDetail( + fact: IndexBodyFact, + entity: (id: string) => string | undefined, +): Readonly> { + const value = (entry: IndexValue | undefined): unknown => + entry === undefined ? undefined : projectValue(entry, entity) + if (fact.kind === 'call') { + const target = fact.target_symbol_id ? entity(fact.target_symbol_id) : undefined + const arguments_ = fact.arguments.some((entry) => valueHas( + entry, (candidate) => candidate.kind === 'literal', + )) ? fact.arguments.map((entry) => projectValue(entry, entity)) : undefined + return { order: fact.order, callee: fact.callee, scheduling: fact.scheduling, + ...(target ? { target } : {}), ...(arguments_ ? { arguments: arguments_ } : {}) } + } + const result: Record = { order: fact.order } + const raw = fact as unknown as Record + for (const key of DETAIL_FIELDS[fact.kind]) result[key] = raw[key] + if (fact.kind === 'parallel') { + result.members = fact.member_fact_ids.map((id) => entity(id)!) + if (fact.input !== undefined) result.input = value(fact.input) + } else if (fact.kind === 'persistence') { + result.call = entity(fact.call_fact_id)! + if (fact.resource !== undefined) result.resource = value(fact.resource) + } else if (['literal', 'return', 'throw', 'mutation'].includes(fact.kind)) { + const item = raw.value as IndexValue | undefined + if (item !== undefined) result.value = value(item) + } + else { + const test = value(raw.test as IndexValue | undefined) + if (fact.kind === 'loop' || test && (typeof test !== 'object' + || !('kind' in test) || test.kind !== 'unknown')) result.test = test } - return actual === expected + return result } -function channelProofs(index: ReadyQueryIndex, ownerId: string): readonly ChannelProof[] { - let byOwner = proofCache.get(index) - if (!byOwner) { - byOwner = new Map() - for (const [, , attrs, edgeId] of index.graph.edgeEntries()) { - const owner = attrs.execution_owner_id - if (typeof owner !== 'string' - || !['publishes_to', 'routes_through', 'consumed_by'] - .includes(String(attrs.relation))) continue - const proofs = byOwner.get(owner) ?? [] - proofs.push([edgeId, attrs]) - byOwner.set(owner, proofs) - } - proofCache.set(index, byOwner) - } - return byOwner.get(ownerId) ?? [] +function statement( + obligation: WorkflowSelection['obligations'][number], + plan: QueryPlan, +): string { + const kind = obligation.kind + return kind === 'subject' ? `${plan.subject}.` + : kind === 'handoff' ? 'Handoffs proven.' + : kind === 'behavior' ? 'Operations proven.' + : kind === 'ordering' ? 'Order proven.' : `${kind} proven.` } -function checkChannelProofs( +function buildDossier( + plan: QueryPlan, + selection: WorkflowSelection, + hydration: ReadyHydration, + emittedOperationIds: readonly string[], index: ReadyQueryIndex, - ownerId: string, - sources: Map, -): { state: 'ready' } | { state: 'corrupt' | 'stale' | 'unavailable'; subject: string } { - for (const [edgeId, attrs] of channelProofs(index, ownerId)) { - const file = attrs.source_file - const evidence = attrs.evidence as Record | undefined - const range = evidence?.statement_range - const expected = evidence?.excerpt_sha256 - if (typeof file !== 'string' || !validRange(range) - || typeof expected !== 'string') { - return { state: 'corrupt', subject: edgeId } +): DossierBuild { + const { + symbolIds, operationIds: allOperationIds, rootSymbolIds: rootIds, + terminalSymbolIds: terminalIds, edges: selectedEdges, + links: selectedLinks, controlGroups: groups, + obligations: selectedObligations, + } = selection + const lookup = (canonical: string): string | undefined => + hydration.entities.get(canonical)?.[0] + const entity = (canonical: string): string => lookup(canonical)! + const edge = (id: string) => { + const proof = hydration.proofs.get(id) + return proof?.[1] === 'edge' ? proof : undefined + } + const proofs: DossierProof[] = [] + for (const proof of hydration.proofs.values()) if (proof[1] === 'edge') { + proofs.push({ + id: proof[0], from: proof[2], to: proof[3], + relation: proof[4], excerpt: proof[5], + }) + } + for (const [canonical, item] of hydration.entities) { + if (item[1] === 'channel' && item[5] && !hydration.entities.has(item[5])) { + return missingBuild('required_reference_missing', item[5]) } - const source = readSource(index, file, sources) - if (source.state !== 'ready') return source - if (!proofMatches(source, range, expected)) { - return { state: 'corrupt', subject: edgeId } + if (item[1] === 'operation' && hydration.proofs.get(canonical)?.[1] !== 'operation') { + return missingBuild('required_proof_missing', canonical) } } - return { state: 'ready' } -} - -function authenticateNode( - index: ReadyQueryIndex, nodeId: string, sources: Map, -): AuthenticatedNode { - if (!index.graph.hasNode(nodeId)) return { state: 'corrupt', subject: nodeId } - const attrs = index.graph.nodeAttributes(nodeId) - const label = stringFact(attrs, 'label') - const nodeKind = stringFact(attrs, 'node_kind') - const file = stringFact(attrs, 'source_file') - const location = stringFact(attrs, 'source_location') - const provenance = attrs.provenance - const contentHash = file ? index.file_hashes.get(file) : undefined - - if (!label || !nodeKind || !file - || !Array.isArray(provenance) || provenance.length === 0 || !contentHash) { - return { state: 'corrupt', subject: nodeId } + const missingEntity = [...symbolIds, ...emittedOperationIds] + .find((id) => !hydration.entities.has(id)) + const missing = missingEntity + ?? selectedEdges.find((item) => !edge(item.id))?.id + if (missing) return missingBuild( + missingEntity ? 'required_reference_missing' : 'required_proof_missing', missing, + ) + const selected: Record = { + rootSymbolIds: symbolIds, terminalSymbolIds: symbolIds, + symbolIds, fromId: symbolIds, toId: symbolIds, + operationIds: allOperationIds, controllerOperationId: allOperationIds, + edgeIds: selectedEdges.map((edge) => edge.id), } - - const source = readSource(index, file, sources) - if (source.state !== 'ready') return source - if (!checkFactProofs(index, nodeId, source)) { - return { state: 'corrupt', subject: nodeId } + let forged = [...rootIds, ...terminalIds].find((id) => !symbolIds.includes(id)) + for (const row of [ + ...selectedLinks, ...groups, ...selectedObligations, + ]) for (const [key, value] of Object.entries(row)) { + const allowed = selected[key as ReferenceKey] + if (allowed) forged ??= [value].flat() + .find((id) => !allowed.includes(id as string)) as string | undefined } - const channelProof = checkChannelProofs( - index, - nodeId, - sources, - ) - if (channelProof.state !== 'ready') return channelProof - const domain = stringFact(attrs, 'source_domain') - const common = { - node_id: nodeId, label, source_file: file, provenance, - content_hash: contentHash, - ...(domain ? { source_domain: domain } : {}), + if (forged) return { state: 'corrupt', subject: forged } + const links: DossierLink[] = [] + for (const [index, link] of selectedLinks.entries()) { + const chain = link.edgeIds.map((id) => edge(id)!) + const from = entity(link.fromId), to = entity(link.toId) + const joined = chain.every((proof, proofIndex) => + proof[2] === (proofIndex === 0 ? from : chain[proofIndex - 1]![3])) + && chain.at(-1)![3] === to + if (!VALID_LINKS.has( + `${link.kind}:${chain.map((proof) => proof[4]).join(',')}`, + ) || !joined) { + return { state: 'corrupt', subject: `${link.fromId}->${link.toId}` } + } + const id = `l${index + 1}` + links.push({ + id, kind: link.kind, from, to, + proofs: [...new Set(chain.map((proof) => proof[0]))], + }) } - if (nodeKind === 'file') { - return { state: 'ready', node: { ...common, evidence_kind: 'structural_file', node_kind: 'file' } } + const entities: DossierEntity[] = [] + const ownerProof = new Map() + const persisted = new Set() + for (const [canonical, item] of hydration.entities) { + const alias = item[0] + const proof = hydration.proofs.get(canonical) + const excerpt = proof?.[1] !== 'edge' ? proof?.[3] : undefined + if (item[1] === 'symbol') { + entities.push({ + id: alias, kind: 'symbol', label: item[2], + ...(/^(?:function|method|class)$/u.test(item[3]) + ? {} : { node_kind: item[3] }), + file: item[4], + ...(excerpt ? { excerpt } : {}), + }) + } else if (item[1] === 'channel') { + const parent = item[5] ? entity(item[5]) : undefined + entities.push({ + id: alias, kind: 'channel', channel_kind: item[2], + transport: item[3], key: item[4], + ...(parent ? { parent } : {}), + ...(item[6] ? { scope: item[6] } : {}), + }) + } else { + const fact = item[3] + const linked = selectedLinks.flatMap((link, index) => + link.operationIds.includes(canonical) ? [index] : []) + if (fact.kind === 'call' && linked.length > 0) { + if (linked.some((index) => selectedLinks[index]!.fromId !== fact.owner_symbol_id)) { + return { state: 'corrupt', subject: canonical } + } + entities.push({ + id: alias, kind: 'operation', links: linked.map((index) => `l${index + 1}`), + order: fact.order, excerpt: excerpt!, + ...(linked.some((index) => selectedLinks[index]!.kind === 'channel') + ? { callee: fact.callee } : {}), + ...(fact.scheduling === 'sync' ? {} : { scheduling: fact.scheduling }), + }) + } else { + const detail = operationDetail(fact, lookup) + entities.push({ + id: alias, kind: 'operation', operation_kind: fact.kind, + owner: item[2], excerpt: excerpt!, detail, + }) + } + ownerProof.set(item[2], fact.kind === 'persistence' + ? alias : ownerProof.get(item[2]) ?? alias) + if (fact.kind === 'persistence') persisted.add(alias) + } } - - const startLine = attrs.line_number - const endLine = attrs.end_line_number - const definition = attrs.definition_range - const declaration = attrs.declaration_range - if (!location || !validLine(startLine) || !validLine(endLine) - ) return { state: 'corrupt', subject: nodeId } - if (!validRange(definition) || !validRange(declaration) - || positionKey(declaration.start) < positionKey(definition.start) - || positionKey(declaration.end) > positionKey(definition.end)) { - return { state: 'stale', subject: file } + const cover = (canonicalIds: readonly string[]): string[] => { + const remaining = new Map(canonicalIds.map((id) => [entity(id), id])) + const result: string[] = [] + for (const proof of hydration.proofs.values()) { + if (proof[1] !== 'edge' || !remaining.has(proof[2]) || !remaining.has(proof[3])) continue + result.push(proof[0]); remaining.delete(proof[2]); remaining.delete(proof[3]) + } + for (const [subject, canonical] of remaining) { + const hydrated = hydration.proofs.get(canonical) + const proof = hydrated && hydrated[1] !== 'edge' ? subject : ownerProof.get(subject) + ?? proofs.find((entry) => entry.from === subject || entry.to === subject)?.id + if (!proof) return [] + result.push(proof) + } + return result.sort(compare) } - const expectedLocation = definition.end.line > definition.start.line - ? `L${definition.start.line}-L${definition.end.line}` - : `L${definition.start.line}` - if (startLine !== definition.start.line || endLine !== definition.end.line - || location !== expectedLocation) return { state: 'stale', subject: file } - const snippet = excerpt(source, declaration) - if (snippet === null || excerpt(source, definition) === null) { - return { state: 'stale', subject: file } + const obligations: AnswerDossier['obligations'][number][] = [] + for (const obligation of selectedObligations) { + const claimRefs = obligation.kind === 'handoff' + ? obligation.edgeIds.map((id) => edge(id)![0]) + : obligation.kind === 'stage' && obligation.edgeIds.length > 0 + ? cover(obligation.symbolIds) + : obligation.kind === 'ordering' + ? obligation.operationIds.flatMap((id) => lookup(id) ? [entity(id)] : []) + : obligation.kind === 'behavior' ? obligation.symbolIds.flatMap((id) => { + const subject = entity(id) + const edge = proofs.find((proof) => proof.from === subject) + return edge?.id ?? ownerProof.get(subject) ?? [] + }) + : obligation.kind === 'subject' && plan.intent === 'locate' && plan.access + ? obligation.operationIds.flatMap((id) => lookup(id) ? [entity(id)] : []) + : obligation.kind === 'terminal' + ? obligation.operationIds.flatMap((id) => + lookup(id) ? [entity(id)] : []).filter((proof) => persisted.has(proof)) + : cover(obligation.symbolIds) + const unique = [...new Set(claimRefs)].sort(compare) + if (obligation.mandatory && unique.length === 0) { + return missingBuild('required_proof_missing', obligation.target, obligation.id) + } + obligations.push({ + id: obligation.id, kind: obligation.kind, + statement: statement(obligation, plan), + proofs: unique, + }) } - + const compactGroups = new Map>() + for (const group of groups) { + const controller = group.controllerOperationId + ? lookup(group.controllerOperationId) : undefined + const controllerFact = group.controllerOperationId + ? index.operation_by_id.get(group.controllerOperationId) : undefined + let detail: Readonly> | undefined + if (controllerFact && ['condition', 'loop', 'parallel'].includes(controllerFact.kind)) { + const { order: _order, ...control } = operationDetail(controllerFact, lookup) + detail = control + } + const operationMembers = group.operationIds.flatMap((id) => + lookup(id) ? [entity(id)] : []) + const members = group.kind === 'cycle' + ? group.symbolIds.map(entity) : operationMembers + const groupProofs = [...operationMembers] + if (group.kind === 'cycle') { + groupProofs.push(...links.filter((link) => + members.includes(link.from) + && members.includes(link.to)).flatMap((link) => link.proofs)) + } + if (groupProofs.length === 0) { + return missingBuild('required_proof_missing', group.kind) + } + const preserveOrder = group.kind === 'sequence' + const row: Omit = { + kind: group.kind, + ...(controller ? { controller } : {}), + ...(group.arm ? { arm: group.arm } : {}), + ...(detail ? { detail } : {}), + members: preserveOrder + ? members : [...new Set(members)].sort(compare), + proofs: preserveOrder ? groupProofs : [...new Set(groupProofs)].sort(compare), + } + const key = JSON.stringify([ + row.kind, row.arm, row.controller, row.detail, row.members, row.proofs, + ]) + const prior = compactGroups.get(key) + compactGroups.set(key, prior ? { ...prior, depth: (prior.depth ?? 1) + 1 } : row) + } + const order = [...compactGroups.values()].map((row, index) => + ({ id: `g${index + 1}`, ...row })) + const roots = rootIds.map(entity) + const terminals = terminalIds.map(entity) return { state: 'ready', - node: { - ...common, evidence_kind: 'symbol_declaration', node_kind: nodeKind, - source_location: location, line_number: startLine, end_line_number: endLine, - definition_range: definition, declaration_range: declaration, snippet, + dossier: { + query: query(plan), + obligations, + flow: { + roots, terminals, + links, order, + }, + evidence: { + digest_algorithm: 'sha256-base64url', + files: [...hydration.files].map(([path, [alias, sha256]]) => ({ + id: alias, path, + digest: Buffer.from(sha256, 'hex').toString('base64url'), + })), + excerpts: [...hydration.excerpts.values()].map(([alias, file, range, , text]) => ({ + id: alias, file, + range: [ + range.start.line, range.start.column, + range.end.line, range.end.column, + ] as const, + text, + })), + entities, + proofs, + }, }, } } -function edgeResult(edge: QueryPathEdge): EvidenceRelationship | null { - const file = edge.attributes.source_file - const location = edge.attributes.source_location - const provenance = edge.attributes.provenance - if (!Array.isArray(provenance) || provenance.length === 0) return null - return { - id: edge.id, - from_id: edge.from, - to_id: edge.to, - relation: edge.relation, - ...(typeof file === 'string' && file.length > 0 ? { source_file: file } : {}), - ...(typeof location === 'string' && location.length > 0 ? { source_location: location } : {}), - provenance, - } -} - -function outcome(nodes: readonly EvidenceNode[], boundaries: readonly EvidenceBoundary[]): RetrieveOutcome { - if (nodes.length > 0) return 'evidence' - for (const state of ['corrupt', 'unavailable', 'stale', 'unsupported', 'missing'] as const) { - if (boundaries.some((limit) => limit.kind === state)) return state +function selectionMissing(selection: WorkflowSelection): MissingRequirement[] { + const rows = new Map() + for (const entry of selection.missing) { + const row: MissingRequirement = { + code: entry.code, + ...(entry.obligationId ? { obligation_id: entry.obligationId } : {}), + ...(entry.target.length <= 96 ? { target: entry.target } : {}), + } + rows.set(`${row.code}\0${row.obligation_id ?? ''}\0${row.target ?? ''}`, row) } - return 'missing' -} - -function limit(kind: EvidenceBoundary['kind'], subject: string): EvidenceBoundary { - return { kind, subject } -} - -function empty( - request: NormalizedRetrieveRequest, outcome: RetrieveOutcome, boundaries: EvidenceBoundary[], -): RetrieveContextResult { - return sliceEvidence({ - request, outcome, - matchedNodes: [], - relationships: [], - boundaries, priorityNodeIds: [], closurePasses: 0, - }) + return [...rows.values()] } export function retrieveContext(index: QueryIndex, input: unknown): RetrieveContextResult { const request = normalizeRetrieveRequest(input) - if (index.state !== 'ready') { - return empty(request, index.state, [limit(index.state, index.subject)]) + const planned = planQuestion(request) + if (planned.status === 'unsupported') { + return fitTerminal({ + ...header('unsupported', request), + reason: planned.reason, + terms: planned.terms.slice(0, 8).map((term) => term.slice(0, 32)), + }, request.budget) } - - const ranking = rankQueryAnchors(index, request) - if (ranking.anchors.length === 0) { - const boundaries = ranking.boundaries.length > 0 - ? ranking.boundaries - : [limit('missing', request.question)] - return empty(request, outcome([], boundaries), boundaries) + const plan = planned.plan + if (index.state !== 'ready') return failure(request, index.state, index.subject) + let selection: WorkflowSelection + try { + selection = selectWorkflow(index, plan) + } catch { + return failure(request, 'corrupt', 'workflow selection') } - - const traversal = traverseEvidencePaths(index, ranking) - const sources = new Map() - let matchedNodes: EvidenceNode[] = [] - const boundaries = [...ranking.boundaries, ...traversal.boundaries] - - for (const nodeId of traversal.nodeIds) { - const checked = authenticateNode(index, nodeId, sources) - if (checked.state === 'ready') { - matchedNodes.push(checked.node) - } else { - boundaries.push(limit(checked.state, checked.subject)) - } + let hydration: InternalHydration + const emittedOperationIds = emittedOperations(plan, selection, index) + try { + const validationOperationIds = selection.operationIds.filter((id) => + !emittedOperationIds.includes(id)) + hydration = hydrateEvidence(index, { + symbolIds: selection.symbolIds, + declarationSymbolIds: declarationTargets(plan, selection, index), + operationIds: emittedOperationIds, + validationOperationIds, + edges: selection.edges, + }) as InternalHydration + } catch { + return failure(request, 'corrupt', 'evidence hydration', selection) } - - const selected = new Set(matchedNodes.map((node) => node.node_id)) - const relationships: EvidenceRelationship[] = [] - for (const edge of traversal.edges) { - if (!selected.has(edge.from) || !selected.has(edge.to)) continue - const relationship = edgeResult(edge) - if (relationship) relationships.push(relationship) - else boundaries.push(limit('corrupt', edge.id)) + if (hydration.state !== 'ready') { + return failure(request, hydration.state, hydration.subject, selection) + } + if (!selection.complete) { + return incomplete( + request, plan, selectionMissing(selection), selection, hydration, + ) + } + const exceeded = hydration.files.size > MAX_RETRIEVE_FILES + ? ['required_file_limit', hydration.files.size, MAX_RETRIEVE_FILES] as const + : hydration.excerpts.size > MAX_RETRIEVE_EXCERPTS + ? ['required_excerpt_limit', hydration.excerpts.size, MAX_RETRIEVE_EXCERPTS] as const + : undefined + if (exceeded) return incomplete( + request, plan, limitMissing(exceeded[0], exceeded[1], exceeded[2]), selection, hydration, + ) + try { + const built = buildDossier(plan, selection, hydration, emittedOperationIds, index) + if (built.state !== 'ready') return built.state === 'incomplete' + ? incomplete(request, plan, [built.missing], selection, hydration, true) + : failure(request, 'corrupt', built.subject, selection) + const ready = stabilize({ + ...header('ready', request, selection, hydration), + dossier: built.dossier, + }) + if (ready.metrics.serialized_tokens > request.budget) { + return incomplete(request, plan, limitMissing( + 'required_token_budget', ready.metrics.serialized_tokens, request.budget, + ), selection, hydration) + } + return ready + } catch { + return failure(request, 'corrupt', 'dossier packing', selection) } - const related = new Set(relationships.flatMap((edge) => [edge.from_id, edge.to_id])) - const orphans = matchedNodes.filter((node) => - node.evidence_kind === 'structural_file' && !related.has(node.node_id)) - for (const node of orphans) boundaries.push(limit('unavailable', node.source_file)) - const orphanIds = new Set(orphans.map((node) => node.node_id)) - matchedNodes = matchedNodes.filter((node) => !orphanIds.has(node.node_id)) - return sliceEvidence({ - request, - outcome: outcome(matchedNodes, boundaries), - matchedNodes, - relationships, - boundaries, - // Direct query anchors are mandatory evidence. Closure intermediates remain - // eligible, but must not evict an explicitly requested endpoint at a hard - // file or token cap. - priorityNodeIds: ranking.priorityAnchorIds - ? [...ranking.priorityAnchorIds] - : ranking.anchors.map((anchor) => anchor.id), - closurePasses: traversal.closurePasses, - structuralRequired: ranking.structuralRequired === true, - structuralCoverageComplete: - ranking.structuralCoverageComplete !== false, - }) } export function serializeRetrieveContextResult(result: RetrieveContextResult): string { diff --git a/src/domain/query/plan.ts b/src/domain/query/plan.ts new file mode 100644 index 00000000..1adece93 --- /dev/null +++ b/src/domain/query/plan.ts @@ -0,0 +1,218 @@ +import type { + LocateAccess, NormalizedRetrieveRequest, ObligationKind, + QueryIntent, QueryObligation, QuestionPlanResult, +} from './types.js' + +const sets = (...values: T): { + [K in keyof T]: ReadonlySet +} => values.map((value) => new Set(value.split(' '))) as { + [K in keyof T]: ReadonlySet +} +const [FLOW, LOCATE, EXPLAIN, COMMON] = sets( + 'flow workflow pipeline lifecycle generate run execute create build produce process work', + 'locate find define declare implement contain handle own write read save set update persist publish consume store use live', + 'explain describe work behave operate handle process validate resolve compute calculate score select update apply evaluate mean control do use choose return reject allow call invoke', + 'a an the this that these those it its they them their we our you your i me my he she what which who where when why how does do did is are was were be been being can could would should will may might must get of for with without by in into on at as and or but if then than from through via to after before during while all every any some each please show trace explain describe end complete initial final full entire code repository file module class function method service handler definition declaration implementation behavior happen', +) +const ACTIONS = new Set([...FLOW, ...LOCATE, ...EXPLAIN, 'complete', 'get', 'happen', 'plan']) +const BEHAVIOR = new Set( + 'apply allow calculate call choose compute consume control evaluate invoke persist publish read reject resolve return save score select store update validate write'.split(' '), +) +const IRREGULAR = new Map('built=build generation=generate got=get getting=get persistence=persist planned=plan planning=plan ran=run running=run setting=set written=write wrote=write'.split(' ').map((pair) => pair.split('=') as [string, string])) + +function canonical(value: string): string { + const irregular = IRREGULAR.get(value) + if (irregular) return irregular + const ing = value.endsWith('ing') ? value.slice(0, -3) : '' + const past = value.endsWith('ed') ? value.slice(0, -2) : '' + const candidates = [value, + /i(?:es|ed)$/u.test(value) ? `${value.slice(0, -3)}y` : '', + ing, ing ? `${ing}e` : '', past, past ? `${past}e` : '', + value.endsWith('es') ? value.slice(0, -2) : '', + value.endsWith('s') ? value.slice(0, -1) : ''] + const action = candidates.find((candidate) => ACTIONS.has(candidate)) + if (action) return action + if (value.length <= 4) return value + if (value.endsWith('ies')) return `${value.slice(0, -3)}y` + return /(? [...set].join('|')) +const AUX = 'is|are|was|were|does|do|did|can|could|would|should|will' +const CLAUSE = 'when|after|before|on|during|if|from|through|via' +const FN = 'flow|workflow|pipeline|lifecycle' +const FV = 'generate|run|execute|create|build|produce|process' +const OWNER = 'file|module|class|function|method|service|handler' + +const isNoise = (token: string, intent: QueryIntent): boolean => + COMMON.has(token) || (intent === 'workflow' ? FLOW.has(token) + : intent === 'locate' ? LOCATE.has(token) : EXPLAIN.has(token)) +const useful = ( + value: string, intent: QueryIntent, common = false, +): string[] => [...new Set(lexicalTokens(value).filter((token) => + !(common ? COMMON.has(token) : isNoise(token, intent))))] + +function pick( + phrase: string, intent: QueryIntent, patterns: readonly RegExp[], common = false, +): string { + for (const pattern of patterns) { + const subject = useful(pattern.exec(phrase)?.[1] ?? '', intent, common).join(' ') + if (subject) return subject + } + return '' +} + +type SubjectMatch = readonly [subject: string, ignored: readonly string[]] + +function flowSubject(phrase: string): SubjectMatch { + const event = pick(phrase, 'workflow', [ + /\bwhat happen when (?:(?:a|an|the) )?(?:user|client|caller) (?:request|submit) (.+)$/, + ]) + if (event) return [event, []] + const walked = pick(phrase, 'workflow', [ + /\bwalk (?:me )?through (.+?)(?= from\b| via\b| to\b|$)/, + ]) + if (walked) return [walked, []] + const traced = pick(phrase, 'workflow', [ + /\btrace (?:the )?(.+)(?= from .+ (?:to|through|via)\b)/, + ], true) + if (traced) return [traced, []] + const passive = pick(phrase, 'workflow', [ + RegExp(`\\bhow (?:is|are|was|were) (.+?) (?:${FW})\\b`), + RegExp(`\\bhow (?:does|do|did) (.+?) get (?:${FW})\\b`), + ]) + if (passive) return [passive, []] + const active = RegExp( + `\\bhow (?:(?:${AUX}) )?(.+?) (${FW}) (.+?)(?= (?:${CLAUSE})\\b| end to end\\b|$)`, + ).exec(phrase) + if (active) { + const object = useful(active[3]!, 'workflow').join(' ') + if (object) return [object, useful(active[1]!, 'workflow', true)] + } + const subject = pick(phrase, 'workflow', [ + /\btrace (?:the )?(.+?)(?= through\b| via\b| to\b|$)/, + RegExp(`\\b(?:${FN}) (?:of|for) (.+?)(?= from\\b|$)`), + RegExp(`(.+?) (?:${FN})\\b`), + RegExp(`\\bhow (?:(?:${AUX}) )?(.+?) (?:${FW})\\b`), + ]) + return [subject || useful(phrase, 'workflow').join(' '), []] +} + +type FlowBounds = { + entry?: string | undefined + stage?: string | undefined + terminal?: string | undefined +} +function flowBounds(phrase: string): FlowBounds { + if (!/\b(?:from|through|via)\b/u.test(phrase)) return {} + const read = (pattern: RegExp): string | undefined => { + const value = useful(pattern.exec(phrase)?.[1] ?? '', 'workflow', true).join(' ') + return value || undefined + } + const walked = /^walk (?:me )?through\b/u.test(phrase) + return { + entry: read(/\bfrom (.+?)(?= (?:through(?: to)?|via|to)\b| how\b|$)/u), + stage: read(/\bvia (.+?)(?= to\b| how\b|$)/u) + ?? read(/\bfrom .+?\bthrough (?!to\b)(.+?)(?= to\b| how\b|$)/u) + ?? (walked ? undefined + : read(/\bthrough (?!to\b)(.+?)(?= to\b| how\b|$)/u)), + terminal: read(/\b(?:through to|to) (.+?)(?= how\b|$)/u), + } +} + +function simpleSubject(phrase: string, intent: 'locate' | 'explain'): string { + const locate = intent === 'locate' + const patterns = locate ? [ + RegExp(`\\bwhere (?:(?:${AUX}) )?(.+?)(?= (?:${LW})\\b| (?:${CLAUSE})\\b|$)`), + RegExp(`\\b(?:which (?:${OWNER}) |what )(?:${LW}) (.+?)(?= (?:${CLAUSE})\\b|$)`), + /\b(?:locate|find)(?: the)? (.+?)(?= (?:definition|declaration|implementation)\b|$)/, + /\b(?:definition|declaration|implementation) (?:of|for) (.+)$/, + ] : [ + RegExp(`\\bwhich (?:${OWNER}) \\S+ (.+)$`), + RegExp(`\\bwhat (?:${FW}|${EW}) (.+)$`), + RegExp(`\\bhow (?:${AUX}) (.+?) (?:create|build|produce)\\b`), + RegExp(`\\b(?:how|why|what) (?:(?:${AUX}) )?(.+?) (?:${EW})\\b`), + /\b(?:explain|describe)(?: how)?(?: the)? (.+)$/, + ] + return pick(phrase, intent, patterns, locate) + || useful(phrase, intent).join(' ') +} + +export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanResult { + const phrase = lexicalTokens(request.question).join(' '), + qualified = /(?:^|[^\p{L}\p{N}_$])([\p{L}_$][\p{L}\p{N}_$]*\.[\p{L}_$][\p{L}\p{N}_$]*)/u + .exec(request.question.normalize('NFKC'))?.[1], + identifier = /\bwhere\s+(?:is|are|was|were)\s+[`'"]?([\p{L}_$][\p{L}\p{N}_$.-]*)[`'"]?\s+(?:defined|declared|implemented)\b/iu + .exec(request.question.normalize('NFKC'))?.[1] + const intent: QueryIntent | undefined = + /\b(?:end to end|what happen when)\b|\bfrom\b.+\b(?:through|via)\b.+\bto\b|\btrace\b.+\bfrom\b.+\bto\b/.test(phrase) + || !qualified && RegExp(`\\bhow (?:(?:${AUX}) (?!${FV}\\b)|(?!(?:${AUX}|${FV})\\b))\\S+(?: \\S+)*? (?:${FV})\\b`).test(phrase) + ? 'workflow' + : /\b(?:where|locate|find|definition|declaration|implementation)\b/.test(phrase) + || RegExp(`\\bwhich (?:${OWNER}) (?:${LW})\\b`).test(phrase) + || RegExp(`\\bwhat (?:${LW})\\b`).test(phrase) ? 'locate' + : /^trace\b|\b(?:flow|workflow|pipeline|lifecycle)\b/.test(phrase) ? 'workflow' + : /\b(?:explain|describe|how|why|behavior)\b|\bwhat (?:does|do|is|are)\b/ + .test(phrase) + || RegExp(`\\b(?:what (?:${FW}|${EW})|which (?:${OWNER}))\\b`) + .test(phrase) ? 'explain' : undefined + if (!intent) { + return { + status: 'unsupported', reason: 'unsupported_intent', + terms: [...new Set(phrase.split(' ').filter((token) => !COMMON.has(token)))].sort(), + } + } + const [subject, ignored] = intent === 'workflow' + ? qualified ? [useful(qualified, 'workflow', true).join(' '), []] + : flowSubject(phrase) + : [qualified + ? useful(qualified, intent, true).join(' ') + : intent === 'locate' && identifier + ? useful(identifier, 'locate', true).join(' ') + : simpleSubject(phrase, intent), []] + const bounds = intent === 'workflow' ? flowBounds(phrase) : {} + const omitted = new Set(ignored) + const terms = new Set(lexicalTokens(phrase).filter((token) => + !isNoise(token, intent) && !omitted.has(token))) + lexicalTokens(subject).forEach((token) => terms.add(token)) + const sorted = [...terms].sort() + if (!subject || sorted.length === 0) { + return { status: 'unsupported', reason: 'missing_subject', terms: sorted } + } + const words = new Set(phrase.split(' ')) + const access: LocateAccess | undefined = intent !== 'locate' ? undefined + : ['read', 'find'].some((word) => words.has(word)) ? 'read' + : ['write', 'save', 'set', 'update', 'persist', 'store'] + .some((word) => words.has(word)) ? 'write' : undefined + const kinds: readonly ObligationKind[] = intent === 'locate' ? ['subject'] + : intent === 'explain' ? ['subject', 'behavior'] + : ['subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal'] + const residual = sorted.filter((token) => !lexicalTokens(subject).includes(token)) + const actions = phrase.split(' ').filter((token) => BEHAVIOR.has(token)) + const behavior = [...new Set(residual.length > 0 ? residual : actions)].join(' ') + return { + status: 'supported', + plan: { + intent, subject, terms: sorted, + obligations: kinds.map((kind, index): QueryObligation => ({ + id: `o${index + 1}`, kind, + target: kind === 'entry' ? bounds.entry ?? subject + : kind === 'stage' ? bounds.stage ?? subject + : kind === 'terminal' ? bounds.terminal ?? subject + : kind === 'behavior' && intent === 'explain' && behavior + ? behavior : subject, + mandatory: true, + })), + ...(access ? { access } : {}), + }, + } +} diff --git a/src/domain/query/rank.ts b/src/domain/query/rank.ts deleted file mode 100644 index 1a82096c..00000000 --- a/src/domain/query/rank.ts +++ /dev/null @@ -1,1098 +0,0 @@ -import { compareCodeUnits as compare } from '../graph/canonical-json.js' -import type { GraphAttributes } from '../graph/directed-multigraph.js' -import type { ReadyQueryIndex } from './index-status.js' -import { - classifySourceDomain, isPollutedSourcePath, sourceDomainOf, type SourceDomain, -} from './source-domain.js' -import type { - EvidenceBoundary, NormalizedRetrieveRequest, RankedQueryNode, RankQueryResult, -} from './types.js' -import { - MAX_RETRIEVE_FILES as FILE_CAP, MAX_RETRIEVE_SNIPPETS as SNIPPET_CAP, -} from './types.js' - -const CAUSAL = new Set(['calls', 'enqueues_job']) -const RELATIONS = ['calls', 'contains', 'enqueues_job', 'imports_from'] -const LAST = Number.MAX_SAFE_INTEGER -const STOP = new Set( - 'a actual an and any applicabl are as at be by bas being can do does exist final for from get gett handl explain how in initial is it its me new of on operat or specific tell that the then through to trace what when which with work you'.split(' '), -) -const UNSUPPORTED = - /^(?:bash|c|cc|cljs|clj|cpp|cs|cxx|dart|elm|ex|exs|fs|fsx|go|groovy|h|hpp|hs|java|jl|kt|kts|lua|m|mm|php|ps1|py|r|rb|rs|scala|sh|sol|sql|svelte|swift|vue|zig)$/u -const DOMAIN_TERMS: Readonly> = { - test: ['test', 'spec', 'e2e'], - benchmark: ['benchmark', 'bench', 'performance'], - fixture: ['fixture', 'mock'], - generated: ['generated'], - docs: ['doc', 'documentation', 'readme'], - config: ['config', 'configuration', 'setting'], -} -const STEMS = [ - [7, 'ization', 'ize'], [5, 'ies', 'y'], [6, 'ence', ''], [6, 'ance', ''], - [8, 'ment', ''], [5, 'ions', ''], [4, 'ion', ''], [5, 'ing', ''], - [4, 'ery', 'er'], [4, 'ed', ''], [4, 's', ''], [5, 'e', ''], -] as const - -interface Field { compact: string; tokens: ReadonlySet; weight: number } -interface Node { - id: string; attributes: GraphAttributes; file: string; kind: string - domain: SourceDomain; fields: readonly Field[]; tokens: ReadonlySet - pathTokens: ReadonlySet; ins: string[]; outs: string[] - owner?: string; eligible: boolean; defined: boolean -} -interface Corpus { - nodes: readonly Node[]; byId: ReadonlyMap - members: ReadonlyMap - files: ReadonlyMap - freq: ReadonlyMap; docs: number -} -interface Scope { - subject: string; tokens: string[]; compact: string; first: number - hard: boolean -} -interface Vocabulary { - terms: string[]; pos: ReadonlyMap - scopes: Scope[]; limits: Scope[]; parts: readonly ReadonlySet[] - mentions: ReadonlySet - domains: ReadonlySet - structural: boolean; expand: boolean; sequential: boolean -} -interface Scored { n: Node; rank: RankedQueryNode } -interface Seed extends Scored { hits: string[]; parts: number[] } -interface Selection { - ids: string[]; flow: boolean; complete: boolean - structuralRequired: boolean; branch?: string[] -} -interface UnsupportedCandidate { - path: string; terms: string[]; weights: ReadonlyMap - score: number; first: number -} - -const corpusCache = new WeakMap>() - -function stem(value: string): string { - if (/^\d+$/.test(value)) return value - let result = value - for (let pass = 0; pass < 2; pass += 1) { - const rule = STEMS.find(([minimum, suffix]) => - result.length > minimum && result.endsWith(suffix) - && (suffix !== 's' || !result.endsWith('ss'))) - if (!rule) break - result = `${result.slice(0, -rule[1].length)}${rule[2]}` - } - return result -} - -function tokens(value: string): string[] { - const separated = value - .replace(/([a-z\d])([A-Z])/g, '$1 $2') - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') - .toLowerCase() - return (separated.match(/[a-z][a-z0-9]*|\d+/g) ?? []) - .flatMap((token) => { - const parts = token.match(/[a-z]+|\d+/g) ?? [] - return parts.length >= 3 ? parts : [token] - }) - .map(stem) - .filter((token) => token.length > 1 || /^\d+$/.test(token)) -} - -function words(value: string): string[] { - return tokens(value).filter((token) => !STOP.has(token)) -} - -function matches(values: ReadonlySet, t: string): boolean { - if (values.has(t)) return true - if (t.length < 3) return false - const singular = t.length >= 4 && t.endsWith('s') && !t.endsWith('ss') - ? t.slice(0, -1) : `${t}s` - const bare = t.endsWith('e') ? t.slice(0, -1) : `${t}e` - for (const form of [t, singular, bare]) { - if (values.has(form)) return true - for (const prefix of ['en', 're', 'un']) { - if (values.has(`${prefix}${form}`) - || (form.startsWith(prefix) && values.has(form.slice(prefix.length)))) return true - } - } - return false -} - -function text(attributes: GraphAttributes, key: string): string { - const value = attributes[key] - return typeof value === 'string' ? value : '' -} - -function field(value: string, weight: number): Field | null { - const lexical = tokens(value) - return lexical.length === 0 ? null : { - compact: lexical.join(''), tokens: new Set(lexical), weight, - } -} - -function buildCorpus(index: ReadyQueryIndex): Corpus { - const cached = corpusCache.get(index)?.deref() - if (cached) return cached - const nodes: Node[] = [] - const paths = new Map() - for (const [id, attributes] of index.graph.nodeEntries()) { - const file = text(attributes, 'source_file') - const kind = text(attributes, 'node_kind') - // Shared execution channels are traversal infrastructure for retrieval v2. - // Keeping them out of the v1 lexical corpus prevents their labels from - // changing document frequency and therefore existing symbol ranking. - if (kind === 'channel') continue - if (!paths.has(file)) paths.set(file, field(file, 7)) - const pathField = paths.get(file) ?? null - const fields = [ - field(text(attributes, 'label'), 12), - field(text(attributes, 'qualified_name'), 12), - field(`${text(attributes, 'framework')} ${ - text(attributes, 'framework_role')}`, 5), - field(JSON.stringify(attributes.framework_metadata) ?? '', 5), - field(kind, 3), - pathField, - ].filter((field): field is Field => !!field) - const eligible = !isPollutedSourcePath(file, index.root_path) - && (kind === 'file' - ? !!file && Array.isArray(attributes.provenance) - && attributes.provenance.length > 0 && index.file_hashes.has(file) - : !!attributes.definition_range && !!attributes.declaration_range - && !(attributes.framework_metadata - && typeof attributes.framework_metadata === 'object' - && 'external_call' in attributes.framework_metadata - && attributes.framework_metadata.external_call === true)) - nodes.push({ - id, attributes, file, kind, fields, - domain: sourceDomainOf(attributes.source_domain, file, index.root_path), - tokens: new Set(fields.flatMap((field) => [...field.tokens])), - pathTokens: pathField?.tokens ?? new Set(), - ins: [], outs: [], eligible, - defined: kind === 'file' - || JSON.stringify(attributes.declaration_range) - !== JSON.stringify(attributes.definition_range), - }) - } - const byId = new Map(nodes.map((n) => [n.id, n])) - const members = new Map() - for (const [from, to, attributes] of index.graph.edgeEntries()) { - const source = byId.get(from) - const target = byId.get(to) - const relation = text(attributes, 'relation') - if (source && target && CAUSAL.has(relation) - && source.kind !== 'file' && target.kind !== 'file') { - source.outs.push(to) - target.ins.push(from) - } - if (source?.kind === 'class' && target - && (relation === 'contains' || relation === 'method')) { - target.owner = from - const owned = members.get(from) ?? [] - owned.push(to) - members.set(from, owned) - } - } - for (const n of nodes) { - n.ins = [...new Set(n.ins)].sort(compare) - n.outs = [...new Set(n.outs)].sort(compare) - } - for (const [owner, owned] of members) { - members.set(owner, [...new Set(owned)].sort(compare)) - } - const docs = new Map>() - const files = new Map() - for (const n of nodes) { - const siblings = files.get(n.file) ?? [] - siblings.push(n) - files.set(n.file, siblings) - const document = docs.get(n.file || n.id) ?? new Set() - for (const field of n.fields) for (const token of field.tokens) document.add(token) - docs.set(n.file || n.id, document) - } - const freq = new Map() - for (const document of docs.values()) { - for (const token of document) { - freq.set(token, (freq.get(token) ?? 0) + 1) - } - } - const c = { - nodes, byId, members, files, freq, docs: docs.size, - } satisfies Corpus - corpusCache.set(index, new WeakRef(c)) - return c -} - -function scopes(question: string): Scope[] { - const result: Scope[] = [] - const seen = new Set() - const patterns = [ - [/`([A-Za-z_$][A-Za-z0-9_$.:]*)`/g, false], - [/\b([A-Za-z_$][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*)\b/g, false], - [/\b(?:[A-Za-z0-9_$.[\]-]+\/)+[A-Za-z0-9_$.[\]-]+\.(?:[cm]?[jt]sx?)\b/g, true], - [/\b(?=[a-z0-9-]*\d)[a-z][a-z0-9]*(?:-[a-z0-9]+)+\b/g, true], - ] as const - for (const [pattern, hard] of patterns) { - for (const match of question.matchAll(pattern)) { - const subject = (match[1] ?? match[0]).trim() - if (subject === match[0] && /^[A-Z]+$/u.test(subject)) continue - const lexical = tokens(subject) - const compact = lexical.join('') - if (!compact || seen.has(compact)) continue - seen.add(compact) - result.push({ - subject, tokens: lexical, compact, - first: match.index ?? LAST, hard, - }) - } - } - const qualified = result.filter((s) => - !s.hard && s.subject.includes('.')) - return result.filter((s) => !qualified.some((parent) => - parent !== s - && s.first > parent.first - && s.first <= parent.first + parent.subject.length + 1 - && parent.subject.split('.').includes(s.subject))) - .sort((a, b) => - a.first - b.first || compare(a.subject, b.subject)) -} - -function vocabulary(question: string): Vocabulary { - const task = question.replace(/\.\s+(?:cite|use|report)\b[\s\S]*$/iu, '') - const raw = tokens(task) - const terms: string[] = [] - const pos = new Map() - for (const [position, t] of raw.entries()) { - if (!STOP.has(t) && !pos.has(t)) { - terms.push(t) - pos.set(t, position) - } - } - const lower = task.toLowerCase() - for (const relation of RELATIONS) { - const variants = [relation, relation.replaceAll('_', ' ')] - const found = variants.map((variant) => lower.indexOf(variant)) - .filter((position) => position >= 0) - if (found.length > 0 && !pos.has(relation)) { - terms.push(relation) - pos.set(relation, Math.min(...found)) - } - } - const explicit = scopes(task) - const clauses = task - .split( - /[,;:\u2013\u2014]+|[!?]+|\.(?=\s+[A-Z])|\b(?:[Aa][Nn][Dd]\s+)?[Tt][Hh][Ee][Nn]\b/u, - ) - .map(words).filter((part) => part.length > 0) - const parts = (clauses.length > 0 ? clauses : [terms]) - .map((part) => new Set(part)) - const connector = raw.findIndex((t, index) => - index > 0 && index < raw.length - 1 - && (t === 'through' || t === 'until')) - const directed = (raw.includes('from') - && raw.some((t) => t === 'to' || t === 'through')) - || connector >= 0 - || raw.some((t, index) => - t === 'end' && raw[index + 1] === 'to' && raw[index + 2] === 'end') - const from = raw.indexOf('from') - const to = raw.indexOf('to', from + 1) - const parallel = from >= 0 && to > from && raw.includes('and') - const explainsProcess = raw.includes('how') && terms.length > 1 - && explicit.every((s) => s.hard) - const structural = directed || parts.length > 1 - || explainsProcess - || terms.some((t) => [ - 'flow', 'handoff', 'journey', 'lifecycl', 'orchestrat', - 'pipelin', 'process', 'queue', 'sequenc', 'stag', - ].includes(t)) - const expand = directed || parts.length > 1 - || (terms.some((t) => t === 'stag' || t === 'stage') - && terms.some((t) => - ['job', 'jobs', 'queue', 'enqueu', 'orchestrat'].includes(t))) - const domains = new Set(Object.entries(DOMAIN_TERMS) - .filter(([, variants]) => - variants.some((variant) => terms.includes(variant))) - .map(([domain]) => domain as SourceDomain)) - return { - terms, pos, scopes: explicit, - limits: explicit.filter((s) => s.hard), - parts, mentions: new Set(task.split(/[^A-Za-z0-9_$]+/u)), domains, - structural, expand, - sequential: connector >= 0 || raw.includes('then') || (directed && !parallel), - } -} - -function inScope(n: Node, s: Scope): boolean { - return s.tokens.every((token) => n.tokens.has(token)) - && n.fields.some((field) => field.compact.includes(s.compact)) -} - -function rarity(c: Corpus, t: string): number { - const seen = c.freq.get(t) ?? 0 - return Math.max(1, Math.round( - (1 + Math.log2((c.docs + 1) / (seen + 1))) * 64, - )) -} - -function exactLabel(n: Node, q: Vocabulary): boolean { - const label = text(n.attributes, 'label') - const identifier = label.replace(/^\./u, '').replace(/\(\)$/u, '') - return (label.endsWith('()') || /[A-Z_$\d.:]/u.test(identifier)) - && words(identifier).length > 0 && q.mentions.has(identifier) -} - -function termsOf(n: Node, q: Vocabulary, semantic = false): string[] { - return q.terms.filter((t) => n.fields.some((field) => - (!semantic || field.weight !== 7) && matches(field.tokens, t))) -} - -function score( - c: Corpus, n: Node, q: Vocabulary, -): RankedQueryNode | null { - const hits = termsOf(n, q) - const context = q.terms.filter((t) => !hits.includes(t) - && [...n.ins, ...n.outs].some((id) => { - const adjacent = c.byId.get(id) - return !!adjacent?.eligible && matches(adjacent.tokens, t) - })) - if (hits.length === 0 && context.length === 0 - && !q.scopes.some((s) => inScope(n, s))) return null - let value = n.domain === 'production' ? 500 - : n.domain === 'test' ? -250 - : n.domain === 'unknown' ? 0 : -500 - for (const t of hits) { - const weight = Math.max(0, ...n.fields - .filter((field) => matches(field.tokens, t)).map((field) => field.weight)) - value += rarity(c, t) * weight - if (n.fields.some((field) => - field.weight !== 7 && matches(field.tokens, t))) { - value += rarity(c, t) * 12 - } - if (matches(n.pathTokens, t)) value += rarity(c, t) * 7 - } - for (const t of context) value += rarity(c, t) * 5 - for (const s of q.scopes.filter((s) => !s.hard)) { - if (n.fields.some((field) => field.compact === s.compact)) value += 2_000_000 - else if (inScope(n, s)) value += 1_000_000 - } - if (exactLabel(n, q)) value += 2_000_000 - const firstMatch = hits.reduce((first, t) => - Math.min(first, q.pos.get(t) ?? LAST), - LAST) - return { - id: n.id, attributes: n.attributes, score: value, - matchedTerms: [...hits, ...context], firstMatch, - } -} - -function byRank(a: Scored, b: Scored): number { - return b.rank.score - a.rank.score - || a.rank.firstMatch - b.rank.firstMatch - || b.n.outs.length - a.n.outs.length - || compare(a.n.file, b.n.file) - || compare(a.n.id, b.n.id) -} - -function inDomain(n: Node, q: Vocabulary): boolean { - return q.domains.size > 0 - ? q.domains.has(n.domain) - : n.domain === 'production' || n.domain === 'unknown' -} - -function scoredNodes( - c: Corpus, q: Vocabulary, keep: (n: Node) => boolean, -): Scored[] { - const items = c.nodes.flatMap((n): Scored[] => { - if (!n.eligible || !keep(n) - || (q.limits.length > 0 - && !q.limits.some((s) => inScope(n, s)))) return [] - if (n.kind === 'file' && !q.terms.includes('imports_from') - && !q.limits.some((s) => - s.subject.includes('/') && inScope(n, s))) return [] - if ((!n.defined || n.kind === 'interface' || n.kind === 'type-alias') - && !q.terms.some((t) => tokens(n.kind).includes(t)) - && !q.terms.some((t) => t === 'defin' || t === 'declar') - && !q.scopes.some((s) => inScope(n, s)) - && !exactLabel(n, q)) return [] - if (!inDomain(n, q) && !q.scopes.some((s) => inScope(n, s))) return [] - const rank = score(c, n, q) - if (!rank) return [] - return [{ n, rank }] - }).sort(byRank) - if (q.parts.length > 1 || q.scopes.length > 0) return items - const locator = q.terms[0] === 'where' - const byFile = new Map() - for (const item of items) { - const current = byFile.get(item.n.file || item.n.id) ?? [] - current.push(item) - byFile.set(item.n.file || item.n.id, current) - } - return [...byFile.values()].flatMap((entries) => - entries.sort((a, b) => { - if (locator) return byRank(a, b) - const ids = new Set(entries.map((entry) => entry.n.id)) - return b.n.outs.filter((id) => ids.has(id)).length - - a.n.outs.filter((id) => ids.has(id)).length - || byRank(a, b) - }).slice(0, 2)).sort(byRank) -} - -function usable(n: Node | undefined, q: Vocabulary): n is Node { - return !!n?.eligible && n.kind !== 'file' && n.kind !== 'class' - && (inDomain(n, q) || n.domain === 'production' || n.domain === 'unknown') -} - -function path( - c: Corpus, from: string, to: string, q: Vocabulary, backwards = false, - maximumDepth = 16, -): string[] | null { - if (from === to) return [from] - const previous = new Map() - const depth = new Map([[from, 0]]) - const queue = [from] - for (let cursor = 0; cursor < queue.length && queue.length < 512; cursor += 1) { - const id = queue[cursor]! - const distance = depth.get(id) ?? 0 - if (distance >= maximumDepth) continue - const adjacent = backwards - ? c.byId.get(id)?.ins ?? [] - : c.byId.get(id)?.outs ?? [] - for (const next of adjacent) { - if (depth.has(next) || !usable(c.byId.get(next), q)) continue - depth.set(next, distance + 1) - previous.set(next, id) - if (next === to) { - const result = [to] - while (result.at(-1) !== from) result.push(previous.get(result.at(-1)!)!) - return result.reverse() - } - queue.push(next) - } - } - return null -} - -function toposort(c: Corpus, input: readonly string[]): { - ids: string[]; depth: number -} { - const allowed = new Set(input) - const index = new Map() - const low = new Map() - const stack: string[] = [] - const active = new Set() - const groups: string[][] = [] - let ordinal = 0 - const visit = (id: string): void => { - index.set(id, ordinal) - low.set(id, ordinal++) - stack.push(id) - active.add(id) - for (const next of c.byId.get(id)?.outs ?? []) { - if (!allowed.has(next)) continue - if (!index.has(next)) { - visit(next) - low.set(id, Math.min(low.get(id)!, low.get(next)!)) - } else if (active.has(next)) { - low.set(id, Math.min(low.get(id)!, index.get(next)!)) - } - } - if (low.get(id) !== index.get(id)) return - const group: string[] = [] - while (stack.length > 0) { - const member = stack.pop()! - active.delete(member) - group.push(member) - if (member === id) break - } - groups.push(group) - } - for (const id of input) if (!index.has(id)) visit(id) - const groupOf = new Map(groups.flatMap((group, groupIndex) => - group.map((id) => [id, groupIndex] as const))) - const outs = new Map>() - const indegree = new Map() - for (const id of input) { - const from = groupOf.get(id)! - for (const next of c.byId.get(id)?.outs ?? []) { - if (!allowed.has(next)) continue - const to = groupOf.get(next)! - if (from === to) continue - const targets = outs.get(from) ?? new Set() - if (targets.has(to)) continue - targets.add(to) - outs.set(from, targets) - indegree.set(to, (indegree.get(to) ?? 0) + 1) - } - } - const pos = new Map(input.map((id, order) => [id, order])) - const ready = groups.map((_, group) => group) - .filter((group) => !indegree.has(group)) - const depths = new Map(ready.map((group) => [ - group, Math.max(0, groups[group]!.length - 1), - ])) - const ids: string[] = [] - let depth = 0 - while (ready.length > 0) { - ready.sort((a, b) => - Math.min(...groups[a]!.map((id) => - pos.get(id) ?? LAST)) - - Math.min(...groups[b]!.map((id) => - pos.get(id) ?? LAST))) - const group = ready.shift()! - const level = depths.get(group) ?? 0 - depth = Math.max(depth, level) - ids.push(...groups[group]!.sort((a, b) => - (pos.get(a) ?? LAST) - - (pos.get(b) ?? LAST) - || compare(a, b))) - for (const next of outs.get(group) ?? []) { - depths.set(next, Math.max( - depths.get(next) ?? 0, - level + 1 + Math.max(0, groups[next]!.length - 1), - )) - const left = (indegree.get(next) ?? 0) - 1 - if (left > 0) indegree.set(next, left) - else { - indegree.delete(next) - ready.push(next) - } - } - } - return { ids: ids.length === input.length ? ids : [...input], depth } -} - -function rootPath( - c: Corpus, target: string, seedMap: ReadonlyMap, - forbidden: ReadonlySet, q: Vocabulary, -): string[] | null { - const next = new Map() - const depth = new Map([[target, 0]]) - const queue = [target] - const roots: string[] = [] - for (let cursor = 0; cursor < queue.length && queue.length < 512; cursor += 1) { - const id = queue[cursor]! - const n = c.byId.get(id) - const distance = depth.get(id) ?? 0 - if (!usable(n, q) || distance >= 16) continue - const parents = n.ins.filter((parent) => usable(c.byId.get(parent), q)) - if (id !== target && parents.length === 0) roots.push(id) - for (const parent of parents) { - if (depth.has(parent)) continue - depth.set(parent, distance + 1) - next.set(parent, id) - queue.push(parent) - } - } - const route = (root: string): string[] => { - const result = [root] - while (result.at(-1) !== target) result.push(next.get(result.at(-1)!)!) - return result - } - const choices = roots.flatMap((root) => { - const ids = route(root) - if (ids.slice(0, -1).some((id) => forbidden.has(id))) return [] - return [{ - ids, - parts: new Set(ids.flatMap((id) => - seedMap.get(id)?.parts ?? [])).size, - terms: new Set(ids.flatMap((id) => - seedMap.get(id)?.hits ?? [])).size, - }] - }) - return choices.sort((a, b) => - a.ids.length - b.ids.length - || b.parts - a.parts - || b.terms - a.terms - || (seedMap.get(b.ids[0]!)?.rank.score ?? 0) - - (seedMap.get(a.ids[0]!)?.rank.score ?? 0) - || compare(a.ids[0]!, b.ids[0]!))[0]?.ids ?? null -} - -function connect( - c: Corpus, seeds: readonly Seed[], q: Vocabulary, -): Selection | null { - const seedMap = new Map(seeds.map((seed) => [seed.n.id, seed])) - const byId = c.byId - const hubs = c.nodes.filter((hub) => - usable(hub, q) && hub.ins.length > 0 && hub.outs.length > 0) - const shapes = new Map>() - for (const file of new Set(hubs.map((hub) => hub.file))) { - shapes.set(file, (c.files.get(file) ?? []).flatMap((registry) => { - if (!usable(registry, q) || registry.ins.length < 2) return [] - const pairs = registry.ins.flatMap((hookId) => { - const registrar = byId.get(hookId) - if (!usable(registrar, q) || !registrar.owner) return [] - const workers = (c.members.get(registrar.owner) ?? []) - .filter((id) => id !== hookId && registrar.outs.includes(id) - && usable(byId.get(id), q)) - return workers.length === 1 - ? [{ hookId, workerId: workers[0]! }] : [] - }) - const hooks = [...new Set(pairs.map((pair) => pair.hookId))] - const workers = [...new Set(pairs.map((pair) => pair.workerId))] - const role = (id: string): string => { - const n = byId.get(id)! - return `${n.kind}\0${text(n.attributes, 'label').replace(/^\./u, '')}` - } - return hooks.length >= 2 && hooks.length === workers.length - && new Set(hooks.map(role)).size === 1 - && new Set(workers.map(role)).size === 1 - && role(hooks[0]!) !== role(workers[0]!) - ? [{ registryId: registry.id, hooks, workers }] : [] - })) - } - const choices = hubs.flatMap((hub) => - (shapes.get(hub.file) ?? []) - .filter(({ registryId }) => registryId !== hub.id) - .flatMap(({ hooks, workers }) => { - const hits = workers.filter((id) => hub.outs.includes(id)).length - if (hits * 2 >= workers.length) return [] - const entry = rootPath( - c, hub.id, seedMap, new Set([...hooks, ...workers]), - q, - ) - if (!entry) return [] - const relevant = [...entry, ...workers] - const scopes = q.scopes.filter((s) => !s.hard) - const matchesQuery = scopes.length > 0 - ? scopes.every((s) => c.nodes.some((n) => - usable(n, q) && inScope(n, s) - && relevant.some((id) => !!path(c, id, n.id, q, false, 3)))) - : relevant.some((id) => seedMap.has(id)) - return matchesQuery ? [{ hub, workers, entry }] : [] - })) - .sort((a, b) => - Number(seedMap.has(b.hub.id)) - Number(seedMap.has(a.hub.id)) - || b.hub.ins.length - a.hub.ins.length - || b.workers.length - a.workers.length - || compare(a.hub.id, b.hub.id)) - const pick = choices[0] - if (!pick) return null - const branches = pick.workers.map((workerId) => { - const worker = byId.get(workerId)! - const services = worker.outs.filter((id) => usable(byId.get(id), q)) - const service = [...services].sort((a, b) => - Number(!!path(c, b, pick.hub.id, q)) - - Number(!!path(c, a, pick.hub.id, q)) - || Number(byId.get(b)?.file !== worker.file) - - Number(byId.get(a)?.file !== worker.file) - || (seedMap.get(b)?.rank.score ?? 0) - - (seedMap.get(a)?.rank.score ?? 0) - || compare(a, b))[0] - return { - workerId, service, - hits: pick.hub.outs.includes(workerId), - returns: !!service && !!path(c, service, pick.hub.id, q), - } - }).sort((a, b) => - Number(b.hits) - Number(a.hits) - || Number(b.returns) - Number(a.returns) - || (byId.get(a.service ?? '')?.outs.length - ?? LAST) - - (byId.get(b.service ?? '')?.outs.length - ?? LAST) - || (seedMap.get(b.workerId)?.rank.score ?? 0) - - (seedMap.get(a.workerId)?.rank.score ?? 0) - || compare(a.workerId, b.workerId)) - const ids: string[] = [] - const files = new Set() - const append = (id?: string): boolean => { - if (!id || ids.includes(id)) return true - const n = byId.get(id) - if (!usable(n, q)) return true - if (ids.length >= SNIPPET_CAP - || (n.file && !files.has(n.file) && files.size >= FILE_CAP)) return false - ids.push(id) - if (n.file) files.add(n.file) - return true - } - let complete = true - for (const id of pick.entry) if (!append(id)) complete = false - const terminal = branches.filter((branch) => !branch.returns) - for (const branch of branches) { - if (branch.returns - || (!!branch.service - && (termsOf(byId.get(branch.service)!, q, true).length > 0 - || (q.parts.length > 1 && terminal.length === 1 - && q.scopes.every((s) => s.hard))))) { - if (!append(branch.workerId)) complete = false - } - if (branch.returns && !append(branch.service)) complete = false - } - const sideBranches: string[] = [] - for (const s of q.scopes.filter((s) => !s.hard)) { - if (ids.some((id) => inScope(byId.get(id)!, s))) continue - const side = c.nodes.filter((n) => - usable(n, q) && !ids.includes(n.id) && inScope(n, s) - && n.ins.some((id) => ids.includes(id))) - .sort((a, b) => - (seedMap.get(b.id)?.rank.score ?? 0) - - (seedMap.get(a.id)?.rank.score ?? 0) - || compare(a.id, b.id))[0] - if (!side) { - complete = false - continue - } - const parent = Math.max(...side.ins.map((id) => ids.indexOf(id))) - const length = ids.length - if (parent < 0 || !append(side.id) || ids.length === length) { - complete = false - continue - } - ids.pop() - ids.splice(parent + 1, 0, side.id) - sideBranches.push(side.id) - } - if (!q.expand && !q.terms.every((t) => t === 'flow' - || ids.some((id) => termsOf(byId.get(id)!, q, true).includes(t)))) return null - return { - ids, flow: true, - complete: complete && ids.length > 1, - structuralRequired: true, - branch: sideBranches, - } -} - -function causal( - c: Corpus, seeds: readonly Seed[], q: Vocabulary, -): Selection | null { - if (seeds.length === 0) return null - const named = q.scopes.filter((s) => !s.hard) - const scoped = new Set(named.flatMap((s) => s.tokens)) - const picked = (named.length > 0 - ? seeds.filter((seed) => - named.some((s) => inScope(seed.n, s)) - || seed.hits.some((t) => !scoped.has(t))) - : seeds).slice(0, 32) - const ids = new Set(picked.map((seed) => seed.n.id)) - for (const n of c.nodes) { - if (!usable(n, q)) continue - const children = n.outs.filter((id) => ids.has(id)).length - if (children < 2) continue - ids.add(n.id) - if (ids.size >= 256) break - } - const ordered = [...new Set(q.scopes.flatMap((s) => - picked.filter((seed) => inScope(seed.n, s))))] - const filtered = [...new Set([ - ...ordered.map(({ n }) => n.id), ...ids, - ])].filter((id) => usable(c.byId.get(id), q)) - const files = new Set() - const bounded = toposort(c, filtered).ids.filter((id) => { - const n = c.byId.get(id)! - if (files.size >= FILE_CAP && n.file && !files.has(n.file)) return false - if (files.size < FILE_CAP && n.file) files.add(n.file) - return true - }).slice(0, SNIPPET_CAP) - const retained = new Set(bounded) - const edges = bounded.reduce((count, id) => - count + c.byId.get(id)!.outs.filter((to) => retained.has(to)).length, 0) - const depth = toposort(c, bounded).depth - const reachable = ordered.some((from, index) => - ordered.slice(index + 1).some((to) => - !!path(c, from.n.id, to.n.id, q))) - if (edges === 0 && (!(q.scopes.length > 1 - || q.terms.filter((t) => /^\d+$/.test(t)).length > 1) - || (q.limits.length === 0 && !reachable))) return { - ids: [], flow: false, complete: false, - structuralRequired: true, - } - if (depth < 2 && edges >= 3 && files.size <= 1) return { - ids: [], flow: false, complete: false, - structuralRequired: true, - } - const conceptCoverage = new Set(picked - .filter((seed) => retained.has(seed.n.id)) - .flatMap((seed) => seed.parts)) - return { - ids: bounded, - flow: bounded.length > 1, - complete: q.parts.every((concept, index) => - concept.size === 0 || conceptCoverage.has(index)), - structuralRequired: true, - } -} - -function selectStructure( - c: Corpus, scored: readonly Scored[], q: Vocabulary, -): Selection | null { - if (!q.structural || q.terms[0] === 'where' - || (q.scopes.some((s) => !s.hard) && !q.expand) - || q.limits.some((s) => s.subject.includes('/'))) return null - if (!q.expand) { - const matches = scored.map(({ n }) => - [n, termsOf(n, q)] as const) - const coverable = new Set(matches.flatMap(([, terms]) => terms)) - if (matches.some(([n, found]) => - found.length * 5 >= coverable.size * 3 - && found.some((t) => - matches.filter(([, terms]) => terms.includes(t)).length <= 2) - && !n.ins.concat(n.outs).some((id) => { - const adjacent = c.byId.get(id) - return !!adjacent && termsOf(adjacent, q) - .some((t) => coverable.has(t) && !found.includes(t)) - }))) return null - } - const seeds = scored.flatMap((item): Seed[] => { - if (!usable(item.n, q)) return [] - const hits = termsOf(item.n, q) - if (hits.length === 0) return [] - return [{ - ...item, hits, - parts: q.parts.flatMap((concept, index) => - hits.some((t) => concept.has(t)) ? [index] : []), - }] - }).sort((a, b) => - Number(b.n.ins.length + b.n.outs.length > 0) - - Number(a.n.ins.length + a.n.outs.length > 0) - || b.hits.length - a.hits.length - || byRank(a, b)) - return connect(c, seeds, q) - ?? causal(c, seeds, q) -} - -function fallback( - index: ReadyQueryIndex, c: Corpus, scored: readonly Scored[], - q: Vocabulary, -): Selection { - const ids: string[] = [] - const files = new Set() - const covered = new Set() - const add = (item: Scored): void => { - if (ids.includes(item.n.id)) return - if (item.n.file && !files.has(item.n.file) - && files.size >= FILE_CAP) return - ids.push(item.n.id) - if (item.n.file) files.add(item.n.file) - for (const t of item.rank.matchedTerms) covered.add(t) - } - if (q.limits.some((s) => s.subject.includes('/'))) { - for (const s of q.limits.filter((item) => - item.subject.includes('/'))) { - const matching = scored.filter((item) => inScope(item.n, s)) - const file = matching.find((item) => item.n.kind === 'file') - const symbol = matching.find((item) => item.n.kind !== 'file' - && (!file || index.graph.edgesBetween(file.n.id, item.n.id) - .some(({ attributes }) => text(attributes, 'relation') === 'contains'))) - if (file) add(file) - if (symbol) add(symbol) - } - } else if (q.parts.length > 1) { - for (const concept of q.parts) { - const next = scored.filter((item) => - item.rank.matchedTerms.some((t) => concept.has(t))) - .sort((a, b) => - Number(!files.has(b.n.file)) - Number(!files.has(a.n.file)) - || byRank(a, b))[0] - if (next) add(next) - } - } else { - const loc = q.terms[0] === 'where' - const pos = (item: Scored): number[] => - termsOf(item.n, q, true).map((t) => q.pos.get(t) ?? LAST) - const start = loc ? [...scored].sort((a, b) => - b.rank.matchedTerms.length - a.rank.matchedTerms.length - || Math.min(LAST, ...pos(a)) - Math.min(LAST, ...pos(b)) - || pos(b).length - pos(a).length - || byRank(a, b))[0] : scored[0] - const exact = start && start.n.kind !== 'class' && start.n.kind !== 'file' - && exactLabel(start.n, q) - const first = start && (exact ? start : scored.find((item) => - item.n.file === start.n.file - && start.n.outs.includes(item.n.id) - && start.rank.matchedTerms.every((t) => - item.rank.matchedTerms.includes(t)) - && termsOf(item.n, q, true).length - >= termsOf(start.n, q, true).length) ?? start) - if (first) add(first) - while (ids.length < (exact && loc ? 1 : loc ? 2 : SNIPPET_CAP)) { - const next = scored.filter((item) => !ids.includes(item.n.id) - && (files.has(item.n.file) || files.size < FILE_CAP)) - .sort((a, b) => { - const link = (item: Scored): number => Number(ids.some((id) => - index.graph.edgesBetween(id, item.n.id).some(({ attributes }) => - CAUSAL.has(text(attributes, 'relation'))))) - const novelty = (item: Scored): number => - item.rank.matchedTerms.filter((t) => !covered.has(t)).length - return (loc - ? link(b) - link(a) - || Math.max(-1, ...pos(b)) - Math.max(-1, ...pos(a)) - || pos(b).length - pos(a).length - || novelty(b) - novelty(a) - : novelty(b) - novelty(a) || link(b) - link(a)) - || a.rank.firstMatch - b.rank.firstMatch - || byRank(a, b) - })[0] - if (!next) break - const novel = next.rank.matchedTerms.some((t) => !covered.has(t)) - const connected = termsOf(next.n, q).length > 0 && ids.some((id) => - index.graph.edgesBetween(id, next.n.id).some(({ attributes }) => - CAUSAL.has(text(attributes, 'relation')))) - if (loc ? !connected : !novel && !connected) break - add(next) - } - } - const ordered = toposort(c, ids).ids - return { - ids: ordered, flow: false, complete: true, structuralRequired: false, - } -} - -function unsupportedCandidates( - index: ReadyQueryIndex, q: Vocabulary, -): UnsupportedCandidate[] { - return index.unsupported_sources.flatMap((source): UnsupportedCandidate[] => { - const extension = source.path.toLowerCase().match(/\.([a-z0-9]+)$/)?.[1] ?? '' - const domain = classifySourceDomain(source.path, index.root_path) - if (!UNSUPPORTED.test(extension) - || isPollutedSourcePath(source.path, index.root_path) - || (domain !== 'production' && domain !== 'unknown')) return [] - const pathTokens = new Set(words(source.path)) - const basename = new Set(words(source.path.split('/').at(-1) ?? source.path)) - const matched = q.terms.filter((t) => - !t.includes('_') && !t.includes('-') && pathTokens.has(t)) - const weights = new Map(matched.map((t) => [t, basename.has(t) ? 4 : 1])) - const s = q.scopes.some((scope) => - scope.tokens.every((t) => pathTokens.has(t)) - && tokens(source.path).join('').includes(scope.compact)) - if (!s && (matched.length === 0 - || matched.every((t) => t.length < 4))) return [] - return [{ - path: source.path, terms: matched, weights, - first: matched.reduce((first, t) => - Math.min(first, q.pos.get(t) ?? LAST), - LAST), - score: matched.reduce((total, t) => - total + t.length ** 2 * (weights.get(t) ?? 1) * 100, - s ? 1_000_000 : 0), - }] - }).sort((a, b) => - b.score - a.score || a.first - b.first - || compare(a.path, b.path)) -} - -function unsupportedBoundaries( - choices: readonly UnsupportedCandidate[], -): EvidenceBoundary[] { - const picked: UnsupportedCandidate[] = [] - const covered = new Set() - const rest = [...choices] - while (rest.length > 0 && picked.length < 4) { - rest.sort((a, b) => { - const novelty = (item: UnsupportedCandidate): number => - item.terms.filter((t) => !covered.has(t)) - .reduce((total, t) => - total + t.length ** 2 * (item.weights.get(t) ?? 1), 0) - return novelty(b) - novelty(a) - || b.score - a.score || compare(a.path, b.path) - }) - const next = rest.shift()! - if (picked.length > 0 - && next.terms.every((t) => covered.has(t)) - && next.score * 3 < picked[0]!.score) break - picked.push(next) - for (const t of next.terms) covered.add(t) - } - const boundaries: EvidenceBoundary[] = picked - .map((item): EvidenceBoundary => ({ - kind: 'unsupported', subject: item.path, - })) - .sort((a, b) => compare(a.subject, b.subject)) - return picked.length >= 4 && rest.length > 0 - ? [...boundaries, { kind: 'truncated', subject: 'unsupported sources' }] - : boundaries -} - -export function rankQueryAnchors( - index: ReadyQueryIndex, request: NormalizedRetrieveRequest, -): RankQueryResult { - const c = buildCorpus(index) - const q = vocabulary(request.question) - const active = (s: Scope): boolean => !s.hard - || s.subject.includes('/') || c.nodes.some((n) => { - const prefix = s.tokens.filter((t) => !/^\d+$/.test(t)) - return prefix.every((t) => n.tokens.has(t)) - && [...n.tokens].some((t) => /^\d+$/.test(t)) - }) - for (const s of q.scopes.filter((s) => !active(s))) { - const outside = new Set(tokens(request.question.replaceAll(s.subject, ''))) - q.terms = q.terms.filter((t) => - !/^\d+$/.test(t) || !s.tokens.includes(t) || outside.has(t)) - } - q.scopes = q.scopes.filter(active) - q.limits = q.scopes.filter((s) => s.hard) - const outside = unsupportedCandidates(index, q) - const unsupportedFacts = unsupportedBoundaries(outside) - const missing = q.scopes.flatMap((s): EvidenceBoundary[] => { - const graphMatches = c.nodes.filter((n) => inScope(n, s)) - if (graphMatches.some((n) => n.eligible) - || outside.some((item) => - tokens(item.path).join('').includes(s.compact))) return [] - return [{ - kind: graphMatches.length > 0 ? 'unavailable' : 'missing', - subject: s.subject, - }] - }) - const found = q.scopes.filter((s) => - c.nodes.some((n) => n.eligible && inScope(n, s))) - const limits = q.limits.filter((s) => - found.includes(s)) - const scoped = new Set(found.flatMap((s) => s.tokens)) - const allScopedTerms = new Set(q.scopes.flatMap((s) => s.tokens)) - const unscopedTerms = new Set(q.terms.filter((t) => !allScopedTerms.has(t))) - const outsideTerms = new Set(q.terms.filter((t) => !scoped.has(t))) - const has = (n: Node, terms: ReadonlySet): boolean => - [...terms].some((t) => matches(n.tokens, t) - || n.ins.concat(n.outs).some((id) => { - const adjacent = c.byId.get(id) - return !!adjacent?.eligible && matches(adjacent.tokens, t) - })) - const keep = (n: Node): boolean => q.limits.length > 0 - ? limits.some((s) => inScope(n, s)) - : q.scopes.length === 0 - || (found.length === 0 - ? has(n, unscopedTerms) - : found.some((s) => inScope(n, s)) - || has(n, outsideTerms)) - const pool = scoredNodes(c, q, keep) - const structural = selectStructure(c, pool, q) - const choice = structural ?? fallback( - index, c, pool, q, - ) - const anchors = choice.ids.flatMap((id, ordinal): RankedQueryNode[] => { - const existing = pool.find((item) => item.n.id === id)?.rank - if (existing) return [existing] - const n = c.byId.get(id) - if (!n?.eligible) return [] - const matchedTerms = termsOf(n, q) - return [{ - id, attributes: n.attributes, - score: Math.max(0, (pool[0]?.rank.score ?? 0) - ordinal), - matchedTerms, - firstMatch: matchedTerms.reduce((first, t) => - Math.min(first, q.pos.get(t) ?? LAST), - LAST), - }] - }) - const picked = new Set(anchors.map((anchor) => anchor.id)) - const selectedFiles = new Set(anchors.map((anchor) => - text(anchor.attributes, 'source_file'))) - const truncated = pool.some(({ n }) => !picked.has(n.id)) - && (anchors.length >= SNIPPET_CAP - || selectedFiles.size >= FILE_CAP) - ? [{ kind: 'truncated', subject: 'query anchors' } satisfies EvidenceBoundary] - : [] - const boundaries = anchors.length === 0 - && unsupportedFacts.length === 0 && missing.length === 0 - ? [{ kind: 'missing', subject: request.question } satisfies EvidenceBoundary] - : [...unsupportedFacts, ...missing, ...truncated] - return { - anchors, boundaries, - queryTerms: q.terms, flow: choice.flow, branch: choice.branch ?? [], - sequential: q.sequential, - priorityAnchorIds: choice.ids, - structuralRequired: choice.structuralRequired, - structuralCoverageComplete: choice.complete - && (!choice.structuralRequired || missing.length === 0), - } -} diff --git a/src/domain/query/slice.ts b/src/domain/query/slice.ts deleted file mode 100644 index e80db3b4..00000000 --- a/src/domain/query/slice.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' - -import { - canonicalJsonString as json, compareCodeUnits as compare, -} from '../graph/canonical-json.js' -import { - MAX_RETRIEVE_FILES, MAX_RETRIEVE_SNIPPETS, - RETRIEVE_RESULT_SCHEMA, RETRIEVE_RESULT_VERSION, - type EvidenceBoundary, type EvidenceNode, type EvidenceRelationship, - type NormalizedRetrieveRequest, type RetrieveContextResult, type RetrieveOutcome, -} from './types.js' - -export interface SliceEvidenceInput { - request: NormalizedRetrieveRequest; outcome: RetrieveOutcome - matchedNodes: readonly EvidenceNode[]; relationships: readonly EvidenceRelationship[] - boundaries: readonly EvidenceBoundary[]; priorityNodeIds: readonly string[]; closurePasses: 0 | 1 - structuralRequired?: boolean - structuralCoverageComplete?: boolean -} - -interface Bundle { - nodes: readonly EvidenceNode[] - edge?: EvidenceRelationship - fact?: EvidenceBoundary - rank: readonly [number, number] - order: number - key: string -} - -const CAUSAL_RELATIONS = new Set(['calls', 'enqueues_job']) - -function causal(edge: EvidenceRelationship): boolean { - return CAUSAL_RELATIONS.has(edge.relation) -} - -function truncation(target?: EvidenceNode): EvidenceBoundary { - if (!target) return { kind: 'truncated', subject: 'retrieve', detail: 'Omitted by limit.' } - return { - kind: 'truncated', - subject: target.evidence_kind === 'symbol_declaration' - ? `${target.source_file}:${target.source_location}` - : target.source_file, - } -} - -function edgeOrder(left: EvidenceRelationship, right: EvidenceRelationship): number { - return compare(left.from_id, right.from_id) - || compare(left.relation, right.relation) - || compare(left.to_id, right.to_id) - || compare(left.id, right.id) -} - -function edgeSlot( - edges: readonly EvidenceRelationship[], - edge: EvidenceRelationship, -): { at: number; delta: number } { - let low = 0 - let high = edges.length - while (low < high) { - const middle = (low + high) >>> 1 - if (edgeOrder(edges[middle]!, edge) < 0) low = middle + 1 - else high = middle - } - const before = [edges[low - 1], edges[low]] - .filter((value): value is EvidenceRelationship => Boolean(value)) - const after = [edges[low - 1], edge, edges[low]] - .filter((value): value is EvidenceRelationship => Boolean(value)) - return { - at: low, - delta: countTokens(json(after)) - countTokens(json(before)), - } -} - -function addEdgeTokens(current: number, delta: number): number { - const body = current - countTokens(String(current)) + delta - let tokens = body - for (let pass = 0; pass < 16; pass += 1) { - const observed = body + countTokens(String(tokens)) - if (observed === tokens) return tokens - tokens = observed - } - throw new Error('Unable to stabilize retrieve serialized token count') -} - -function factOrder(left: EvidenceBoundary, right: EvidenceBoundary): number { - return compare(left.kind, right.kind) - || compare(left.subject, right.subject) - || compare(left.detail ?? '', right.detail ?? '') -} - -function unique( - values: readonly T[], - identityOf: (value: T) => string, - name: string, -): T[] { - const facts = new Map() - for (const value of values) { - const identity = identityOf(value) - const serialized = json(value) - const previous = facts.get(identity) - if (previous && previous.serialized !== serialized) { - throw new TypeError(`Conflicting ${name} facts share identity ${JSON.stringify(identity)}`) - } - if (!previous) facts.set(identity, { serialized, value }) - } - return [...facts.values()].map(({ value }) => value) -} - -function uniqueFacts(facts: readonly EvidenceBoundary[]): EvidenceBoundary[] { - return unique(facts, json, 'boundary').sort(factOrder) -} - -function handoffEnds(fact: EvidenceBoundary): readonly [string, string] | null { - if (fact.kind !== 'disconnected') return null - const separator = ' -> ' - const at = fact.subject.indexOf(separator) - if (at <= 0 || fact.subject.indexOf(separator, at + separator.length) >= 0) return null - return [fact.subject.slice(0, at), fact.subject.slice(at + separator.length)] -} - -function pruneFiles( - nodes: readonly EvidenceNode[], - relationships: readonly EvidenceRelationship[], -): EvidenceNode[] { - const related = new Set(relationships.flatMap(({ from_id, to_id }) => [from_id, to_id])) - return nodes.filter((node) => - node.evidence_kind !== 'structural_file' || related.has(node.node_id)) -} - -function finalize( - input: Pick< - SliceEvidenceInput, - 'request' | 'outcome' | 'closurePasses' | 'structuralRequired' - | 'structuralCoverageComplete' - >, - nodes: readonly EvidenceNode[], - edges: readonly EvidenceRelationship[], - facts: readonly EvidenceBoundary[], -): RetrieveContextResult { - const sortedEdges = [...edges].sort(edgeOrder) - const kept = pruneFiles(nodes, sortedEdges) - const ids = new Set(kept.map(({ node_id }) => node_id)) - const handoff = facts.some((fact) => { - const ends = handoffEnds(fact) - return ends !== null && ends.every((id) => ids.has(id)) - }) - const hasEdge = sortedEdges.some(causal) - const ready = !input.structuralRequired - || (input.structuralCoverageComplete !== false - && (hasEdge || handoff)) - const missing = input.outcome === 'evidence' && !ready - const outputFacts = uniqueFacts([ - ...facts, - ...missing ? [{ - kind: 'missing' as const, - subject: 'structural coverage', - }] : [], - ]) - const files = new Set([ - ...kept.map(({ source_file }) => source_file), - ...sortedEdges.flatMap(({ source_file }) => source_file ? [source_file] : []), - ]).size - const snippets = kept.filter(({ snippet }) => Boolean(snippet)).length - const result = (tokenCount: number): RetrieveContextResult => ({ - schema: RETRIEVE_RESULT_SCHEMA, - version: RETRIEVE_RESULT_VERSION, - outcome: missing - || (kept.length === 0 && input.outcome === 'evidence') - ? 'missing' - : input.outcome, - matched_nodes: kept, - relationships: sortedEdges, - boundaries: outputFacts, - metrics: { - selected_files: files, - snippets, - closure_passes: input.closurePasses, - serialized_tokens: tokenCount, - truncated: outputFacts.some(({ kind }) => kind === 'truncated'), - }, - }) - - let tokens = 0 - for (let pass = 0; pass < 16; pass += 1) { - const value = result(tokens) - const seen = countTokens(json(value)) - if (seen === tokens) return value - tokens = seen - } - for (tokens = 0; tokens <= 10_000; tokens += 1) { - const value = result(tokens) - if (countTokens(json(value)) === tokens) return value - } - throw new Error('Unable to stabilize retrieve serialized token count') -} - -function pack( - input: SliceEvidenceInput, - nodes: readonly EvidenceNode[], - edges: readonly EvidenceRelationship[], - facts: readonly EvidenceBoundary[], - budget?: number, -): { - nodes: EvidenceNode[] - relationships: EvidenceRelationship[] - boundaries: EvidenceBoundary[] - omitted: boolean -} { - const byId = new Map(nodes.map((node) => [node.node_id, node])) - const priorityIds = [...new Set(input.priorityNodeIds)] - const prioritySet = new Set(priorityIds) - const ordered = [ - ...priorityIds.flatMap((id) => { - const node = byId.get(id) - return node ? [node] : [] - }), - ...nodes.filter(({ node_id }) => !prioritySet.has(node_id)), - ] - const ordinals = new Map(priorityIds.map((id, index) => [id, index])) - const priority = (ids: readonly string[]): readonly [number, number] => { - const ranks = ids.map((id) => ordinals.get(id) ?? Number.POSITIVE_INFINITY) - return [Math.max(...ranks), Math.min(...ranks)] - } - const queue: Bundle[] = [] - const loose: EvidenceBoundary[] = [] - let omitted = false - - for (const edge of edges) { - const ids = [...new Set([edge.from_id, edge.to_id])] - const ends = ids.map((id) => byId.get(id)) - if (ends.some((node) => !node)) { - omitted = true - continue - } - queue.push({ - nodes: ends as EvidenceNode[], - edge, - rank: priority(ids), - order: causal(edge) ? 0 : 2, - key: json(edge), - }) - } - for (const fact of facts) { - if (fact.kind !== 'disconnected') { - if (budget === undefined || fact.kind !== 'truncated') { - loose.push(fact) - } - continue - } - const ids = handoffEnds(fact) - const ends = ids?.map((id) => byId.get(id)) - if (!ids || !ends || ends.some((node) => !node)) { - omitted = true - continue - } - queue.push({ - nodes: ends as EvidenceNode[], - fact, - rank: priority(ids), - order: 1, - key: json(fact), - }) - } - const rank = (left: number, right: number): number => - left === right ? 0 : left < right ? -1 : 1 - queue.sort((left, right) => - Number(Number.isFinite(right.rank[0])) - - Number(Number.isFinite(left.rank[0])) - || left.order - right.order - || rank(left.rank[0], right.rank[0]) - || rank(left.rank[1], right.rank[1]) - || compare(left.key, right.key)) - - const chosen = new Set() - const keptEdges: EvidenceRelationship[] = [] - let keptFacts = budget === undefined ? [] : [truncation()] - const files = new Set() - const blocked = new Set() - let snippets = 0 - let tokenCount: number | undefined - const selectedNodes = (ids: ReadonlySet = chosen): EvidenceNode[] => - ordered.filter(({ node_id }) => ids.has(node_id)) - const tryAdd = (item: Pick): boolean => { - const missing = item.nodes.filter(({ node_id }) => !chosen.has(node_id)) - const addedFiles = new Set( - missing.map(({ source_file }) => source_file).filter((file) => !files.has(file)), - ) - const edgeFile = item.edge?.source_file - if (edgeFile && !files.has(edgeFile)) addedFiles.add(edgeFile) - const addedSnippets = missing.filter(({ snippet }) => Boolean(snippet)).length - if (files.size + addedFiles.size > MAX_RETRIEVE_FILES - || snippets + addedSnippets > MAX_RETRIEVE_SNIPPETS) return false - - const candidateIds = new Set(chosen) - for (const { node_id } of missing) candidateIds.add(node_id) - const candidateEdges = item.edge - ? [...keptEdges, item.edge] - : keptEdges - const candidateFacts = item.fact - ? uniqueFacts([...keptFacts, item.fact]) - : keptFacts - let insertion: { at: number; delta: number } | undefined - let nextTokens: number | undefined - const edge = item.edge - const stableStructure = !input.structuralRequired - || !edge || !causal(edge) - || keptEdges.some(causal) - if (budget !== undefined) { - if (tokenCount !== undefined && edge && !item.fact - && missing.length === 0 && addedFiles.size === 0 && stableStructure) { - insertion = edgeSlot(keptEdges, edge) - nextTokens = addEdgeTokens(tokenCount, insertion.delta) - } else { - nextTokens = finalize( - input, - selectedNodes(candidateIds), - candidateEdges, - candidateFacts, - ).metrics.serialized_tokens - } - if (nextTokens > budget) return false - } - - for (const node of missing) chosen.add(node.node_id) - for (const file of addedFiles) files.add(file) - snippets += addedSnippets - if (edge) { - const at = insertion?.at - ?? edgeSlot(keptEdges, edge).at - keptEdges.splice(at, 0, edge) - } - if (item.fact) keptFacts = candidateFacts - tokenCount = nextTokens - return true - } - - for (const item of queue) { - if (tryAdd(item)) continue - omitted = true - if (item.fact) { - for (const { node_id } of item.nodes) { - if (!chosen.has(node_id)) blocked.add(node_id) - } - } - } - for (const node of ordered) { - if (chosen.has(node.node_id) || blocked.has(node.node_id)) continue - if (node.evidence_kind === 'structural_file' || !tryAdd({ nodes: [node] })) { - omitted = true - } - } - for (const fact of loose.sort(factOrder)) { - if (!tryAdd({ nodes: [], fact })) omitted = true - } - - return { - nodes: selectedNodes(), - relationships: keptEdges, - boundaries: keptFacts, - omitted, - } -} - -export function sliceEvidence(input: SliceEvidenceInput): RetrieveContextResult { - const nodes = unique(input.matchedNodes, ({ node_id }) => node_id, 'node') - const relationships = unique( - input.relationships, ({ id }) => id, 'relationship', - ).sort(edgeOrder) - const boundaries = uniqueFacts(input.boundaries) - const capped = pack(input, nodes, relationships, boundaries) - if (capped.omitted && !capped.boundaries.some(({ kind }) => kind === 'truncated')) { - capped.boundaries = uniqueFacts([...capped.boundaries, truncation()]) - } - const cappedResult = finalize( - input, capped.nodes, capped.relationships, capped.boundaries, - ) - if (cappedResult.metrics.serialized_tokens <= input.request.budget) return cappedResult - - const retained = pack( - input, capped.nodes, capped.relationships, capped.boundaries, input.request.budget, - ) - const omittedTarget = capped.nodes.find(({ node_id }) => - !retained.nodes.some((node) => node.node_id === node_id)) - if (omittedTarget) { - const targeted = retained.boundaries.map((boundary) => - boundary.kind === 'truncated' ? truncation(omittedTarget) : boundary) - if (finalize( - input, retained.nodes, retained.relationships, targeted, - ).metrics.serialized_tokens <= input.request.budget) retained.boundaries = targeted - } - return finalize( - input, retained.nodes, retained.relationships, retained.boundaries, - ) -} diff --git a/src/domain/query/traverse.ts b/src/domain/query/traverse.ts deleted file mode 100644 index 91b9f9f5..00000000 --- a/src/domain/query/traverse.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { compareCodeUnits as compare } from '../graph/canonical-json.js' -import type { GraphEdge } from '../graph/directed-multigraph.js' -import type { QueryGraph, ReadyQueryIndex } from './index-status.js' -import type { - EvidenceBoundary, QueryPathEdge, QuerySlice, RankedQueryNode, RankQueryResult, -} from './types.js' -import { sourceDomainOf as domainOf } from './source-domain.js' - -interface TraversalState { origin: number; nodeId: string } -interface PathPredecessor { nodeId: string; edge: QueryPathEdge } -function relationWords(value: string): string[] { - const normalized = value.toLowerCase() - return [normalized, ...normalized.split(/[^a-z0-9]+/u)] - .filter((term, index, terms) => term.length > 0 && terms.indexOf(term) === index) -} -function mentions(relation: string, queryTerms: ReadonlySet): boolean { - const terms = relationWords(relation) - return queryTerms.has(terms[0]!) - || (terms.length > 1 && terms.slice(1).every((term) => queryTerms.has(term))) -} - -function pathEdge(edge: GraphEdge): QueryPathEdge { - const relation = edge.attributes.relation - if (typeof relation !== 'string' || relation.length === 0) { - throw new Error(`Graph edge ${edge.id} has no relation`) - } - return { id: edge.id, from: edge.source, to: edge.target, relation, attributes: edge.attributes } -} - -function allowed(graph: QueryGraph, edge: QueryPathEdge): boolean { - const from = graph.nodeAttributes(edge.from) - const to = graph.nodeAttributes(edge.to) - const fromFile = from.node_kind === 'file' - const toFile = to.node_kind === 'file' - if ((!fromFile && (!from.definition_range || !from.declaration_range)) - || (!toFile && (!to.definition_range || !to.declaration_range))) return false - if (edge.relation === 'contains') { - return fromFile && !toFile && from.source_file === to.source_file - } - if (fromFile || toFile) return edge.relation === 'imports_from' && fromFile && toFile - return edge.relation === 'calls' || edge.relation === 'enqueues_job' -} - -function outgoing( - graph: QueryGraph, nodeId: string, terms: ReadonlySet, -): QueryPathEdge[] { - return graph.successors(nodeId) - .flatMap((targetId) => graph.edgesBetween(nodeId, targetId)) - .map(pathEdge) - .filter((edge) => allowed(graph, edge)) - .sort((left, right) => { - const leftMentioned = mentions(left.relation, terms) - const rightMentioned = mentions(right.relation, terms) - if (leftMentioned !== rightMentioned) return leftMentioned ? -1 : 1 - const line = (edge: QueryPathEdge): number => - Number(String(edge.attributes.source_location ?? '').match(/\d+/)?.[0] ?? Number.MAX_SAFE_INTEGER) - return line(left) - line(right) - || compare(left.to, right.to) - || compare(left.relation, right.relation) - || compare(left.id, right.id) - }) -} - -function direct( - graph: QueryGraph, - from: string, - to: string, -): boolean { - return graph.edgesBetween(from, to) - .map(pathEdge) - .some((edge) => allowed(graph, edge)) -} - -function rebuild( - sourceId: string, targetId: string, parents: ReadonlyMap, -): QueryPathEdge[] { - const reversed: QueryPathEdge[] = [] - let currentId = targetId - while (currentId !== sourceId) { - const predecessor = parents.get(currentId) - if (!predecessor) { - throw new Error(`Traversal predecessor missing for ${sourceId} -> ${targetId}`) - } - reversed.push(predecessor.edge) - currentId = predecessor.nodeId - } - return reversed.reverse() -} - -function unique(boundaries: readonly EvidenceBoundary[]): EvidenceBoundary[] { - const seen = new Set() - return boundaries.filter((boundary) => { - const key = `${boundary.kind}\u0000${boundary.subject}\u0000${boundary.detail ?? ''}` - if (seen.has(key)) return false - seen.add(key) - return true - }) -} - -function verify(graph: QueryGraph, nodeId: string): string { - const attributes = graph.nodeAttributes(nodeId) - return [attributes.source_file, attributes.source_location] - .filter((value): value is string => - typeof value === 'string' && value.length > 0) - .join(':') || nodeId -} - -function valid( - graph: QueryGraph, ranking: RankQueryResult, boundaries: EvidenceBoundary[], -): RankedQueryNode[] { - const seen = new Set() - return ranking.anchors.filter((anchor) => { - if (!graph.hasNode(anchor.id)) { - boundaries.push({ - kind: 'corrupt', - subject: anchor.id, - detail: 'ranked anchor is absent from the authoritative graph', - }) - return false - } - if (seen.has(anchor.id)) return false - seen.add(anchor.id) - return true - }) -} - -export function traverseEvidencePaths( - index: ReadyQueryIndex, ranking: RankQueryResult, -): QuerySlice { - const facts = [...ranking.boundaries] - const anchors = valid(index.graph, ranking, facts) - if (anchors.length <= 1) { - return { - nodeIds: anchors.map((anchor) => anchor.id), - edges: [], - boundaries: unique(facts), - closurePasses: 0, - } - } - - const branches = new Set(ranking.branch) - const chain = anchors.filter(({ id }) => !branches.has(id)) - const sources = chain.slice(0, -1).map((source, origin) => ({ - source, - targets: ranking.flow - ? [ - chain[origin + 1]!, - ...anchors.slice( - anchors.indexOf(source) + 1, - anchors.indexOf(chain[origin + 1]!), - ).filter(({ id }) => branches.has(id)), - ] - : anchors.slice(anchors.indexOf(source) + 1), - })) - const visited = sources.map(({ source }) => new Set([source.id])) - const parents = sources.map(() => new Map()) - const paths = sources.map(() => new Map()) - const queue: TraversalState[] = sources.map(({ source }, origin) => ({ - origin, - nodeId: source.id, - })) - const terms = new Set(ranking.queryTerms.map((term) => term.toLowerCase())) - const forest = !ranking.sequential - const domain = (id: string) => { - const a = index.graph.nodeAttributes(id) - return domainOf(a.source_domain, String(a.source_file ?? ''), index.root_path) - } - const domains = new Set([ - 'production', 'unknown', ...anchors.map(({ id }) => domain(id)), - ]) - - for (let cursor = 0; cursor < queue.length; cursor += 1) { - const state = queue[cursor]! - const search = sources[state.origin]! - const found = paths[state.origin]! - if (found.size === search.targets.length) continue - const seen = visited[state.origin]! - const nextEdges = outgoing(index.graph, state.nodeId, terms) - .filter(({ to }) => domains.has(domain(to))) - const previous = parents[state.origin]! - - for (const edge of nextEdges) { - if (seen.has(edge.to)) continue - seen.add(edge.to) - previous.set(edge.to, { nodeId: state.nodeId, edge }) - if (search.targets.some((target) => target.id === edge.to)) { - found.set(edge.to, rebuild(search.source.id, edge.to, previous)) - } - queue.push({ origin: state.origin, nodeId: edge.to }) - } - } - - const nodeIds: string[] = [] - const edges: QueryPathEdge[] = [] - const nodes = new Set() - const edgeIds = new Set() - const include = (nodeId: string): void => { - if (nodes.has(nodeId)) return - nodes.add(nodeId) - nodeIds.push(nodeId) - } - for (const anchor of anchors) include(anchor.id) - - for (const [origin, search] of sources.entries()) { - for (const [targetIndex, target] of search.targets.entries()) { - const path = paths[origin]!.get(target.id) - const adjacent = !ranking.flow && targetIndex > 0 - && anchors.slice(origin, origin + targetIndex + 1).every((_, offset) => - paths[origin + offset]!.has(anchors[origin + offset + 1]!.id)) - const commonParent = forest - && sources.slice(0, origin).some((_, earlier) => - paths[earlier]!.has(search.source.id) && paths[earlier]!.has(target.id)) - const fanOut = forest && anchors.some((anchor) => - anchor.id !== search.source.id - && anchor.id !== target.id - && direct(index.graph, anchor.id, search.source.id) - && direct(index.graph, anchor.id, target.id)) - const targetSource = sources.findIndex(({ source }) => source.id === target.id) - const fanIn = forest && targetSource >= 0 - && anchors.some((anchor) => - anchor.id !== search.source.id - && anchor.id !== target.id - && visited[origin]!.has(anchor.id) - && visited[targetSource]!.has(anchor.id)) - if (adjacent) continue - if (!path && targetIndex === 0 - && !commonParent && !fanOut && !fanIn) { - facts.push({ - kind: 'disconnected', - subject: `${search.source.id} -> ${target.id}`, - detail: `${verify(index.graph, search.source.id)} -> ${verify(index.graph, target.id)}`, - }) - } - for (const edge of path ?? []) { - include(edge.from) - include(edge.to) - if (edgeIds.has(edge.id)) continue - edgeIds.add(edge.id) - edges.push(edge) - } - } - } - - // A selected evidence skeleton can be a forest, fan-in, or cycle rather than - // one linear path. Preserve every authenticated direct evidence edge between - // retained nodes so traversal ordering cannot silently discard a branch or - // back-edge. - for (const from of nodeIds) { - for (const to of index.graph.successors(from)) { - if (!nodes.has(to)) continue - for (const graphEdge of index.graph.edgesBetween(from, to)) { - const edge = pathEdge(graphEdge) - if (!allowed(index.graph, edge) || edgeIds.has(edge.id)) continue - edgeIds.add(edge.id) - edges.push(edge) - } - } - } - - return { nodeIds, edges, boundaries: unique(facts), closurePasses: 1 } -} diff --git a/src/domain/query/types.ts b/src/domain/query/types.ts index 9fbc2948..7397bc92 100644 --- a/src/domain/query/types.ts +++ b/src/domain/query/types.ts @@ -1,69 +1,265 @@ -import type { GraphAttributes } from '../graph/directed-multigraph.js' -import type { IndexRange } from '../index/model.js' +import type { IndexBodyFact, IndexRange, IndexValue } from '../index/model.js' + export const RETRIEVE_RESULT_SCHEMA = 'madar.retrieve' as const -export const RETRIEVE_RESULT_VERSION = 1 as const +export const RETRIEVE_RESULT_VERSION = 2 as const export const DEFAULT_RETRIEVE_BUDGET = 4000 export const MIN_RETRIEVE_BUDGET = 256 export const MAX_RETRIEVE_BUDGET = 4000 export const MAX_RETRIEVE_QUESTION_LENGTH = 512 export const MAX_RETRIEVE_FILES = 12 -export const MAX_RETRIEVE_SNIPPETS = 25 -export interface NormalizedRetrieveRequest { question: string; budget: number } -export type EvidenceBoundaryKind = - | 'missing' | 'disconnected' | 'unsupported' | 'stale' - | 'unavailable' | 'corrupt' | 'truncated' -export interface EvidenceBoundary { kind: EvidenceBoundaryKind; subject: string; detail?: string } -export interface RankedQueryNode { - id: string; attributes: GraphAttributes; score: number - matchedTerms: string[]; firstMatch: number -} -export interface RankQueryResult { - anchors: RankedQueryNode[]; boundaries: EvidenceBoundary[]; queryTerms: string[] - flow: boolean; branch: readonly string[]; sequential?: boolean - priorityAnchorIds?: readonly string[] - coveredTerms?: readonly string[] - structuralRequired?: boolean - structuralCoverageComplete?: boolean -} -export interface QueryPathEdge { - id: string; from: string; to: string; relation: string; attributes: GraphAttributes -} -export interface QuerySlice { - nodeIds: string[]; edges: QueryPathEdge[] - boundaries: EvidenceBoundary[]; closurePasses: 0 | 1 -} - -interface EvidenceNodeBase { - node_id: string; label: string; source_file: string; source_domain?: string - provenance: unknown[]; content_hash: string -} -export type EvidenceNode = EvidenceNodeBase & ({ - evidence_kind: 'structural_file'; node_kind: 'file'; snippet?: undefined - definition_range?: undefined; declaration_range?: undefined +export const MAX_RETRIEVE_EXCERPTS = 25 + +export function valueHas( + value: IndexValue, test: (candidate: IndexValue) => boolean, +): boolean { + return test(value) + || value.kind === 'array' && value.elements.some((entry) => valueHas(entry, test)) + || value.kind === 'object' && value.entries.some((entry) => valueHas(entry.value, test)) + || value.kind === 'template' && value.parts.some((entry) => valueHas(entry, test)) +} + +export interface NormalizedRetrieveRequest { + question: string + budget: number +} + +export type RetrieveIntent = 'locate' | 'explain' | 'workflow' +type List = readonly T[] +type FailureState = 'stale' | 'unavailable' | 'corrupt' +type EvidenceFailure = { state: FailureState; subject: string } +export type RetrieveObligationKind = + | 'subject' + | 'entry' + | 'stage' + | 'handoff' + | 'behavior' + | 'ordering' + | 'terminal' +export type RetrieveState = + | 'ready' + | 'incomplete' + | 'unsupported' + | FailureState + +type MetricKey = `${'budget' | 'serialized'}_tokens` | 'selected_files' + | 'authenticated_excerpts' | `${'required' | 'proven'}_obligations` + | 'optional_bundles_omitted' | `${'root' | 'initial'}_candidates` + | 'explored_nodes' | 'causal_hops' | 'recovery_frontier_nodes' | 'alternate_seeds' +export type RetrieveMetrics = Record & { + recovery_passes: 0 | 1 | 2 +} + +export interface QuerySummary { + intent: RetrieveIntent + subject: string + terms: List +} +export type QueryIntent = RetrieveIntent +export type LocateAccess = 'read' | 'write' +export type ObligationKind = RetrieveObligationKind +export interface QueryObligation { + id: `o${number}`; kind: ObligationKind; target: string; mandatory: boolean +} +export interface QueryPlan { + intent: QueryIntent; subject: string; terms: List + obligations: List; access?: LocateAccess +} +export type QuestionPlanResult = { + status: 'supported'; plan: QueryPlan } | { - evidence_kind: 'symbol_declaration'; node_kind: string; source_location: string - line_number: number; end_line_number: number; definition_range: IndexRange - declaration_range: IndexRange; snippet: string -}) + status: 'unsupported' + reason: 'unsupported_intent' | 'missing_subject' + terms: List +} + +export type RetrieveMissingCode = + | `${'subject' | 'entrypoint' | 'terminal_persistence' | 'corridor' + | 'obligation_target' | 'adjacent_handoff' | 'behavior' + | 'controller_dependency'}_unproven` + | 'selection_bound_reached' + | `required_${'file_limit' | 'excerpt_limit' | 'token_budget' + | 'proof_missing' | 'reference_missing'}` -export interface EvidenceRelationship { - id: string; from_id: string; to_id: string; relation: string - source_file?: string; source_location?: string; provenance: unknown[] +export interface MissingRequirement { + code: RetrieveMissingCode + obligation_id?: string + target?: string + required?: number + limit?: number } -export type RetrieveOutcome = - | 'evidence' | 'missing' | 'unsupported' | 'stale' | 'unavailable' | 'corrupt' +type StringFields = Record +type Tagged = { kind: K } +type DossierRow = StringFields<'id' | K> +type ProvenRow = DossierRow & { + proofs: List +} +type OperationRefs = { operationIds: List } +type WorkflowRefs = OperationRefs & { symbolIds: List } -export interface RetrieveContextResult { - schema: typeof RETRIEVE_RESULT_SCHEMA; version: typeof RETRIEVE_RESULT_VERSION - outcome: RetrieveOutcome; matched_nodes: EvidenceNode[] - relationships: EvidenceRelationship[]; boundaries: EvidenceBoundary[] +export type WorkflowRelation = + | 'calls' | 'publishes_to' | 'routes_through' | 'consumed_by' +export type WorkflowMissingCode = Extract< + RetrieveMissingCode, `${string}_unproven` | 'selection_bound_reached' +> +export type WorkflowEdge = StringFields<'id' | 'fromId' | 'toId'> & { + relation: WorkflowRelation +} +export type WorkflowHandoff = OperationRefs & StringFields<'fromId' | 'toId'> & { + kind: 'direct' | 'channel'; edgeIds: List +} +export type WorkflowControlGroup = WorkflowRefs & { + kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' + controllerOperationId?: string; arm?: string +} +export type WorkflowObligationProof = WorkflowRefs & { + id: `o${number}`; kind: RetrieveObligationKind; target: string + mandatory: boolean; proven: boolean; edgeIds: List +} +export type WorkflowMissingReason = { + code: WorkflowMissingCode; obligationId?: string; target: string +} +export type WorkflowSelection = WorkflowRefs & { + complete: boolean + rootSymbolIds: List + terminalSymbolIds: List + edges: List + links: List + controlGroups: List + obligations: List + missing: List metrics: { - selected_files: number; snippets: number; closure_passes: 0 | 1 - serialized_tokens: number; truncated: boolean + candidateCount: number; rootCandidateCount: number + actualNodeCount: number; causalRelationHops: number + recoveryPasses: 0 | 1 | 2; recoveryFrontierCount: number; bounded: boolean + } +} + +export type ProvenObligation = ProvenRow<'statement'> & { + kind: RetrieveObligationKind +} + +export type DossierFile = DossierRow<'path' | 'digest'> + +export type DossierExcerpt = DossierRow<'file' | 'text'> & { + range: readonly [number, number, number, number] +} + +export type DossierEntity = DossierRow & (Tagged<'symbol'> & StringFields<'label' | 'file'> & { + node_kind?: string + excerpt?: string +} | Tagged<'channel'> & StringFields<'transport' | 'key'> & { + channel_kind: 'queue' | 'job' | 'event' + parent?: string + scope?: string +} | Tagged<'operation'> & { excerpt: string } & (StringFields<'operation_kind' | 'owner'> & { + detail: Readonly> + links?: never +} | { + links: List; order: List + callee?: string + scheduling?: string +})) + +export type DossierProof = DossierRow<'excerpt' | 'from' | 'to' | 'relation'> + +export type DossierLink = ProvenRow<'from' | 'to'> & { + kind: 'direct' | 'channel' +} + +export type DossierOrderGroup = ProvenRow & { + kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' + controller?: string + arm?: string + depth?: number + detail?: Readonly> + members: List +} + +export interface AnswerDossier { + query: QuerySummary + obligations: List + flow: { + roots: List + terminals: List + links: List + order: List + } + evidence: { + digest_algorithm: 'sha256-base64url' + files: List + excerpts: List + entities: List + proofs: List } } +export type SelectedEvidenceEdge = DossierRow<'fromId' | 'toId'> & { + relation?: string +} + +export interface EvidenceHydrationTargets { + symbolIds: List + declarationSymbolIds: List + operationIds: List + validationOperationIds?: List + edges: List +} + +export type HydratedFile = readonly [alias: string, sha256: string] +export type HydratedExcerpt = readonly [ + alias: string, file: string, range: IndexRange, sha256: string, text: string, +] +export type HydratedEntity = + | readonly [ + alias: string, kind: 'symbol', label: string, nodeKind: string, file: string, + ] + | readonly [ + alias: string, kind: 'channel', channelKind: 'queue' | 'job' | 'event', + transport: string, key: string, + parentChannelId: string | undefined, scope: string | undefined, + ] + | readonly [ + alias: string, kind: 'operation', owner: string, fact: IndexBodyFact, + ] +export type HydratedProof = + | readonly [ + alias: string, kind: 'declaration' | 'operation', + subject: string, excerpt: string, + ] + | readonly [ + alias: string, kind: 'edge', from: string, to: string, + relation: string, excerpt: string, + ] +export type HydratedEvidenceResult = { + state: 'ready' + files: ReadonlyMap + entities: ReadonlyMap + excerpts: ReadonlyMap + proofs: ReadonlyMap +} | EvidenceFailure + +interface RetrieveResultBase { + schema: typeof RETRIEVE_RESULT_SCHEMA + version: typeof RETRIEVE_RESULT_VERSION + state: S + metrics: RetrieveMetrics +} + +export type RetrieveContextResult = + | RetrieveResultBase<'ready'> & { dossier: AnswerDossier } + | RetrieveResultBase<'incomplete'> & { + query: QuerySummary + missing: List + } + | RetrieveResultBase<'unsupported'> & { + reason: 'unsupported_intent' | 'missing_subject' + terms: List + } + | RetrieveResultBase & { + failures: List + } + export function normalizeRetrieveRequest(value: unknown): NormalizedRetrieveRequest { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new TypeError('retrieve input must be an object') @@ -74,15 +270,20 @@ export function normalizeRetrieveRequest(value: unknown): NormalizedRetrieveRequ } const question = typeof request.question === 'string' ? request.question.trim() : '' if (question.length === 0 || question.length > MAX_RETRIEVE_QUESTION_LENGTH) { - throw new TypeError(`retrieve question must be between 1 and ${MAX_RETRIEVE_QUESTION_LENGTH} characters`) + throw new TypeError( + `retrieve question must be between 1 and ${MAX_RETRIEVE_QUESTION_LENGTH} characters`, + ) } const budget = request.budget if (budget !== undefined && (typeof budget !== 'number' || !Number.isSafeInteger(budget) || budget <= 0)) { throw new TypeError('retrieve budget must be a positive integer') } - return { question, budget: Math.max( - MIN_RETRIEVE_BUDGET, - Math.min(budget ?? DEFAULT_RETRIEVE_BUDGET, MAX_RETRIEVE_BUDGET), - ) } + return { + question, + budget: Math.max( + MIN_RETRIEVE_BUDGET, + Math.min(budget ?? DEFAULT_RETRIEVE_BUDGET, MAX_RETRIEVE_BUDGET), + ), + } } diff --git a/src/domain/query/workflow.ts b/src/domain/query/workflow.ts new file mode 100644 index 00000000..9df67125 --- /dev/null +++ b/src/domain/query/workflow.ts @@ -0,0 +1,1070 @@ +import { compareCodeUnits as cmp } from '../graph/canonical-json.js' +import type { GraphAttributes } from '../graph/directed-multigraph.js' +import type { IndexBodyFact, IndexValue } from '../index/model.js' +import type { ReadyQueryIndex } from './index-status.js' +import { lexicalTokens as words } from './plan.js' +import { sourceDomainOf as domainOf, type SourceDomain } from './source-domain.js' +import { + valueHas, type ObligationKind, type QueryObligation, type QueryPlan, + type WorkflowControlGroup, type WorkflowMissingCode, type WorkflowMissingReason, + type WorkflowObligationProof, type WorkflowRelation, type WorkflowSelection, +} from './types.js' +const [CANDIDATES, NODES, HOPS, RECOVERY] = [32, 512, 24, 64] +const FAILURE_WORD = /^(?:abort|cancel|error|fail(?:ed|ure)?|refund|reject(?:ed)?|retry|rollback)$/u, + READ = /^(?:read|file_read|object_read)$/u +const MISSING_CODES: Partial> = { + handoff: 'adjacent_handoff_unproven', behavior: 'behavior_unproven', + subject: 'subject_unproven', entry: 'entrypoint_unproven', + terminal: 'terminal_persistence_unproven', +} +type SymbolNode = readonly [ + id: string, compact: string, nameCompact: string, + tokens: ReadonlySet, nameTokens: ReadonlySet, + facts: readonly IndexBodyFact[], persists: boolean, + domain: SourceDomain, requestEntry: boolean, +] +type IndexedEdge = readonly [ + id: string, from: string, to: string, relation: WorkflowRelation, + range: string, statement: string, owner: string, operation: string, + dispatchPayload: number | undefined, +] +type Arc = readonly [ + from: string, to: string, kind: 'direct' | 'channel', + edges: readonly IndexedEdge[], operations: readonly string[], +] +type ExecutionView = readonly [ + symbols: readonly SymbolNode[], byId: ReadonlyMap, + outgoing: ReadonlyMap, + incoming: ReadonlyMap, blocked: ReadonlySet, + nonEntries: ReadonlySet, operations: ReadonlyMap, +] +type Candidate = readonly [ + symbol: SymbolNode, lexical: number, rank: number, + affinity: number, exact: number, +] +type Reach = readonly [ + distance: Map, actual: Set, bounded: boolean, + previous: Map, +] +type Selection = readonly [ + symbols: Set, arcs: Arc[], terminals: string[], + actual: Set, bounded: boolean, +] +type Control = readonly [ + operations: string[], groups: WorkflowControlGroup[], proven: boolean, + terminalOperations: string[], +] +const cache = new WeakMap() +function append(map: Map, key: K, value: V): void { + map.get(key)?.push(value) ?? map.set(key, [value]) +} +const text = (attrs: GraphAttributes, key: string): string => + typeof attrs[key] === 'string' ? attrs[key] : '' +const factText = (fact: IndexBodyFact): string => + fact.kind === 'call' ? fact.callee + : fact.kind === 'persistence' + ? `${fact.receiver_type} ${JSON.stringify(fact.resource ?? '')}` + : fact.kind === 'mutation' ? fact.target + : fact.kind === 'literal' ? JSON.stringify(fact.value) + : fact.kind === 'condition' ? `condition ${fact.condition_kind}` + : fact.kind === 'loop' ? `loop ${fact.loop_kind}` + : fact.kind === 'parallel' + ? `parallel ${fact.combinator} ${fact.completion}` + : fact.kind +const behavior = (fact: IndexBodyFact): boolean => fact.kind !== 'literal' +const terminal = ( + fact: IndexBodyFact, +): fact is Extract => + fact.kind === 'persistence' && !READ.test(fact.operation) +function adverseFact(fact: IndexBodyFact | undefined): boolean { + return !!fact && (fact.control.some((frame) => + frame.kind === 'exception' && frame.arm === 'catch') + || fact.kind === 'call' + && fact.arguments.some((argument) => + valueHas(argument, (value) => value.kind === 'literal' + && typeof value.value === 'string' + && words(value.value).some((word) => + word !== 'retry' && FAILURE_WORD.test(word))))) +} +const terminalAt = (v: ExecutionView, id: string, adverse: boolean): boolean => + v[1].get(id)?.[5].some((fact) => terminal(fact) + && (adverse || !adverseFact(fact) && !adverseFact(v[6].get(fact.call_fact_id)))) ?? false +type LooseRange = { + start?: { line?: unknown; column?: unknown } + end?: { line?: unknown; column?: unknown } +} | undefined +function rangeKey(value: unknown): string { + return `${(value as LooseRange)?.start?.line}:${(value as LooseRange)?.start?.column + }:${(value as LooseRange)?.end?.line}:${(value as LooseRange)?.end?.column}` +} +const idsOf = (arc: Arc): string[] => arc[3].map((edge) => edge[0]) +const penalty = (domain: SourceDomain): number => + domain === 'production' ? 0 : domain === 'unknown' ? 4 : 32 +function requestEntry(attrs: GraphAttributes): boolean { + const role = text(attrs, 'framework_role'), + kind = text(attrs, 'node_kind') + return kind === 'route' + || /(?:_route|_api|_server_action|router_(?:loader|action)|trpc_procedure_)/u + .test(role) +} +function buildView(i: ReadyQueryIndex): ExecutionView { + const prior = cache.get(i) + if (prior) return prior + const symbols: SymbolNode[] = [] + for (const [id, attrs] of i.graph.nodeEntries()) { + if (['channel', 'file'].includes(text(attrs, 'node_kind'))) continue + const facts = i.operations_by_owner.get(id) ?? [], + file = text(attrs, 'source_file'), + name = [ + text(attrs, 'label'), text(attrs, 'qualified_name'), + text(attrs, 'node_kind'), text(attrs, 'framework_role'), + ].join(' '), + nameWords = words(name), + lexicon = words([name, file, ...facts.map(factText)].join(' ')) + symbols.push([id, lexicon.join(''), nameWords.join(''), + new Set(lexicon), new Set(nameWords), facts, facts.some(terminal), + domainOf(attrs.source_domain, file, i.root_path), requestEntry(attrs)]) + } + symbols.sort((left, right) => cmp(left[0], right[0])) + const byId = new Map(symbols.map((symbol) => [symbol[0], symbol])), + exact = new Map(), + routes = new Map(), + consumers = new Map(), publishes: IndexedEdge[] = [], + nonEntries = new Set() + const keep = (key: string, edge: IndexedEdge): void => { + const prior = exact.get(key) + if (!prior || cmp(edge[0], prior[0]) < 0) exact.set(key, edge) + } + for (const [from, to, attrs, id] of i.graph.edgeEntries()) { + const relation = String(attrs.relation) as WorkflowRelation + const evidence = attrs.evidence as { + source?: unknown; range?: unknown; statement_range?: unknown + excerpt_sha256?: unknown + } | undefined + if (!['calls', 'publishes_to', 'routes_through', 'consumed_by'].includes(relation) + || !/^(?:typescript-(?:semantic|syntactic)|framework-decorator|wrapper-summary)$/u + .test(String(evidence?.source))) continue + const owner = text(attrs, 'execution_owner_id'), + calls = relation === 'consumed_by' && owner && owner !== to + ? (i.operations_by_owner.get(owner) ?? []).filter((fact) => + fact.kind === 'call' + && rangeKey(fact.evidence.statement_range) + === rangeKey(evidence?.statement_range) + && fact.evidence.excerpt_sha256 === evidence?.excerpt_sha256) + : [], + payload = attrs.dispatch_payload_argument, + edge: IndexedEdge = [id, from, to, relation, + rangeKey(evidence?.range), rangeKey(evidence?.statement_range), + owner, calls.length === 1 ? calls[0]!.id : '', + typeof payload === 'number' && Number.isSafeInteger(payload) && payload >= 0 + ? payload : undefined] + if (byId.has(to) && (relation === 'consumed_by' + || relation === 'calls' && byId.get(from)?.[7] === 'production')) nonEntries.add(to) + if (relation === 'calls') keep(`c\0${from}\0${to}\0${edge[4]}`, edge) + else if (relation === 'routes_through') append(routes, `${from}\0${to}`, edge) + else if (relation === 'consumed_by') append(consumers, from, edge) + else publishes.push(edge) + } + publishes.sort((a, b) => cmp(a[0], b[0])) + for (const entries of consumers.values()) entries.sort((a, b) => cmp(a[0], b[0])) + const arcs: Arc[] = [], + publishCalls = new Map []>() + for (const owner of symbols) { + for (const fact of owner[5]) { + if (fact.kind !== 'call' || !fact.target_symbol_id + || !byId.has(fact.target_symbol_id)) continue + append(publishCalls, `${owner[0]}\0${rangeKey(fact.evidence.range)}\0${ + rangeKey(fact.evidence.statement_range)}`, fact) + const edge = exact.get( + `c\0${owner[0]}\0${fact.target_symbol_id}\0${rangeKey(fact.evidence.range)}`, + ) + if (edge) arcs.push([owner[0], fact.target_symbol_id, 'direct', [edge], [fact.id]]) + } + } + for (const publish of publishes) { + if (!byId.has(publish[1]) || !i.channels_by_id.has(publish[2])) continue + const channel = i.channels_by_id.get(publish[2])!, + matchingRoutes = channel.channel_kind === 'job' + ? (routes.get(`${channel.id}\0${channel.parent_channel_id}`) ?? []) + .filter((edge) => edge[6] === publish[1] + && edge[4] === publish[4] && edge[5] === publish[5]) + : [], + route = matchingRoutes.length === 1 ? matchingRoutes[0] : undefined + if (channel.channel_kind === 'job' && !route) continue + const destination = route?.[2] ?? channel.id + for (const consume of consumers.get(destination) ?? []) { + if (!byId.has(consume[2])) continue + const registration = !consume[6] || consume[6] === consume[2] + ? [] : consume[7] ? [consume[7]] : undefined + if (!registration) continue + const edges = route ? [publish, route, consume] : [publish, consume], + calls = publishCalls.get( + `${publish[1]}\0${publish[4]}\0${publish[5]}`, + ) ?? [] + arcs.push([publish[1], consume[2], 'channel', edges, + calls.length === 1 ? [calls[0]!.id, ...registration] : []]) + } + } + arcs.sort((a, b) => cmp(a[0], b[0]) || cmp(a[1], b[1]) + || cmp(a[3][0]![0], b[3][0]![0])) + const outgoing = new Map(), incoming = new Map() + for (const arc of arcs) { + append(outgoing, arc[0], arc); append(incoming, arc[1], arc) + } + const usedEdges = new Set(arcs.flatMap(idsOf)), + blocked = new Set(publishes.filter((edge) => byId.has(edge[1]) + && !usedEdges.has(edge[0])).map((edge) => edge[1])), + v: ExecutionView = [ + symbols, byId, outgoing, incoming, blocked, nonEntries, i.operation_by_id, + ] + cache.set(i, v) + return v +} +function score(symbol: SymbolNode, targets: readonly string[]): number { + let result = 0 + for (const target of targets) { + const terms = words(target), compact = terms.join('') + if (compact && symbol[2].includes(compact)) result += 128 + if (terms.length > 0 && terms.every((term) => symbol[4].has(term))) result += 64 + if (compact && symbol[1].includes(compact)) result += 32 + for (const term of terms) { + if (symbol[3].has(term)) result += 8 + if (symbol[4].has(term)) result += 8 + } + } + return result +} +function rootScore(v: ExecutionView, symbol: SymbolNode, lexical: number): number { + const degree = (v[3].get(symbol[0])?.length ?? 0) + + (v[2].get(symbol[0])?.length ?? 0) + + (v[4].has(symbol[0]) ? 1 : 0), + unresolved = symbol[5].filter((fact) => fact.kind === 'call' + && !fact.target_symbol_id).length + return lexical - penalty(symbol[7]) - Math.min(24, Math.max(0, degree - 8) * 2) + - Math.min(16, unresolved * 4) + - (degree === 0 && !symbol[6] ? 12 : 0) + - (symbol[6] ? 16 : 0) +} +function adverseArc(v: ExecutionView, arc: Arc): boolean { + return arc[4].some((id) => { + const fact = v[6].get(id) + if (adverseFact(fact)) return true + if (arc[2] !== 'channel' || fact?.kind !== 'call' || !fact.target_symbol_id) { + return false + } + const matching = (v[2].get(fact.target_symbol_id) ?? []).filter((inner) => + inner[2] === 'channel' && inner[1] === arc[1]) + return matching.length > 0 && matching.every((inner) => + inner[4].some((operation) => adverseFact(v[6].get(operation)))) + }) +} +function reach( + v: ExecutionView, seeds: readonly string[], reverse: boolean, + limit: number, accept?: (arc: Arc) => boolean, + allowed?: { has(id: string): boolean }, stops?: ReadonlySet, +): Reach { + const distance = new Map(seeds.map((seed) => [seed, 0])) + const actual = new Set(seeds), queue = [...seeds] + const previous = new Map(), overflow = new Set() + let bounded = false + while (queue.length > 0) { + queue.sort((left, right) => distance.get(left)! - distance.get(right)! + || cmp(left, right)) + const current = queue.shift()! + if (stops?.has(current)) continue + const base = distance.get(current)!, + arcs = (reverse ? v[3] : v[2]).get(current) ?? [] + for (const arc of arcs) { + if (accept && !accept(arc)) continue + const next = reverse ? arc[0] : arc[1], hops = base + arc[3].length + if (hops > HOPS) { overflow.add(next); continue } + if ((allowed && !allowed.has(next)) || (distance.get(next) ?? Infinity) <= hops) continue + const additions = [...new Set(arc[3].flatMap((edge) => [edge[1], edge[2]]))] + .filter((id) => !actual.has(id)) + if (actual.size + additions.length > limit) { bounded = true; continue } + additions.forEach((id) => actual.add(id)) + distance.set(next, hops) + previous.set(next, arc) + if (!queue.includes(next)) queue.push(next) + } + } + return [ + distance, actual, bounded || [...overflow].some((id) => !distance.has(id)), + previous, + ] +} +function bestPath( + v: ExecutionView, root: string, end: string, + allowed: ReadonlySet, targets: readonly string[], + accept?: (arc: Arc) => boolean, +): readonly [path: Arc[], bounded: boolean] { + let best: Arc[] = [], bestRank = -1, bestHops = 0, + count = 0, bounded = false + const visit = ( + at: string, path: Arc[], seen: Set, hops: number, rank: number, + ): void => { + if (count++ >= NODES) { bounded = true; return } + if (at === end) { + if (rank > bestRank || rank === bestRank && hops > bestHops) { + best = path; bestRank = rank; bestHops = hops + } + return + } + for (const arc of v[2].get(at) ?? []) { + if (bounded) break + const next = arc[1], nextHops = hops + arc[3].length + if (!allowed.has(next) || seen.has(next) || nextHops > HOPS + || accept && !accept(arc)) continue + seen.add(next) + visit(next, [...path, arc], seen, nextHops, + rank + score(v[1].get(next)!, targets)) + seen.delete(next) + } + } + visit(root, [], new Set([root]), 0, 0) + return [best, bounded || bestRank < 0] +} +function orderCmp( + left: readonly number[], right: readonly number[], +): number { + let i = 0 + while (i < left.length && i < right.length && left[i] === right[i]) i += 1 + return (left[i] ?? 0) - (right[i] ?? 0) || left.length - right.length +} +function corridor( + v: ExecutionView, root: string, limit: number, targets: readonly string[], + terminalTarget?: string, +): Selection { + const failureIntent = targets.some((target) => + words(target).some((word) => FAILURE_WORD.test(word))), + accept = failureIntent ? undefined : (arc: Arc) => !adverseArc(v, arc), + forward = reach(v, [root], false, limit, accept) + const found = [...forward[0].keys()].filter((id) => + terminalAt(v, id, failureIntent)), + requested = terminalTarget + ? targetSymbols(v, found, terminalTarget, 'terminal') : [], + candidates = terminalTarget ? requested : found + const structural = new Set(candidates.filter((id) => + v[3].get(id)?.some((arc) => arc[2] === 'channel'))), + relevant = candidates.filter((id) => score(v[1].get(id)!, targets) > 0), + scoped = new Set(structural), + stable = reach(v, [root], false, limit, (arc) => + (!accept || accept(arc)) && !arc[4].some((id) => + v[6].get(id)?.control.some((frame) => + frame.kind === 'branch' || frame.kind === 'loop')))[0] + const direct = (arc: Arc) => + arc[2] === 'direct' && (!accept || accept(arc)) && forward[0].has(arc[1]) + for (const seed of structural) { + const first = (v[2].get(seed) ?? []).filter(direct), + hits = new Map() + let branches = 0 + for (const id of new Set(first.map((arc) => arc[1]))) { + const below = reach( + v, [id], false, limit, direct, forward[0], + )[0], + reached = [...below.keys()].filter((candidate) => candidates.includes(candidate)) + if (!reached.some((candidate) => candidate !== seed)) continue + const unconditional = first.some((arc) => arc[1] === id + && arc[4].some((operation) => { + const fact = v[1].get(seed)?.[5].find((entry) => entry.id === operation) + return fact?.kind === 'call' && !fact.control.some((frame) => + frame.kind === 'branch' || frame.kind === 'loop') + })) + branches += 1 + for (const candidate of reached) { + const prior = hits.get(candidate) ?? [0, false] + hits.set(candidate, [prior[0] + 1, prior[1] || unconditional]) + } + } + for (const [candidate, [count, unconditional]] of hits) { + if (count > 1 || branches === 1 && unconditional) scoped.add(candidate) + } + } + const pool = (structural.size > 0 + ? candidates.filter((id) => scoped.has(id) + || relevant.includes(id) && stable.has(id)) + : relevant.length > 0 ? relevant : candidates) + .sort((left, right) => forward[0].get(right)! - forward[0].get(left)! + || Number(structural.has(right)) - Number(structural.has(left)) + || score(v[1].get(right)!, targets) - score(v[1].get(left)!, targets) + || cmp(left, right)) + const allowed = forward[0] + const terminals = pool.filter((id) => { + const below = reach( + v, [id], false, limit, accept, allowed, + )[0] + return !pool.some((other) => other !== id && below.has(other)) + }) + if (terminals.length === 0) terminals.push(...pool.slice(0, 1)) + const selected = terminals.length > 0 ? forward + : reach(v, [root], false, limit, accept, undefined, v[4]), + backward = reach(v, terminals, true, limit, accept, allowed) + let symbols = new Set([...selected[0].keys()].filter((id) => + terminals.length === 0 || backward[0].has(id))) + let arcs = [...v[2].values()].flat().filter((arc) => + symbols.has(arc[0]) && symbols.has(arc[1]) && (!accept || accept(arc))) + let pruned = false + const relationCount = new Set(arcs.flatMap(idsOf)).size, + hardLimit = relationCount > HOPS + if (relationCount > 20) { + if (terminals[0]) { + const all = arcs + if (terminals.length === 1) { + const selected = bestPath( + v, root, terminals[0], symbols, targets, accept, + ) + arcs = selected[0] + const originalCycles = cycleGroups(symbols, all) + for (const cycle of originalCycles) { + const members = new Set(cycle.symbolIds) + if (![...members].every((id) => + arcs.some((arc) => arc[0] === id || arc[1] === id))) continue + const closes = () => cycleGroups( + new Set(arcs.flatMap((arc) => [arc[0], arc[1]])), arcs, + ).some((group) => group.symbolIds.every((id) => members.has(id)) + && [...members].every((id) => group.symbolIds.includes(id))) + const candidates = all.filter((arc) => !arcs.includes(arc) + && members.has(arc[0]) && members.has(arc[1])) + .sort((left, right) => + Number(arcs.some((arc) => arc[0] === left[0] && arc[1] === left[1])) + - Number(arcs.some((arc) => arc[0] === right[0] && arc[1] === right[1])) + || cmp(left[3][0]![0], right[3][0]![0])) + for (const candidate of candidates) { + if (closes()) break + if (new Set([...arcs, candidate].flatMap(idsOf)).size <= HOPS) { + arcs.push(candidate) + } + } + } + const kept = new Set(arcs.flatMap((arc) => + arc[3].flatMap((edge) => [edge[1], edge[2]]))) + const omitted = all.filter((arc) => !arcs.includes(arc)), + newNodes = new Set(omitted.flatMap((arc) => [arc[0], arc[1]]) + .filter((id) => !kept.has(id))), + safe = new Set(kept), + keptOperations = new Set(arcs.flatMap((arc) => arc[4])), + unsafeSameEndpoint = omitted.some((arc) => + kept.has(arc[0]) && kept.has(arc[1]) + && !arc[4].some((id) => keptOperations.has(id) + || v[6].get(id)?.control.some((frame) => frame.kind !== 'exception'))) + let changed = true + while (changed) { + changed = false + for (const arc of omitted) if (safe.has(arc[0]) && !safe.has(arc[1]) + && (!kept.has(arc[0]) || arc[4].some((id) => + v[6].get(id)?.control.some((frame) => frame.kind !== 'exception')))) { + safe.add(arc[1]); changed = true + } + } + const cyclesPreserved = originalCycles.every((cycle) => + cycle.symbolIds.some((id) => !kept.has(id)) + || cycleGroups(new Set(kept), arcs).some((group) => + group.symbolIds.length === cycle.symbolIds.length + && group.symbolIds.every((id) => cycle.symbolIds.includes(id)))) + pruned = selected[1] || !cyclesPreserved + || newNodes.size > 2 || [...newNodes].some((id) => !safe.has(id)) + || unsafeSameEndpoint + if (pruned && !hardLimit) { arcs = all; pruned = false } + } else { + const paths = terminals.map((terminal) => { + const path: Arc[] = [] + for (let id = terminal; id !== root;) { + const arc = forward[3].get(id) + if (!arc) return [] + path.unshift(arc); id = arc[0] + } + return path + }) + const need = [...new Map(paths.flat().map((arc) => + [idsOf(arc).join('\0'), arc])).values()] + const kept = new Set(need.flatMap((arc) => + arc[3].flatMap((edge) => [edge[1], edge[2]]))) + pruned = paths.some((path) => path.length === 0) + || new Set(need.flatMap(idsOf)).size > HOPS + || all.some((arc) => (!accept || accept(arc)) && !need.includes(arc) + && arc[3].some((edge) => !kept.has(edge[1]) || !kept.has(edge[2]))) + arcs = pruned ? paths[0]! : need + if (pruned && !hardLimit) { arcs = all; pruned = false } + } + symbols = new Set([root, ...arcs.flatMap((arc) => [arc[0], arc[1]])]) + } else { + arcs = []; symbols = new Set([root]) + pruned = true + } + } + arcs.sort((left, right) => + (forward[0].get(left[0]) ?? Infinity) - (forward[0].get(right[0]) ?? Infinity) + || (forward[0].get(left[1]) ?? Infinity) - (forward[0].get(right[1]) ?? Infinity) + || cmp(left[0], right[0]) || cmp(left[1], right[1]) + || cmp(left[3][0]![0], right[3][0]![0])) + return [symbols, arcs, terminals.filter((id) => symbols.has(id)), + forward[1], terminals.length === 0 && (forward[2] || selected[2]) || pruned] +} +type PersistenceFact = Extract +function typedCase(value: IndexValue | undefined): string | undefined { + if (!value || value.kind !== 'literal') return undefined + return `case:${Buffer.from(JSON.stringify([ + typeof value.value, value.value, + ])).toString('base64url')}` +} +function objectPath(value: IndexValue, path: readonly string[]): IndexValue | undefined { + let current: IndexValue | undefined = value + for (const key of path) { + if (current?.kind !== 'object') return undefined + current = current.entries.find((entry) => entry.key === key)?.value + } + return current +} +function channelTerminals( + i: ReadyQueryIndex, arc: Arc, candidates: readonly PersistenceFact[], +): PersistenceFact[] { + if (arc[2] !== 'channel') return [...candidates] + const publish = arc[3][0], position = publish?.[8] + if (!publish || position === undefined) return [] + const call = arc[4].map((id) => i.operation_by_id.get(id)).find((fact) => + fact?.kind === 'call' && fact.owner_symbol_id === arc[0] + && rangeKey(fact.evidence.range) === publish[4] + && rangeKey(fact.evidence.statement_range) === publish[5]) + if (call?.kind !== 'call' || position >= call.arguments.length) return [] + const transport = i.channels_by_id.get(publish[2])?.transport, + matches: Array = [] + for (const condition of i.operations_by_owner.get(arc[1]) ?? []) { + if (condition.kind !== 'condition' || condition.condition_kind !== 'switch' + || condition.test?.kind !== 'template') continue + const [parameter, ...rawPath] = condition.test.parts + if (parameter?.kind !== 'parameter' || parameter.position !== 0 + || rawPath.some((part) => part.kind !== 'literal' + || typeof part.value !== 'string')) continue + const path = rawPath.map((part) => (part as Extract).value as string) + if (transport === 'bullmq' && path[0] === 'data') path.shift() + const arm = typedCase(objectPath(call.arguments[position]!, path)) + if (!arm) continue + const eligible = candidates.filter((fact) => fact.control.some((frame) => + frame.kind === 'branch' && frame.controller_fact_id === condition.id + && frame.arm === arm)) + if (eligible.length > 0) matches.push([condition.id, eligible]) + } + return matches.length === 1 ? matches[0]![1] : [] +} +function controls( + i: ReadyQueryIndex, arcs: readonly Arc[], terminals: readonly string[], + seeds: readonly string[], +): Control { + const close = (ids: Iterable): [Set, boolean] => { + const result = new Set(ids), queue = [...result] + let valid = true + const add = (id: string): void => { + if (!result.has(id)) { result.add(id); queue.push(id) } + } + for (let cursor = 0; cursor < queue.length; cursor += 1) { + const fact = i.operation_by_id.get(queue[cursor]!) + if (!fact) { valid = false; continue } + for (const frame of fact.control) { + if (frame.kind !== 'exception') add(frame.controller_fact_id) + } + if (fact.kind === 'parallel') fact.member_fact_ids.forEach(add) + if (fact.kind === 'persistence') add(fact.call_fact_id) + } + return [result, valid] + } + const cost = (fact: IndexBodyFact): readonly [number, number, number, string] => { + const closure = close([fact.id])[0], + call = fact.kind === 'persistence' + ? i.operation_by_id.get(fact.call_fact_id) : fact, + adverse = call?.kind === 'call' + && call.control.some((frame) => + frame.kind === 'exception' && frame.arm === 'catch') ? 1 : 0, + range = fact.evidence.statement_range, + span = (range.end.line - range.start.line) * 1_000 + + range.end.column - range.start.column + return [adverse, closure.size, span, fact.id] + } + const prefer = (left: IndexBodyFact, right: IndexBodyFact): number => { + const a = cost(left), b = cost(right) + return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] + || orderCmp(right.order, left.order) || cmp(a[3], b[3]) + } + const primary = new Set(seeds), + facts = [...new Set(arcs.flatMap((arc) => arc[4]))] + .map((id) => i.operation_by_id.get(id)) + .filter((fact): fact is IndexBodyFact => fact !== undefined) + facts.forEach((fact) => primary.add(fact.id)) + const terminalOperations = new Set() + for (const id of [...terminals].sort(cmp)) { + const candidates = (i.operations_by_owner.get(id) ?? []).filter(terminal), + incoming = arcs.filter((arc) => arc[1] === id && arc[2] === 'channel'), + groups = incoming.length > 0 + ? incoming.map((arc) => channelTerminals(i, arc, candidates)) : [candidates] + if (groups.some((group) => group.length === 0)) continue + for (const group of groups) { + const fact = group.sort(prefer)[0] + if (fact) { primary.add(fact.id); terminalOperations.add(fact.id) } + } + } + type Group = [kind: 'branch' | 'loop' | 'parallel', controller: string, + arm: string | undefined, operations: Set, symbols: Set] + const grouped = new Map(), + sequences = new Map[]>() + for (const fact of facts) { + if (fact.kind === 'call' + && !fact.control.some((frame) => frame.kind === 'parallel')) { + append(sequences, `${fact.owner_symbol_id}\0${JSON.stringify(fact.control)}`, fact) + } + } + const orderGroups: WorkflowControlGroup[] = [] + for (const calls of sequences.values()) if (calls.length > 1) { + calls.sort((a, b) => orderCmp(a.order, b.order) || cmp(a.id, b.id)) + orderGroups.push({ + kind: 'sequence', operationIds: calls.map((fact) => fact.id), + symbolIds: calls.flatMap((fact) => fact.target_symbol_id ? [fact.target_symbol_id] : []), + }) + } + const [needed, proven] = close(primary) + for (const id of needed) { + const fact = i.operation_by_id.get(id) + if (!fact) continue + for (const frame of fact.control) { + if (frame.kind === 'exception') continue + const arm = frame.kind === 'branch' ? frame.arm : undefined + const key = `${frame.kind}\0${frame.controller_fact_id}\0${arm ?? ''}` + const group = grouped.get(key) + ?? [frame.kind, frame.controller_fact_id, arm, new Set(), new Set()] as Group + grouped.set(key, group) + group[3].add(fact.id); group[4].add(fact.owner_symbol_id) + } + } + const groups = [...grouped.values()] + .map(([ + kind, controllerOperationId, arm, operations, symbols, + ]) => ({ + kind, controllerOperationId, ...(arm ? { arm } : {}), + operationIds: [...operations].sort(cmp), symbolIds: [...symbols].sort(cmp), + })).concat(orderGroups) + return [[...needed].sort(cmp), groups, proven, [...terminalOperations].sort(cmp)] +} +function cycleGroups(symbols: ReadonlySet, arcs: readonly Arc[]): WorkflowControlGroup[] { + const paths = new Map([...symbols].map((id) => [id, new Set()])) + for (const arc of arcs) paths.get(arc[0])?.add(arc[1]) + for (const through of symbols) for (const from of symbols) { + if (!paths.get(from)?.has(through)) continue + for (const to of paths.get(through) ?? []) paths.get(from)!.add(to) + } + const groups: WorkflowControlGroup[] = [] + for (const symbol of symbols) { + const members = [...symbols].filter((candidate) => + paths.get(symbol)?.has(candidate) && paths.get(candidate)?.has(symbol)).sort(cmp) + if (members[0] !== symbol) continue + groups.push({ kind: 'cycle', operationIds: [], symbolIds: members }) + } + return groups +} +function matches( + symbol: SymbolNode, lexical: readonly string[], names: boolean, +): boolean { + if (lexical.length === 0) return true + const source = names ? symbol[2] : symbol[1], + tokens = names ? symbol[4] : symbol[3] + return source.includes(lexical.join('')) + || lexical.every((term) => tokens.has(term)) +} +function covering( + v: ExecutionView, + ids: readonly string[], + target: string, + names: boolean, +): string[] { + const lexical = words(target), + exact = ids.filter((id) => matches(v[1].get(id)!, lexical, names)) + if (exact.length > 0) return exact + const related = ids.filter((id) => { + const symbol = v[1].get(id)!, + tokens = names ? symbol[4] : symbol[3] + return lexical.some((term) => tokens.has(term)) + }) + return lexical.length > 0 + && lexical.every((term) => related.some((id) => + (names ? v[1].get(id)![4] : v[1].get(id)![3]).has(term))) + ? related : [] +} +function targetSymbols( + v: ExecutionView, ids: readonly string[], target: string, + role: 'entry' | 'stage' | 'behavior' | 'terminal', +): string[] { + const tokens = words(target) + if (role === 'entry' && tokens.includes('request')) { + const entries = ids.filter((id) => v[1].get(id)![8]), + rest = tokens.filter((token) => token !== 'request') + return rest.length === 0 ? entries : entries.filter((id) => + rest.every((token) => v[1].get(id)![3].has(token))) + } + const exact = ids.filter((id) => matches(v[1].get(id)!, tokens, false)) + if (exact.length > 0) return exact + if (role === 'terminal' && tokens.length > 0) { + const generic = new Set([ + 'data', 'persist', 'persistence', 'record', 'storage', 'store', 'write', + ]), + specific = tokens.filter((token) => !generic.has(token)) + if (specific.length === 0) return [...ids] + const stored = ids.filter((id) => specific.every((token) => { + const lexicon = v[1].get(id)![3] + return lexicon.has(token) || /^(?:database|db)$/u.test(token) + && ['database', 'db', 'mongo', 'mongodb', 'repository', 'sql'] + .some((candidate) => lexicon.has(candidate)) + })) + if (stored.length > 0) return stored + } + return role === 'stage' || role === 'behavior' + ? covering(v, ids, target, false) : [] +} +function channelMatches( + i: ReadyQueryIndex, id: string, target: string, +): boolean { + const channel = i.channels_by_id.get(id) + if (!channel) return false + const expected = words(target), actual = new Set(words( + `${channel.channel_kind} ${channel.transport} ${channel.key}`, + )), + compact = expected.join(''), + forms = [ + `${channel.channel_kind} ${channel.transport} ${channel.key}`, + `${channel.transport} ${channel.channel_kind} ${channel.key}`, channel.key, + ].map((value) => words(value).join('')) + return expected.length > 0 && (expected.every((token) => actual.has(token)) + || forms.some((value) => value.includes(compact))) +} +function stageMatches( + i: ReadyQueryIndex, v: ExecutionView, selection: Selection, target: string, +): boolean { + const steps = [...selection[0]].filter((id) => + id !== selection[2][0] && targetSymbols(v, [id], target, 'stage').length > 0) + return steps.length > 0 || selection[1].some((arc) => + arc[3].some((edge) => + channelMatches(i, edge[1], target) || channelMatches(i, edge[2], target))) +} +const named = (v: ExecutionView, id: string, target: string): boolean => + matches(v[1].get(id)!, words(target), true) +function findRoots( + v: ExecutionView, + ranked: readonly Candidate[], + targets: readonly string[], +): readonly [ids: string[], actual: Set, bounded: boolean] { + const traversal = reach( + v, ranked.map((entry) => entry[0][0]), true, RECOVERY, + ) + const rank = (id: string): number => { + const symbol = v[1].get(id)! + return rootScore(v, symbol, score(symbol, targets)) + } + const ids = [...traversal[0].keys()].filter((id) => + !v[5].has(id) + && !v[1].get(id)?.[6]) + .sort((left, right) => + penalty(v[1].get(left)![7]) - penalty(v[1].get(right)![7]) + || rank(right) - rank(left) + || (v[2].get(right)?.length ?? 0) - (v[2].get(left)?.length ?? 0) + || cmp(left, right)) + return [ids, traversal[1], traversal[2]] +} +export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSelection { + const v = buildView(i), + { intent, subject: target, terms, obligations, access } = plan, + isFlow = intent === 'workflow', + targets = [...new Set([ + target, ...terms, ...obligations.map((entry) => entry.target), + ])], + terminalTarget = obligations.find((entry) => entry.kind === 'terminal')?.target, + terminalHint = terminalTarget === target ? undefined : terminalTarget, + stageTarget = obligations.find((entry) => entry.kind === 'stage' + && entry.target !== target)?.target + const candidate = (symbol: SymbolNode): Candidate => { + const lexical = score(symbol, targets), + outgoing = v[2].get(symbol[0]) ?? [], + affinity = isFlow + ? outgoing.some((arc) => arc[2] === 'channel') ? 2 + : Number(outgoing.some((arc) => arc[2] === 'direct' + && v[2].get(arc[1])?.some((next) => next[2] === 'channel'))) + : intent === 'explain' + ? Number(v[2].has(symbol[0]) || v[3].has(symbol[0])) + + Number(symbol[5].some((fact) => + ['condition', 'loop', 'parallel'].includes(fact.kind))) + : intent !== 'locate' || !access ? 0 + : access === 'write' ? Number(symbol[6] + || symbol[5].some((fact) => fact.kind === 'mutation') + || [...symbol[4]].some((word) => + ['persist', 'save', 'set', 'store', 'update', 'write'].includes(word))) + : Number(symbol[5].some((fact) => fact.kind === 'persistence' + && READ.test(fact.operation)) + || [...symbol[4]].some((word) => + ['find', 'get', 'load', 'read'].includes(word))), + exact = isFlow ? Number(!v[5].has(symbol[0])) + : intent === 'locate' + ? Number(named(v, symbol[0], target)) + : Number(named(v, symbol[0], target)) + return [symbol, lexical, rootScore(v, symbol, lexical), affinity, exact] + } + const ranked = v[0].map(candidate) + .filter((entry) => entry[1] > 0 && (!isFlow + || v[2].has(entry[0][0]) || v[3].has(entry[0][0]) + || v[4].has(entry[0][0]))) + .sort((a, b) => isFlow + ? b[4] - a[4] || b[3] - a[3] || b[2] - a[2] + || cmp(a[0][0], b[0][0]) + : intent === 'locate' + ? b[3] - a[3] || b[4] - a[4] + || penalty(a[0][7]) - penalty(b[0][7]) + || b[1] - a[1] || cmp(a[0][0], b[0][0]) + : b[4] - a[4] || b[3] - a[3] + || b[2] - a[2] || b[1] - a[1] || cmp(a[0][0], b[0][0])) + .slice(0, CANDIDATES), + focus = ranked[0]?.[0][0] + const entryTarget = obligations.find((entry) => entry.kind === 'entry' + && entry.target !== target)?.target, + entryPool = isFlow ? ranked.filter((entry) => !v[5].has(entry[0][0])) : [], + constrainedEntries = entryTarget + ? new Set(targetSymbols(v, entryPool.map((entry) => entry[0][0]), + entryTarget, 'entry')) : undefined + let entries = entryPool.filter((entry) => + !constrainedEntries || constrainedEntries.has(entry[0][0])).slice(0, 3) + let bridge: ReturnType | undefined + if (!entryTarget && isFlow && ranked.length > 0 && (entries.length === 0 + || entries.every((entry) => entry[0][7] !== 'production'))) { + bridge = findRoots(v, ranked, targets) + const recovered = bridge[0].map((id) => candidate(v[1].get(id)!)) + entries = [...new Map([...recovered, ...entries].map((entry) => + [entry[0][0], entry])).values()] + .sort((left, right) => + penalty(left[0][7]) - penalty(right[0][7]) + || right[2] - left[2] || cmp(left[0][0], right[0][0])) + .slice(0, 3) + } + let root: string | undefined + const subjectTerms = new Set(words(target)), + callTarget = terms.filter((term) => !subjectTerms.has(term)).join(' '), + direct = focus && intent === 'explain' + ? (v[2].get(focus) ?? []).filter((arc) => arc[2] === 'direct') : [], + requested = callTarget ? direct.find((arc) => + named(v, arc[1], callTarget)) : undefined, + callArcs = direct.filter((arc) => + arc === requested || score(v[1].get(arc[1])!, targets) > 0) + .sort((a, b) => Number(b === requested) - Number(a === requested) + || score(v[1].get(b[1])!, targets) - score(v[1].get(a[1])!, targets) + || cmp(a[1], b[1])).slice(0, 3), + ids = focus ? [focus, ...callArcs.map((arc) => arc[1])] : [] + let flow: Selection = isFlow ? [new Set(), [], [], new Set(), false] + : [new Set(ids), callArcs, [], new Set(ids), false] + const seen = new Set() + let tries = isFlow ? 0 : focus ? 1 : 0 + let locked = false + for (const entry of entries) { + const id = entry[0][0] + const room = NODES - RECOVERY - seen.size + if (room <= 0) break + const trial = corridor(v, id, room, targets, terminalHint) + trial[3].forEach((candidate) => seen.add(candidate)) + tries += 1 + const stageFit = !stageTarget || stageMatches(i, v, trial, stageTarget), + priorStageFit = !stageTarget || stageMatches(i, v, flow, stageTarget) + if (root === undefined || !locked && (Number(stageFit) > Number(priorStageFit) + || stageFit === priorStageFit + && (Number(trial[2].length > 0) > Number(flow[2].length > 0) + || Boolean(trial[2].length) === Boolean(flow[2].length) + && flow[4] && !trial[4]))) { + root = id + flow = trial + locked = !terminalHint && !stageTarget && trial[2].length === 0 + && named(v, id, target) + && (trial[1].length > 0 || v[4].has(id)) + } + } + + // Pass two is a bounded, shared recovery frontier. Alternates are admitted + // only when they are structural entries; disconnected middle-stage matches + // can never manufacture an entry-to-persistence corridor. + const recovery = new Set() + bridge?.[1].forEach((id) => recovery.add(id)) + flow[3].forEach((id) => seen.add(id)) + let bounded = flow[4] + let passes: 0 | 1 | 2 = bridge ? 1 : 0 + if (!entryTarget && isFlow && (flow[2].length === 0 || flow[4]) + && ranked.length > 0) { + bridge ??= findRoots(v, ranked, targets) + bridge[1].forEach((id) => { recovery.add(id); seen.add(id) }) + bounded ||= bridge[2] + passes = 1 + const alternates = flow[2].length === 0 && !locked + ? bridge[0].filter((id) => id !== root).slice(0, 3 - tries) : [] + if (alternates.length > 0) passes = 2 + for (const id of alternates) { + tries += 1 + const room = RECOVERY - recovery.size + 1 + if (room <= 0) { bounded = true; break } + const trial = corridor(v, id, room, targets, terminalHint) + bounded ||= trial[4] + trial[3].forEach((entry) => { recovery.add(entry); seen.add(entry) }) + if (root !== undefined && v[4].has(root) && named(v, root, target)) continue + root = id; flow = trial; break + } + } else if (intent === 'explain' && focus + && !(v[1].get(focus)?.[5].some(behavior) ?? false)) { + const alternate = ranked.slice(1, 4).find((entry) => + entry[0][5].some(behavior)) + if (alternate) { + passes = 1 + tries += 1 + const id = alternate[0][0] + recovery.add(id); seen.add(id) + flow = [new Set([id]), [], [], new Set([id]), false] + } + } + const rootIds = isFlow + ? root && flow[0].has(root) ? [root] : [] + : callArcs.length > 0 && focus ? [focus] : [] + const causal = [...new Set([ + ...rootIds, ...flow[2], ...flow[1].flatMap((arc) => [arc[0], arc[1]]), + ])].sort(cmp) + const symbolIds = [...flow[0]].sort(cmp), + edges = [...new Map(flow[1].flatMap((arc) => + arc[3].map((edge) => [edge[0], edge] as const))).values()] + .map(([id, fromId, toId, relation]) => + ({ id, fromId, toId, relation })).sort((a, b) => cmp(a.id, b.id)), + subjects = covering( + v, symbolIds, target, intent === 'locate' && !access, + ), + behaviors = isFlow ? causal : subjects, + edgeOwners = new Set(flow[1].map((arc) => arc[0])), + failureIntent = targets.some((entry) => + words(entry).some((word) => FAILURE_WORD.test(word))) + const factSeeds = intent === 'locate' ? [] : behaviors + .filter((id) => !edgeOwners.has(id) && !flow[2].includes(id)).flatMap((id) => { + const facts = v[1].get(id)?.[5].filter((fact) => + behavior(fact) && (failureIntent || !adverseFact(fact))) ?? [] + return [...new Map(facts.map((fact) => [fact.kind, fact.id])).values()] + }) + const locateOps = intent === 'locate' && access + ? subjects.flatMap((id) => { + const expected = words(target) + return (v[1].get(id)?.[5] ?? []) + .filter((fact) => { + const compatible = fact.kind === 'persistence' + ? (access === 'read') + === READ.test(fact.operation) + : access === 'write' && fact.kind === 'mutation' + const actual = new Set(words(factText(fact))) + return compatible && expected.length > 0 + && expected.every((word) => actual.has(word)) + }) + .map((fact) => fact.id) + }) + : [] + const ctl: Control = intent === 'locate' + ? locateOps.length > 0 ? controls(i, [], [], locateOps) : [[], [], true, []] + : controls(i, flow[1], flow[2], factSeeds) + const steps = isFlow ? causal : symbolIds, + edgeIds = edges.map((edge) => edge.id), + chosenOps = new Set(ctl[0]), + terminalSymbols = [...new Set(ctl[3].map((id) => + i.operation_by_id.get(id)?.owner_symbol_id).filter( + (id): id is string => id !== undefined, + ))].sort(cmp), + arcOps = [...new Set(flow[1].flatMap((arc) => arc[4]))] + .filter((id) => chosenOps.has(id)) + const owned = (ids: readonly string[]): string[] => ctl[0].filter((id) => + ids.includes(i.operation_by_id.get(id)?.owner_symbol_id as string)) + const behaviorOps = owned(behaviors) + const inert = behaviors.filter((id) => !edgeOwners.has(id) + && !behaviorOps.some((operation) => + i.operation_by_id.get(operation)?.owner_symbol_id === id)) + const incomplete = causal.filter((id) => v[4].has(id)) + type ProofData = readonly [ + symbols: readonly string[], operations: readonly string[], proven: boolean, + ] + const data: Record = { + subject: [subjects, intent === 'locate' && access ? locateOps : owned(subjects), + subjects.length > 0 && (!access || locateOps.length > 0)], + entry: [rootIds, owned(rootIds), rootIds.length > 0], + stage: [steps, ctl[0], steps.length > 0], + handoff: [causal, isFlow ? arcOps : owned(causal), + flow[1].length > 0 && (!isFlow || incomplete.length === 0)], + behavior: [behaviors, behaviorOps, + behaviors.length > 0 && inert.length === 0], + ordering: [steps, arcOps, + flow[1].length > 0 && incomplete.length === 0 && ctl[2] + && flow[1].every((arc) => arc[4].length > 0)], + terminal: [terminalSymbols, ctl[3], terminalSymbols.length > 0], + } + const missing: WorkflowMissingReason[] = [] + const proofs = obligations.map((obligation): WorkflowObligationProof => { + let [symbolIds, operationIds, proven] = data[obligation.kind] + let proofEdges = /^(?:stage|handoff|behavior|ordering)$/u + .test(obligation.kind) ? edgeIds : [] + if (obligation.target !== target + && /^(?:entry|stage|behavior|terminal)$/u.test(obligation.kind)) { + const role = obligation.kind as 'entry' | 'stage' | 'behavior' | 'terminal', + domain = role === 'entry' ? rootIds + : role === 'terminal' ? terminalSymbols + : role === 'stage' ? steps.filter((id) => + !rootIds.includes(id) && !flow[2].includes(id)) : [...flow[0]] + let matched = targetSymbols(v, domain, obligation.target, role) + if (role === 'stage') proofEdges = [] + if (role === 'stage' && matched.length === 0) { + const expected = words(obligation.target) + proofEdges = edges.filter((edge) => + [edge.fromId, edge.toId].some((id) => { + return expected.length > 0 && channelMatches(i, id, obligation.target) + })).map((edge) => edge.id) + const ids = new Set(proofEdges) + const arcs = flow[1].filter((arc) => + arc[3].some((edge) => ids.has(edge[0]))) + matched = [...new Set(arcs.flatMap((arc) => [arc[0], arc[1]]))].sort(cmp) + operationIds = [...new Set(arcs.flatMap((arc) => arc[4]))] + .filter((id) => chosenOps.has(id)).sort(cmp) + } else { + operationIds = ctl[0].filter((id) => { + const fact = i.operation_by_id.get(id) + return !!fact && (matched.includes(fact.owner_symbol_id) + || fact.kind === 'call' && !!fact.target_symbol_id + && matched.includes(fact.target_symbol_id)) + }) + } + symbolIds = matched + proven = proven && matched.length > 0 + } + const proof = { + ...obligation, proven, symbolIds, operationIds, edgeIds: proofEdges, + } + if (proof.mandatory && !proof.proven) { + missing.push({ code: MISSING_CODES[proof.kind] ?? 'obligation_target_unproven', + target: proof.kind === 'handoff' && incomplete.length > 0 + ? incomplete.join(',') : proof.target, + obligationId: proof.id }) + } + return proof + }) + if (!ctl[2]) missing.push({ code: 'controller_dependency_unproven', target }) + if (bounded) missing.push({ code: 'selection_bound_reached', target }) + return { + complete: missing.length === 0, + symbolIds, + operationIds: ctl[0], + rootSymbolIds: rootIds, + terminalSymbolIds: terminalSymbols, + edges, + links: flow[1].map((arc) => ({ + fromId: arc[0], toId: arc[1], kind: arc[2], + edgeIds: idsOf(arc), + operationIds: arc[4].filter((id) => + chosenOps.has(id) && i.operation_by_id.get(id)?.owner_symbol_id === arc[0]), + })), + controlGroups: [...ctl[1], ...cycleGroups(new Set(causal), flow[1])], + obligations: proofs, + missing, + metrics: { + candidateCount: ranked.length, rootCandidateCount: tries, + actualNodeCount: seen.size, + causalRelationHops: edges.length, recoveryPasses: passes, + recoveryFrontierCount: recovery.size, bounded, + }, + } +} diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/ideas/interface/http/idea-generation.controller.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/ideas/interface/http/idea-generation.controller.ts index 91c0e992..cc8ceb4f 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/ideas/interface/http/idea-generation.controller.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/ideas/interface/http/idea-generation.controller.ts @@ -1,16 +1,10 @@ +import { Controller, Post } from '@nestjs/common' + import { generateIdeaReportSuggestedNextSteps } from '../../application/helpers/idea-report-suggested-next-steps.helper.js' import { getIdeaReportStatusMessage } from '../../application/helpers/idea-report-status-message.helper.js' import { generateIdeaTitle } from '../../application/helpers/idea-title-generation.helper.js' import { startPipeline } from '../../../pipeline/api/pipeline-trigger.service.js' -function Controller(_path: string): any { - return () => {} -} - -function Post(_path: string): any { - return () => {} -} - @Controller('ideas') export class IdeaGenerationController { @Post('analyze') diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/pipeline-trigger.service.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/pipeline-trigger.service.ts index da721dca..458f2522 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/pipeline-trigger.service.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/pipeline-trigger.service.ts @@ -5,5 +5,9 @@ export async function startPipeline( problem: string, ideaId: string, ): Promise<{ jobId: string }> { - return enqueueJob({ userId, problem, ideaId }) + return enqueueJob( + 'orchestration-queue', + 'pipeline.orchestrator.process', + { userId, problem, ideaId }, + ) } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/queue-registry.service.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/queue-registry.service.ts index 309f1a78..582fba92 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/queue-registry.service.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/api/queue-registry.service.ts @@ -1,3 +1,5 @@ +import { Queue, Worker, type Job } from 'bullmq' + export type PipelineJobPayload = { userId: string problem: string @@ -6,37 +8,55 @@ export type PipelineJobPayload = { report?: { content: string } } -class PipelineQueue { - async add( +export class QueueRegistryService { + private readonly queues = new Map>() + + constructor() { + this.queues.set( + 'orchestration-queue', + new Queue('orchestration-queue'), + ) + this.queues.set( + 'section-research-queue', + new Queue('section-research-queue'), + ) + this.queues.set( + 'assembly-queue', + new Queue('assembly-queue'), + ) + this.queues.set( + 'db-sync-queue', + new Queue('db-sync-queue'), + ) + } + + addJob( + queueName: string, jobName: string, input: PipelineJobPayload, - ): Promise<{ id: string }> { - return { - id: `${jobName}:${input.ideaId}`, - } + ): Promise> { + const queue = this.queues.get(queueName) + if (!queue) throw new Error(`Queue not registered: ${queueName}`) + return queue.add(jobName, input) } -} - -const pipelineQueue = new PipelineQueue() -const workers = new Map Promise>() -export function registerWorker( - queueName: string, - worker: (input: PipelineJobPayload) => Promise, -): void { - workers.set(queueName, worker) + registerWorker( + queueName: string, + processor: (job: Job) => Promise, + ): Worker { + return new Worker(queueName, processor) + } } +export const queueRegistry = new QueueRegistryService() + export async function enqueueJob( - queueOrInput: string | PipelineJobPayload, - suppliedInput?: PipelineJobPayload, + queueName: string, + jobName: string, + input: PipelineJobPayload, ): Promise<{ jobId: string }> { - if (typeof queueOrInput !== 'string') { - const job = await pipelineQueue.add('pipeline.orchestrator.process', queueOrInput) - return { jobId: job.id } - } - const job = await pipelineQueue.add(queueOrInput, suppliedInput!) + const job = await queueRegistry.addJob(queueName, jobName, input) return { - jobId: job.id, + jobId: String(job.id), } } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/assembly/assembly.worker.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/assembly/assembly.worker.ts index 492f605f..0a967d28 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/assembly/assembly.worker.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/assembly/assembly.worker.ts @@ -1,5 +1,5 @@ import { - registerWorker, + type QueueRegistryService, type PipelineJobPayload, } from '../api/queue-registry.service.js' import { AssemblyService } from '../../reports/assembly.service.js' @@ -7,10 +7,12 @@ import { AssemblyService } from '../../reports/assembly.service.js' export class AssemblyWorker { private readonly assembly = new AssemblyService() + constructor(private readonly registry: QueueRegistryService) {} + onModuleInit(): void { - registerWorker( + this.registry.registerWorker( 'assembly-queue', - async (input) => this.process(input), + async (job) => this.process(job.data), ) } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/db-sync.worker.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/db-sync.worker.ts index 9235a286..8bf28201 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/db-sync.worker.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/db-sync.worker.ts @@ -1,4 +1,12 @@ -import { registerWorker } from '../api/queue-registry.service.js' +import type { QueueRegistryService } from '../api/queue-registry.service.js' +import type { MongoRepository } from 'typeorm' + +type StoredReport = { + id: string + content: string +} + +declare const reportRepository: MongoRepository function hasIdeaId(ideaId: string): boolean { return ideaId.length > 0 @@ -12,14 +20,18 @@ export async function saveStructuredReport( ideaId: string, report: { content: string }, ): Promise<{ saved: boolean }> { - return { saved: hasIdeaId(ideaId) && hasReportContent(report) } + if (!hasIdeaId(ideaId) || !hasReportContent(report)) return { saved: false } + await reportRepository.update(ideaId, { id: ideaId, content: report.content }) + return { saved: true } } export class DbSyncWorker { + constructor(private readonly registry: QueueRegistryService) {} + onModuleInit(): void { - registerWorker( + this.registry.registerWorker( 'db-sync-queue', - async (input) => this.process(input), + async (job) => this.process(job.data), ) } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/orchestrator.worker.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/orchestrator.worker.ts index 96859583..15eb8502 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/orchestrator.worker.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/pipeline/workers/orchestrator.worker.ts @@ -1,5 +1,5 @@ import { - registerWorker, + type QueueRegistryService, type PipelineJobPayload, } from '../api/queue-registry.service.js' import { PlannerService } from '../../planning/planner.service.js' @@ -20,10 +20,12 @@ function Process(_jobName: string): any { export class OrchestratorWorker { private readonly planner = new PlannerService() + constructor(private readonly registry: QueueRegistryService) {} + onModuleInit(): void { - registerWorker( + this.registry.registerWorker( 'orchestration-queue', - async (input) => this.process({ data: input }), + async (job) => this.process(job), ) } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/planning/planner.service.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/planning/planner.service.ts index c84b3c6d..328328af 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/planning/planner.service.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/planning/planner.service.ts @@ -19,6 +19,10 @@ export class PlannerService { input: PipelineJobPayload, section: string, ): Promise { - await enqueueJob('section-research-queue', { ...input, section }) + await enqueueJob( + 'section-research-queue', + 'research.section.process', + { ...input, section }, + ) } } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/reports/assembly.service.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/reports/assembly.service.ts index 82a6ceb6..6778a045 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/reports/assembly.service.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/reports/assembly.service.ts @@ -31,6 +31,6 @@ export class AssemblyService { } private async dispatchPersistence(input: PipelineJobPayload): Promise { - await enqueueJob('db-sync-queue', input) + await enqueueJob('db-sync-queue', 'db.sync.process', input) } } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/research-agent.service.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/research-agent.service.ts index 627a4d21..8b7b5875 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/research-agent.service.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/research-agent.service.ts @@ -20,6 +20,6 @@ export class ResearchAgentService { } private async checkAndDispatchNext(input: PipelineJobPayload): Promise { - await enqueueJob('assembly-queue', input) + await enqueueJob('assembly-queue', 'assembly.report.process', input) } } diff --git a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/workers/section-research.worker.ts b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/workers/section-research.worker.ts index 0f3d2a19..0b050786 100644 --- a/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/workers/section-research.worker.ts +++ b/tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace/src/modules/research/workers/section-research.worker.ts @@ -1,16 +1,18 @@ import { ResearchAgentService } from '../research-agent.service.js' import { - registerWorker, + type QueueRegistryService, type PipelineJobPayload, } from '../../pipeline/api/queue-registry.service.js' export class SectionResearchWorker { private readonly researchAgent = new ResearchAgentService() + constructor(private readonly registry: QueueRegistryService) {} + onModuleInit(): void { - registerWorker( + this.registry.registerWorker( 'section-research-queue', - async (input) => this.process(input), + async (job) => this.process(job.data), ) } diff --git a/tests/unit/benchmark-quality.test.ts b/tests/unit/benchmark-quality.test.ts index 93842fe2..b4615069 100644 --- a/tests/unit/benchmark-quality.test.ts +++ b/tests/unit/benchmark-quality.test.ts @@ -9,6 +9,7 @@ import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact. import { evaluateRetrievalQuality, formatQualityReport, + GOLD_QUESTIONS, } from '../../tools/eval/lib/infrastructure/benchmark/quality.js' const sandboxes: string[] = [] @@ -20,8 +21,15 @@ function qualityWorkspace(): { root: string; graphPath: string } { writeFileSync( join(root, 'src', 'events.ts'), [ - 'export function publishEvent(name: string): string {', - ' return `published:${name}`', + "import type { MongoRepository } from 'typeorm'", + '', + 'type EventRecord = { id: string }', + '', + 'export async function persistRequest(', + ' repository: MongoRepository,', + ' id: string,', + '): Promise {', + ' await repository.update(id, { id })', '}', '', ].join('\n'), @@ -30,9 +38,15 @@ function qualityWorkspace(): { root: string; graphPath: string } { writeFileSync( join(root, 'src', 'handler.ts'), [ - "import { publishEvent } from './events.js'", - 'export function handleRequest(): string {', - " return publishEvent('request.handled')", + "import type { MongoRepository } from 'typeorm'", + "import { persistRequest } from './events.js'", + '', + 'type EventRecord = { id: string }', + '', + 'export function handleRequest(', + ' repository: MongoRepository,', + '): Promise {', + " return persistRequest(repository, 'request.handled')", '}', '', ].join('\n'), @@ -46,13 +60,23 @@ afterEach(() => { }) describe('Core Reset retrieval quality evaluator', () => { + it('targets the v2 planner, workflow selector and evidence hydrator', () => { + const labels = GOLD_QUESTIONS.flatMap((question) => question.expected_labels) + expect(labels).toEqual(expect.arrayContaining([ + 'planquestion', 'selectworkflow', 'hydrateevidence', 'retrievecontext', + ])) + expect(labels).not.toEqual(expect.arrayContaining([ + 'rankqueryanchors', 'traverseevidencepaths', 'sliceevidence', + ])) + }) + it('grades only authenticated nodes returned by the one query', () => { const { graphPath } = qualityWorkspace() const report = evaluateRetrievalQuality( loadGraphArtifact(graphPath), [{ - question: 'How does handle request publish event?', - expected_labels: ['handleRequest()', 'publishEvent()'], + question: 'How does request flow end to end?', + expected_labels: ['handleRequest()', 'persistRequest()'], }], 3_000, { graphPath }, @@ -73,7 +97,7 @@ describe('Core Reset retrieval quality evaluator', () => { const { graphPath } = qualityWorkspace() const report = evaluateRetrievalQuality( loadGraphArtifact(graphPath), - [{ question: 'publish event', expected_labels: ['publish'] }], + [{ question: 'Where is persistRequest defined?', expected_labels: ['persist'] }], 3_000, { graphPath }, ) @@ -81,12 +105,30 @@ describe('Core Reset retrieval quality evaluator', () => { expect(report.questions[0]?.matched_labels).toEqual([]) }) + it('does not grade a non-ready response as partial evidence', () => { + const { graphPath } = qualityWorkspace() + const report = evaluateRetrievalQuality( + loadGraphArtifact(graphPath), + [{ + question: 'Should this architecture be rewritten?', + expected_labels: ['handleRequest()'], + }], + 3_000, + { graphPath }, + ) + + expect(report.questions[0]).toMatchObject({ + returned_labels: [], matched_labels: [], recall: 0, + snippet_coverage: 0, grounded_match_rate: 0, + }) + }) + it('skips unlabeled questions and renders a compact report', () => { const { graphPath } = qualityWorkspace() const report = evaluateRetrievalQuality( loadGraphArtifact(graphPath), [ - { question: 'handle request', expected_labels: ['handleRequest()'] }, + { question: 'Where is handleRequest defined?', expected_labels: ['handleRequest()'] }, { question: 'unlabeled evaluation prompt' }, ], 3_000, diff --git a/tests/unit/benchmark-runtime-proof.test.ts b/tests/unit/benchmark-runtime-proof.test.ts index 935b7f3f..6e055d68 100644 --- a/tests/unit/benchmark-runtime-proof.test.ts +++ b/tests/unit/benchmark-runtime-proof.test.ts @@ -44,14 +44,16 @@ describe('benchmark retrieval adapter', () => { const result = retrieveBenchmarkContext( graph, generated.graphPath, - 'How does process invoice call store invoice?', + 'Where is processInvoice defined?', 4_000, ) expect(process.cwd()).toBe(originalCwd) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toEqual( - expect.arrayContaining(['processInvoice()', 'storeInvoice()']), - ) + expect(result.state).toBe('ready') + if (result.state === 'ready') { + expect(result.dossier.evidence.entities).toContainEqual( + expect.objectContaining({ kind: 'symbol', label: 'processInvoice()' }), + ) + } }) }) diff --git a/tests/unit/evidence-hydrator.test.ts b/tests/unit/evidence-hydrator.test.ts new file mode 100644 index 00000000..26d8f2a6 --- /dev/null +++ b/tests/unit/evidence-hydrator.test.ts @@ -0,0 +1,504 @@ +import { createHash } from 'node:crypto' +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + hydrateEvidence, +} from '../../src/application/evidence-hydrator.js' +import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' +import { + indexChannelId, + type IndexBodyFact, + type IndexCallFact, + type IndexRange, +} from '../../src/domain/index/model.js' +import type { QueryGraph, ReadyQueryIndex } from '../../src/domain/query/index-status.js' +import type { EvidenceHydrationTargets } from '../../src/domain/query/types.js' + +const roots: string[] = [] +const path = 'src/flow.ts' +const fileId = 'file:flow' +const ownerId = 'symbol:owner' +const targetId = 'symbol:target' +const operationId = 'operation:call' +const sourceText = [ + 'export function owner() { return target("secret") }', + 'export function target(value: string) { return value }', + '', +].join('\n') + +interface Fixture { + root: string + graph: KnowledgeGraph + index: ReadyQueryIndex + operations: Map + operation: IndexCallFact + directEdgeId: string + publishEdgeId: string + channelId: string + targets: EvidenceHydrationTargets +} + +function hash(value: string | Buffer): string { + return createHash('sha256').update(value).digest('hex') +} +const values = (map: ReadonlyMap): T[] => [...map.values()] + +function position(text: string, offset: number): IndexRange['start'] { + const lines = text.slice(0, offset).split('\n') + return { line: lines.length, column: lines.at(-1)!.length + 1 } +} + +function span(text: string, needle: string): IndexRange { + const start = text.indexOf(needle) + if (start < 0) throw new Error(`Missing fixture span ${needle}`) + return { start: position(text, start), end: position(text, start + needle.length) } +} + +function location(range: IndexRange): string { + return range.start.line === range.end.line + ? `L${range.start.line}` + : `L${range.start.line}-L${range.end.line}` +} + +function write(root: string, relative: string, contents: string | Buffer): void { + const absolute = join(root, relative) + mkdirSync(dirname(absolute), { recursive: true }) + writeFileSync(absolute, contents) +} + +function fixture(): Fixture { + const root = mkdtempSync(join(tmpdir(), 'madar-hydrator-')) + roots.push(root) + write(root, path, sourceText) + const fileHash = hash(sourceText) + const ownerDefinition = span(sourceText, sourceText.split('\n')[0]!) + const targetDefinition = span(sourceText, sourceText.split('\n')[1]!) + const ownerDeclaration = span(sourceText, 'export function owner()') + const targetDeclaration = span(sourceText, 'export function target(value: string)') + const statementRange = span(sourceText, 'return target("secret")') + const callRange = span(sourceText, 'target("secret")') + const statementHash = hash('return target("secret")') + const graph = new KnowledgeGraph({ root_path: root }) + graph.addNode(fileId, { + label: 'flow.ts', node_kind: 'file', source_file: path, + source_location: 'L1', content_hash: fileHash, provenance: [{}], + }) + graph.addNode(ownerId, { + label: 'owner()', node_kind: 'function', source_file: path, + source_location: location(ownerDefinition), line_number: 1, end_line_number: 1, + definition_range: ownerDefinition, declaration_range: ownerDeclaration, + provenance: [{}], + }) + graph.addNode(targetId, { + label: 'target()', node_kind: 'function', source_file: path, + source_location: location(targetDefinition), line_number: 2, end_line_number: 2, + definition_range: targetDefinition, declaration_range: targetDeclaration, + provenance: [{}], + }) + const channel = { + channel_kind: 'queue' as const, + transport: 'bullmq' as const, + key: 'reports', + } + const channelId = indexChannelId(channel) + graph.addNode(channelId, { + label: channel.key, node_kind: 'channel', ...channel, + }) + const operation: IndexCallFact = { + id: operationId, owner_symbol_id: ownerId, kind: 'call', + order: [1, 3, 2, 0], control: [], confidence: 'high', + source: 'typescript-semantic', callee: 'target', target_symbol_id: targetId, + scheduling: 'sync', + arguments: [{ kind: 'redacted', sha256: 'a'.repeat(64), byte_length: 6 }], + evidence: { + file_id: fileId, range: callRange, statement_range: statementRange, + excerpt_sha256: statementHash, + }, + } + const directEdgeId = graph.addEdge(ownerId, targetId, { + relation: 'calls', source_file: path, source_location: location(callRange), + evidence: { source: 'typescript-semantic', range: callRange }, + provenance: [{}], + }) + const publishEdgeId = graph.addEdge(ownerId, channelId, { + relation: 'publishes_to', source_file: path, + source_location: location(statementRange), execution_owner_id: ownerId, + evidence: { + source: 'typescript-semantic', range: callRange, + statement_range: statementRange, excerpt_sha256: statementHash, + }, + provenance: [{}], + }) + const operations = new Map([[operation.id, operation]]) + const channelNode = { id: channelId, node_kind: 'channel' as const, ...channel } + const index: ReadyQueryIndex = { + state: 'ready', graph, root_path: root, + file_hashes: new Map([[path, fileHash]]), unsupported_sources: [], + operation_by_id: operations, + operations_by_owner: new Map([[ownerId, [operation]]]), + channels_by_id: new Map([[channelId, channelNode]]), + channels_by_key: new Map([[channel.key, [channelNode]]]), + } + return { + root, graph, index, operations, operation, directEdgeId, publishEdgeId, channelId, + targets: { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], + operationIds: [operationId], + edges: [ + { id: directEdgeId, fromId: ownerId, toId: targetId, relation: 'calls' }, + { id: publishEdgeId, fromId: ownerId, toId: channelId, relation: 'publishes_to' }, + ], + }, + } +} + +function addConsumedEdge(value: Fixture): string { + const statementRange = span(sourceText, 'return target("secret")') + const callRange = span(sourceText, 'target("secret")') + return value.graph.addEdge(value.channelId, targetId, { + relation: 'consumed_by', source_file: path, + source_location: location(statementRange), execution_owner_id: ownerId, + evidence: { + source: 'wrapper-summary', range: callRange, + statement_range: statementRange, excerpt_sha256: hash('return target("secret")'), + }, + provenance: [{}], + }) +} + +function setWrapperBinding( + value: Fixture, id = operationId, reference = targetId, +): IndexCallFact { + const operation: IndexCallFact = { + id, owner_symbol_id: ownerId, kind: 'call', order: [1, 3, 2, 0], control: [], + confidence: 'high', source: 'wrapper-summary', callee: 'registerWorker', + scheduling: 'sync', evidence: value.operation.evidence, + arguments: [{ + kind: 'object', entries: [{ key: 'handler', value: { + kind: 'array', elements: [{ kind: 'symbol', symbol_id: reference }], + } }], + }], + } + value.operations.set(id, operation) + ;(value.index.operations_by_owner as Map).set(ownerId, [operation]) + return operation +} + +function duplicateSelectedEdge(index: ReadyQueryIndex, id: string): QueryGraph { + const graph = index.graph + const methods = { + hasNode: graph.hasNode.bind(graph), hasEdge: graph.hasEdge.bind(graph), + nodeEntries: graph.nodeEntries.bind(graph), predecessors: graph.predecessors.bind(graph), + successors: graph.successors.bind(graph), edgesBetween: graph.edgesBetween.bind(graph), + nodeAttributes: graph.nodeAttributes.bind(graph), + } + return { + ...methods, + edgeEntries: () => { + const rows = graph.edgeEntries() + const selected = rows.find((row) => row[3] === id)! + return [...rows, selected] + }, + } +} + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +describe('selected evidence hydration', () => { + it('authenticates exact symbol, operation, direct-call and channel-edge targets', () => { + const value = fixture() + const first = hydrateEvidence(value.index, value.targets) + const second = hydrateEvidence(value.index, { + symbolIds: [...value.targets.symbolIds].reverse(), + declarationSymbolIds: [...value.targets.declarationSymbolIds].reverse(), + operationIds: [...value.targets.operationIds].reverse(), + edges: [...value.targets.edges].reverse(), + }) + + expect(second).toEqual(first) + expect(first.state).toBe('ready') + if (first.state !== 'ready') return + expect(first.files.get(path)).toEqual(['f0', hash(sourceText)]) + expect(values(first.entities).map((entity) => entity[1])).toEqual([ + 'symbol', 'symbol', 'operation', 'channel', + ]) + const operation = values(first.entities).find((entity) => entity[1] === 'operation') + expect(operation?.[3]).toMatchObject({ + kind: 'call', arguments: [ + { kind: 'redacted', sha256: 'a'.repeat(64), byte_length: 6 }, + ], + }) + const edgeProofs = values(first.proofs).filter((proof) => proof[1] === 'edge') + expect(edgeProofs).toHaveLength(2) + expect(edgeProofs.find((proof) => proof[4] === 'calls')).toHaveLength(6) + expect(values(first.proofs).some((proof) => proof[1] === 'operation')).toBe(true) + expect(values(first.excerpts).map((item) => item[4])).toEqual([ + 'export function owner()', + 'return target("secret")', + ]) + expect(first.proofs.has(targetId)).toBe(false) + }) + + it('deduplicates repeated targets and shared statement proofs', () => { + const value = fixture() + const result = hydrateEvidence(value.index, { + symbolIds: [ownerId, ownerId, targetId], + declarationSymbolIds: [ownerId, ownerId], + operationIds: [operationId, operationId], + edges: [...value.targets.edges, ...value.targets.edges], + }) + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.files.size).toBe(1) + expect(result.entities.size).toBe(4) + expect(result.excerpts.size).toBe(2) + expect(result.proofs.size).toBe(4) + }) + + it('authenticates validation-only operations without emitting their excerpts', () => { + const value = fixture() + const targets = { + symbolIds: [ownerId], declarationSymbolIds: [ownerId], + operationIds: [], validationOperationIds: [operationId], edges: [], + } + const result = hydrateEvidence(value.index, targets) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.entities.has(operationId)).toBe(false) + expect(result.proofs.has(operationId)).toBe(false) + expect(values(result.excerpts).map((item) => item[4])) + .toEqual(['export function owner()']) + + value.operations.set(operationId, { + ...value.operation, + evidence: { ...value.operation.evidence, excerpt_sha256: 'b'.repeat(64) }, + }) + expect(hydrateEvidence(value.index, targets)) + .toEqual({ state: 'corrupt', subject: operationId }) + }) + + it('rejects metadata-only symbols without an incident authenticated proof', () => { + const value = fixture() + expect(hydrateEvidence(value.index, { + symbolIds: [targetId], declarationSymbolIds: [], operationIds: [], edges: [], + })).toEqual({ state: 'corrupt', subject: targetId }) + }) + + it('binds an indirect consumed_by owner to one authenticated consumer call', () => { + const value = fixture() + const edgeId = addConsumedEdge(value) + const result = hydrateEvidence(value.index, { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], + operationIds: [operationId], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(values(result.proofs).find((proof) => + proof[1] === 'edge' && proof[4] === 'consumed_by')).toHaveLength(6) + expect(values(result.proofs).some((proof) => proof[1] === 'operation')).toBe(true) + }) + + it('binds a wrapper call through its exact authenticated statement', () => { + const value = fixture() + setWrapperBinding(value, operationId, ownerId) + const edgeId = addConsumedEdge(value) + const result = hydrateEvidence(value.index, { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], operationIds: [], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(values(result.proofs).find((proof) => proof[1] === 'edge')).toHaveLength(6) + expect(values(result.entities).some((entity) => entity[1] === 'operation')).toBe(false) + }) + + it('accepts consumed_by evidence owned directly by the consumer', () => { + const value = fixture() + const statementRange = span(sourceText, 'return value') + const edgeId = value.graph.addEdge(value.channelId, targetId, { + relation: 'consumed_by', source_file: path, + source_location: location(statementRange), execution_owner_id: targetId, + evidence: { + source: 'wrapper-summary', range: statementRange, + statement_range: statementRange, excerpt_sha256: hash('return value'), + }, + provenance: [{}], + }) + const result = hydrateEvidence(value.index, { + symbolIds: [targetId], declarationSymbolIds: [], operationIds: [], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(values(result.proofs).find((proof) => proof[1] === 'edge')).toHaveLength(6) + }) + + it('rejects consumed_by evidence whose indirect owner lacks the authenticated call', () => { + const value = fixture() + const edgeId = addConsumedEdge(value) + ;(value.index.operations_by_owner as Map).set(ownerId, []) + + expect(hydrateEvidence(value.index, { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], + operationIds: [], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + })).toEqual({ state: 'corrupt', subject: edgeId }) + }) + + it('rejects an indirect consumed_by binding inconsistent across operation indexes', () => { + const value = fixture() + const operation = setWrapperBinding(value) + const edgeId = addConsumedEdge(value) + value.operations.set(operationId, { + ...operation, + evidence: { ...operation.evidence, excerpt_sha256: 'b'.repeat(64) }, + }) + + expect(hydrateEvidence(value.index, { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], + operationIds: [], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + })).toEqual({ state: 'corrupt', subject: edgeId }) + }) + + it('rejects ambiguous wrapper calls with the same authenticated statement', () => { + const value = fixture() + const first = setWrapperBinding(value) + const second = { ...first, id: 'operation:ambiguous' } + value.operations.set(second.id, second) + ;(value.index.operations_by_owner as Map).set( + ownerId, [first, second], + ) + const edgeId = addConsumedEdge(value) + + expect(hydrateEvidence(value.index, { + symbolIds: [ownerId, targetId], declarationSymbolIds: [ownerId], operationIds: [], + edges: [{ id: edgeId, fromId: value.channelId, toId: targetId, + relation: 'consumed_by' }], + })).toEqual({ state: 'corrupt', subject: edgeId }) + }) + + it('returns stale when selected source bytes changed', () => { + const value = fixture() + write(value.root, path, `${sourceText}// changed\n`) + expect(hydrateEvidence(value.index, value.targets)).toEqual({ state: 'stale', subject: path }) + }) + + it('returns unavailable when a selected source is missing', () => { + const value = fixture() + unlinkSync(join(value.root, path)) + expect(hydrateEvidence(value.index, value.targets)).toEqual({ + state: 'unavailable', subject: path, + }) + }) + + it('rejects corrupt ranges, hashes and operation references', () => { + const badRange = fixture() + const owner = badRange.graph.nodeAttributes(ownerId) + badRange.graph.replaceNodeAttributes(ownerId, { + ...owner, + declaration_range: { start: { line: 1, column: 1 }, end: { line: 1, column: 999 } }, + }) + expect(hydrateEvidence(badRange.index, badRange.targets)).toEqual({ + state: 'corrupt', subject: ownerId, + }) + + const badHash = fixture() + badHash.operations.set(operationId, { + ...badHash.operation, + evidence: { ...badHash.operation.evidence, excerpt_sha256: 'f'.repeat(64) }, + }) + expect(hydrateEvidence(badHash.index, badHash.targets)).toEqual({ + state: 'corrupt', subject: operationId, + }) + + const badReference = fixture() + badReference.operations.set(operationId, { + ...badReference.operation, target_symbol_id: 'symbol:missing', + }) + expect(hydrateEvidence(badReference.index, badReference.targets)).toEqual({ + state: 'corrupt', subject: 'symbol:missing', + }) + }) + + it('isolates corruption in an unselected owner fact', () => { + const value = fixture() + const bad: IndexCallFact = { + ...value.operation, id: 'operation:unselected', target_symbol_id: 'symbol:missing', + evidence: { ...value.operation.evidence, excerpt_sha256: 'b'.repeat(64) }, + } + value.operations.set(bad.id, bad) + ;(value.index.operations_by_owner as Map).set( + ownerId, [value.operation, bad], + ) + expect(hydrateEvidence(value.index, value.targets).state).toBe('ready') + }) + + it('rejects a selected edge with mismatched or ambiguous identity', () => { + const mismatch = fixture() + expect(hydrateEvidence(mismatch.index, { + ...mismatch.targets, + edges: [{ id: mismatch.directEdgeId, fromId: targetId, toId: ownerId }], + })).toEqual({ state: 'corrupt', subject: mismatch.directEdgeId }) + + const ambiguous = fixture() + const index = { ...ambiguous.index, + graph: duplicateSelectedEdge(ambiguous.index, ambiguous.directEdgeId) } + expect(hydrateEvidence(index, ambiguous.targets)).toEqual({ + state: 'corrupt', subject: ambiguous.directEdgeId, + }) + }) + + it('rejects a selected source that resolves outside the indexed root', () => { + const value = fixture() + const outside = mkdtempSync(join(tmpdir(), 'madar-hydrator-outside-')) + roots.push(outside) + write(outside, 'flow.ts', sourceText) + unlinkSync(join(value.root, path)) + symlinkSync(join(outside, 'flow.ts'), join(value.root, path)) + expect(hydrateEvidence(value.index, value.targets)).toEqual({ + state: 'unavailable', subject: path, + }) + const implementation = readFileSync( + new URL('../../src/application/evidence-hydrator.ts', import.meta.url), + 'utf8', + ) + expect(implementation.indexOf('const relativePath = relative(root, candidate)')) + .toBeLessThan(implementation.indexOf('bytes = readFileSync(candidate)')) + }) + + it('rejects fatal UTF-8 while distinguishing it from stale bytes', () => { + const value = fixture() + const bytes = Buffer.from([0xff]) + write(value.root, path, bytes) + const fileHash = hash(bytes) + const fileNode = value.graph.nodeAttributes(fileId) + value.graph.replaceNodeAttributes(fileId, { ...fileNode, content_hash: fileHash }) + const index = { ...value.index, file_hashes: new Map([[path, fileHash]]) } + expect(hydrateEvidence(index, value.targets)).toEqual({ state: 'corrupt', subject: path }) + }) +}) diff --git a/tests/unit/query-index-execution-validation.test.ts b/tests/unit/query-index-execution-validation.test.ts index 29fc621d..4537e9f3 100644 --- a/tests/unit/query-index-execution-validation.test.ts +++ b/tests/unit/query-index-execution-validation.test.ts @@ -638,12 +638,10 @@ describe('query execution index validation', () => { question: 'Explain the `run` function.', budget: 4_000, }) - expect(result.outcome).toBe('corrupt') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toContainEqual({ - kind: 'corrupt', - subject: current.runId, - }) + expect(result.state).toBe('corrupt') + if (result.state === 'corrupt') { + expect(result.failures[0]?.subject).toBeTruthy() + } }) it('rejects a re-sealed sparse persistence ordinal', () => { @@ -667,7 +665,7 @@ describe('query execution index validation', () => { expect(inspectQueryIndex(current.graph)).toMatchObject({ state: 'corrupt' }) }) - it('authenticates selected-owner channel edge bytes before returning evidence', () => { + it('isolates a corrupt unselected channel edge from a focused explanation', () => { const current = fixture() const edge = current.graph.edgeEntries().find(([from, , attributes]) => from === current.runId && attributes.relation === 'publishes_to') @@ -683,11 +681,8 @@ describe('query execution index validation', () => { question: 'Explain the `run` function.', budget: 4_000, }) - expect(result.outcome).toBe('corrupt') - expect(result.boundaries).toContainEqual({ - kind: 'corrupt', - subject: forgedId, - }) + expect(forgedId).toBeTruthy() + expect(result.state).toBe('ready') }) it('reports stale when selected-owner source bytes change', () => { @@ -704,11 +699,9 @@ describe('query execution index validation', () => { budget: 4_000, }) - expect(result.outcome).toBe('stale') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toContainEqual({ - kind: 'stale', - subject: 'src/run.ts', + expect(result).toMatchObject({ + state: 'stale', + failures: [{ state: 'stale', subject: 'src/run.ts' }], }) }) @@ -719,8 +712,12 @@ describe('query execution index validation', () => { budget: 4_000, }) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.some((node) => node.node_id === current.runId)).toBe(true) + expect(result.state).toBe('ready') + if (result.state === 'ready') { + expect(result.dossier.evidence.entities).toContainEqual( + expect.objectContaining({ kind: 'symbol', label: 'run()' }), + ) + } }) it('reports stale before decoding mutated invalid UTF-8 bytes', () => { @@ -733,10 +730,9 @@ describe('query execution index validation', () => { budget: 4_000, }) - expect(result.outcome).toBe('stale') - expect(result.boundaries).toContainEqual({ - kind: 'stale', - subject: 'src/run.ts', + expect(result).toMatchObject({ + state: 'stale', + failures: [{ state: 'stale', subject: 'src/run.ts' }], }) }) }) diff --git a/tests/unit/query-plan.test.ts b/tests/unit/query-plan.test.ts new file mode 100644 index 00000000..18a6e8bd --- /dev/null +++ b/tests/unit/query-plan.test.ts @@ -0,0 +1,322 @@ +import { describe, expect, it } from 'vitest' + +import { + planQuestion, +} from '../../src/domain/query/plan.js' +import type { + NormalizedRetrieveRequest, QuestionPlanResult, QueryPlan, +} from '../../src/domain/query/types.js' + +function plan(question: string, budget = 4000): QueryPlan { + const result = planQuestion({ question, budget }) + if (result.status !== 'supported') { + throw new Error(`Expected a supported plan, received ${result.reason}`) + } + return result.plan +} + +function unsupported(question: string): Extract { + const result = planQuestion({ question, budget: 4000 }) + if (result.status !== 'unsupported') { + throw new Error(`Expected an unsupported plan, received ${result.plan.intent}`) + } + return result +} + +describe('planQuestion', () => { + it('keeps locators subject-only', () => { + expect(plan('Where is `pipelineState` written?')).toEqual({ + intent: 'locate', + subject: 'pipeline state', + terms: ['pipeline', 'state'], + access: 'write', + obligations: [ + { id: 'o1', kind: 'subject', target: 'pipeline state', mandatory: true }, + ], + }) + }) + + it('preserves action-shaped words inside captured identifier subjects', () => { + expect(plan('Where is handleClick defined?').subject).toBe('handle click') + expect(plan('Where is updateIndex defined?').subject).toBe('update index') + expect(plan('Where is generateIdeaReport defined?').subject).toBe('generate idea report') + expect(plan('Where is generateFromProblem defined?').subject).toBe('generate problem') + }) + + it('keeps an explicit workflow-named definition question as a locator', () => { + expect(plan('Where is WorkflowEngine defined?')).toEqual({ + intent: 'locate', + subject: 'workflow engine', + terms: ['engine', 'workflow'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'workflow engine', mandatory: true }, + ], + }) + }) + + it('plans subject and behavior for explanations', () => { + expect(plan('How does cache invalidation work?')).toEqual({ + intent: 'explain', + subject: 'cache invalidation', + terms: ['cache', 'invalidation'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'cache invalidation', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'cache invalidation', mandatory: true }, + ], + }) + }) + + it('does not treat a generate-prefixed subject as a workflow verb', () => { + expect(plan('How does generateInvoice validate input?')).toEqual({ + intent: 'explain', + subject: 'generate invoice', + terms: ['generate', 'input', 'invoice'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input', mandatory: true }, + ], + }) + }) + + it('treats explicit call questions as behavior of the caller', () => { + expect(plan('How does submit order call save order?')).toEqual({ + intent: 'explain', + subject: 'submit order', + terms: ['order', 'save', 'submit'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'submit order', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'save', mandatory: true }, + ], + }) + }) + + it.each([ + [ + 'Which module sends invoice receipt emails?', + 'invoice receipt email', + ['email', 'invoice', 'receipt', 'send'], + ], + [ + 'What runs the monthly billing close?', + 'monthly billing close', + ['billing', 'close', 'monthly', 'run'], + ], + ])('plans a bounded responsibility explanation: %s', (question, subject, terms) => { + const result = plan(question) + + expect(result.intent).toBe('explain') + expect(result.subject).toBe(subject) + expect(result.terms).toEqual(terms) + expect(result.obligations.map(({ kind }) => kind)) + .toEqual(['subject', 'behavior']) + }) + + it('plans every explicit workflow obligation', () => { + expect(plan('How is an idea report generated end-to-end?')).toEqual({ + intent: 'workflow', + subject: 'idea report', + terms: ['idea', 'report'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'idea report', mandatory: true }, + { id: 'o2', kind: 'entry', target: 'idea report', mandatory: true }, + { id: 'o3', kind: 'stage', target: 'idea report', mandatory: true }, + { id: 'o4', kind: 'handoff', target: 'idea report', mandatory: true }, + { id: 'o5', kind: 'behavior', target: 'idea report', mandatory: true }, + { id: 'o6', kind: 'ordering', target: 'idea report', mandatory: true }, + { id: 'o7', kind: 'terminal', target: 'idea report', mandatory: true }, + ], + }) + }) + + it('plans a trace from request to persistence as a workflow', () => { + const result = plan('Trace the idea report from request to persistence') + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('idea report') + expect(result.terms).toEqual(['idea', 'persist', 'report', 'request']) + expect(result.obligations.map(({ kind }) => kind)).toEqual([ + 'subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal', + ]) + }) + + it('keeps a camel-cased trace entrypoint separate from its from-boundary', () => { + const result = plan('Trace generateFromProblem from request to persistence') + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('generate problem') + expect(result.terms).toEqual(['generate', 'persist', 'problem', 'request']) + }) + + it.each([ + [ + 'Trace a failed invoice from the route through retry scheduling.', + 'failed invoice', ['failed', 'invoice', 'retry', 'route', 'scheduling'], + ], + [ + 'Trace invoice retry scheduling.', + 'invoice retry scheduling', ['invoice', 'retry', 'scheduling'], + ], + ])('plans imperative trace phrasing as a workflow: %s', (question, subject, terms) => { + const result = plan(question) + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe(subject) + expect(result.terms).toEqual(terms) + }) + + it('keeps explicit location semantics ahead of a bare trace cue', () => { + expect(plan('Trace where WorkflowEngine is defined')).toEqual({ + intent: 'locate', + subject: 'workflow engine', + terms: ['engine', 'workflow'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'workflow engine', mandatory: true }, + ], + }) + }) + + it.each([ + ['How does GoValidate generate an idea report end to end?', 'idea report'], + ['Can you explain how GoValidate generate ideas report?', 'idea report'], + ['How does ExampleEngine produce release artifacts end to end?', 'release artifact'], + ['How does ExampleEngine produce release artifacts?', 'release artifact'], + ['How does password policy login create a tenant session?', 'tenant session'], + ['How is the monthly revenue report built?', 'monthly revenue report'], + ])('extracts the object, not the actor, from active workflows: %s', (question, subject) => { + expect(plan(question)).toEqual({ + intent: 'workflow', + subject, + terms: subject.split(' ').sort(), + obligations: [ + { id: 'o1', kind: 'subject', target: subject, mandatory: true }, + { id: 'o2', kind: 'entry', target: subject, mandatory: true }, + { id: 'o3', kind: 'stage', target: subject, mandatory: true }, + { id: 'o4', kind: 'handoff', target: subject, mandatory: true }, + { id: 'o5', kind: 'behavior', target: subject, mandatory: true }, + { id: 'o6', kind: 'ordering', target: subject, mandatory: true }, + { id: 'o7', kind: 'terminal', target: subject, mandatory: true }, + ], + }) + }) + + it('keeps a get-passive workflow subject ahead of boundary clauses', () => { + const result = plan( + 'How does the idea report get generated from the initial request through to the completed report?', + ) + + expect(result.subject).toBe('idea report') + expect(result.obligations.map(({ kind }) => kind)).toEqual([ + 'subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal', + ]) + }) + + it.each([ + 'Where does the ingestion pipeline flow from request through validation to storage?', + 'Where is the end-to-end ingestion pipeline implemented?', + 'Explain how the ingestion workflow runs.', + ])('gives workflow semantics priority over locator or explanation cues: %s', (question) => { + expect(plan(question).intent).toBe('workflow') + }) + + it('normalizes punctuation without changing the subject or terms', () => { + const plain = plan('How is the idea report generated end to end') + const punctuated = plan('HOW is the idea-report generated, end-to-end?!') + + expect(punctuated).toEqual(plain) + }) + + it('keeps the canonical subject and sorted terms stable when clauses move', () => { + const prefix = plan( + 'From request through planning to persistence, how is the idea report generated?', + ) + const suffix = plan( + 'How is the idea report generated, from request through planning to persistence?', + ) + + expect(prefix.subject).toBe('idea report') + expect(prefix.terms).toEqual(['idea', 'persist', 'plan', 'report', 'request']) + expect(suffix).toEqual(prefix) + }) + + it('extracts the workflow object from event phrasing', () => { + const result = plan('What happens when a user requests an idea report?') + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('idea report') + }) + + it('keeps walk-through syntax separate from its structural bounds', () => { + const result = plan( + 'Walk me through idea report generation from HTTP request via queues to database persistence', + ) + + expect(result.subject).toBe('idea report') + expect(result.obligations.find(({ kind }) => kind === 'entry')?.target) + .toBe('http request') + expect(result.obligations.find(({ kind }) => kind === 'stage')?.target) + .toBe('queue') + expect(result.obligations.find(({ kind }) => kind === 'terminal')?.target) + .toBe('database persist') + }) + + it('canonicalizes field-incident phrasings independently of clause order', () => { + const suffix = plan('Which method persists retryCount on failure?') + const prefix = plan('On failure, what persists retryCount?') + + expect(prefix).toEqual(suffix) + expect(prefix.intent).toBe('locate') + expect(prefix.subject).toBe('retry count') + expect(prefix.terms).toEqual(['failure', 'retry', 'count'].sort()) + expect(prefix.access).toBe('write') + }) + + it('recognizes field reads as locators rather than explanations', () => { + const result = plan('What reads accountStatus after authentication completes?') + + expect(result.intent).toBe('locate') + expect(result.subject).toBe('account status') + expect(result.access).toBe('read') + expect(result.obligations.map(({ kind }) => kind)).toEqual(['subject']) + }) + + it('uses stable one-based obligation IDs for every supported intent', () => { + expect(plan('Find the retry policy definition').obligations.map(({ id }) => id)) + .toEqual(['o1']) + expect(plan('Explain how the retry policy behaves').obligations.map(({ id }) => id)) + .toEqual(['o1', 'o2']) + expect(plan('Trace the retry policy workflow end to end').obligations.map(({ id }) => id)) + .toEqual(['o1', 'o2', 'o3', 'o4', 'o5', 'o6', 'o7']) + }) + + it('does not let budget alter semantic planning', () => { + expect(plan('How does cache invalidation work?', 256)) + .toEqual(plan('How does cache invalidation work?', 4000)) + }) + + it('does not mutate the normalized request', () => { + const request: NormalizedRetrieveRequest = { + question: 'How is an idea report generated end to end?', + budget: 1024, + } + const snapshot = structuredClone(request) + + planQuestion(request) + + expect(request).toEqual(snapshot) + }) + + it.each([ + 'Compare these approaches', + 'Write a migration for this service', + 'Summarize every file', + ])('returns unsupported for an unplanned intent: %s', (question) => { + expect(unsupported(question).reason).toBe('unsupported_intent') + }) + + it('returns an exact missing-subject reason instead of planning a pronoun', () => { + const result = unsupported('Where is it?') + + expect(result.reason).toBe('missing_subject') + expect(result.terms).toEqual([]) + }) +}) diff --git a/tests/unit/query-workflow.test.ts b/tests/unit/query-workflow.test.ts new file mode 100644 index 00000000..5c0eeace --- /dev/null +++ b/tests/unit/query-workflow.test.ts @@ -0,0 +1,1910 @@ +import { describe, expect, it } from 'vitest' + +import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' +import type { + IndexBodyFact, IndexChannelNode, IndexControlFrame, IndexRange, + IndexScalarValue, IndexValue, +} from '../../src/domain/index/model.js' +import type { ReadyQueryIndex } from '../../src/domain/query/index-status.js' +import type { QueryPlan } from '../../src/domain/query/types.js' +import { selectWorkflow } from '../../src/domain/query/workflow.js' + +const evidence = { + file_id: 'file', + range: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + statement_range: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + excerpt_sha256: 'a'.repeat(64), +} as const + +function typedCase(value: IndexScalarValue): `case:${string}` { + return `case:${Buffer.from(JSON.stringify([typeof value, value])).toString('base64url')}` +} + +function triggerPayload(value: IndexScalarValue): IndexValue { + return { + kind: 'object', + entries: [{ key: 'trigger', value: { kind: 'literal', value } }], + } +} + +function plan(intent: QueryPlan['intent'], subject = 'idea report'): QueryPlan { + const obligations = intent === 'workflow' ? [ + { id: 'o1', kind: 'subject', target: subject, mandatory: true }, + { id: 'o2', kind: 'entry', target: subject, mandatory: true }, + { id: 'o3', kind: 'stage', target: subject, mandatory: true }, + { id: 'o4', kind: 'handoff', target: subject, mandatory: true }, + { id: 'o5', kind: 'behavior', target: subject, mandatory: true }, + { id: 'o6', kind: 'ordering', target: subject, mandatory: true }, + { id: 'o7', kind: 'terminal', target: subject, mandatory: true }, + ] : intent === 'explain' ? [ + { id: 'o1', kind: 'subject', target: subject, mandatory: true }, + { id: 'o2', kind: 'behavior', target: subject, mandatory: true }, + ] : [{ id: 'o1', kind: 'subject', target: subject, mandatory: true }] + return { intent, subject, terms: subject.split(' ').sort(), obligations } as QueryPlan +} + +class Fixture { + readonly graph = new KnowledgeGraph({ root_path: '/workspace' }) + readonly facts = new Map() + readonly owners = new Map() + readonly channels = new Map() + private order = 0 + + symbol( + id: string, label: string, sourceFile = `src/${id}.ts`, + nodeKind = 'function', frameworkRole?: string, + ): this { + this.graph.addNode(id, { + node_kind: nodeKind, label: `${label}()`, qualified_name: label, + source_file: sourceFile, source_location: 'L1', provenance: [{}], + definition_range: evidence.statement_range, + declaration_range: evidence.range, + ...(frameworkRole ? { framework_role: frameworkRole } : {}), + }) + return this + } + + private add(owner: string, fact: IndexBodyFact): void { + this.facts.set(fact.id, fact) + this.owners.set(owner, [...(this.owners.get(owner) ?? []), fact]) + } + + private base(owner: string, id: string, control: readonly IndexControlFrame[] = []) { + return { + id, owner_symbol_id: owner, order: [this.order++, 3, 0, 0], evidence, + control, confidence: 'high' as const, source: 'typescript-semantic' as const, + } + } + + call( + from: string, to: string, id = `${from}-calls-${to}`, + control: readonly IndexControlFrame[] = [], + args: readonly IndexValue[] = [], + statementRange?: IndexRange, + ): this { + const base = this.base(from, id, control) + const range = { + start: { line: base.order[0]! + 1, column: 1 }, + end: { line: base.order[0]! + 1, column: 2 }, + } + this.add(from, { + ...base, evidence: { + ...evidence, range, statement_range: statementRange ?? range, + }, + kind: 'call', callee: to, + target_symbol_id: to, arguments: args, scheduling: 'awaited', + }) + this.graph.addEdge(from, to, { + relation: 'calls', evidence: { source: 'typescript-semantic', range }, + provenance: [{}], + }) + return this + } + + behavior(owner: string, id = `${owner}-returns`): this { + this.add(owner, { ...this.base(owner, id), kind: 'return' }) + return this + } + + literal( + owner: string, + id = `${owner}-literal`, + value = 'report', + ): this { + this.add(owner, { + ...this.base(owner, id), kind: 'literal', + value: { kind: 'literal', value }, role: 'initializer', + }) + return this + } + + dynamicCall(owner: string, id = `${owner}-dynamic`): this { + this.add(owner, { + ...this.base(owner, id), kind: 'call', callee: 'dynamic', + arguments: [], scheduling: 'sync', + }) + return this + } + + persistence( + owner: string, id = `${owner}-persistence`, + operation: 'read' | 'update' = 'update', + resource?: IndexValue, + receiverType = 'Repository', + control: readonly IndexControlFrame[] = [], + ): this { + const callId = `${id}-call` + this.add(owner, { + ...this.base(owner, callId, control), kind: 'call', callee: 'save', + arguments: [], scheduling: 'awaited', + }) + this.add(owner, { + ...this.base(owner, id, control), kind: 'persistence', operation, + call_fact_id: callId, receiver_type: receiverType, + ...(resource ? { resource } : {}), + }) + return this + } + + persistenceInCase( + owner: string, controller: string, value: IndexScalarValue, + id = `${owner}-persistence`, + ): this { + return this.persistence(owner, id, 'update', undefined, 'Repository', [{ + kind: 'branch', controller_fact_id: controller, arm: typedCase(value), + }]) + } + + switchSelector( + owner: string, id: string, + path: readonly string[] = ['data', 'trigger'], + ): this { + this.add(owner, { + ...this.base(owner, id), kind: 'condition', condition_kind: 'switch', + test: { + kind: 'template', + parts: [ + { kind: 'parameter', position: 0 }, + ...path.map((value): IndexValue => ({ kind: 'literal', value })), + ], + }, + }) + return this + } + + controller( + owner: string, id: string, kind: 'condition' | 'loop' | 'parallel', + members: readonly string[] = [], + control: readonly IndexControlFrame[] = [], + ): this { + if (kind === 'condition') this.add(owner, { + ...this.base(owner, id, control), kind, condition_kind: 'if', + }) + else if (kind === 'loop') this.add(owner, { + ...this.base(owner, id, control), kind, loop_kind: 'while', + }) + else this.add(owner, { + ...this.base(owner, id, control), kind, combinator: 'all', + completion: 'all_or_first_rejection', lane_count: members.length, + member_fact_ids: members, + }) + return this + } + + channel(channel: IndexChannelNode): this { + this.channels.set(channel.id, channel) + this.graph.addNode(channel.id, channel) + return this + } + + edge(from: string, to: string, relation: string, source = 'typescript-semantic'): this { + this.graph.addEdge(from, to, { relation, + evidence: { ...evidence, source }, provenance: [{}] }) + return this + } + + route(from: string, to: string, owner: string, publishId: string): this { + const fact = this.facts.get(publishId) + if (!fact) throw new Error(`Missing route fact ${publishId}`) + this.graph.addEdge(from, to, { + relation: 'routes_through', execution_owner_id: owner, + evidence: { + source: 'typescript-semantic', + range: fact.evidence.range, + statement_range: fact.evidence.statement_range, + excerpt_sha256: fact.evidence.excerpt_sha256, + }, + provenance: [{}], + }) + return this + } + + publish( + from: string, helper: string, channel: string, id: string, + args: readonly IndexValue[] = [], dispatchPayloadArgument?: number, + ): this { + this.call(from, helper, id, [], args) + const fact = this.facts.get(id) + if (!fact) throw new Error(`Missing publish fact ${id}`) + this.graph.addEdge(from, channel, { + relation: 'publishes_to', + ...(dispatchPayloadArgument === undefined + ? {} : { dispatch_payload_argument: dispatchPayloadArgument }), + evidence: { + source: 'typescript-semantic', + range: fact.evidence.range, + statement_range: fact.evidence.statement_range, + excerpt_sha256: fact.evidence.excerpt_sha256, + }, + provenance: [{}], + }) + return this + } + + publishWithNestedCall( + from: string, helper: string, nested: string, channel: string, + id: string, args: readonly IndexValue[], dispatchPayloadArgument?: number, + ): this { + this.call(from, helper, id, [], args) + const fact = this.facts.get(id)! + this.call(from, nested, `${id}-nested`, [], [], fact.evidence.statement_range) + this.graph.addEdge(from, channel, { + relation: 'publishes_to', + ...(dispatchPayloadArgument === undefined + ? {} : { dispatch_payload_argument: dispatchPayloadArgument }), + evidence: { + source: 'wrapper-summary', + range: fact.evidence.range, + statement_range: fact.evidence.statement_range, + excerpt_sha256: fact.evidence.excerpt_sha256, + }, + provenance: [{}], + }) + return this + } + + publishWithMismatchedRange( + from: string, helper: string, channel: string, id: string, + ): this { + this.call(from, helper, id) + const fact = this.facts.get(id)! + this.graph.addEdge(from, channel, { + relation: 'publishes_to', + evidence: { + source: 'wrapper-summary', + range: { + start: { + line: fact.evidence.range.start.line, + column: fact.evidence.range.start.column + 1, + }, + end: fact.evidence.range.end, + }, + statement_range: fact.evidence.statement_range, + excerpt_sha256: fact.evidence.excerpt_sha256, + }, + provenance: [{}], + }) + return this + } + + index(): ReadyQueryIndex { + return { + state: 'ready', graph: this.graph, root_path: '/workspace', + file_hashes: new Map(), unsupported_sources: [], + operation_by_id: this.facts, operations_by_owner: this.owners, + channels_by_id: this.channels, channels_by_key: new Map(), + } + } +} + +function directFixture(): Fixture { + return new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .call('entry', 'stage').call('stage', 'terminal') + .persistence('terminal') +} + +function channelFixture(consumerQueue: 'queue' | 'other' | null): Fixture { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry') + .symbol('enqueue', 'enqueueJob') + .symbol('worker', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble', parent_channel_id: 'queue', + }) + .publish('entry', 'enqueue', 'job', 'publish-report') + .route('job', 'queue', 'entry', 'publish-report') + .call('worker', 'terminal') + if (consumerQueue === 'other') fixture + .channel({ + id: 'other', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'other', + }) + .edge('other', 'worker', 'consumed_by') + else if (consumerQueue === 'queue') fixture + .edge('queue', 'worker', 'consumed_by') + return fixture +} + +function discriminatedChannelFixture(options: { + dispatchPayloadArgument?: number | null + payload?: IndexScalarValue + persistenceCase?: IndexScalarValue | null +} = {}): Fixture { + const dispatchPayloadArgument = options.dispatchPayloadArgument === undefined + ? 0 : options.dispatchPayloadArgument, + payload = options.payload === undefined ? 'assembly_complete' : options.payload, + persistenceCase = options.persistenceCase === undefined + ? 'assembly_complete' : options.persistenceCase, + fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'terminal-trigger') + if (persistenceCase === null) fixture.persistence('terminal') + else fixture.persistenceInCase( + 'terminal', 'terminal-trigger', persistenceCase, + ) + return fixture + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publish( + 'entry', 'enqueue', 'queue', 'publish-report', + [triggerPayload(payload)], dispatchPayloadArgument ?? undefined, + ) + .edge('queue', 'terminal', 'consumed_by') +} + +function multipleIncomingChannelFixture(secondProducerProven: boolean): Fixture { + return new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'terminal-trigger') + .persistenceInCase( + 'terminal', 'terminal-trigger', 'primary_complete', 'primary-persistence', + ) + .persistenceInCase( + 'terminal', 'terminal-trigger', 'archive_complete', 'archive-persistence', + ) + .channel({ + id: 'primary-queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'primary', + }) + .channel({ + id: 'archive-queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'archive', + }) + .publish( + 'entry', 'enqueue', 'primary-queue', 'publish-primary', + [triggerPayload('primary_complete')], 0, + ) + .publish( + 'entry', 'enqueue', 'archive-queue', 'publish-archive', + [triggerPayload('archive_complete')], secondProducerProven ? 0 : undefined, + ) + .edge('primary-queue', 'terminal', 'consumed_by') + .edge('archive-queue', 'terminal', 'consumed_by') +} + +describe('deterministic workflow selection', () => { + it('proves an explicit request entry only from structural request metadata', () => { + const requestPlan = { + ...plan('workflow'), + obligations: plan('workflow').obligations.map((obligation) => + obligation.kind === 'entry' ? { ...obligation, target: 'request' } : obligation), + } + const migration = new Fixture() + .symbol('migration', 'migrateIdeaReportRequest', 'src/migrations/request.ts') + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .call('migration', 'stage').call('stage', 'terminal').persistence('terminal') + const fakeHandler = new Fixture() + .symbol('handler', 'ideaReportHandler', 'src/http/helper.ts') + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .call('handler', 'stage').call('stage', 'terminal').persistence('terminal') + const route = new Fixture() + .symbol( + 'route', 'IdeaReportController.submit', + 'src/http/idea-report.controller.ts', 'route', + ) + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .call('route', 'stage').call('stage', 'terminal').persistence('terminal') + + const rejected = selectWorkflow(migration.index(), requestPlan) + const rejectedHandler = selectWorkflow(fakeHandler.index(), requestPlan) + const accepted = selectWorkflow(route.index(), requestPlan) + + expect(rejected.complete).toBe(false) + expect(rejectedHandler.complete).toBe(false) + expect(rejected.missing).toContainEqual({ + code: 'entrypoint_unproven', target: 'request', obligationId: 'o2', + }) + expect(accepted.complete).toBe(true) + expect(accepted.rootSymbolIds).toEqual(['route']) + }) + + it('uses explicit entry, stage, and terminal bounds to choose the proven corridor', () => { + const boundedPlan = { + ...plan('workflow'), + obligations: plan('workflow').obligations.map((obligation) => + obligation.kind === 'entry' ? { ...obligation, target: 'request' } + : obligation.kind === 'stage' ? { ...obligation, target: 'planning' } + : obligation.kind === 'terminal' + ? { ...obligation, target: 'database persistence' } : obligation), + } + const fixture = new Fixture() + .symbol('a-cli', 'generateIdeaReport') + .symbol('file-stage', 'assembleIdeaReport') + .symbol('file', 'persistIdeaReportFile') + .persistence('file', 'file-write', 'update', undefined, 'FileSystem') + .call('a-cli', 'file-stage').call('file-stage', 'file') + .symbol('z-route', 'IdeaReportController.submit', 'src/http/report.ts', 'route') + .symbol('planning', 'planIdeaReport') + .symbol('database', 'persistIdeaReportDatabase') + .persistence('database', 'db-write') + .call('z-route', 'planning').call('planning', 'database') + + const result = selectWorkflow(fixture.index(), boundedPlan) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['z-route']) + expect(result.terminalSymbolIds).toEqual(['database']) + expect(result.symbolIds).toContain('planning') + }) + + it('does not relabel file persistence as database persistence', () => { + const boundedPlan = { + ...plan('workflow'), + obligations: plan('workflow').obligations.map((obligation) => + obligation.kind === 'terminal' + ? { ...obligation, target: 'database persistence' } : obligation), + } + const result = selectWorkflow(new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('file', 'persistIdeaReportFile') + .persistence('file', 'file-write', 'update', undefined, 'FileSystem') + .call('entry', 'file').index(), boundedPlan) + + expect(result.complete).toBe(false) + expect(result.missing).toContainEqual({ + code: 'terminal_persistence_unproven', + target: 'database persistence', obligationId: 'o7', + }) + }) + + it('selects a direct authenticated chain through terminal persistence', () => { + const result = selectWorkflow(directFixture().index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.terminalSymbolIds).toEqual(['terminal']) + expect(result.symbolIds).toEqual(['entry', 'stage', 'terminal']) + expect(result.edges.map((edge) => edge.relation)).toEqual(['calls', 'calls']) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'entry-calls-stage', 'stage-calls-terminal', + 'terminal-persistence', 'terminal-persistence-call', + ])) + expect(result.links.flatMap(({ operationIds }) => operationIds)) + .toEqual(['entry-calls-stage', 'stage-calls-terminal']) + const selectedOperations = new Set(result.operationIds) + expect([ + ...result.links.flatMap(({ operationIds }) => operationIds), + ...result.obligations.flatMap(({ operationIds }) => operationIds), + ...result.controlGroups.flatMap(({ operationIds, controllerOperationId }) => [ + ...operationIds, ...(controllerOperationId ? [controllerOperationId] : []), + ]), + ].every((id) => selectedOperations.has(id))).toBe(true) + expect(result.obligations.map((entry) => [entry.id, entry.kind, entry.proven])) + .toEqual([ + ['o1', 'subject', true], ['o2', 'entry', true], + ['o3', 'stage', true], ['o4', 'handoff', true], + ['o5', 'behavior', true], ['o6', 'ordering', true], + ['o7', 'terminal', true], + ]) + }) + + it('does not seed redundant behavior for edge-incident or persisted stages', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry', 'entry-return') + .symbol('stage', 'assembleIdeaReport').behavior('stage', 'stage-return') + .symbol('terminal', 'persistIdeaReport').behavior('terminal', 'terminal-return') + .call('entry', 'stage').call('stage', 'terminal').persistence('terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.operationIds).not.toEqual(expect.arrayContaining([ + 'entry-return', 'stage-return', 'terminal-return', + ])) + expect(result.obligations.find(({ kind }) => kind === 'behavior')?.proven).toBe(true) + }) + + it('selects only a complete exact job-to-queue channel macro', () => { + const queue: IndexChannelNode = { + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + } + const job: IndexChannelNode = { + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble', parent_channel_id: 'queue', + } + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry') + .symbol('enqueue', 'enqueueJob') + .symbol('worker', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .channel(queue).channel(job) + .publish('entry', 'enqueue', 'job', 'publish-report') + .route('job', 'queue', 'entry', 'publish-report') + .edge('queue', 'worker', 'consumed_by') + .call('worker', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.edges.map((edge) => edge.relation).sort()).toEqual([ + 'calls', 'consumed_by', 'publishes_to', 'routes_through', + ]) + expect(result.metrics.actualNodeCount).toBe(6) + }) + + it('binds acronym channel and generic database persistence targets structurally', () => { + const bounded = { + ...plan('workflow'), + obligations: plan('workflow').obligations.map((obligation) => + obligation.kind === 'stage' + ? { ...obligation, target: 'bull mq queue' } + : obligation.kind === 'terminal' + ? { ...obligation, target: 'database persist' } + : obligation), + } + + const result = selectWorkflow(channelFixture('queue').index(), bounded) + + expect(result.complete).toBe(true) + expect(result.obligations.find(({ kind }) => kind === 'stage')?.proven).toBe(true) + expect(result.obligations.find(({ kind }) => kind === 'terminal')?.proven).toBe(true) + }) + + it.each([ + ['missing', null], + ['changed to an out-of-range position', 1], + ])('rejects a terminal channel with a %s dispatch payload argument', ( + _name, dispatchPayloadArgument, + ) => { + expect(selectWorkflow( + discriminatedChannelFixture().index(), plan('workflow'), + ).complete).toBe(true) + + const result = selectWorkflow(discriminatedChannelFixture({ + dispatchPayloadArgument, + }).index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.missing.map(({ code }) => code)) + .toContain('terminal_persistence_unproven') + }) + + it.each([ + ['value', 'section_complete' as IndexScalarValue, 'assembly_complete' as IndexScalarValue], + ['type', 1 as IndexScalarValue, '1' as IndexScalarValue], + ])('rejects a terminal channel after a trigger %s mismatch', ( + _name, payload, persistenceCase, + ) => { + const result = selectWorkflow(discriminatedChannelFixture({ + payload, persistenceCase, + }).index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.missing.map(({ code }) => code)) + .toContain('terminal_persistence_unproven') + }) + + it.each([ + ['moved to a sibling case', 'section_complete' as IndexScalarValue], + ['removed from the matching case', null], + ])('rejects terminal persistence %s', (_name, persistenceCase) => { + const result = selectWorkflow(discriminatedChannelFixture({ + persistenceCase, + }).index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.missing.map(({ code }) => code)) + .toContain('terminal_persistence_unproven') + }) + + it('requires exact proof from every incoming channel producer', () => { + expect(selectWorkflow( + multipleIncomingChannelFixture(true).index(), plan('workflow'), + ).complete).toBe(true) + + const result = selectWorkflow( + multipleIncomingChannelFixture(false).index(), plan('workflow'), + ) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.missing.map(({ code }) => code)) + .toContain('terminal_persistence_unproven') + }) + + it.each([ + ['unrelated route owner', 'noise', 'publish-report'], + ['unrelated route statement', 'entry', 'unrelated-call'], + ])('rejects a job route proved by an %s', (_name, owner, proof) => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry') + .symbol('enqueue', 'enqueueJob') + .symbol('noise', 'unrelatedHelper') + .symbol('worker', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble', parent_channel_id: 'queue', + }) + .call('entry', 'noise', 'unrelated-call') + .publish('entry', 'enqueue', 'job', 'publish-report') + .route('job', 'queue', owner, proof) + .edge('queue', 'worker', 'consumed_by') + .call('worker', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.links.some((link) => link.kind === 'channel')).toBe(false) + }) + + it('keeps shared producer APIs out of the causal dossier', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('producer2', 'dispatchIdeaReport') + .symbol('worker1', 'planIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'terminal-trigger') + .persistenceInCase('terminal', 'terminal-trigger', 'two.process') + .symbol('enqueue', 'enqueueJob', 'src/queue-registry.ts').behavior('enqueue') + .symbol('string', 'String') + .edge('enqueue', 'string', 'calls', 'heuristic') + .channel({ + id: 'queue1', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'one', + }) + .channel({ + id: 'job1', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'one.process', parent_channel_id: 'queue1', + }) + .channel({ + id: 'queue2', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'two', + }) + .channel({ + id: 'job2', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'two.process', parent_channel_id: 'queue2', + }) + .publish('entry', 'enqueue', 'job1', 'publish-one') + .route('job1', 'queue1', 'entry', 'publish-one') + .edge('queue1', 'worker1', 'consumed_by') + .call('worker1', 'producer2') + .publish( + 'producer2', 'enqueue', 'job2', 'publish-two', + [triggerPayload('two.process')], 0, + ) + .route('job2', 'queue2', 'producer2', 'publish-two') + .edge('queue2', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.symbolIds).not.toContain('enqueue') + expect(result.links.filter((link) => link.kind === 'channel')).toHaveLength(2) + expect(result.links.some((link) => link.toId === 'enqueue')).toBe(false) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'publish-one', 'publish-two', + ])) + for (const kind of ['stage', 'handoff', 'behavior', 'ordering']) { + expect(result.obligations.find((proof) => proof.kind === kind)?.symbolIds) + .not.toContain('enqueue') + } + }) + + it.each([ + ['consumer removal', null], + ['queue mismatch', 'other' as const], + ])('turns a ready channel dossier incomplete after %s', (_name, mutation) => { + expect(selectWorkflow(channelFixture('queue').index(), plan('workflow')).complete) + .toBe(true) + + const result = selectWorkflow(channelFixture(mutation).index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.symbolIds).toEqual(['entry']) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.obligations.map(({ kind, proven }) => [kind, proven])).toEqual([ + ['subject', true], ['entry', true], ['stage', true], ['handoff', false], + ['behavior', true], ['ordering', false], ['terminal', false], + ]) + expect(result.missing).toEqual([ + { code: 'adjacent_handoff_unproven', target: 'entry', obligationId: 'o4' }, + { code: 'obligation_target_unproven', target: 'idea report', obligationId: 'o6' }, + { code: 'terminal_persistence_unproven', target: 'idea report', obligationId: 'o7' }, + ]) + }) + + it.each([ + ['removed consumer', (fixture: Fixture) => fixture], + ['channel mismatch', (fixture: Fixture) => fixture + .channel({ id: 'other', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'other' }) + .edge('other', 'worker', 'consumed_by')], + ])('fails closed for %s', (_name, mutate) => { + const fixture = mutate(new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry') + .symbol('worker', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .channel({ id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports' }) + .edge('entry', 'queue', 'publishes_to').call('worker', 'terminal')) + + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(false) + expect(result.missing.map((entry) => entry.code)).toContain( + 'terminal_persistence_unproven', + ) + }) + + it('fails closed when an otherwise connected flow has no persistence terminal', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('stage', 'assembleIdeaReport') + .call('entry', 'stage').behavior('stage') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.metrics.recoveryPasses).toBe(1) + expect(result.metrics.recoveryFrontierCount).toBeGreaterThan(0) + expect(result.metrics.recoveryFrontierCount).toBeLessThanOrEqual(64) + expect(result.missing.map((entry) => entry.code)) + .toContain('terminal_persistence_unproven') + }) + + it('retains authenticated generic stages when only persistence is absent', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('alpha', 'alpha') + .symbol('omega', 'omega').behavior('omega') + .call('entry', 'alpha').call('alpha', 'omega') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.symbolIds).toEqual(['alpha', 'entry', 'omega']) + expect(result.links).toHaveLength(2) + expect(result.metrics.recoveryPasses).toBe(1) + expect(result.metrics.recoveryFrontierCount).toBeGreaterThan(0) + expect(result.metrics.recoveryFrontierCount).toBeLessThanOrEqual(64) + expect(result.obligations.map(({ kind, proven }) => [kind, proven])).toEqual([ + ['subject', true], ['entry', true], ['stage', true], ['handoff', true], + ['behavior', true], ['ordering', true], ['terminal', false], + ]) + expect(result.missing).toEqual([ + { code: 'terminal_persistence_unproven', target: 'idea report', obligationId: 'o7' }, + ]) + }) + + it('rejects a graph call edge that has no matching owner call fact', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport').behavior('entry') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .edge('entry', 'terminal', 'calls') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(false) + expect(result.links).toEqual([]) + expect(result.terminalSymbolIds).toEqual([]) + }) + + it('preserves fan-out, fan-in, branch, loop, parallel and cycle groups', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('left', 'researchIdeaReportLeft') + .symbol('right', 'researchIdeaReportRight') + .symbol('join', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + fixture.controller('entry', 'parallel', 'parallel', ['to-left', 'to-right']) + .call('entry', 'left', 'to-left', [{ kind: 'parallel', controller_fact_id: 'parallel', lane: 0 }]) + .call('entry', 'right', 'to-right', [{ kind: 'parallel', controller_fact_id: 'parallel', lane: 1 }]) + .controller('left', 'branch', 'condition') + .call('left', 'join', 'left-join', [{ kind: 'branch', controller_fact_id: 'branch', arm: 'then' }]) + .controller('right', 'loop', 'loop') + .call('right', 'join', 'right-join', [{ kind: 'loop', controller_fact_id: 'loop' }]) + .call('join', 'terminal').call('terminal', 'join', 'cycle-back') + .persistence('terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.edges).toHaveLength(6) + expect(new Set(result.controlGroups.map((group) => group.kind))) + .toEqual(new Set(['branch', 'loop', 'parallel', 'cycle'])) + expect(result.controlGroups.find((group) => group.kind === 'cycle')?.symbolIds) + .toEqual(['join', 'terminal']) + }) + + it('preserves a bounded unequal-length branch detour to the same terminal', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('left', 'ideaReportLeft').symbol('right', 'ideaReportRight') + .symbol('middle', 'ideaReportMiddle').symbol('join', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .call('entry', 'left').call('left', 'join') + .call('entry', 'right').call('right', 'middle').call('middle', 'join') + .call('join', 'terminal') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual([ + 'entry', 'join', 'left', 'middle', 'right', 'terminal', + ]) + expect(result.links).toHaveLength(6) + }) + + it('retains a valid branch more than four relations longer than its sibling', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('short', 'ideaReportShort') + .symbol('long1', 'ideaReportLongOne') + .symbol('long2', 'ideaReportLongTwo') + .symbol('long3', 'ideaReportLongThree') + .symbol('long4', 'ideaReportLongFour') + .symbol('long5', 'ideaReportLongFive') + .symbol('long6', 'ideaReportLongSix') + .symbol('join', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .call('entry', 'short').call('short', 'join') + .call('entry', 'long1').call('long1', 'long2') + .call('long2', 'long3').call('long3', 'long4') + .call('long4', 'long5').call('long5', 'long6') + .call('long6', 'join').call('join', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual([ + 'entry', 'join', 'long1', 'long2', 'long3', 'long4', 'long5', 'long6', + 'short', 'terminal', + ]) + expect(result.links).toHaveLength(10) + }) + + it('fails closed when distinct terminal routes exceed the relation bound', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('start', 'startPipeline') + .symbol('research', 'researchSection') + .symbol('check', 'checkAndDispatchNext') + .symbol('enqueue', 'enqueueJob') + .symbol('assembly-worker', 'AssemblyWorker.process') + .symbol('assembly', 'AssemblyService.assembleReport') + .symbol('dispatch', 'dispatchDbSync') + .symbol('terminal', 'DbSyncWorker.process').persistence('terminal') + .channel({ + id: 'assembly-queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'assembly-queue', + }) + .channel({ + id: 'assembly-job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble_report', + parent_channel_id: 'assembly-queue', + }) + .call('entry', 'start') + .call('start', 'research') + .call('entry', 'research', 'request-wrapper-shortcut') + .call('research', 'terminal', 'progress-db-sync') + .call('research', 'check') + .publish('check', 'enqueue', 'assembly-job', 'publish-assembly') + .route('assembly-job', 'assembly-queue', 'check', 'publish-assembly') + .edge('assembly-queue', 'assembly-worker', 'consumed_by') + .call('assembly-worker', 'assembly') + .call('assembly', 'dispatch') + .call('assembly', 'terminal', 'sync-wrapper-shortcut') + .call('dispatch', 'terminal') + for (let index = 0; index < 11; index += 1) { + const id = `sibling-${index}` + fixture.symbol(id, `unrelatedStage${index}`) + .call('research', id).call(id, 'terminal') + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.bounded).toBe(true) + expect(result.missing).toContainEqual({ + code: 'selection_bound_reached', target: 'idea report', + }) + }) + + it('chooses the deepest relevant persistence instead of an early write', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('early', 'createIdeaReport').persistence('early') + .symbol('assembly', 'assembleIdeaReport') + .symbol('terminal', 'syncIdeaReport').persistence('terminal') + .call('entry', 'early').call('early', 'assembly').call('assembly', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['terminal']) + expect(result.symbolIds).toContain('early') + }) + + it('keeps a persisted channel consumer as a stage when a later final write exists', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueJob') + .symbol('draft', 'persistIdeaReportDraft').persistence('draft') + .symbol('final', 'omega').persistence('final') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'draft', parent_channel_id: 'queue', + }) + .publish('entry', 'enqueue', 'job', 'publish-draft') + .route('job', 'queue', 'entry', 'publish-draft') + .edge('queue', 'draft', 'consumed_by') + .call('draft', 'final') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['final']) + expect(result.symbolIds).toContain('draft') + expect(result.links).toHaveLength(2) + }) + + it('follows persisted channel-consumer fan-out through a shared final write', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueJob') + .symbol('draft', 'persistIdeaReportDraft').persistence('draft') + .symbol('left', 'alpha') + .symbol('right', 'beta') + .symbol('join', 'gamma') + .symbol('final', 'omega').persistence('final') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publish('entry', 'enqueue', 'queue', 'publish-draft') + .edge('queue', 'draft', 'consumed_by') + .call('draft', 'left') + .call('draft', 'right') + .call('left', 'join') + .call('right', 'join') + .call('join', 'final') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['final']) + expect(result.symbolIds).toEqual([ + 'draft', 'entry', 'final', 'join', 'left', 'right', + ]) + }) + + it('preserves independent persisted terminals and sibling-call order', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('left', 'buildIdeaReportPrimary') + .symbol('right', 'buildIdeaReportArchive') + .symbol('leftTerminal', 'persistIdeaReportPrimary').persistence('leftTerminal') + .symbol('rightTerminal', 'persistIdeaReportArchive').persistence('rightTerminal') + .call('entry', 'left').call('entry', 'right') + .call('left', 'leftTerminal').call('right', 'rightTerminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['leftTerminal', 'rightTerminal']) + expect(result.symbolIds).toEqual([ + 'entry', 'left', 'leftTerminal', 'right', 'rightTerminal', + ]) + expect(result.controlGroups).toContainEqual({ + kind: 'sequence', + operationIds: ['entry-calls-left', 'entry-calls-right'], + symbolIds: ['left', 'right'], + }) + expect(result.obligations.find(({ kind }) => kind === 'ordering')) + .toEqual(expect.objectContaining({ + proven: true, edgeIds: result.edges.map(({ id }) => id), + operationIds: expect.arrayContaining(['entry-calls-left', 'entry-calls-right']), + })) + }) + + it('keeps authenticated sibling order when target names sort in reverse', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('zTarget', 'persistFirst').persistence('zTarget') + .symbol('aTarget', 'persistSecond').persistence('aTarget') + .call('entry', 'zTarget', 'call-z') + .call('entry', 'aTarget', 'call-a') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.controlGroups).toContainEqual({ + kind: 'sequence', + operationIds: ['call-z', 'call-a'], + symbolIds: ['zTarget', 'aTarget'], + }) + }) + + it('closes every independently controlled selected handoff', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('zBranch', 'researchFirst') + .symbol('aBranch', 'researchSecond') + .symbol('join', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .controller('entry', 'guard-z', 'condition') + .call('entry', 'zBranch', 'call-z', [ + { kind: 'branch', controller_fact_id: 'guard-z', arm: 'then' }, + ]) + .controller('entry', 'guard-a', 'condition') + .call('entry', 'aBranch', 'call-a', [ + { kind: 'branch', controller_fact_id: 'guard-a', arm: 'then' }, + ]) + .call('zBranch', 'join') + .call('aBranch', 'join') + .call('join', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'call-z', 'guard-z', 'call-a', 'guard-a', + ])) + expect(result.links.find(({ fromId, toId }) => + fromId === 'entry' && toId === 'zBranch')?.operationIds).toEqual(['call-z']) + expect(result.links.find(({ fromId, toId }) => + fromId === 'entry' && toId === 'aBranch')?.operationIds).toEqual(['call-a']) + }) + + it('preserves recursively nested control groups', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .controller('entry', 'outer-loop', 'loop') + .controller('entry', 'inner-guard', 'condition', [], [{ + kind: 'loop', controller_fact_id: 'outer-loop', + }]) + .call('entry', 'terminal', 'persist-call', [{ + kind: 'branch', controller_fact_id: 'inner-guard', arm: 'then', + }]) + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'outer-loop', 'inner-guard', 'persist-call', + ])) + expect(result.controlGroups).toEqual(expect.arrayContaining([ + expect.objectContaining({ + kind: 'branch', controllerOperationId: 'inner-guard', + operationIds: ['persist-call'], + }), + expect.objectContaining({ + kind: 'loop', controllerOperationId: 'outer-loop', + operationIds: ['inner-guard'], + }), + ])) + }) + + it('fails closed instead of dropping an over-limit controlled branch', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .controller('entry', 'guard', 'condition') + for (const arm of ['then', 'else'] as const) { + let owner = 'entry' + for (let index = 0; index < 12; index += 1) { + const next = `${arm}-${index}` + fixture.symbol(next, `${arm}IdeaReport${index}`) + .call(owner, next, `${arm}-call-${index}`, [ + { kind: 'branch', controller_fact_id: 'guard', arm }, + ]) + owner = next + } + fixture.call(owner, 'terminal', `${arm}-terminal`, [ + { kind: 'branch', controller_fact_id: 'guard', arm }, + ]) + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.bounded).toBe(true) + expect(result.missing).toContainEqual({ + code: 'selection_bound_reached', target: 'idea report', + }) + }) + + it('keeps independent direct and queued persistence terminals', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('async-terminal', 'persistIdeaReportAsync') + .switchSelector('async-terminal', 'async-trigger') + .persistenceInCase('async-terminal', 'async-trigger', 'assembly_complete') + .symbol('direct-terminal', 'persistIdeaReportDirect').persistence('direct-terminal') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publish( + 'entry', 'enqueue', 'queue', 'publish-job', + [triggerPayload('assembly_complete')], 0, + ) + .edge('queue', 'async-terminal', 'consumed_by') + .call('entry', 'direct-terminal', 'entry-to-direct') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['async-terminal', 'direct-terminal']) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'publish-job', 'entry-to-direct', + ])) + expect(result.links).toEqual(expect.arrayContaining([ + expect.objectContaining({ + fromId: 'entry', toId: 'async-terminal', kind: 'channel', + }), + expect.objectContaining({ + fromId: 'entry', toId: 'direct-terminal', kind: 'direct', + }), + ])) + }) + + it('fails closed instead of dropping an over-limit unconditional branch', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + for (const branch of ['a', 'b']) { + let owner = 'entry' + for (let index = 0; index < 12; index += 1) { + const next = `${branch}-${index}` + fixture.symbol(next, `${branch}IdeaReport${index}`) + .call(owner, next, `${branch}-call-${index}`) + owner = next + } + fixture.call(owner, 'terminal', `${branch}-terminal`) + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.bounded).toBe(true) + expect(result.missing).toContainEqual({ + code: 'selection_bound_reached', target: 'idea report', + }) + }) + + it('does not prefer a deeper persistence branch unrelated to the subject', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('relevant', 'persistIdeaReport').persistence('relevant') + .symbol('noise1', 'recordTelemetry').symbol('noise2', 'flushTelemetry') + .symbol('noise3', 'storeTelemetry').persistence('noise3') + .call('entry', 'relevant').call('entry', 'noise1') + .call('noise1', 'noise2').call('noise2', 'noise3') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['relevant']) + }) + + it('keeps locate and explain focused without imposing workflow terminals', () => { + const locate = new Fixture().symbol('subject', 'ideaReport').index() + const explain = new Fixture().symbol('subject', 'ideaReport') + .behavior('subject').index() + const located = selectWorkflow(locate, plan('locate')) + expect(located.complete).toBe(true) + expect(located.operationIds).toEqual([]) + expect(located.controlGroups).toEqual([]) + const explained = selectWorkflow(explain, plan('explain')) + expect(explained.complete).toBe(true) + expect(explained.obligations.find(({ kind }) => kind === 'behavior')) + .toEqual(expect.objectContaining({ + proven: true, operationIds: ['subject-returns'], edgeIds: [], + })) + }) + + it.each(['x', 'Δ', 'مرحبا'])( + 'locates Unicode and one-character identifiers: %s', + (label) => { + const result = selectWorkflow( + new Fixture().symbol('subject', label).index(), + plan('locate', label), + ) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual(['subject']) + }, + ) + + it('closes a nonterminal persistence behavior over its authenticated backing call', () => { + const fixture = new Fixture().symbol('subject', 'ideaReport').persistence('subject') + fixture.owners.set('subject', [...fixture.owners.get('subject')!].reverse()) + const result = selectWorkflow(fixture.index(), plan('explain')) + + expect(result.complete).toBe(true) + expect(result.operationIds).toEqual([ + 'subject-persistence', 'subject-persistence-call', + ]) + }) + + it('cannot prove explanation behavior from a declaration alone', () => { + const result = selectWorkflow( + new Fixture().symbol('subject', 'ideaReport').index(), + plan('explain'), + ) + + expect(result.complete).toBe(false) + expect(result.operationIds).toEqual([]) + expect(result.obligations.find(({ kind }) => kind === 'behavior')) + .toEqual(expect.objectContaining({ proven: false, operationIds: [], edgeIds: [] })) + expect(result.missing).toEqual([ + { code: 'behavior_unproven', target: 'idea report', obligationId: 'o2' }, + ]) + }) + + it('includes both endpoints when explaining an authenticated direct call', () => { + const fixture = new Fixture() + .symbol('submit', 'submitOrder') + .symbol('save', 'saveOrder') + .call('submit', 'save') + .behavior('save') + const query: QueryPlan = { + ...plan('explain', 'submit order'), + terms: ['order', 'save', 'submit'], + } + + const result = selectWorkflow(fixture.index(), query) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual(['save', 'submit']) + expect(result.rootSymbolIds).toEqual(['submit']) + expect(result.links).toEqual([expect.objectContaining({ + kind: 'direct', fromId: 'submit', toId: 'save', + })]) + expect(result.obligations.find(({ kind }) => kind === 'behavior')) + .toEqual(expect.objectContaining({ + proven: true, + operationIds: ['submit-calls-save'], + edgeIds: result.edges.map(({ id }) => id), + })) + expect(result.metrics.causalRelationHops).toBe(1) + }) + + it('cannot prove a requested behavior with an unrelated call', () => { + const fixture = new Fixture() + .symbol('entry', 'submitOrder') + .symbol('telemetry', 'logTelemetry') + .call('entry', 'telemetry') + const query = { + ...plan('explain', 'submit order'), + terms: ['order', 'save', 'submit'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'submit order', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'save', mandatory: true }, + ], + } satisfies QueryPlan + + const result = selectWorkflow(fixture.index(), query) + + expect(result.complete).toBe(false) + expect(result.missing).toContainEqual({ + code: 'behavior_unproven', target: 'save', obligationId: 'o2', + }) + }) + + it('proves a compound responsibility across a bounded direct-call bundle', () => { + const fixture = new Fixture() + .symbol('close', 'runMonthlyCloseJob') + .symbol('collect', 'collectOutstandingInvoices', 'src/billing/invoice-service.ts') + .symbol('report', 'buildMonthlyRevenueReport') + .call('close', 'collect').call('close', 'report') + .behavior('collect').behavior('report') + const query: QueryPlan = { + ...plan('explain', 'monthly billing close'), + terms: ['billing', 'close', 'monthly', 'run'], + } + + const result = selectWorkflow(fixture.index(), query) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual(['close', 'collect', 'report']) + expect(result.links).toEqual(expect.arrayContaining([ + expect.objectContaining({ fromId: 'close', toId: 'collect' }), + expect.objectContaining({ fromId: 'close', toId: 'report' }), + ])) + expect(result.obligations.find(({ kind }) => kind === 'subject')) + .toEqual(expect.objectContaining({ + proven: true, + symbolIds: expect.arrayContaining(['close', 'collect']), + })) + expect(result.obligations.find(({ kind }) => kind === 'behavior')) + .toEqual(expect.objectContaining({ + proven: true, + operationIds: expect.arrayContaining([ + 'close-calls-collect', 'close-calls-report', + ]), + })) + }) + + it('keeps an exact locator on the declaration instead of a called suffix match', () => { + const fixture = new Fixture() + .symbol('handle', 'handleClick') + .symbol('track', 'trackClick') + .symbol('redirect', 'redirectToDestination') + .call('handle', 'track') + .call('handle', 'redirect') + + const result = selectWorkflow(fixture.index(), plan('locate', 'handle click')) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual(['handle']) + }) + + it('uses authenticated read/write intent to disambiguate exact field locators', () => { + const fixture = new Fixture() + .symbol('read', 'findUserByResetToken') + .symbol('write', 'saveResetToken') + .persistence( + 'read', 'read-token', 'read', + { kind: 'literal', value: 'reset token' }, + ) + .persistence( + 'write', 'write-token', 'update', + { kind: 'literal', value: 'reset token' }, + ) + .index() + const base = plan('locate', 'reset token') + const terms = ['email', 'job', 'reset', 'run', 'token'] + + expect(selectWorkflow(fixture, { ...base, terms, access: 'write' }).symbolIds) + .toEqual(['write']) + expect(selectWorkflow(fixture, { ...base, terms, access: 'read' }).symbolIds) + .toEqual(['read']) + }) + + it('requires every significant subject term and meaningful explanation behavior', () => { + const partial = new Fixture().symbol('subject', 'reportTelemetry').index() + const literalOnly = new Fixture().symbol('subject', 'ideaReport') + .literal('subject').index() + expect(selectWorkflow(partial, plan('locate')).complete).toBe(false) + const explanation = selectWorkflow(literalOnly, plan('explain')) + expect(explanation.complete).toBe(false) + expect(explanation.missing.map((entry) => entry.code)).toContain('behavior_unproven') + }) + + it('fails closed when a selected intermediate stage has an incomplete handoff', () => { + const fixture = directFixture() + .channel({ + id: 'orphan-job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'orphan', + parent_channel_id: 'missing-queue', + }) + .edge('stage', 'orphan-job', 'publishes_to') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.symbolIds).toEqual(['entry', 'stage', 'terminal']) + expect(result.missing.map((entry) => entry.code)) + .toContain('adjacent_handoff_unproven') + }) + + it('does not accept read-only persistence as a terminal write', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('reader', 'readIdeaReport').persistence('reader', 'read-only', 'read') + .call('entry', 'reader') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.missing.map((entry) => entry.code)) + .toContain('terminal_persistence_unproven') + }) + + it('preserves repeated authenticated calls between the same symbols', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .call('entry', 'terminal', 'first-call') + .call('entry', 'terminal', 'second-call') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'first-call', 'second-call', + ])) + expect(result.links).toHaveLength(2) + expect(result.links.flatMap((link) => link.operationIds).sort()).toEqual([ + 'first-call', 'second-call', + ]) + }) + + it('selects the same topology after file and symbol identifiers are renamed', () => { + const build = (ids: readonly [string, string, string], prefix: string): Fixture => + new Fixture() + .symbol(ids[0], 'alpha', `${prefix}/one.ts`) + .literal(ids[0], `${ids[0]}-subject`, 'idea report') + .symbol(ids[1], 'beta', `${prefix}/two.ts`) + .symbol(ids[2], 'gamma', `${prefix}/three.ts`) + .persistence(ids[2]) + .call(ids[0], ids[1]) + .call(ids[1], ids[2]) + const first = selectWorkflow( + build(['a', 'b', 'c'], 'src/original').index(), + plan('workflow'), + ) + const renamed = selectWorkflow( + build(['x', 'y', 'z'], 'src/renamed').index(), + plan('workflow'), + ) + const shape = (result: typeof first) => ({ + complete: result.complete, + symbols: result.symbolIds.length, + terminals: result.terminalSymbolIds.length, + relations: result.edges.map((edge) => edge.relation), + handoffs: result.links.map((link) => link.kind), + controls: result.controlGroups.map((group) => group.kind), + }) + + expect(shape(renamed)).toEqual(shape(first)) + expect(first.complete).toBe(true) + }) + + it('rejects an isolated lexical decoy before bounded recovery', () => { + const fixture = new Fixture() + .symbol('entry', 'generateReport') + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport') + .call('entry', 'stage').call('stage', 'terminal') + .persistence('terminal') + .symbol('decoy', 'ideaReport') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.metrics.recoveryPasses).toBe(0) + expect(result.metrics.recoveryFrontierCount).toBe(0) + }) + + it('prefers an executable entry corridor over a one-link report UI decoy', () => { + const fixture = new Fixture() + .symbol( + 'ui-root', + 'ReportMetaPanel', + 'src/features/idea/FullReportView.tsx', + ) + .symbol('format-date', 'formatHeaderDate') + .call('ui-root', 'format-date') + .symbol( + 'entry', + 'generateFromProblem', + 'src/modules/ideas/idea-generation.controller.ts', + ) + .symbol('stage', 'startPipeline') + .symbol('terminal', 'persistIdeaReport') + .call('entry', 'stage') + .call('stage', 'terminal') + .persistence('terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.symbolIds).not.toContain('ui-root') + }) + + it('keeps the generation entry when retry and migration roots share its pipeline', () => { + const fixture = new Fixture() + .symbol('entry', 'generateFromProblem', 'src/ideas/idea-generation.controller.ts') + .symbol('retry', 'retryPipeline', 'src/ideas/idea-pipeline.controller.ts') + .symbol('resume', 'resumePipeline') + .symbol('migration', 'main', 'src/scripts/migrate-old-ideas.ts') + .symbol('migrate', 'migrateIdea') + .symbol('start', 'startPipeline') + .symbol('enqueue', 'enqueueJob') + .symbol('worker', 'processIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .call('entry', 'start') + .call('retry', 'resume') + .call('resume', 'start') + .call('migration', 'migrate') + .call('migrate', 'start') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publish('start', 'enqueue', 'queue', 'publish-report') + .edge('queue', 'worker', 'consumed_by') + .call('worker', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.symbolIds).not.toEqual(expect.arrayContaining(['retry', 'migration'])) + }) + + it('does not replace a stronger partial corridor with a disconnected complete backup', () => { + const fixture = new Fixture() + .symbol('a-primary', 'generateIdeaReport') + .symbol('alpha', 'alpha') + .symbol('omega', 'omega').behavior('omega') + .call('a-primary', 'alpha').call('alpha', 'omega') + .symbol('z-backup', 'generateIdeaReportBackup') + .symbol('backup-terminal', 'persistBackup').persistence('backup-terminal') + .call('z-backup', 'backup-terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.rootSymbolIds).toEqual(['a-primary']) + expect(result.symbolIds).toEqual(['a-primary', 'alpha', 'omega']) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.metrics.recoveryPasses).toBe(1) + expect(result.metrics.recoveryFrontierCount).toBeGreaterThan(0) + expect(result.metrics.recoveryFrontierCount).toBeLessThanOrEqual(64) + expect(result.missing).toEqual([ + { code: 'terminal_persistence_unproven', target: 'idea report', obligationId: 'o7' }, + ]) + }) + + it('ranks a connected production root ahead of a high-degree test hub in recovery', () => { + const fixture = new Fixture() + .symbol('decoy', 'ideaReport', 'tests/decoy.test.ts') + .symbol('semantic-root', 'bootstrap', 'src/semantic.ts') + .symbol('semantic-stage', 'ideaReportStage', 'src/semantic.ts') + .symbol('semantic-terminal', 'omega', 'src/semantic.ts') + .persistence('semantic-terminal') + .call('semantic-root', 'semantic-stage') + .call('semantic-stage', 'semantic-terminal') + .symbol('hub', 'ideaReportHub', 'tests/hub.test.ts') + .symbol('hub-terminal', 'sink', 'tests/hub.test.ts') + .persistence('hub-terminal') + .call('hub', 'hub-terminal') + for (let index = 0; index < 20; index += 1) { + fixture.symbol(`noise-${index}`, `noise${index}`, 'tests/hub.test.ts') + .call('hub', `noise-${index}`) + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['semantic-root']) + expect(result.terminalSymbolIds).toEqual(['semantic-terminal']) + expect(result.symbolIds).not.toContain('hub') + }) + + it('does not let a test caller disqualify a production entrypoint', () => { + const fixture = new Fixture() + .symbol('harness', 'runIdeaReportHarness', 'tests/report.test.ts') + .symbol('entry', 'generateIdeaReport', 'src/report.ts') + .symbol('terminal', 'persistIdeaReport', 'src/report.ts') + .persistence('terminal') + .call('harness', 'entry') + .call('entry', 'terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.symbolIds).not.toContain('harness') + }) + + it('is deterministic and bounded with 10k disconnected distractors', () => { + const fixture = directFixture() + fixture.symbol('hub', 'generateIdeaReport', 'tests/hub.test.ts').dynamicCall('hub') + for (let index = 0; index < 40; index += 1) { + fixture.symbol(`test-decoy-${index}`, 'generateIdeaReport', `tests/decoy-${index}.test.ts`) + } + for (let index = 0; index < 10_000; index += 1) { + fixture.symbol( + `noise-${index.toString().padStart(5, '0')}`, + `generateIdeaReportNoise${index}`, + ) + } + const index = fixture.index(), query = plan('workflow') + const first = selectWorkflow(index, query) + const second = selectWorkflow(index, query) + expect(second).toEqual(first) + expect(first.complete).toBe(true) + expect(first.rootSymbolIds).toEqual(['entry']) + expect(first.metrics.candidateCount).toBeLessThanOrEqual(32) + expect(first.metrics.actualNodeCount).toBeLessThanOrEqual(512) + expect(first.metrics.causalRelationHops).toBeLessThanOrEqual(24) + expect(first.metrics.recoveryFrontierCount).toBeLessThanOrEqual(64) + expect(first.rootSymbolIds).toHaveLength(1) + }) + + it('fails closed when every complete corridor exceeds the 24-relation bound', () => { + const fixture = new Fixture().symbol('n0', 'generateIdeaReport') + for (let index = 1; index <= 25; index += 1) { + fixture.symbol(`n${index}`, `ideaReportStage${index}`) + .call(`n${index - 1}`, `n${index}`) + } + fixture.persistence('n25') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + expect(result.metrics.actualNodeCount).toBeLessThanOrEqual(512) + expect(result.metrics.causalRelationHops).toBeLessThanOrEqual(24) + expect(result.metrics.bounded).toBe(true) + expect(result.metrics.recoveryPasses).toBe(1) + expect(result.metrics.recoveryFrontierCount).toBeLessThanOrEqual(64) + expect(result.missing.map((entry) => entry.code)).toContain('selection_bound_reached') + }) + + it('never substitutes a catch shortcut for an over-limit success corridor', () => { + const fixture = new Fixture().symbol('entry', 'generateIdeaReport') + let previous = 'entry' + for (let index = 1; index <= 24; index += 1) { + const current = `stage-${index}` + fixture.symbol(current, `ideaReportStage${index}`) + .call(previous, current) + previous = current + } + fixture.symbol('terminal', 'persistIdeaReport') + .call(previous, 'terminal') + .call('entry', 'terminal', 'failure-shortcut', [{ + kind: 'exception', arm: 'catch', + }]) + .persistence('terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.operationIds).not.toContain('failure-shortcut') + expect(result.metrics.bounded).toBe(true) + expect(result.missing.map(({ code }) => code)).toContain('selection_bound_reached') + }) + + it('excludes a catch shortcut whose endpoints share the success corridor', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('stage', 'assembleIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .call('entry', 'stage', 'success-entry') + .call('stage', 'terminal', 'success-terminal') + .call('entry', 'terminal', 'failure-shortcut', [{ + kind: 'exception', arm: 'catch', + }]) + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'success-entry', 'success-terminal', + ])) + expect(result.operationIds).not.toContain('failure-shortcut') + expect(result.links).toHaveLength(2) + }) + + it('retains a guaranteed finally handoff in the normal workflow', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .symbol('audit', 'persistIdeaReportAudit').persistence('audit') + .call('entry', 'terminal', 'try-call', [{ + kind: 'exception', arm: 'try', + }]) + .call('entry', 'audit', 'finally-call', [{ + kind: 'exception', arm: 'finally', + }]) + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['audit', 'terminal']) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'try-call', 'finally-call', + ])) + }) + + it('keeps ordinary stages whose alpha-renamed identifiers contain failure words', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('stage', 'recordFailureMetrics') + .symbol('terminal', 'persistIdeaReport') + .call('entry', 'stage') + .call('stage', 'terminal') + .persistence('terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toEqual(['entry', 'stage', 'terminal']) + }) + + it('binds a nested publish statement by exact range and avoids explicit failure data', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('failure', 'alpha') + .symbol('normal', 'beta') + .symbol('enqueue', 'enqueue') + .symbol('nested', 'format') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'terminal-trigger') + .persistenceInCase('terminal', 'terminal-trigger', 'assembly_complete') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .call('entry', 'failure') + .call('entry', 'normal') + .publishWithNestedCall( + 'failure', 'enqueue', 'nested', 'queue', 'failed-publish', + [{ kind: 'object', entries: [{ + key: 'status', value: { kind: 'literal', value: 'FAILED' }, + }] }], + ) + .publish( + 'normal', 'enqueue', 'queue', 'normal-publish', + [triggerPayload('assembly_complete')], 0, + ) + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.symbolIds).toContain('normal') + expect(result.symbolIds).not.toContain('failure') + expect(result.operationIds).toContain('normal-publish') + expect(result.operationIds).not.toContain('failed-publish') + }) + + it('does not authenticate a publisher from statement range alone', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueue') + .symbol('terminal', 'persistIdeaReport').persistence('terminal') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publishWithMismatchedRange('entry', 'enqueue', 'queue', 'publish') + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.operationIds).not.toContain('publish') + expect(result.missing).toContainEqual({ + code: 'obligation_target_unproven', + target: 'idea report', + obligationId: 'o6', + }) + }) + + it('fails closed when distinct channel and direct routes exceed the relation bound', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueJob') + .symbol('a-merge', 'alpha') + .symbol('z-short', 'beta') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble', parent_channel_id: 'queue', + }) + .publish('entry', 'enqueue', 'job', 'slow-publish') + .route('job', 'queue', 'entry', 'slow-publish') + .edge('queue', 'a-merge', 'consumed_by') + .call('entry', 'z-short') + .call('z-short', 'a-merge') + let previous = 'a-merge' + for (let index = 1; index <= 22; index += 1) { + const current = `n${index}` + fixture.symbol(current, index === 22 ? 'persistIdeaReport' : `alpha${index}`) + .call(previous, current) + previous = current + } + fixture.persistence('n22') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.bounded).toBe(true) + expect(result.missing).toContainEqual({ + code: 'selection_bound_reached', target: 'idea report', + }) + }) + + it('fails closed when 25 distinct persisted terminal edges are required', () => { + const fixture = new Fixture().symbol('entry', 'generateIdeaReport') + for (let index = 0; index < 25; index += 1) { + const terminal = `terminal-${index}` + fixture.symbol(terminal, `persistIdeaReport${index}`).persistence(terminal) + .call('entry', terminal) + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.causalRelationHops).toBeLessThanOrEqual(24) + expect(result.metrics.bounded).toBe(true) + expect(result.missing.map(({ code }) => code)).toContain('selection_bound_reached') + }) + + it('fails closed when a multi-terminal selection would drop a distinct long route', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('terminal-a', 'persistIdeaReportA').persistence('terminal-a') + .symbol('terminal-b', 'persistIdeaReportB').persistence('terminal-b') + .call('entry', 'terminal-a', 'direct-a') + .call('entry', 'terminal-b', 'direct-b') + let owner = 'entry' + for (let index = 0; index < 24; index += 1) { + const next = `stage-${index}` + fixture.symbol(next, `ideaReportStage${index}`) + .call(owner, next, `long-${index}`) + owner = next + } + fixture.call(owner, 'terminal-a', 'long-terminal') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.metrics.bounded).toBe(true) + expect(result.missing.map(({ code }) => code)).toContain('selection_bound_reached') + }) + + it('keeps a terminal-free fan-out inside the global relation bound', () => { + const fixture = new Fixture().symbol('entry', 'generateIdeaReport').behavior('entry') + for (let index = 0; index < 25; index += 1) { + fixture.symbol(`stage${index}`, `ideaReportStage${index}`).behavior(`stage${index}`) + .call('entry', `stage${index}`) + } + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.rootSymbolIds).toEqual(['entry']) + expect(result.metrics.causalRelationHops).toBeLessThanOrEqual(24) + expect(result.metrics.bounded).toBe(true) + expect(result.missing.map((entry) => entry.code)).toContain('selection_bound_reached') + }) + + it('reports zero attempted roots when no lexical candidate exists', () => { + const fixture = new Fixture().symbol('unrelated', 'telemetry') + const result = selectWorkflow(fixture.index(), plan('workflow')) + expect(result.complete).toBe(false) + expect(result.metrics.rootCandidateCount).toBe(0) + expect(result.metrics.recoveryPasses).toBe(0) + }) +}) diff --git a/tests/unit/retrieve-context-proof-eviction.test.ts b/tests/unit/retrieve-context-proof-eviction.test.ts new file mode 100644 index 00000000..2e373fea --- /dev/null +++ b/tests/unit/retrieve-context-proof-eviction.test.ts @@ -0,0 +1,200 @@ +import { describe, expect, it, vi } from 'vitest' + +import type { + HydratedEvidenceResult, WorkflowSelection, +} from '../../src/domain/query/types.js' + +const mocks = vi.hoisted(() => ({ + hydration: null as HydratedEvidenceResult | null, + selection: null as WorkflowSelection | null, +})) + +vi.mock('../../src/application/evidence-hydrator.js', () => ({ + hydrateEvidence: () => mocks.hydration, +})) +vi.mock('../../src/domain/query/workflow.js', () => ({ + selectWorkflow: () => mocks.selection, +})) + +import { retrieveContext } from '../../src/application/retrieve-context.js' + +const range = { + start: { line: 1, column: 1 }, end: { line: 1, column: 20 }, +} +const metrics = { + candidateCount: 1, rootCandidateCount: 1, actualNodeCount: 1, + causalRelationHops: 0, recoveryPasses: 0 as const, + recoveryFrontierCount: 0, bounded: false, +} +const obligation = { + id: 'o1' as const, kind: 'subject' as const, target: 'report', mandatory: true, + proven: true, symbolIds: ['symbol:report'], operationIds: [], edgeIds: [], +} + +function selection(): WorkflowSelection { + return { + complete: true, symbolIds: ['symbol:report'], operationIds: [], + rootSymbolIds: ['symbol:report'], terminalSymbolIds: [], edges: [], links: [], + controlGroups: [], obligations: [obligation], missing: [], metrics, + } +} + +function hydratedReport(): HydratedEvidenceResult { + return { + state: 'ready', + files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), + excerpts: new Map(), + proofs: new Map([['symbol:report', ['p0', 'declaration', 'e0', 'x0'] as const]]), + entities: new Map([[ + 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, + ]]), + } +} + +describe('retrieve dossier eviction failures', () => { + it('returns required_proof_missing when a required declaration proof is evicted', () => { + mocks.selection = selection() + mocks.hydration = { + state: 'ready', + files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), + excerpts: new Map(), proofs: new Map(), + entities: new Map([[ + 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, + ]]), + } + + expect(retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'incomplete', + missing: [{ code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }], + metrics: { required_obligations: 1, proven_obligations: 0 }, + }) + }) + + it('returns required_reference_missing when a channel parent is evicted', () => { + mocks.selection = selection() + mocks.hydration = { + state: 'ready', files: new Map(), excerpts: new Map(), proofs: new Map(), + entities: new Map([[ + 'channel:job', + ['e0', 'channel', 'job', 'bullmq', 'assemble', 'channel:missing', undefined] as const, + ]]), + } + + expect(retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'incomplete', + missing: [{ code: 'required_reference_missing', target: 'channel:missing' }], + metrics: { required_obligations: 1, proven_obligations: 0 }, + }) + }) + + it('keeps every bounded missing-obligation identity at the minimum budget', () => { + mocks.selection = { + ...selection(), complete: false, + missing: Array.from({ length: 7 }, (_, index) => ({ + code: 'behavior_unproven' as const, + obligationId: `o${index}`, + target: `symbol:${index}:${'x'.repeat(256)}`, + })), + } + mocks.hydration = { + state: 'ready', files: new Map(), excerpts: new Map(), + entities: new Map(), proofs: new Map(), + } + + const result = retrieveContext({ state: 'ready' } as never, { + question: 'How does report work?', budget: 256, + }) + expect(result.state).toBe('incomplete') + expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) + if (result.state !== 'incomplete') return + expect(result.missing).toEqual(Array.from({ length: 7 }, (_, index) => ({ + code: 'behavior_unproven', obligation_id: `o${index}`, + }))) + }) + + it('returns corrupt when forged terminal diagnostics cannot fit the minimum budget', () => { + mocks.selection = { + ...selection(), complete: false, + missing: Array.from({ length: 500 }, (_, index) => ({ + code: 'behavior_unproven' as const, + obligationId: `o${index}`, + target: `symbol:${index}:${'x'.repeat(256)}`, + })), + } + mocks.hydration = { + state: 'ready', files: new Map(), excerpts: new Map(), + entities: new Map(), proofs: new Map(), + } + + const result = retrieveContext({ state: 'ready' } as never, { + question: 'How does report work?', budget: 256, + }) + + expect(result).toMatchObject({ + state: 'corrupt', + failures: [{ state: 'corrupt', subject: 'terminal result budget' }], + metrics: { serialized_tokens: expect.any(Number) }, + }) + expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) + }) + + it('returns corrupt for a forged control-group controller', () => { + mocks.selection = { + ...selection(), + controlGroups: [{ + kind: 'branch', controllerOperationId: 'operation:forged', + operationIds: [], symbolIds: ['symbol:report'], + }], + } + mocks.hydration = hydratedReport() + + expect(retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'corrupt', + failures: [{ state: 'corrupt', subject: 'operation:forged' }], + }) + }) + + it('returns corrupt for an unselected root or control-group member', () => { + mocks.hydration = hydratedReport() + for (const candidate of [ + { ...selection(), rootSymbolIds: ['symbol:missing'] }, + { + ...selection(), controlGroups: [{ + kind: 'branch' as const, operationIds: [], symbolIds: ['symbol:missing'], + }], + }, + ]) { + mocks.selection = candidate + expect(retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'corrupt', + failures: [{ state: 'corrupt', subject: 'symbol:missing' }], + }) + } + }) + + it.each([ + ['symbolIds', { symbolIds: ['symbol:missing'] }], + ['operationIds', { operationIds: ['operation:missing'] }], + ['edgeIds', { edgeIds: ['edge:missing'] }], + ] as const)('returns corrupt for missing obligation %s', (_field, reference) => { + mocks.selection = { + ...selection(), obligations: [{ ...obligation, ...reference }], + } + mocks.hydration = hydratedReport() + + const target = Object.values(reference)[0]![0]! + expect(retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'corrupt', failures: [{ state: 'corrupt', subject: target }], + }) + }) +}) diff --git a/tests/unit/retrieve-context.test.ts b/tests/unit/retrieve-context.test.ts index 0af2e25f..c9e2b245 100644 --- a/tests/unit/retrieve-context.test.ts +++ b/tests/unit/retrieve-context.test.ts @@ -1,17 +1,15 @@ -import { createHash } from 'node:crypto' import { cpSync, mkdirSync, mkdtempSync, - readFileSync, rmSync, - unlinkSync, writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' -import { dirname, join, relative } from 'node:path' -import { fileURLToPath } from 'node:url' +import { dirname, join, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' +import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' import { afterEach, describe, expect, it } from 'vitest' import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' @@ -20,1853 +18,509 @@ import { retrieveContext, serializeRetrieveContextResult, } from '../../src/application/retrieve-context.js' -import { - inspectQueryIndex, - type QueryIndex, - type ReadyQueryIndex, -} from '../../src/domain/query/index-status.js' -import { traverseEvidencePaths } from '../../src/domain/query/traverse.js' -import { sliceEvidence } from '../../src/domain/query/slice.js' -import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' +import { inspectQueryIndex, type ReadyQueryIndex } from '../../src/domain/query/index-status.js' const roots: string[] = [] -function sandbox(name = 'workspace'): string { - const root = mkdtempSync(join(tmpdir(), `madar-retrieve-${name}-`)) +function workspace(source: string): { + root: string + path: string + source: string + index: ReadyQueryIndex +} { + const root = mkdtempSync(join(tmpdir(), 'madar-retrieve-v2-')) roots.push(root) - return root + const path = join(root, 'src/report.ts') + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, source, 'utf8') + const generated = generateIndex(root) + const index = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) + if (index.state !== 'ready') { + throw new Error(`Expected ready index, received ${index.state}: ${index.subject}`) + } + return { root, path, source, index } } -function write(root: string, path: string, contents: string): string { - const absolute = join(root, path) - mkdirSync(dirname(absolute), { recursive: true }) - writeFileSync(absolute, contents, 'utf8') - return absolute +function multiFileWorkspace(files: Readonly>): ReadyQueryIndex { + const root = mkdtempSync(join(tmpdir(), 'madar-retrieve-v2-')) + roots.push(root) + for (const [path, source] of Object.entries(files)) { + const absolute = join(root, path) + mkdirSync(dirname(absolute), { recursive: true }) + writeFileSync(absolute, source, 'utf8') + } + const generated = generateIndex(root) + const index = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) + if (index.state !== 'ready') { + throw new Error(`Expected ready index, received ${index.state}: ${index.subject}`) + } + return index } -function indexedWorkspace(root: string): { graph: KnowledgeGraph; index: ReadyQueryIndex } { +function reportFlowFixture(): ReadyQueryIndex { + const root = mkdtempSync(join(tmpdir(), 'madar-retrieve-flow-')) + roots.push(root) + cpSync(resolve( + 'tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace', + ), root, { recursive: true }) const generated = generateIndex(root) - const graph = loadGraphArtifact(generated.graphPath) - const index = inspectQueryIndex(graph) + const index = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) if (index.state !== 'ready') { - throw new Error(`Expected a ready query index, received ${index.state}: ${index.subject}`) + throw new Error(`Expected ready index, received ${index.state}: ${index.subject}`) } - return { graph, index } + return index } -function readyIndex(root: string): ReadyQueryIndex { - return indexedWorkspace(root).index -} +function reportSource(persist = true): string { + return `import type { MongoRepository } from 'typeorm' -interface FlowFixture { - root: string - source: Record - graph: KnowledgeGraph - index: ReadyQueryIndex -} +type ReportRow = { id: string; body: string } -function flowFixture(): FlowFixture { - const root = sandbox('flow') - const source = { - 'src/flow-001/entry-local-00.ts': [ - "import { processLocal01 } from './process-local-01.js'", - '', - 'export function entryLocal00(value: string): string {', - ' return processLocal01(value)', - '}', - ].join('\n'), - 'src/flow-001/process-local-01.ts': [ - "import { storageLocal02 } from './storage-local-02.js'", - '', - 'export function processLocal01(value: string): string {', - ' return storageLocal02(value.trim())', - '}', - ].join('\n'), - 'src/flow-001/storage-local-02.js': [ - 'export function storageLocal02(value) {', - " return `${value}:stored`", - '}', - ].join('\n'), - } - for (const [path, contents] of Object.entries(source)) write(root, path, `${contents}\n`) - write(root, 'src/checker/checker.go', 'package checker\n') - write(root, 'src/tinybird/client.go', 'package tinybird\n') - write(root, 'package.json', '{"type":"module"}\n') - write(root, 'tsconfig.json', JSON.stringify({ - compilerOptions: { - allowJs: true, - module: 'NodeNext', - moduleResolution: 'NodeNext', - strict: true, - }, - })) - return { root, source, ...indexedWorkspace(root) } +export async function generateIdeaReport( + repository: MongoRepository, + body: string, +): Promise { + return planIdeaReport(repository, body) } -function structuredQuestion(flow: string, phases: readonly string[]): string { - return `Trace calls in ${flow} from ${phases.join(' through ')}.` +async function planIdeaReport( + repository: MongoRepository, + body: string, +): Promise { + return assembleIdeaReport(repository, body.trim()) } -function authFlowFixture(): FlowFixture { - const root = sandbox('auth-flow') - const source = { - 'src/auth/auth-route.ts': [ - "import { authService } from './auth-service.js'", - '', - 'export function authRoute(value: string): string {', - ' return authService(value)', - '}', - ].join('\n'), - 'src/auth/auth-service.ts': [ - "import { authRepository } from './auth-repository.js'", - '', - 'export function authService(value: string): string {', - ' return authRepository(value)', - '}', - ].join('\n'), - 'src/auth/auth-repository.ts': [ - 'export function authRepository(value: string): string {', - ' return value', - '}', - ].join('\n'), - } - for (const [path, contents] of Object.entries(source)) write(root, path, `${contents}\n`) - write(root, 'tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'NodeNext', - moduleResolution: 'NodeNext', - strict: true, - }, - })) - return { root, source, ...indexedWorkspace(root) } +async function assembleIdeaReport( + repository: MongoRepository, + body: string, +): Promise { + ${persist + ? "await repository.update('report-1', { id: 'report-1', body })" + : 'const unchanged = body'} + return body } - -function reportGenerationFixture(includeDistractor = false): { - fixture: FlowFixture - prompt: string - shortControlPrompt: string - longFlowPrompt: string - masterAgentFollowupPrompt: string - masterAgentFollowupParaphrasePrompts: string[] - paraphrasePrompts: string[] - focusedQueuePrompts: string[] - expectedWorkflowCenters: string[] - expectedRelationships: Array<{ from: string; relation: string; to: string }> -} { - const fixtureDirectory = fileURLToPath(new URL( - '../fixtures/pack-quality/runtime-generation-explain-report-flow/', - import.meta.url, - )) - const root = sandbox('report-generation') - const workspace = join(root, 'workspace') - cpSync(join(fixtureDirectory, 'workspace'), workspace, { recursive: true }) - if (includeDistractor) { - write(workspace, 'platform/src/app/entry.worker.js/route.ts', [ - 'export function GET(): Response {', - " return new Response('unrelated')", - '}', - '', - ].join('\n')) - } - const metadata = JSON.parse( - readFileSync(join(fixtureDirectory, 'fixture.json'), 'utf8'), - ) as { - prompt: string - short_control_prompt: string - long_flow_prompt: string - master_agent_followup_prompt: string - master_agent_followup_paraphrase_prompts: string[] - paraphrase_prompts: string[] - focused_queue_prompts: string[] - expected_workflow_centers: string[] - expected_relationships: Array<{ from: string; relation: string; to: string }> - } - return { - prompt: metadata.prompt, - shortControlPrompt: metadata.short_control_prompt, - longFlowPrompt: metadata.long_flow_prompt, - masterAgentFollowupPrompt: metadata.master_agent_followup_prompt, - masterAgentFollowupParaphrasePrompts: metadata.master_agent_followup_paraphrase_prompts, - paraphrasePrompts: metadata.paraphrase_prompts, - focusedQueuePrompts: metadata.focused_queue_prompts, - expectedWorkflowCenters: metadata.expected_workflow_centers, - expectedRelationships: metadata.expected_relationships, - fixture: { - root: workspace, - source: {}, - ...indexedWorkspace(workspace), - }, - } -} - -function writeDisconnectedFlow( - root: string, - flow: string, - entries: readonly { phase: string; ordinal: string; file: string }[], -): string { - for (const entry of entries) { - write(root, `src/${flow}/${entry.file}`, [ - `export function ${entry.phase}Local${entry.ordinal}(): string {`, - ` return '${entry.phase}:${entry.ordinal}'`, - '}', - '', - ].join('\n')) - } - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - return structuredQuestion( - flow, - entries.map((entry) => `${entry.phase} local ${entry.ordinal}`), - ) +` } afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }) + } }) -describe('retrieve context', () => { - it('returns exact authenticated excerpts and directed typed paths deterministically', () => { - const fixture = flowFixture() - const question = structuredQuestion('flow-001', [ - 'entry local 00', - 'process local 01', - 'storage local 02', - ]) - - const first = retrieveContext(fixture.index, { question }) - const second = retrieveContext(fixture.index, { question }) - - expect(first.outcome).toBe('evidence') - expect(first.boundaries).toEqual([]) - expect(first.metrics).toMatchObject({ - selected_files: 3, - snippets: 3, - closure_passes: 1, - truncated: false, - }) - expect(first.metrics.serialized_tokens).toBeLessThanOrEqual(4000) - expect(Object.fromEntries(first.matched_nodes.map((node) => [ - node.source_file, - node.snippet, - ]))).toEqual({ - 'src/flow-001/entry-local-00.ts': - 'export function entryLocal00(value: string): string ', - 'src/flow-001/process-local-01.ts': - 'export function processLocal01(value: string): string ', - 'src/flow-001/storage-local-02.js': - 'export function storageLocal02(value) ', - }) - - const nodesById = new Map(first.matched_nodes.map((node) => [node.node_id, node])) - const directedRelationships = first.relationships.map((relationship) => ({ - from: nodesById.get(relationship.from_id)?.source_file, - relation: relationship.relation, - to: nodesById.get(relationship.to_id)?.source_file, - })) - expect(directedRelationships).toHaveLength(2) - expect(directedRelationships).toEqual(expect.arrayContaining([ - { - from: 'src/flow-001/entry-local-00.ts', - relation: 'calls', - to: 'src/flow-001/process-local-01.ts', - }, - { - from: 'src/flow-001/process-local-01.ts', - relation: 'calls', - to: 'src/flow-001/storage-local-02.js', - }, - ])) - expect(new Set(first.matched_nodes.map((node) => node.node_id)).size) - .toBe(first.matched_nodes.length) - expect(new Set(first.relationships.map((relationship) => relationship.id)).size) - .toBe(first.relationships.length) - expect(serializeRetrieveContextResult(second)).toBe(serializeRetrieveContextResult(first)) - }) - - it('keeps ordered evidence in the first comma-delimited obligation', () => { - const fixture = flowFixture() - const result = retrieveContext(fixture.index, { - question: - 'Trace flow-001 from entry local 00 through calls to process local 01, then storage local 02.', - }) - - expect(result.matched_nodes.map((node) => node.source_file).sort()).toEqual([ - 'src/flow-001/entry-local-00.ts', - 'src/flow-001/process-local-01.ts', - 'src/flow-001/storage-local-02.js', - ]) - expect(result.relationships.map((relationship) => relationship.relation)) - .toEqual(['calls', 'calls']) - expect(result.boundaries).toEqual([]) - }) - - it('omits an alternate non-adjacent path when adjacent anchors form a complete chain', () => { - const root = sandbox('adjacent-chain') - write(root, 'src/start.ts', [ - "import { alternateAnchor } from './alternate.js'", - "import { middleAnchor } from './middle.js'", - '', - 'export function startAnchor(value: string): string {', - ' alternateAnchor(value)', - ' return middleAnchor(value)', - '}', - '', - ].join('\n')) - write(root, 'src/alternate.ts', [ - "import { finishAnchor } from './finish.js'", - '', - 'export function alternateAnchor(value: string): string {', - ' return finishAnchor(value)', - '}', - '', - ].join('\n')) - write(root, 'src/middle.ts', [ - "import { finishAnchor } from './finish.js'", - '', - 'export function middleAnchor(value: string): string {', - ' return finishAnchor(value)', - '}', - '', - ].join('\n')) - write(root, 'src/finish.ts', [ - 'export function finishAnchor(value: string): string {', - ' return value', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace `startAnchor` through `middleAnchor` to `finishAnchor`.', - }) - - expect(result.matched_nodes.map((node) => node.label).sort()).toEqual([ - 'finishAnchor()', - 'middleAnchor()', - 'startAnchor()', - ]) - const nodesById = new Map(result.matched_nodes.map((node) => [node.node_id, node])) - expect(result.relationships.map((relationship) => ({ - from: nodesById.get(relationship.from_id)?.label, - relation: relationship.relation, - to: nodesById.get(relationship.to_id)?.label, - }))).toEqual(expect.arrayContaining([ - { from: 'startAnchor()', relation: 'calls', to: 'middleAnchor()' }, - { from: 'middleAnchor()', relation: 'calls', to: 'finishAnchor()' }, - ])) - expect(result.relationships).toHaveLength(2) - expect(result.boundaries).toEqual([]) - }) - - it('keeps a non-adjacent path when an adjacent anchor handoff is disconnected', () => { - const root = sandbox('non-adjacent-handoff') - write(root, 'src/start.ts', [ - "import { finishAnchor } from './finish.js'", - '', - 'export function startAnchor(value: string): string {', - ' return finishAnchor(value)', - '}', - '', - ].join('\n')) - write(root, 'src/middle.ts', [ - "import { finishAnchor } from './finish.js'", - '', - 'export function middleAnchor(value: string): string {', - ' return finishAnchor(value)', - '}', - '', - ].join('\n')) - write(root, 'src/finish.ts', [ - 'export function finishAnchor(value: string): string {', - ' return value', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace `startAnchor` through `middleAnchor` to `finishAnchor`.', - }) - - expect(result.matched_nodes.map((node) => node.label).sort()).toEqual([ - 'finishAnchor()', - 'middleAnchor()', - 'startAnchor()', - ]) - const nodesById = new Map(result.matched_nodes.map((node) => [node.node_id, node])) - expect(result.relationships.map((relationship) => ({ - from: nodesById.get(relationship.from_id)?.label, - relation: relationship.relation, - to: nodesById.get(relationship.to_id)?.label, - }))).toEqual(expect.arrayContaining([ - { from: 'startAnchor()', relation: 'calls', to: 'finishAnchor()' }, - { from: 'middleAnchor()', relation: 'calls', to: 'finishAnchor()' }, - ])) - expect(result.relationships).toHaveLength(2) - expect(result.boundaries.filter((boundary) => boundary.kind === 'disconnected')) - .toHaveLength(1) - }) - - it('returns a directed evidence path for a broad natural flow question', () => { - const fixture = authFlowFixture() - - const result = retrieveContext(fixture.index, { - question: 'Trace the auth flow.', - }) +describe('retrieveContext v2', () => { + it('returns one deterministic, authenticated workflow dossier', () => { + const fixture = workspace(reportSource()) + const input = { + question: 'How is an idea report generated end to end?', + budget: 4_000, + } + const first = retrieveContext(fixture.index, input) + const second = retrieveContext(fixture.index, input) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label).sort()).toEqual([ - 'authRepository()', - 'authRoute()', - 'authService()', + expect(second).toEqual(first) + expect(first.schema).toBe('madar.retrieve') + expect(first.version).toBe(2) + expect(first.state).toBe('ready') + expect(countTokens(serializeRetrieveContextResult(first))) + .toBe(first.metrics.serialized_tokens) + expect(first.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) + expect(first.metrics.selected_files).toBeLessThanOrEqual(12) + expect(first.metrics.authenticated_excerpts).toBeLessThanOrEqual(25) + expect(first.metrics.causal_hops).toBeLessThanOrEqual(24) + if (first.state !== 'ready') return + + expect(first.dossier.obligations.map(({ kind }) => kind)).toEqual([ + 'subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal', ]) - expect(result.relationships.map((relationship) => relationship.relation)).toEqual([ - 'calls', - 'calls', + expect(first.dossier.obligations.every((claim) => claim.proofs.length > 0)).toBe(true) + expect(first.dossier.flow.links.map(({ kind }) => kind)).toEqual([ + 'direct', 'direct', ]) - expect(result.boundaries).toEqual([]) - expect(result.metrics.closure_passes).toBe(1) - }) - - it('recovers the known report-generation path instead of response-format helpers', () => { - for (const includeDistractor of [false, true]) { - const { - fixture, - prompt, - expectedWorkflowCenters, - expectedRelationships, - } = reportGenerationFixture(includeDistractor) - - for (const question of [ - prompt, - 'can you tell me how is the flow generating report for idea?', - ]) { - const result = retrieveContext(fixture.index, { question }) - const selectedFiles = result.matched_nodes.map((node) => node.source_file) - const labels = new Map(result.matched_nodes.map((node) => [node.node_id, node.label])) - const relationships = result.relationships.map((relationship) => ({ - from: labels.get(relationship.from_id), - relation: relationship.relation, - to: labels.get(relationship.to_id), - })) - - expect(result.outcome).toBe('evidence') - expect(selectedFiles).toEqual(expect.arrayContaining(expectedWorkflowCenters)) - expect(selectedFiles).not.toContain( - 'src/modules/ideas/application/helpers/idea-report-status-message.helper.ts', - ) - expect(selectedFiles).not.toContain( - 'src/modules/ideas/application/helpers/idea-report-suggested-next-steps.helper.ts', - ) - expect(selectedFiles).not.toContain('platform/src/app/entry.worker.js/route.ts') - expect(relationships).toEqual(expect.arrayContaining(expectedRelationships)) - expect(result.boundaries.every(({ kind }) => - kind === 'disconnected')).toBe(true) - } + expect(first.dossier.flow.terminals).toHaveLength(1) + expect(first.dossier.evidence.entities.some((entity) => + entity.kind === 'operation' && 'operation_kind' in entity + && entity.operation_kind === 'persistence')).toBe(true) + const declarations = first.dossier.evidence.entities.filter((entity) => + entity.kind === 'symbol' && entity.excerpt !== undefined) + expect(declarations).toEqual([]) + const links = new Map(first.dossier.flow.links.map((link) => [link.id, link])) + const incident = new Set(first.dossier.evidence.entities.flatMap((entity) => + entity.kind !== 'operation' ? [] : 'owner' in entity ? [entity.owner] + : entity.links.map((id) => links.get(id)!.from)).concat( + first.dossier.evidence.proofs.flatMap((proof) => [proof.from, proof.to]), + )) + expect(first.dossier.evidence.entities.filter((entity) => + entity.kind === 'symbol' && entity.excerpt === undefined) + .every((entity) => incident.has(entity.id))).toBe(true) + expect(serializeRetrieveContextResult(first)) + .toContain('"state":"ready"') + }) + + it('converges public report-flow paraphrases within the warm p95 gate', () => { + const index = reportFlowFixture() + const active = { + question: 'How is an idea report generated? Explain the pipeline flow from request to final report.', + budget: 4_000, } - }) - - it('keeps the full report pipeline and MasterAgent follow-up useful at the protocol budget', () => { - const { - fixture, - shortControlPrompt, - longFlowPrompt, - masterAgentFollowupPrompt, - masterAgentFollowupParaphrasePrompts, - paraphrasePrompts, - focusedQueuePrompts, - } = reportGenerationFixture() - const corePipeline = [ - ['src/modules/ideas/interface/http/idea-generation.controller.ts', '.generateFromProblem()'], - ['src/modules/pipeline/api/pipeline-trigger.service.ts', 'startPipeline()'], - ['src/modules/pipeline/api/queue-registry.service.ts', 'enqueueJob()'], - ['src/modules/pipeline/workers/orchestrator.worker.ts', '.process()'], - ['src/modules/planning/planner.service.ts', '.plan()'], - ['src/modules/research/workers/section-research.worker.ts', '.process()'], - ['src/modules/research/research-agent.service.ts', '.researchSection()'], - ['src/modules/pipeline/assembly/assembly.worker.ts', '.process()'], - ['src/modules/reports/assembly.service.ts', '.assembleReport()'], - ['src/modules/pipeline/workers/db-sync.worker.ts', '.process()'], - ] - const exactBoundaryDetails = [ - 'src/modules/planning/planner.service.ts:L13-L16 -> ' - + 'src/modules/research/workers/section-research.worker.ts:L17-L19', - 'src/modules/research/research-agent.service.ts:L10-L14 -> ' - + 'src/modules/pipeline/assembly/assembly.worker.ts:L17-L19', - 'src/modules/reports/assembly.service.ts:L20-L31 -> ' - + 'src/modules/pipeline/workers/db-sync.worker.ts:L26-L35', - ] - const assertLimitsAndSignal = ( - result: ReturnType, - ): void => { - expect(result.outcome).toBe('evidence') - expect(result.relationships.length).toBeGreaterThan(0) - expect(result.matched_nodes.map((node) => node.source_file)).not.toContain( - 'platform/src/components/InProgressIdeasDropdown.tsx', - ) - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.snippets).toBeLessThanOrEqual(25) - expect(result.metrics.closure_passes).toBeLessThanOrEqual(1) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) - expect(result.metrics.truncated).toBe(false) + const passive = { + question: 'How does the idea report get generated from the initial request through to the completed report?', + budget: 4_000, } - const selected = (result: ReturnType): string[][] => - result.matched_nodes.map((node) => [node.source_file, node.label]) - const disconnected = (result: ReturnType): string[] => - result.boundaries.filter(({ kind }) => kind === 'disconnected') - .map(({ detail }) => detail ?? '').sort() - const relationships = (result: ReturnType): string[] => { - const nodes = new Map(result.matched_nodes.map((node) => [ - node.node_id, - `${node.source_file}#${node.label}`, - ])) - return result.relationships.map((edge) => - `${nodes.get(edge.from_id)} --${edge.relation}--> ${nodes.get(edge.to_id)}`).sort() + const named = { + question: 'How does GoValidate generate an idea report end to end?', + budget: 4_000, } - const coreRelationships = [ - 'src/modules/ideas/interface/http/idea-generation.controller.ts#.generateFromProblem() ' - + '--calls--> src/modules/pipeline/api/pipeline-trigger.service.ts#startPipeline()', - 'src/modules/pipeline/api/pipeline-trigger.service.ts#startPipeline() ' - + '--calls--> src/modules/pipeline/api/queue-registry.service.ts#enqueueJob()', - 'src/modules/pipeline/api/queue-registry.service.ts#enqueueJob() ' - + '--enqueues_job--> src/modules/pipeline/workers/orchestrator.worker.ts#.process()', - 'src/modules/pipeline/workers/orchestrator.worker.ts#.process() ' - + '--calls--> src/modules/planning/planner.service.ts#.plan()', - 'src/modules/research/workers/section-research.worker.ts#.process() ' - + '--calls--> src/modules/research/research-agent.service.ts#.researchSection()', - 'src/modules/pipeline/assembly/assembly.worker.ts#.process() ' - + '--calls--> src/modules/reports/assembly.service.ts#.assembleReport()', - ].sort() - const assertCorePipeline = (result: ReturnType): void => { - assertLimitsAndSignal(result) - expect(selected(result)).toEqual(corePipeline) - expect(relationships(result)).toEqual(coreRelationships) - expect(disconnected(result)).toEqual([...exactBoundaryDetails].sort()) + const first = retrieveContext(index, active) + const second = retrieveContext(index, passive) + const third = retrieveContext(index, named) + + expect(first.state).toBe('ready') + expect(second.state).toBe('ready') + expect(third.state).toBe('ready') + if (first.state !== 'ready' || second.state !== 'ready' + || third.state !== 'ready') return + expect(second.dossier.query.subject).toBe(first.dossier.query.subject) + expect(second.dossier.flow).toEqual(first.dossier.flow) + expect(second.dossier.evidence).toEqual(first.dossier.evidence) + expect(third.dossier.query.subject).toBe(first.dossier.query.subject) + expect(third.dossier.flow).toEqual(first.dossier.flow) + expect(third.dossier.evidence).toEqual(first.dossier.evidence) + + const links = new Map(first.dossier.flow.links.map((link) => [link.id, link])) + const proofRows = new Map(first.dossier.evidence.proofs.map((proof) => [proof.id, proof])) + for (const link of first.dossier.flow.links) { + const path = link.proofs.map((id) => proofRows.get(id)!) + expect(path[0]?.from).toBe(link.from) + expect(path.at(-1)?.to).toBe(link.to) + path.slice(1).forEach((proof, index) => expect(path[index]!.to).toBe(proof.from)) + expect(path.map(({ relation }) => relation)).toEqual(link.kind === 'direct' + ? ['calls'] : path.length === 2 + ? ['publishes_to', 'consumed_by'] + : ['publishes_to', 'routes_through', 'consumed_by']) } - - const shortControl = retrieveContext(fixture.index, { - question: shortControlPrompt, - budget: 8_000, - }) - assertCorePipeline(shortControl) - - const fullFlow = retrieveContext(fixture.index, { - question: longFlowPrompt, - budget: 8_000, - }) - assertCorePipeline(fullFlow) - for (const question of paraphrasePrompts) { - assertCorePipeline(retrieveContext(fixture.index, { question, budget: 8_000 })) - } - - for (const question of [ - masterAgentFollowupPrompt, - ...masterAgentFollowupParaphrasePrompts, - ]) { - const masterAgentFollowup = retrieveContext(fixture.index, { - question, - budget: 8_000, - }) - assertLimitsAndSignal(masterAgentFollowup) - expect(selected(masterAgentFollowup)).toEqual([ - ...corePipeline.slice(0, 7), - ['src/modules/pipeline/agent/master-agent.service.ts', '.call()'], - ...corePipeline.slice(7, 9), - ]) - expect(relationships(masterAgentFollowup)).toEqual([ - ...coreRelationships, - 'src/modules/research/research-agent.service.ts#.researchSection() ' - + '--calls--> src/modules/pipeline/agent/master-agent.service.ts#.call()', - ].sort()) - expect(disconnected(masterAgentFollowup)).toEqual( - exactBoundaryDetails.slice(0, 2).sort(), - ) - } - - const focusedQueueEvidence = [ - [ - ['src/modules/pipeline/assembly/assembly.worker.ts', '.onModuleInit()'], - ['src/modules/pipeline/api/queue-registry.service.ts', 'registerWorker()'], - ], - [ - ['src/modules/pipeline/api/queue-registry.service.ts', 'PipelineQueue'], - ], - ] - for (const [index, question] of focusedQueuePrompts.entries()) { - const focused = retrieveContext(fixture.index, { question }) - expect(selected(focused)).toEqual(focusedQueueEvidence[index]) - expect(focused.metrics.selected_files).toBeLessThanOrEqual(2) - expect(focused.metrics.snippets).toBeLessThanOrEqual(2) - expect(focused.metrics.truncated).toBe(false) - expect(focused.boundaries).toEqual([]) - expect(focused.matched_nodes.every((node) => - node.source_file.includes('/pipeline/'))).toBe(true) - if (focused.matched_nodes.length === 2) { - expect(focused.relationships.length).toBeGreaterThan(0) - } - expect(selected(focused)).not.toEqual(corePipeline) - expect(selected(focused)).not.toContainEqual(corePipeline[0]) - expect(selected(focused)).not.toContainEqual(corePipeline[4]) - expect(selected(focused)).not.toContainEqual(corePipeline[9]) - } - }) - - it('reports the exact first omitted authenticated target under a tiny budget', () => { - const { fixture, prompt } = reportGenerationFixture() - const constrained = retrieveContext(fixture.index, { - question: prompt, - budget: 40, - }) - expect(constrained.boundaries).toContainEqual({ - kind: 'truncated', - subject: - 'src/modules/ideas/interface/http/idea-generation.controller.ts:L16-L28', - }) - - const partial = retrieveContext(fixture.index, { - question: prompt, - budget: 400, - }) - const retainedLocations = partial.matched_nodes.map((node) => - node.evidence_kind === 'symbol_declaration' - ? `${node.source_file}:${node.source_location}` - : node.source_file) - const exactOmissions = partial.boundaries - .filter((boundary) => boundary.kind === 'truncated' && boundary.subject !== 'retrieve') - .map((boundary) => boundary.subject) - expect(exactOmissions.length).toBeGreaterThan(0) - expect(exactOmissions.every((target) => !retainedLocations.includes(target))).toBe(true) - }) - - it('keeps nearby report subflows focused while improving the broad flow', () => { - const { fixture } = reportGenerationFixture() - const cases = [ - ['Where is idea title generated?', 'generateIdeaTitle()'], - ['How is idea title generated?', 'generateIdeaTitle()'], - ['How does title generation work?', 'generateIdeaTitle()'], - ['Where is idea report status message built?', 'getIdeaReportStatusMessage()'], - ['How is idea report quality validated?', 'validateIdeaReportQuality()'], - ['How are quality gate failures handled?', 'handleQualityGateFailure()'], - ['How does failure report storage work?', 'writeRawFailureReport()'], - ['How is database sync performed for reports?', 'saveStructuredReport()'], - ] as const - for (const [question, expectedLabel] of cases) { - const result = retrieveContext(fixture.index, { question }) - expect(result.matched_nodes[0]?.label).toBe(expectedLabel) - } - }) - - it('uses bounded successor context to recover a zero-overlap route branch', () => { - const root = sandbox('route-fanout') - write(root, 'src/entry.ts', [ - "import { distractAlpha } from './noise/alpha.js'", - "import { distractBeta } from './noise/beta.js'", - "import { forward } from './worker/forward.js'", - '', - 'export function handleSignal(): string {', - ' distractAlpha()', - ' distractBeta()', - ' return forward()', - '}', - '', - ].join('\n')) - for (const name of ['alpha', 'beta']) { - write(root, `src/noise/${name}.ts`, [ - `import { ${name}One, ${name}Two, ${name}Three } from './${name}-helpers.js'`, - `export function distract${name[0]!.toUpperCase()}${name.slice(1)}(): string {`, - ` return ${name}One() + ${name}Two() + ${name}Three()`, - '}', - '', - ].join('\n')) - write(root, `src/noise/${name}-helpers.ts`, [ - `export const ${name}One = () => '1'`, - `export const ${name}Two = () => '2'`, - `export const ${name}Three = () => '3'`, - '', - ].join('\n')) + const behavior = first.dossier.obligations.find(({ kind }) => kind === 'behavior')! + const behaviorProofs = new Set(behavior.proofs) + const stages = new Set(first.dossier.flow.links.flatMap(({ from, to }) => [from, to])) + for (const stage of stages) { + const outgoing = first.dossier.evidence.proofs.some((proof) => + proof.from === stage && behaviorProofs.has(proof.id)) + const operation = first.dossier.evidence.entities.some((entity) => + entity.kind === 'operation' && behaviorProofs.has(entity.id) + && ('owner' in entity ? entity.owner === stage + : entity.links.some((id) => links.get(id)?.from === stage))) + expect(outgoing || operation).toBe(true) } - write(root, 'src/worker/forward.ts', [ - "import { getBackgroundProducer } from './producer.js'", - 'export function forward(): string {', - ' return getBackgroundProducer()', - '}', - '', - ].join('\n')) - write(root, 'src/worker/producer.ts', - 'export declare function getBackgroundProducer(): string\n') - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace signal handling toward the background producer.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toContain('forward()') - }) - - it('ranks interfaces only for explicit kind or definition intent', () => { - const root = sandbox('interface-ranking') - write(root, 'src/auth.ts', [ - 'export interface AuthFlow {', - ' ready: boolean', - '}', - 'export function authFlow(): boolean {', - ' return true', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const index = readyIndex(root) - - const ordinary = retrieveContext(index, { question: 'Trace the auth flow.' }) - expect(ordinary.matched_nodes.map((node) => node.node_kind)).not.toContain('interface') - const requested = retrieveContext(index, { question: 'Which interface defines AuthFlow?' }) - expect(requested.matched_nodes.map((node) => node.node_kind)).toContain('interface') - const definition = retrieveContext(index, { question: 'Where is AuthFlow defined?' }) - expect(definition.matched_nodes.map((node) => node.label)).toEqual(['AuthFlow']) - }) - - it('keeps downstream branches but excludes low-signal callers after query terms are covered', () => { - const root = sandbox('directed-query-frontier') - write(root, 'src/app.ts', [ - "import { sendInvoiceReceipt } from './billing.js'", - 'export function runDemoScenario(): void {', - ' sendInvoiceReceipt()', - '}', - '', - ].join('\n')) - write(root, 'src/billing.ts', [ - "import { sendReceiptEmail } from './notifications.js'", - 'export function sendInvoiceReceipt(): void {', - ' sendReceiptEmail()', - '}', - 'export function collectInvoiceBatch(): number {', - ' return 4', - '}', - '', - ].join('\n')) - write(root, 'src/monthly.ts', [ - "import { collectInvoiceBatch } from './billing.js'", - "import { buildMonthlyRevenueReport } from './reports.js'", - 'export function runMonthlyCloseJob(): number {', - ' return collectInvoiceBatch() + buildMonthlyRevenueReport()', - '}', - '', - ].join('\n')) - write(root, 'src/notifications.ts', [ - 'export function sendReceiptEmail(): void {}', - '', - ].join('\n')) - write(root, 'src/reports.ts', [ - 'export function buildMonthlyRevenueReport(): number {', - ' return 1200', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Which module sends invoice receipt emails?', - }) - - expect(result.matched_nodes.map((node) => node.label)).toEqual([ - 'sendInvoiceReceipt()', - 'sendReceiptEmail()', - ]) - expect(result.relationships).toHaveLength(1) - expect(result.boundaries).toEqual([]) - - const fanout = retrieveContext(readyIndex(root), { - question: 'What runs the monthly billing close?', - }) - expect(fanout.matched_nodes.map((node) => node.label)).toEqual([ - 'runMonthlyCloseJob()', - 'buildMonthlyRevenueReport()', - 'collectInvoiceBatch()', - ]) - expect(fanout.relationships).toHaveLength(2) - expect(fanout.boundaries).toEqual([]) - }) - - it('normalizes derivational suffixes without a domain vocabulary', () => { - const root = sandbox('phase-morphology') - write(root, 'src/lifecycle/migrate-record.ts', [ - "import { assignRecord } from './assign-record.js'", - '', - 'export function migrateRecord(): string {', - ' return assignRecord()', - '}', - '', - ].join('\n')) - write(root, 'src/lifecycle/assign-record.ts', [ - "import { recoverRecord } from './recover-record.js'", - '', - 'export function assignRecord(): string {', - ' return recoverRecord()', - '}', - '', - ].join('\n')) - write(root, 'src/lifecycle/recover-record.ts', [ - 'export function recoverRecord(): string {', - " return 'done'", - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace record migration through assignment and recovery.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file).sort()).toEqual([ - 'src/lifecycle/assign-record.ts', - 'src/lifecycle/migrate-record.ts', - 'src/lifecycle/recover-record.ts', - ]) - expect(result.relationships.map((relationship) => relationship.relation)).toEqual([ - 'calls', - 'calls', - ]) - expect(result.boundaries).toEqual([]) - }) - - it('keeps repository nouns that can also appear in answer instructions', () => { - const root = sandbox('repository-noun') - write(root, 'src/order.ts', 'export function Order(): string { return "ready" }\n') - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { question: 'What is order?' }) + expect(first.dossier.evidence.entities).toContainEqual(expect.objectContaining({ + kind: 'operation', links: expect.any(Array), callee: 'enqueueJob', + scheduling: 'awaited', excerpt: expect.any(String), + })) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toContain('Order()') + for (let pass = 0; pass < 3; pass += 1) retrieveContext(index, active) + const samples = Array.from({ length: 20 }, () => { + const start = performance.now() + const result = retrieveContext(index, active) + expect(result.state).toBe('ready') + return performance.now() - start + }).sort((left, right) => left - right) + expect(samples[Math.ceil(samples.length * 0.95) - 1]).toBeLessThan(500) }) - it('does not treat ordinary hyphenated prose as an exact identifier', () => { - const root = sandbox('hyphenated-prose') - write(root, 'src/delivery.ts', [ - 'export function deliverNotification(): string {', - " return 'sent'", - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'How is at-least-once delivery implemented?', + it('keeps focused locators declaration-only', () => { + const result = retrieveContext(workspace(reportSource()).index, { + question: 'Where is generateIdeaReport defined?', + budget: 2_000, }) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toContain('deliverNotification()') - expect(result.boundaries).not.toContainEqual({ kind: 'missing', subject: 'at-least-once' }) - }) - - it('ranks production concepts ahead of requested-output and test-file noise', () => { - const root = sandbox('instruction-noise') - write(root, 'src/workflow/hydrate-session.ts', [ - "import { validateSession } from './validate-session.js'", - '', - 'export function hydrateSession(): string {', - ' return validateSession()', - '}', - '', - ].join('\n')) - write(root, 'src/workflow/validate-session.ts', [ - 'export function validateSession(): string {', - " return 'valid'", - '}', - '', - ].join('\n')) - write(root, 'tests/session-output-format.test.ts', [ - 'export function sessionExactFileSymbols(): string {', - " return 'test-only'", - '}', - '', - ].join('\n')) - write(root, 'assets/exact-files-symbols-evidence.png', 'not an image decoder fixture\n') - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: [ - 'Trace session hydration through validation.', - 'Cite exact files and symbols for every phase, preserve causal order,', - 'and identify any missing evidence.', - ].join(' '), + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.flow.links).toEqual([]) + expect(result.dossier.evidence.entities.map(({ kind }) => kind)) + .toEqual(['symbol']) + expect(result.dossier.evidence.entities[0]).toMatchObject({ + kind: 'symbol', + label: 'generateIdeaReport()', + excerpt: expect.any(String), }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file).sort()).toEqual([ - 'src/workflow/hydrate-session.ts', - 'src/workflow/validate-session.ts', - ]) - expect(result.relationships.map((relationship) => relationship.relation)).toEqual(['calls']) - expect(result.boundaries).toEqual([]) }) - it('retains test-domain evidence when the question explicitly asks for tests', () => { - const root = sandbox('requested-tests') - write(root, 'src/auth-flow.ts', [ - 'export function authFlow(): string {', - " return 'production'", + it('proves writes from body evidence and does not invent unsupported reads', () => { + const index = workspace([ + 'export const retryCount = 0', + 'export function updateMetrics(state: { retryCount: number }) {', + ' state.retryCount = state.retryCount + 1', '}', - '', - ].join('\n')) - write(root, 'tests/auth-flow.test.ts', [ - 'export function testAuthFlow(): string {', - " return 'verified'", + 'export function readMetrics(state: { retryCount: number }) {', + ' return state.retryCount', '}', '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Which test verifies the auth flow?', + ].join('\n')).index + const write = retrieveContext(index, { + question: 'What updates retryCount?', budget: 2_000, }) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file)) - .toEqual(['tests/auth-flow.test.ts']) - expect(result.boundaries).toEqual([]) - }) - - it('does not report media files as unsupported code evidence', () => { - const root = sandbox('unsupported-media') - write(root, 'src/public/status-page.ts', [ - 'export function publicStatusPage(): string {', - " return 'ready'", - '}', - '', - ].join('\n')) - write(root, 'assets/public-status-page.png', 'binary placeholder\n') - write(root, 'docs/public-status-page.md', '# Public status page\n') - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') + expect(write.state).toBe('ready') + if (write.state !== 'ready') return + expect(write.dossier.evidence.entities).toContainEqual(expect.objectContaining({ + kind: 'symbol', label: 'updateMetrics()', + })) + const mutation = write.dossier.evidence.entities.find((entity) => + entity.kind === 'operation' && 'operation_kind' in entity + && entity.operation_kind === 'mutation') + expect(mutation).toEqual(expect.objectContaining({ + kind: 'operation', owner: expect.any(String), excerpt: expect.any(String), + detail: expect.objectContaining({ target: 'state . retryCount' }), + })) + expect(write.dossier.obligations[0]?.proofs).toContain(mutation?.id) - const result = retrieveContext(readyIndex(root), { - question: 'Explain the public status-page implementation.', + const read = retrieveContext(index, { + question: 'What reads retryCount?', budget: 2_000, }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file)) - .toEqual(['src/public/status-page.ts']) - expect(result.boundaries).toEqual([]) - }) - - it('reports a connected lexical frontier omitted by the anchor cap', () => { - const root = sandbox('broad-anchor-cap') - for (let index = 0; index < 15; index += 1) { - const ordinal = String(index).padStart(2, '0') - const next = String(index + 1).padStart(2, '0') - write(root, `src/auth/auth-node-${ordinal}.ts`, [ - ...(index < 14 ? [`import { authNode${next} } from './auth-node-${next}.js'`, ''] : []), - `export function authNode${ordinal}(): string {`, - index < 14 ? ` return authNode${next}()` : " return 'done'", - '}', - '', - ].join('\n')) + expect(read.state).toBe('incomplete') + if (read.state === 'incomplete') { + expect(read.missing).toContainEqual(expect.objectContaining({ + code: 'subject_unproven', + })) } - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace the auth flow.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.metrics.selected_files).toBe(12) - expect(result.boundaries).toContainEqual({ - kind: 'truncated', - subject: 'query anchors', - }) - }) - - it('uses an exact symbol as a seed without excluding downstream query phases', () => { - const fixture = authFlowFixture() - - const result = retrieveContext(fixture.index, { - question: 'Trace `authRoute` through the auth service and repository.', - }) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label).sort()).toEqual([ - 'authRepository()', - 'authRoute()', - 'authService()', - ]) - expect(result.relationships).toHaveLength(2) - expect(result.boundaries).toEqual([]) - }) - - it('preserves disconnected anchors and reports the missing directed handoff', () => { - const root = sandbox('disconnected') - const question = writeDisconnectedFlow(root, 'flow-002', [ - { phase: 'alpha', ordinal: '00', file: 'alpha-local-00.ts' }, - { phase: 'beta', ordinal: '01', file: 'beta-local-01.ts' }, - ]) - - const result = retrieveContext(readyIndex(root), { question }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes).toHaveLength(2) - expect(result.relationships).toEqual([]) - expect(result.boundaries).toEqual([ - expect.objectContaining({ - kind: 'disconnected', - detail: - 'src/flow-002/alpha-local-00.ts:L1-L3 -> src/flow-002/beta-local-01.ts:L1-L3', - }), - ]) - expect(result.metrics.closure_passes).toBe(1) - }) - - it('binds each structured locator to its nearest explicit scope', () => { - const root = sandbox('multiple-scopes') - write(root, 'src/flow-021/route-local-00.ts', [ - 'export function routeLocal00(): string {', - " return 'route:00'", - '}', - '', - ].join('\n')) - write(root, 'src/flow-022/service-local-01.ts', [ - 'export function serviceLocal01(): string {', - " return 'service:01'", + const unrelated = retrieveContext(workspace([ + 'export function updateRetryCount(state: { status: string }) {', + " state.status = 'done'", '}', '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace flow-021 route local 00 to flow-022 service local 01.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file).sort()).toEqual([ - 'src/flow-021/route-local-00.ts', - 'src/flow-022/service-local-01.ts', - ]) - expect(result.boundaries).toEqual([ - expect.objectContaining({ kind: 'disconnected' }), - ]) - }) - - it('seals the inspected graph from later mutation', () => { - const fixture = flowFixture() - const before = retrieveContext(fixture.index, { - question: structuredQuestion('flow-123', ['entry local 00']), - }) - expect(before.outcome).toBe('missing') - - const source = fixture.graph.nodeEntries().find(([, attributes]) => - attributes.qualified_name === 'entryLocal00') - if (!source) throw new Error('Canonical fixture did not index entryLocal00') - fixture.graph.addNode('mutated-flow-123-entry', { - ...source[1], - label: 'flow-123 entry local 00', - qualified_name: 'flow123EntryLocal00', - }) - const exposed = fixture.index.graph as unknown as Record - expect(Object.isFrozen(fixture.index.graph)).toBe(true) - expect(Object.keys(exposed)).not.toContain('nodeMap') - expect(Object.keys(exposed)).not.toContain('edgeMap') - expect(exposed.nodeMap).toBeUndefined() - expect(exposed.edgeMap).toBeUndefined() - expect(exposed.addNode).toBeUndefined() - const returnedAttributes = fixture.index.graph.nodeAttributes(source[0]) - returnedAttributes.line_number = 1 - - const after = retrieveContext(fixture.index, { - question: structuredQuestion('flow-123', ['entry local 00']), - }) - expect(after).toEqual(before) - }) - - it('returns one exact missing boundary for an absent explicit subject', () => { - const fixture = flowFixture() - - const result = retrieveContext(fixture.index, { - question: 'Which evidence path implements flow-999?', - }) - - expect(result).toMatchObject({ - outcome: 'missing', - matched_nodes: [], - relationships: [], - boundaries: [{ kind: 'missing', subject: 'flow-999' }], + ].join('\n')).index, { + question: 'What updates retryCount?', budget: 2_000, }) + expect(unrelated.state).toBe('incomplete') + if (unrelated.state === 'incomplete') { + expect(unrelated.missing).toContainEqual(expect.objectContaining({ + code: 'subject_unproven', + })) + } }) - it('does not turn ordinary word-number terminology into a mandatory scope', () => { - const root = sandbox('technical-term') - write(root, 'src/hash.ts', [ - 'export function computeSourceHash(value: string): string {', - ' return value', + it('keeps the backing call for an emitted persistence fact', () => { + const index = workspace([ + "import { writeFile } from 'node:fs/promises'", + 'export async function validateIdeaReport(approved: boolean) {', + " if (approved) await writeFile('report.txt', 'ok')", '}', '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const index = readyIndex(root) + ].join('\n')).index const result = retrieveContext(index, { - question: 'How does SHA-256 source hash computation work?', + question: 'Explain how validateIdeaReport behaves', budget: 4_000, }) - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.source_file)).toEqual(['src/hash.ts']) - expect(result.boundaries).toEqual([]) - }) - - it('keeps present scoped evidence beside an exact missing boundary', () => { - const root = sandbox('mixed-scopes') - write(root, 'src/hash.ts', [ - 'export function computeSourceHash(value: string): string {', - ' return value', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Compare `computeSourceHash` with `missingHasher`.', + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const persistence = result.dossier.evidence.entities.find((entity) => + entity.kind === 'operation' && 'operation_kind' in entity + && entity.operation_kind === 'persistence') + expect(persistence).toMatchObject({ + kind: 'operation', + detail: { call: expect.any(String) }, }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toEqual(['computeSourceHash()']) - expect(result.boundaries).toEqual([{ kind: 'missing', subject: 'missingHasher' }]) + expect(() => serializeRetrieveContextResult(result)).not.toThrow() }) - it('keeps unscoped supported evidence beside an exact missing boundary', () => { - const root = sandbox('missing-and-unscoped') - write(root, 'src/hash.ts', [ - 'export function computeSourceHash(value: string): string {', - ' return value', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Compare `missingHasher` with source hash computation.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toEqual(['computeSourceHash()']) - expect(result.boundaries).toEqual([{ kind: 'missing', subject: 'missingHasher' }]) - }) - - it('retrieves exact natural symbol names without requiring kind words or backticks', () => { - const root = sandbox('natural-symbol') - write(root, 'src/config.ts', [ - 'export const MAX_RETRIES = 3', - 'export const MAX_TIMEOUT = 30', - 'export const providerToFunction = { email: () => "sent" }', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const index = readyIndex(root) - - expect(retrieveContext(index, { question: 'What is MAX_RETRIES?' }) - .matched_nodes.map((node) => node.label)).toEqual(['MAX_RETRIES']) - expect(retrieveContext(index, { question: 'How does providerToFunction work?' }) - .matched_nodes.map((node) => node.label)).toEqual(['providerToFunction']) - }) - - it('keeps an exact qualified method ahead of same-file fallback callees', () => { - const root = sandbox('qualified-method') - write(root, 'src/title-generation.service.ts', [ - 'export class TitleGenerationService {', - ' generateTitle(): string {', - ' return this.generateFallbackTitle()', - ' }', - '', - ' private generateFallbackTitle(): string {', - " return 'fallback'", - ' }', - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const index = readyIndex(root) - - expect(retrieveContext(index, { - question: 'Where is TitleGenerationService.generateTitle defined?', - }).matched_nodes.map(({ label }) => label)).toEqual(['.generateTitle()']) - expect(retrieveContext(index, { - question: 'What does TitleGenerationService.generateTitle do?', - }).matched_nodes[0]?.label).toBe('.generateTitle()') - }) - - it('reports exact synthetic framework targets as unavailable without unrelated evidence', () => { - const root = sandbox('synthetic-framework') - write(root, 'src/router.ts', [ - "import { initTRPC } from '@trpc/server'", - 'const t = initTRPC.create()', - "const namedHealth = t.procedure.query(() => 'ok')", - "export const appRouter = t.router({ namedHealth, inlineHealth: t.procedure.query(() => 'ok') })", - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const index = readyIndex(root) - - expect(retrieveContext(index, { question: 'Explain `appRouter.inlineHealth`.' })) - .toMatchObject({ - outcome: 'unavailable', - matched_nodes: [], - relationships: [], - boundaries: [{ kind: 'unavailable', subject: 'appRouter.inlineHealth' }], - metrics: { selected_files: 0, snippets: 0, closure_passes: 0 }, - }) - expect(retrieveContext(index, { question: 'Explain `namedHealth`.' }) - .matched_nodes.map((node) => node.label)).toEqual(['namedHealth']) - }) - - it('reports a canonical file-only exact path as unavailable, not corrupt', () => { - const root = sandbox('file-only') - write(root, 'src/setup.ts', "import 'node:fs'\n") - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Explain src/setup.ts.', - }) - - expect(result).toMatchObject({ - outcome: 'unavailable', - matched_nodes: [], - relationships: [], - boundaries: [{ kind: 'unavailable', subject: 'src/setup.ts' }], - }) - }) - - it('follows a finite directed evidence path beyond eight hops', () => { - const root = sandbox('complete-traversal') - for (let index = 0; index < 10; index += 1) { - const ordinal = String(index).padStart(2, '0') - const next = String(index + 1).padStart(2, '0') - write(root, `src/flow-030/node-local-${ordinal}.ts`, [ - ...(index < 9 ? [`import { nodeLocal${next} } from './node-local-${next}.js'`, ''] : []), - `export function nodeLocal${ordinal}(): string {`, - index < 9 ? ` return nodeLocal${next}()` : " return 'done'", - '}', + it('declares only the proven explain subject and proof-binds its call target', () => { + const index = multiFileWorkspace({ + 'repository.ts': 'export function saveOrder(id: string) { return id }\n', + 'service.ts': [ + "import { saveOrder } from './repository.js'", + 'export function submitOrder(id: string) { return saveOrder(id) }', '', - ].join('\n')) - } - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace `nodeLocal00` to `nodeLocal09`.', + ].join('\n'), }) - - expect(result.outcome).toBe('evidence') - expect(result.relationships).toHaveLength(9) - expect(result.relationships.every((edge) => edge.relation === 'calls')).toBe(true) - expect(result.matched_nodes.map((node) => node.label)).toEqual(expect.arrayContaining([ - 'nodeLocal00()', - 'nodeLocal09()', - ])) - expect(result.boundaries).toEqual([]) - expect(result.metrics.truncated).toBe(false) - }) - - it('preserves every direct phase anchor when a causal path exceeds the file cap', () => { - const root = sandbox('anchor-cap') - for (let index = 0; index < 15; index += 1) { - const ordinal = String(index).padStart(2, '0') - const nextOrdinal = String(index + 1).padStart(2, '0') - const phase = index === 0 ? 'start' : index === 7 ? 'middle' : index === 14 ? 'finish' : 'step' - const nextPhase = index + 1 === 7 - ? 'middle' - : index + 1 === 14 - ? 'finish' - : 'step' - write(root, `src/chain/${phase}-local-${ordinal}.ts`, [ - ...(index < 14 - ? [`import { ${nextPhase}Local${nextOrdinal} } from './${nextPhase}-local-${nextOrdinal}.js'`, ''] - : []), - `export function ${phase}Local${ordinal}(): string {`, - index < 14 ? ` return ${nextPhase}Local${nextOrdinal}()` : " return 'done'", - '}', - '', - ].join('\n')) - } - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace `startLocal00` through `middleLocal07` to `finishLocal14`.', + const result = retrieveContext(index, { + question: 'How does submit order call save order?', budget: 4_000, }) - expect(result.outcome).toBe('evidence') - expect(result.metrics.selected_files).toBe(12) - expect(result.metrics.truncated).toBe(true) - expect(result.matched_nodes.map((node) => node.label)).toEqual(expect.arrayContaining([ - 'startLocal00()', - 'middleLocal07()', - 'finishLocal14()', + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const symbols = result.dossier.evidence.entities + .filter((entity) => entity.kind === 'symbol') + expect(symbols).toEqual(expect.arrayContaining([ + expect.objectContaining({ label: 'submitOrder()', excerpt: expect.any(String) }), + expect.objectContaining({ label: 'saveOrder()' }), ])) - expect(result.boundaries).toContainEqual(expect.objectContaining({ kind: 'truncated' })) - }) - - it('preserves identical authenticated excerpts at distinct graph locations', () => { - const root = sandbox('identical-snippets') - const source = [ - 'export function handle(): string {', - " return 'same'", - '}', - '', - ].join('\n') - write(root, 'src/left/handler.ts', source) - write(root, 'src/right/handler.ts', source) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Compare src/left/handler.ts with src/right/handler.ts.', - }) - - expect(result.outcome).toBe('evidence') - const symbols = result.matched_nodes.filter((node) => - node.evidence_kind === 'symbol_declaration') - expect(symbols).toHaveLength(2) - expect(symbols.every((node) => - node.snippet === 'export function handle(): string ')).toBe(true) - expect(result.matched_nodes.filter((node) => - node.evidence_kind === 'structural_file')).toHaveLength(2) - }) - - it('reports recognized unsupported sources without claiming graph evidence', () => { - const fixture = flowFixture() - - const result = retrieveContext(fixture.index, { - question: 'How does the Go checker call the Tinybird client?', + expect(symbols.find(({ label }) => label === 'saveOrder()')) + .not.toHaveProperty('excerpt') + expect(result.dossier.evidence.proofs).toEqual(expect.arrayContaining([ + expect.objectContaining({ relation: 'calls' }), + ])) + const call = result.dossier.evidence.entities.find((entity) => + entity.kind === 'operation' && 'links' in entity) + expect(call).toEqual(expect.objectContaining({ + kind: 'operation', order: expect.any(Array), + links: expect.any(Array), excerpt: expect.any(String), + })) + expect(call).not.toHaveProperty('scheduling') + expect(call).not.toHaveProperty('callee') + expect(call).not.toHaveProperty('operation_kind') + expect(call).not.toHaveProperty('arguments') + }) + + it('keeps linked-call arguments only in the exact authenticated excerpt', () => { + const index = multiFileWorkspace({ + 'repository.ts': 'export function saveOrder(id: string) { return id }\n', + 'service.ts': [ + "import { saveOrder } from './repository.js'", + "export function submitOrder() { return saveOrder('order-1') }", + '', + ].join('\n'), }) - - expect(result.outcome).toBe('unsupported') - expect(result.matched_nodes).toEqual([]) - expect(result.relationships).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'unsupported', subject: 'src/checker/checker.go' }, - { kind: 'unsupported', subject: 'src/tinybird/client.go' }, - ]) - }) - - it('reports when the unsupported-source boundary cap omits recognized files', () => { - const root = sandbox('unsupported-cap') - for (const name of ['alpha', 'bravo', 'charlie', 'delta', 'echo']) { - write(root, `src/${name}.go`, `package ${name}\n`) - } - write(root, 'src/index.ts', 'export const ready = true\n') - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Compare alpha, bravo, charlie, delta, and echo Go sources.', + const result = retrieveContext(index, { + question: 'How does submit order call save order?', budget: 4_000, + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const call = result.dossier.evidence.entities.find((entity) => + entity.kind === 'operation' && 'links' in entity) + expect(call).toBeDefined() + if (!call || call.kind !== 'operation' || !('links' in call)) return + expect(call).not.toHaveProperty('arguments') + expect(result.dossier.evidence.excerpts.find(({ id }) => id === call?.excerpt)?.text) + .toContain("saveOrder('order-1')") + }) + + it('preserves authenticated sibling-call sequence order in the dossier', () => { + const index = workspace(`import type { MongoRepository } from 'typeorm' +type Row = { id: string } +export async function generateIdeaReport(repository: MongoRepository) { + await persistIdeaReport(repository, 'first') + return persistIdeaReport(repository, 'second') +} +async function persistIdeaReport(repository: MongoRepository, id: string) { + await repository.update(id, { id }) + return id +} +`).index + const result = retrieveContext(index, { + question: 'How is an idea report generated end to end?', budget: 4_000, }) - expect(result.outcome).toBe('unsupported') - expect(result.boundaries.filter((boundary) => boundary.kind === 'unsupported')) - .toHaveLength(4) - expect(result.boundaries).toContainEqual({ - kind: 'truncated', - subject: 'unsupported sources', + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const sequence = result.dossier.flow.order.find(({ kind }) => kind === 'sequence') + expect(sequence).toBeDefined() + expect(sequence?.members).toHaveLength(2) + expect(sequence?.proofs).toHaveLength(2) + sequence?.members.forEach((member, index) => { + expect(result.dossier.evidence.entities).toContainEqual(expect.objectContaining({ + id: member, kind: 'operation', excerpt: expect.any(String), + })) + expect(sequence.proofs[index]).toBe(member) }) - expect(result.metrics.truncated).toBe(true) }) - it('omits stale excerpts when the complete source hash changes', () => { - const fixture = flowFixture() - write( - fixture.root, - 'src/flow-001/storage-local-02.js', - 'export function storageLocal02() { return "changed" }\n', - ) - - const result = retrieveContext(fixture.index, { - question: 'Explain `storageLocal02`.', + it('keeps an exact imported-caller locator ahead of a called suffix match', () => { + const index = multiFileWorkspace({ + 'route.ts': [ + "import { trackClick } from './analytics.js'", + "import { redirectToDestination } from './redirect.js'", + 'export function handleClick() { trackClick(); redirectToDestination() }', + '', + ].join('\n'), + 'analytics.ts': 'export function trackClick() {}\n', + 'redirect.ts': 'export function redirectToDestination() {}\n', }) - expect(result.outcome).toBe('stale') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'stale', subject: 'src/flow-001/storage-local-02.js' }, - ]) - }) - - it('reports unavailable excerpts when an authenticated source disappears', () => { - const fixture = flowFixture() - unlinkSync(join(fixture.root, 'src/flow-001/storage-local-02.js')) - - const result = retrieveContext(fixture.index, { - question: 'Explain `storageLocal02`.', + const result = retrieveContext(index, { + question: 'Where is handleClick defined?', + budget: 1_200, }) - expect(result.outcome).toBe('unavailable') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'unavailable', subject: 'src/flow-001/storage-local-02.js' }, + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.evidence.entities).toEqual([ + expect.objectContaining({ kind: 'symbol', label: 'handleClick()' }), ]) }) - it('rejects a graph-selected source that escapes the authenticated root', () => { - const fixture = flowFixture() - const outsideRoot = sandbox('outside') - const outside = write(outsideRoot, 'escape.ts', [ - 'export function escapeLocal00(): string {', - " return 'outside'", - '}', - '', - ].join('\n')) - const entry = fixture.graph.nodeEntries().find(([, attributes]) => - attributes.qualified_name === 'storageLocal02') - if (!entry) throw new Error('Canonical fixture did not index storageLocal02') - const [nodeId, attributes] = entry - fixture.graph.replaceNodeAttributes(nodeId, { - ...attributes, - label: 'flow-003 escape local 00', - qualified_name: 'escapeLocal00', - source_file: relative(fixture.root, outside), - source_location: 'L1-L3', - line_number: 1, - end_line_number: 3, + it('returns exact incomplete states for missing persistence and budget', () => { + const noTerminal = retrieveContext(workspace(reportSource(false)).index, { + question: 'How is an idea report generated end to end?', + budget: 4_000, }) - const escapedSource = relative(fixture.root, outside) - const escapedIndex: ReadyQueryIndex = { - ...fixture.index, - graph: fixture.graph, - file_hashes: new Map([ - ...fixture.index.file_hashes, - [escapedSource, createHash('sha256').update(readFileSync(outside)).digest('hex')], - ]), + expect(noTerminal.state).toBe('incomplete') + if (noTerminal.state === 'incomplete') { + expect(noTerminal.missing.map(({ code }) => code)) + .toContain('terminal_persistence_unproven') } - const result = retrieveContext(escapedIndex, { - question: 'Explain `escapeLocal00`.', - }) - - expect(result.outcome).toBe('unavailable') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'unavailable', subject: escapedSource }, - ]) - }) - - it('does not replace an exact hard-ignored graph target with unrelated evidence', () => { - const fixture = flowFixture() - const ignoredSource = 'tmp/escape.ts' - const outside = write(fixture.root, ignoredSource, [ - 'export function ignoredEscapeLocal00(): string {', - " return 'ignored'", - '}', - '', - ].join('\n')) - const entry = fixture.graph.nodeEntries().find(([, attributes]) => - attributes.qualified_name === 'storageLocal02') - if (!entry) throw new Error('Canonical fixture did not index storageLocal02') - fixture.graph.replaceNodeAttributes(entry[0], { - ...entry[1], - label: 'ignored escape local 00', - qualified_name: 'ignoredEscapeLocal00', - source_file: ignoredSource, - source_location: 'L1-L3', - line_number: 1, - end_line_number: 3, + const tooSmall = retrieveContext(workspace(reportSource()).index, { + question: 'How is an idea report generated end to end?', + budget: 1, }) - const ignoredIndex: ReadyQueryIndex = { - ...fixture.index, - graph: fixture.graph, - file_hashes: new Map([ - ...fixture.index.file_hashes, - [ignoredSource, createHash('sha256').update(readFileSync(outside)).digest('hex')], - ]), + expect(tooSmall.state).toBe('incomplete') + if (tooSmall.state === 'incomplete') { + expect(tooSmall.missing).toContainEqual(expect.objectContaining({ + code: 'required_token_budget', + limit: 256, + })) } - - expect(retrieveContext(ignoredIndex, { - question: 'Explain `ignoredEscapeLocal00`.', - })).toMatchObject({ - outcome: 'unavailable', - matched_nodes: [], - relationships: [], - boundaries: [{ kind: 'unavailable', subject: 'ignoredEscapeLocal00' }], - }) - }) - - it('classifies an authenticated symbol with an invalid graph range as stale', () => { - const root = sandbox('invalid-range') - write(root, 'src/range.ts', [ - 'export function invalidRange(): string {', - " return 'value'", - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const indexed = indexedWorkspace(root) - const graph = indexed.graph - const entry = graph.nodeEntries().find(([, attributes]) => - attributes.qualified_name === 'invalidRange') - if (!entry) throw new Error('Canonical fixture did not index invalidRange') - graph.replaceNodeAttributes(entry[0], { - ...entry[1], - end_line_number: 999, - source_location: 'L1-L999', - definition_range: { - ...(entry[1].definition_range as { start: { line: number; column: number } }), - end: { line: 999, column: 1 }, - }, - }) - - const result = retrieveContext({ ...indexed.index, graph }, { - question: 'Explain `invalidRange`.', - }) - - expect(result.outcome).toBe('stale') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'stale', subject: 'src/range.ts' }, - ]) - }) - - it.each([ - ['CRLF', '\r\n'], - ['bare CR', '\r'], - ['Unicode line separator', '\u2028'], - ['Unicode paragraph separator', '\u2029'], - ])('authenticates TypeScript graph ranges across %s terminators', (_name, terminator) => { - const root = sandbox('ecmascript-lines') - const source = [ - 'const before = 1;', - 'export function lineTarget(): number {', - ' return 1', - '}', - '', - ].join(terminator) - write(root, 'src/lines.ts', source) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Explain `lineTarget`.', - }) - - expect(result.outcome).toBe('evidence') - expect(result.boundaries).toEqual([]) - expect(result.matched_nodes).toHaveLength(1) - expect(result.matched_nodes[0]).toMatchObject({ - source_file: 'src/lines.ts', - line_number: 2, - end_line_number: 4, - definition_range: { - start: { line: 2, column: 1 }, - end: { line: 4, column: 2 }, - }, - declaration_range: { - start: { line: 2, column: 1 }, - end: { line: 2, column: 38 }, - }, - snippet: 'export function lineTarget(): number ', - }) - }) - - it('classifies malformed symbol provenance as corrupt instead of missing', () => { - const root = sandbox('malformed-provenance') - write(root, 'src/provenance.ts', [ - 'export function malformedProvenance(): string {', - " return 'value'", - '}', - '', - ].join('\n')) - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const indexed = indexedWorkspace(root) - const graph = indexed.graph - const entry = graph.nodeEntries().find(([, attributes]) => - attributes.qualified_name === 'malformedProvenance') - if (!entry) throw new Error('Canonical fixture did not index malformedProvenance') - graph.replaceNodeAttributes(entry[0], { - ...entry[1], - provenance: [], - }) - - const result = retrieveContext({ ...indexed.index, graph }, { - question: 'Explain `malformedProvenance`.', - }) - - expect(result.outcome).toBe('corrupt') - expect(result.matched_nodes).toEqual([]) - expect(result.boundaries).toEqual([ - { kind: 'corrupt', subject: entry[0] }, - ]) - }) - - it('returns a corrupt boundary for an unauthenticated canonical index', () => { - const fixture = flowFixture() - fixture.graph.graph.canonical_typescript_index = false - const corrupt: QueryIndex = inspectQueryIndex(fixture.graph) - - expect(corrupt.state).toBe('corrupt') - expect(retrieveContext(corrupt, { question: 'trace entry' })).toMatchObject({ - outcome: 'corrupt', - matched_nodes: [], - relationships: [], - boundaries: [{ kind: 'corrupt', subject: 'canonical TypeScript index metadata' }], - }) }) - it('enforces the snippet and file caps with one truncation boundary', () => { - const phases = [ - 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', - 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', - 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega', 'amber', 'cedar', - ] + it('fails closed on stale selected source bytes', () => { + const fixture = workspace(reportSource()) + writeFileSync(fixture.path, `${fixture.source}// changed\n`, 'utf8') - const snippetRoot = sandbox('snippet-cap') - write(snippetRoot, 'src/flow-010/all-phases.ts', phases.flatMap((phase, index) => { - const ordinal = String(index).padStart(2, '0') - return [ - `export function ${phase}Local${ordinal}(): string {`, - ` return '${phase}:${ordinal}'`, - '}', - '', - ] - }).join('\n')) - write(snippetRoot, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - const snippetQuestion = `Trace flow-010 from ${phases.map((phase, index) => - `${phase} local ${String(index).padStart(2, '0')}`).join(' -> ')}.` - const snippetResult = retrieveContext(readyIndex(snippetRoot), { - question: snippetQuestion, - }) - - expect(snippetResult.metrics.snippets).toBeLessThanOrEqual(25) - expect(snippetResult.boundaries.filter((boundary) => boundary.kind === 'truncated')) - .toHaveLength(1) - - const fileRoot = sandbox('file-cap') - const fileQuestion = writeDisconnectedFlow( - fileRoot, - 'flow-011', - phases.slice(0, 13).map((phase, index) => ({ - phase, - ordinal: String(index).padStart(2, '0'), - file: `${phase}-local-${String(index).padStart(2, '0')}.ts`, - })), - ) - const fileResult = retrieveContext(readyIndex(fileRoot), { question: fileQuestion }) - - expect(fileResult.metrics.selected_files).toBeLessThanOrEqual(12) - expect(fileResult.boundaries.filter((boundary) => boundary.kind === 'truncated')) - .toHaveLength(1) - }) - - it('keeps the canonical result within a small budget by omitting whole facts', () => { - const fixture = flowFixture() const result = retrieveContext(fixture.index, { - question: structuredQuestion('flow-001', [ - 'entry local 00', - 'process local 01', - 'storage local 02', - ]), - budget: 256, + question: 'Where is generateIdeaReport defined?', + budget: 4_000, }) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) - expect(result.metrics.truncated).toBe(true) - expect(result.boundaries).toContainEqual(expect.objectContaining({ kind: 'truncated' })) - const selectedNodeIds = new Set(result.matched_nodes.map((node) => node.node_id)) - expect(result.relationships.every((relationship) => - selectedNodeIds.has(relationship.from_id) && selectedNodeIds.has(relationship.to_id))).toBe(true) - }) - - it('keeps fitting priority evidence ahead of verbose diagnostics under budget', () => { - const node = { - node_id: 'priority', - evidence_kind: 'symbol_declaration' as const, - label: 'priority()', - node_kind: 'function', - source_file: 'src/priority.ts', - source_location: 'L1', - line_number: 1, - end_line_number: 1, - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 30 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 30 }, - }, - source_domain: 'production', - provenance: [{}], - content_hash: 'a'.repeat(64), - snippet: 'export function priority() {}', - } - const result = sliceEvidence({ - request: { question: 'priority', budget: 400 }, - outcome: 'evidence', - matchedNodes: [node], - relationships: [], - boundaries: Array.from({ length: 10 }, (_, index) => ({ - kind: 'disconnected' as const, - subject: `phase-${index}`, - detail: `long diagnostic ${String(index).repeat(120)}`, - })), - priorityNodeIds: [node.node_id], - closurePasses: 1, + expect(result).toMatchObject({ + state: 'stale', + failures: [{ state: 'stale', subject: 'src/report.ts' }], }) - - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes).toEqual([node]) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(400) - expect(result.metrics.truncated).toBe(true) }) - it('prunes a structural file when the token budget drops its relationship', () => { - const structural = { - node_id: 'file', - evidence_kind: 'structural_file' as const, - label: 'priority.ts', - node_kind: 'file' as const, - source_file: 'src/priority.ts', - source_domain: 'production', - provenance: [{}], - content_hash: 'b'.repeat(64), - } - const result = sliceEvidence({ - request: { question: 'priority', budget: 256 }, - outcome: 'evidence', - matchedNodes: [structural, { - node_id: 'symbol', - evidence_kind: 'symbol_declaration', - label: 'priority()', - node_kind: 'function', - source_file: 'src/priority.ts', - source_location: 'L1', - line_number: 1, - end_line_number: 1, - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 30 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 30 }, - }, - source_domain: 'production', - provenance: [{}], - content_hash: 'b'.repeat(64), - snippet: 'export function priority() {}', - }], - relationships: [{ - id: 'contains', - from_id: 'file', - to_id: 'symbol', - relation: 'contains', - source_file: 'src/priority.ts', - source_location: 'L1', - provenance: [{ detail: 'x'.repeat(2_000) }], - }], - boundaries: [], - priorityNodeIds: ['file'], - closurePasses: 1, - }) - - expect(result.matched_nodes).not.toContainEqual(expect.objectContaining({ - evidence_kind: 'structural_file', - })) - const ids = new Set(result.matched_nodes.map((entry) => entry.node_id)) - expect(result.relationships.every((edge) => - ids.has(edge.from_id) && ids.has(edge.to_id))).toBe(true) - }) + it('plans unsupported questions before consulting an unavailable index', () => { + const result = retrieveContext( + { state: 'unavailable', subject: 'out/graph.json' }, + { question: 'Compare every architecture in this repository.', budget: 4_000 }, + ) - it('does not share mutable truncation facts between identical requests', () => { - const fixture = flowFixture() - const input = { - question: structuredQuestion('flow-001', [ - 'entry local 00', - 'process local 01', - 'storage local 02', - ]), + expect(result).toMatchObject({ + schema: 'madar.retrieve', + version: 2, + state: 'unsupported', + reason: 'unsupported_intent', + }) + }) + + it('keeps every non-ready terminal state within the effective budget', () => { + const unsupported = retrieveContext({ state: 'unavailable', subject: 'ignored' }, { + question: 'Compare every architecture in this repository.', budget: 256, + }) + const unavailable = retrieveContext({ + state: 'unavailable', subject: 'x'.repeat(2_000), + }, { question: 'Where is report defined?', budget: 256 }) + const corrupt = retrieveContext({ + state: 'corrupt', subject: 'x'.repeat(2_000), + }, { question: 'Where is report defined?', budget: 256 }) + const fixture = workspace(reportSource()) + writeFileSync(fixture.path, `${fixture.source}// changed\n`, 'utf8') + const stale = retrieveContext(fixture.index, { + question: 'Where is generateIdeaReport defined?', budget: 256, + }) + const incompleteResult = retrieveContext(workspace(reportSource(false)).index, { + question: 'How is an idea report generated end to end?', budget: 256, - } - const first = retrieveContext(fixture.index, input) - const original = serializeRetrieveContextResult(first) - const truncated = first.boundaries.find((candidate) => candidate.kind === 'truncated') - if (!truncated) throw new Error('Expected a truncated boundary') - truncated.detail = 'caller mutation' - - const second = retrieveContext(fixture.index, input) - - expect(serializeRetrieveContextResult(second)).toBe(original) - }) - - it('rejects every input key except required question and optional budget', () => { - const fixture = flowFixture() - - expect(() => retrieveContext(fixture.index, { - question: 'trace entry', - semantic: true, - })).toThrow('retrieve accepts only question and optional budget') - expect(() => retrieveContext(fixture.index, { budget: 4000 })) - .toThrow('retrieve question must be between 1 and 512 characters') - expect(() => retrieveContext(fixture.index, { question: 'x'.repeat(513) })) - .toThrow('retrieve question must be between 1 and 512 characters') - expect(retrieveContext(fixture.index, { question: 'trace entry', budget: 1 }).metrics.serialized_tokens) - .toBeLessThanOrEqual(256) - expect(retrieveContext(fixture.index, { question: 'trace entry', budget: 20_000 }).metrics.serialized_tokens) - .toBeLessThanOrEqual(4000) - }) - - it('traverses a side branch between adjacent flow anchors without mixing index spaces', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - const chain = ['entry', 'plan', 'research', 'assembly'] - const anchors = [...chain.slice(0, -1), 'notification', chain.at(-1)!] - const range = { - start: { line: 1, column: 1 }, - end: { line: 1, column: 20 }, - } - for (const id of anchors) { - graph.addNode(id, { - label: `${id}()`, - node_kind: 'function', - source_file: `src/${id}.ts`, - source_location: 'L1', - definition_range: range, - declaration_range: range, - }) - } - for (const [from, to] of [ - ['entry', 'plan'], - ['plan', 'research'], - ['research', 'notification'], - ['research', 'assembly'], - ]) { - graph.addEdge(from!, to!, { - relation: 'calls', - source_file: `src/${from!}.ts`, - source_location: 'L1', - provenance: [], - }) - } - const index: ReadyQueryIndex = { - state: 'ready', - graph, - root_path: '/workspace', - file_hashes: new Map(), - unsupported_sources: [], - operation_by_id: new Map(), - operations_by_owner: new Map(), - channels_by_id: new Map(), - channels_by_key: new Map(), - } - - const slice = traverseEvidencePaths(index, { - anchors: anchors.map((id, firstMatch) => ({ - id, - attributes: graph.nodeAttributes(id), - score: 1, - matchedTerms: [], - firstMatch, - })), - boundaries: [], - queryTerms: ['flow'], - flow: true, - branch: ['notification'], }) - expect(slice.edges.map(({ from, to }) => `${from}->${to}`)).toEqual([ - 'entry->plan', - 'plan->research', - 'research->assembly', - 'research->notification', + expect([unsupported, unavailable, corrupt, stale, incompleteResult] + .map((result) => result.state)).toEqual([ + 'unsupported', 'unavailable', 'corrupt', 'stale', 'incomplete', ]) - expect(slice.boundaries).toEqual([]) + for (const result of [unsupported, unavailable, corrupt, stale, incompleteResult]) { + expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) + expect(countTokens(serializeRetrieveContextResult(result))) + .toBe(result.metrics.serialized_tokens) + } }) }) diff --git a/tests/unit/sample-workspace.test.ts b/tests/unit/sample-workspace.test.ts index 3edb21ff..1de32e41 100644 --- a/tests/unit/sample-workspace.test.ts +++ b/tests/unit/sample-workspace.test.ts @@ -56,18 +56,22 @@ describe('examples/sample-workspace', () => { const graph = loadGraphArtifact(generated.graphPath) const result = retrieveContext(inspectQueryIndex(graph), { question: prompt?.question ?? '', - budget: 1800, + budget: 4000, }) expect(generated.nodeCount).toBeGreaterThan(0) expect(result.schema).toBe('madar.retrieve') - expect(result.version).toBe(1) - expect(result.outcome).toBe('evidence') - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(1800) - expect( - (prompt?.expected_labels ?? []).some((label) => - result.matched_nodes.some((node) => node.label === label)), - ).toBe(true) + expect(result.version).toBe(2) + expect(result.state).toBe('ready') + expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4000) + if (result.state === 'ready') { + expect( + (prompt?.expected_labels ?? []).some((label) => + result.dossier.evidence.entities.some((entity) => + entity.kind === 'symbol' + && entity.label.replace(/^\./, '') === label.replace(/^\./, ''))), + ).toBe(true) + } }) }) diff --git a/tests/unit/stdio-server.test.ts b/tests/unit/stdio-server.test.ts index f3fe7577..c6f08e26 100644 --- a/tests/unit/stdio-server.test.ts +++ b/tests/unit/stdio-server.test.ts @@ -178,6 +178,7 @@ describe('MCP tools-only protocol', () => { )).toEqual(['tools']) expect(result(listed!).tools?.map((tool) => tool.name)).toEqual(['retrieve']) expect(result(listed!).tools?.[0]).toMatchObject({ + description: expect.stringContaining('authenticated answer dossier'), inputSchema: { properties: { budget: { @@ -358,12 +359,19 @@ describe('MCP reconciliation lifecycle', () => { expect(JSON.parse(text)).toMatchObject({ schema: 'madar.retrieve', - outcome: 'unavailable', - boundaries: [{ - kind: 'unavailable', + version: 2, + state: 'unavailable', + failures: [{ + state: 'unavailable', subject: 'canonical graph for current workspace', }], + metrics: { + selected_files: 0, + authenticated_excerpts: 0, + }, }) + expect(JSON.parse(text)).not.toHaveProperty('dossier') + expect(JSON.parse(text)).not.toHaveProperty('missing') expect(text.toLowerCase()).not.toContain('retry') expect(response?.error).toBeUndefined() expect(starter).toHaveBeenCalledTimes(1) @@ -421,13 +429,32 @@ describe('MCP reconciliation lifecycle', () => { .map((line) => JSON.parse(line) as JsonRpcResponse) .find((entry) => entry.id === 3) const retrieved = JSON.parse(textResult(response!)) as { - outcome: string - matched_nodes: Array<{ source_file: string }> + schema: string + version: number + state: string + dossier?: { + obligations: Array<{ proofs: string[] }> + evidence: { + files: Array<{ path: string }> + entities: Array<{ kind: string; label?: string }> + } + } } - expect(retrieved.outcome).toBe('evidence') - expect(retrieved.matched_nodes).toEqual(expect.arrayContaining([ - expect.objectContaining({ source_file: 'src/payment-retry.ts' }), + expect(retrieved).toMatchObject({ + schema: 'madar.retrieve', + version: 2, + state: 'ready', + }) + expect(retrieved).not.toHaveProperty('missing') + expect(retrieved.dossier?.evidence.files).toEqual(expect.arrayContaining([ + expect.objectContaining({ path: 'src/payment-retry.ts' }), + ])) + expect(retrieved.dossier?.evidence.entities).toEqual(expect.arrayContaining([ + expect.objectContaining({ kind: 'symbol', label: 'retryPayment()' }), ])) + expect(retrieved.dossier?.obligations.length).toBeGreaterThan(0) + expect(retrieved.dossier?.obligations.every((claim) => claim.proofs.length > 0)) + .toBe(true) }, 30_000) it('keeps the stream alive after parse and oversized-line errors', async () => { diff --git a/tools/eval/lib/infrastructure/benchmark/quality.ts b/tools/eval/lib/infrastructure/benchmark/quality.ts index 57058712..9f664e14 100644 --- a/tools/eval/lib/infrastructure/benchmark/quality.ts +++ b/tools/eval/lib/infrastructure/benchmark/quality.ts @@ -94,8 +94,8 @@ export interface QualityOptions { */ export const GOLD_QUESTIONS: GoldQuestion[] = [ { - question: 'how does the retrieve application rank query anchors', - expected_labels: ['retrievecontext', 'rankqueryanchors'], + question: 'how does retrieveContext plan a repository question', + expected_labels: ['retrievecontext', 'planquestion'], }, { question: 'how does the retrieve MCP tool find relevant nodes', @@ -106,12 +106,12 @@ export const GOLD_QUESTIONS: GoldQuestion[] = [ expected_labels: ['retrievecontext'], }, { - question: 'how does retrieval traverse directed evidence paths', - expected_labels: ['traverseevidencepaths'], + question: 'how does retrieval select an ordered workflow', + expected_labels: ['selectworkflow'], }, { - question: 'how does retrieval fit evidence into the result budget', - expected_labels: ['sliceevidence'], + question: 'how does retrieval hydrate authenticated evidence for the answer dossier', + expected_labels: ['hydrateevidence'], }, { question: 'how does canonical TypeScript indexing build graph nodes', @@ -206,7 +206,29 @@ function buildQualityResult( ): QualityResult { const expectedLabels = gold.expected_labels const normalizedExpectedLabels = expectedLabels.map((label) => normalizeExpectedLabel(label)) - const returnedLabels = result.matched_nodes.map((node) => normalizeExpectedLabel(node.label)) + const returnedNodes = result.state === 'ready' + ? result.dossier.evidence.entities.filter((entity) => entity.kind === 'symbol') + : [] + const returnedLabels = returnedNodes.map((node) => normalizeExpectedLabel(node.label)) + const groundedSymbols = new Set() + if (result.state === 'ready') { + const excerptIds = new Set(result.dossier.evidence.excerpts.map(({ id }) => id)) + const links = new Map(result.dossier.flow.links.map((link) => [link.id, link])) + for (const proof of result.dossier.evidence.proofs) { + groundedSymbols.add(proof.from) + groundedSymbols.add(proof.to) + } + for (const entity of result.dossier.evidence.entities) { + if (entity.kind === 'symbol' && entity.excerpt + && excerptIds.has(entity.excerpt)) { + groundedSymbols.add(entity.id) + } + if (entity.kind === 'operation' && excerptIds.has(entity.excerpt)) { + if ('owner' in entity) groundedSymbols.add(entity.owner) + else entity.links.forEach((id) => groundedSymbols.add(links.get(id)!.from)) + } + } + } const matchedLabels = expectedLabels.filter((expected) => returnedLabels.some((returned) => isExactMatch(returned, normalizeExpectedLabel(expected))), @@ -229,13 +251,12 @@ function buildQualityResult( const precision = returnedLabels.length > 0 ? matchedLabels.length / returnedLabels.length : 0 const recall = expectedLabels.length > 0 ? matchedLabels.length / expectedLabels.length : 0 const snippetCoverage = - result.matched_nodes.length > 0 - ? result.matched_nodes.filter((node) => typeof node.snippet === 'string' && node.snippet.trim().length > 0).length / result.matched_nodes.length + returnedNodes.length > 0 + ? returnedNodes.filter((node) => groundedSymbols.has(node.id)).length / returnedNodes.length : 0 - const groundedMatches = result.matched_nodes.filter((node) => ( + const groundedMatches = returnedNodes.filter((node) => ( normalizedExpectedLabels.includes(normalizeExpectedLabel(node.label)) && - typeof node.snippet === 'string' && - node.snippet.trim().length > 0 + groundedSymbols.has(node.id) )).length const groundedMatchRate = expectedLabels.length > 0 ? groundedMatches / expectedLabels.length : 0 @@ -243,7 +264,7 @@ function buildQualityResult( question: gold.question, bucket: questionBucket(gold.question), expected_labels: expectedLabels, - returned_labels: result.matched_nodes.map((node) => node.label), + returned_labels: returnedNodes.map((node) => node.label), matched_labels: matchedLabels, missing_labels: missingLabels, precision, diff --git a/tools/eval/lib/infrastructure/benchmark/questions.ts b/tools/eval/lib/infrastructure/benchmark/questions.ts index 9d5d92e3..eead447f 100644 --- a/tools/eval/lib/infrastructure/benchmark/questions.ts +++ b/tools/eval/lib/infrastructure/benchmark/questions.ts @@ -116,10 +116,13 @@ function queryEvidenceMatch( budget = 4_000, ): { queryTokens: number; labels: Set } | null { const result = retrieveBenchmarkContext(graph, graphPath, question, budget) - if (result.outcome !== 'evidence' || result.matched_nodes.length === 0) return null + if (result.state !== 'ready') return null + const symbols = result.dossier.evidence.entities.filter((entity) => + entity.kind === 'symbol') + if (symbols.length === 0) return null return { queryTokens: result.metrics.serialized_tokens, - labels: new Set(result.matched_nodes.map((node) => normalizeExpectedLabel(node.label))), + labels: new Set(symbols.map((node) => normalizeExpectedLabel(node.label))), } } From f4e6b304f47fb2b001ca3faf746e684b960c7c60 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sun, 2 Aug 2026 01:27:09 +0400 Subject: [PATCH 2/4] feat: return obligation-driven retrieval dossiers --- .../verify-packed-retrieval-parity.mjs | 82 +- docs/core-reset/removal-manifest.yml | 251 +++- docs/core-reset/scorecard.md | 29 +- docs/designs/2026-07-19-core-reset.md | 16 +- docs/roadmap.md | 14 +- src/adapters/mcp/protocol.ts | 6 +- src/application/evidence-hydrator.ts | 520 +++---- src/application/retrieve-context.ts | 922 ++++++------ src/domain/query/plan.ts | 218 +-- src/domain/query/types.ts | 206 ++- src/domain/query/workflow.ts | 1089 ++++++-------- tests/unit/benchmark-quality.test.ts | 57 +- .../benchmark-suite-isolation-docs.test.ts | 7 +- tests/unit/benchmark.test.ts | 11 +- tests/unit/core-reset-governance.test.ts | 380 +++-- tests/unit/evidence-hydrator.test.ts | 48 +- tests/unit/query-plan.test.ts | 59 +- tests/unit/query-workflow.test.ts | 77 + .../retrieve-context-proof-eviction.test.ts | 256 +++- tests/unit/retrieve-context.test.ts | 258 +++- ...ieve-evidence-skeleton-adversarial.test.ts | 1268 ----------------- ...rieve-evidence-skeleton-regression.test.ts | 804 ----------- tests/unit/retrieve-v2-contract-gaps.test.ts | 353 +++++ tests/unit/try-command.test.ts | 16 +- tools/eval/core-reset/benchmark.mjs | 113 ++ tools/eval/core-reset/isolation-support.mjs | 11 +- tools/eval/core-reset/verify-isolation.mjs | 14 +- .../lib/infrastructure/benchmark/quality.ts | 45 +- 28 files changed, 3096 insertions(+), 4034 deletions(-) delete mode 100644 tests/unit/retrieve-evidence-skeleton-adversarial.test.ts delete mode 100644 tests/unit/retrieve-evidence-skeleton-regression.test.ts create mode 100644 tests/unit/retrieve-v2-contract-gaps.test.ts create mode 100644 tools/eval/core-reset/benchmark.mjs diff --git a/.github/scripts/verify-packed-retrieval-parity.mjs b/.github/scripts/verify-packed-retrieval-parity.mjs index b5d1dd58..89851355 100644 --- a/.github/scripts/verify-packed-retrieval-parity.mjs +++ b/.github/scripts/verify-packed-retrieval-parity.mjs @@ -56,7 +56,8 @@ function assertPackageMeasurement(record, tarballPath) { 'utf8', )) const evaluationTooling = manifest.items?.find((item) => item.id === 'evaluation-tooling') - const budget = evaluationTooling?.npm_package_budget + const activePhase = manifest.items?.find((item) => item.id === manifest.current?.active_phase) + const budget = activePhase?.npm_package_budget ?? evaluationTooling?.npm_package_budget const receipt = manifest.current const actual = { npm_files: requiredNumber(record.entryCount, 'npm pack entryCount'), @@ -85,14 +86,14 @@ function assertPackageMeasurement(record, tarballPath) { ) } if ( - actual.npm_files > requiredNumber(budget?.files_max, 'Evaluation Tooling files_max') + actual.npm_files > requiredNumber(budget?.files_max, 'active files_max') || actual.npm_packed_bytes - > requiredNumber(budget?.packed_bytes_max, 'Evaluation Tooling packed_bytes_max') + > requiredNumber(budget?.packed_bytes_max, 'active packed_bytes_max') || actual.npm_unpacked_bytes - > requiredNumber(budget?.unpacked_bytes_max, 'Evaluation Tooling unpacked_bytes_max') + > requiredNumber(budget?.unpacked_bytes_max, 'active unpacked_bytes_max') ) { throw new Error( - `Fresh npm package exceeds Evaluation Tooling budgets: ${JSON.stringify(actual)}`, + `Fresh npm package exceeds the active package budget: ${JSON.stringify(actual)}`, ) } return actual @@ -240,11 +241,18 @@ function successfulRetrieve(response, label, expectedLabels) { } catch { throw new Error(`${label} did not return canonical JSON evidence`) } - if (result?.schema !== 'madar.retrieve' || result?.outcome !== 'evidence') { - throw new Error(`${label} did not complete successful evidence retrieval`) + if ( + result?.schema !== 'madar.retrieve' + || result?.version !== 2 + || result?.state !== 'ready' + || !result?.dossier?.evidence + ) { + throw new Error(`${label} did not complete a ready v2 dossier retrieval`) } - const labels = new Set((result.matched_nodes ?? []).map((node) => - String(node.label ?? '').replaceAll(/[^a-z0-9]/gi, '').toLowerCase())) + const labels = new Set(result.dossier.evidence.entities + .filter((entity) => entity.kind === 'symbol') + .map((entity) => String(entity.label ?? '') + .replaceAll(/[^a-z0-9]/gi, '').toLowerCase())) for (const expected of expectedLabels) { if (!labels.has(expected.toLowerCase())) { throw new Error( @@ -451,7 +459,7 @@ try { name: 'retrieve', arguments: { question: 'How is an idea report generated? Explain the pipeline flow from request to final report.', - budget: 8_000, + budget: 4_000, }, }, } @@ -490,15 +498,14 @@ try { const flowResult = successfulRetrieve(packedFlow, 'Packed full-flow runtime', [ 'generatefromproblem', 'startpipeline', - 'enqueuejob', 'plan', 'researchsection', 'assemblereport', + 'savestructuredreport', ]) const expectedFlowFiles = [ 'src/modules/ideas/interface/http/idea-generation.controller.ts', 'src/modules/pipeline/api/pipeline-trigger.service.ts', - 'src/modules/pipeline/api/queue-registry.service.ts', 'src/modules/pipeline/workers/orchestrator.worker.ts', 'src/modules/planning/planner.service.ts', 'src/modules/research/workers/section-research.worker.ts', @@ -507,26 +514,43 @@ try { 'src/modules/reports/assembly.service.ts', 'src/modules/pipeline/workers/db-sync.worker.ts', ] - const actualFlowFiles = flowResult.matched_nodes?.map((node) => node.source_file) - const expectedBoundaries = [ - 'src/modules/planning/planner.service.ts:L13-L16 -> src/modules/research/workers/section-research.worker.ts:L17-L19', - 'src/modules/research/research-agent.service.ts:L10-L14 -> src/modules/pipeline/assembly/assembly.worker.ts:L17-L19', - 'src/modules/reports/assembly.service.ts:L20-L31 -> src/modules/pipeline/workers/db-sync.worker.ts:L26-L35', - ] - const actualBoundaries = flowResult.boundaries - ?.filter((boundary) => boundary.kind === 'disconnected') - .map((boundary) => boundary.detail) + const actualFlowFiles = flowResult.dossier.evidence.files.map(({ path }) => path).sort() + const channelLinks = flowResult.dossier.flow.links.filter(({ kind }) => kind === 'channel') + const flowProofs = new Map(flowResult.dossier.evidence.proofs.map((proof) => [proof.id, proof])) + const obligationKinds = flowResult.dossier.obligations.map(({ kind }) => kind) + const hasPersistenceProof = flowResult.dossier.evidence.entities.some((entity) => + entity.kind === 'operation' && entity.operation_kind === 'persistence') if ( - JSON.stringify(actualFlowFiles) !== JSON.stringify(expectedFlowFiles) - || !expectedBoundaries.every((boundary) => actualBoundaries?.includes(boundary)) - || flowResult.relationships?.length === 0 + JSON.stringify(actualFlowFiles) !== JSON.stringify(expectedFlowFiles.sort()) + || JSON.stringify(obligationKinds) !== JSON.stringify([ + 'subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal', + ]) + || flowResult.dossier.obligations.some(({ proofs }) => proofs.length === 0) + || channelLinks.length !== 4 + || channelLinks.some(({ proofs }) => { + const relations = proofs.map((proof) => flowProofs.get(proof)?.relation) + const publishAt = relations.indexOf('publishes_to') + return publishAt < 0 + || !relations.slice(0, publishAt).every((relation) => relation === 'calls') + || ![ + JSON.stringify(['publishes_to', 'consumed_by']), + JSON.stringify(['publishes_to', 'routes_through', 'consumed_by']), + ].includes(JSON.stringify(relations.slice(publishAt))) + }) + || flowResult.dossier.flow.terminals.length === 0 + || !hasPersistenceProof || flowResult.metrics?.selected_files > 12 - || flowResult.metrics?.snippets > 25 - || flowResult.metrics?.closure_passes > 1 + || flowResult.metrics?.authenticated_excerpts > 25 + || flowResult.metrics?.root_candidates > 3 + || flowResult.metrics?.initial_candidates > 32 + || flowResult.metrics?.explored_nodes > 512 + || flowResult.metrics?.causal_hops > 24 + || flowResult.metrics?.recovery_passes > 2 + || flowResult.metrics?.recovery_frontier_nodes > 64 + || flowResult.metrics?.alternate_seeds > 3 || flowResult.metrics?.serialized_tokens > 4_000 - || flowResult.metrics?.truncated !== false ) { - throw new Error(`Packed full-flow evidence violated #622: ${JSON.stringify(flowResult)}`) + throw new Error(`Packed full-flow dossier violated #630: ${JSON.stringify(flowResult)}`) } const workerRoot = join(tempRoot, 'worker-workspace') @@ -582,7 +606,7 @@ try { method: 'tools/call', params: { name: 'retrieve', - arguments: { question: 'What is value0?' }, + arguments: { question: 'Where is value0 defined?' }, }, })}\n`) input.write(`${JSON.stringify({ jsonrpc: '2.0', id: 702, method: 'ping' })}\n`) diff --git a/docs/core-reset/removal-manifest.yml b/docs/core-reset/removal-manifest.yml index b6411b53..ba728b73 100644 --- a/docs/core-reset/removal-manifest.yml +++ b/docs/core-reset/removal-manifest.yml @@ -21,30 +21,30 @@ review: unowned_files: 0 overlapping_files: 0 disposition_changes: 11 - amendment: 'Source lists remain complete and de-overlapped. Issue #588 moved four guaranteed extraction orphans into the completed delete contract. Issue #592 transferred stage.ts, freshness.ts, and source-discovery.ts to evidence-path-query and doctor.ts to thin-delivery. Approved issues #596 and #599 combined the original 54-file / 29,441-LOC query closure with nine finalizer files / 3,590 LOC, yielding one completed 63-file / 33,031-LOC predecessor contract and 22 ownership transfers. proof-report.ts plus review-compare.ts remain move-to-delete changes; serve.ts changed from rebuild to delete, raising disposition_changes from 4 to 7. Owner-approved issue #602 removed stale thin-delivery ownership of deleted serve.ts, transferred package-metadata.ts and shell.ts from rebuild to evaluation-tooling move ownership, raising disposition_changes from 7 to 9, and absorbed the remaining non-core-graph-products and activation-and-extra-integrations production owners into one exact 16-file / 7,277-LOC thin-delivery deletion contract. PR #604 completed that contract without further ownership change. Owner-approved issue #606 transferred graph-source-root.ts and workspace-copy.ts from safe-workspace-primitives to evaluation-tooling, raising disposition_changes from 9 to 11, and activated the exact 20-file / 4,698-LOC move contract from protected base 317dda89f2ea5c75e7626a26b104ceca1bd04ce5. Governance activation merged at 452ad84890c012392c5e6af613e8bfeb17de45db without production source changes. PR #608 completed the exact move without changing any surviving production TypeScript or dependency. First-stage owner-approved issue #610 governance activation merged at dcb52596a3efa89f9ef5d372231ce97a91ae5f9f, then independent review stopped its uncommitted implementation under conditions 7, 8, and 13 before any implementation PR, campaign lock, provider request, or spend. First-stage owner-approved issue #612 authorizes only an eight-path governance-only v2 candidate from that exact merge; its separate activation merge approval remains required. It changes no production ownership or disposition. Issue #625 modifies five existing evidence-path-query production paths and changes no ownership or disposition. Graph/index generation, schemas, CLI, MCP, package dependencies, publication surfaces, and main remain frozen.' + amendment: 'Source lists remain complete and de-overlapped. Issue #588 moved four guaranteed extraction orphans into the completed delete contract. Issue #592 transferred stage.ts, freshness.ts, and source-discovery.ts to evidence-path-query and doctor.ts to thin-delivery. Approved issues #596 and #599 combined the original 54-file / 29,441-LOC query closure with nine finalizer files / 3,590 LOC, yielding one completed 63-file / 33,031-LOC predecessor contract and 22 ownership transfers. proof-report.ts plus review-compare.ts remain move-to-delete changes; serve.ts changed from rebuild to delete, raising disposition_changes from 4 to 7. Owner-approved issue #602 removed stale thin-delivery ownership of deleted serve.ts, transferred package-metadata.ts and shell.ts from rebuild to evaluation-tooling move ownership, raising disposition_changes from 7 to 9, and absorbed the remaining non-core-graph-products and activation-and-extra-integrations production owners into one exact 16-file / 7,277-LOC thin-delivery deletion contract. PR #604 completed that contract without further ownership change. Owner-approved issue #606 transferred graph-source-root.ts and workspace-copy.ts from safe-workspace-primitives to evaluation-tooling, raising disposition_changes from 9 to 11, and activated the exact 20-file / 4,698-LOC move contract from protected base 317dda89f2ea5c75e7626a26b104ceca1bd04ce5. Governance activation merged at 452ad84890c012392c5e6af613e8bfeb17de45db without production source changes. PR #608 completed the exact move without changing any surviving production TypeScript or dependency. First-stage owner-approved issue #610 governance activation merged at dcb52596a3efa89f9ef5d372231ce97a91ae5f9f, then independent review stopped its uncommitted implementation under conditions 7, 8, and 13 before any implementation PR, campaign lock, provider request, or spend. First-stage owner-approved issue #612 authorizes only an eight-path governance-only v2 candidate from that exact merge; its separate activation merge approval remains required. It changes no production ownership or disposition. Issue #625 modifies five existing evidence-path-query production paths and changes no ownership or disposition. Corrective #632 completed at c88823ecbeb6da6284cf74ecbd304e9315ffd4fa. Active #630 adds exactly three new production-source owners for its planner, workflow builder and evidence hydrator; existing adapter and query surfaces retain their historical owners. Graph/index generation, schemas, CLI, MCP, package dependencies, publication surfaces, and main remain frozen.' cancellation_amendment: 'On 2026-07-28 the owner closed Capability Validation issues #610, #612, #614, #615, and #616 as not planned and revoked every unconsumed preparation, activation, implementation, campaign, provider, spend, and target-execution authority. No campaign ran, no comparative result exists, provider requests remain zero, and spend remains USD 0. The governance-only v2 activation remains immutable history. Issue #618 is a separate bounded retrieval repair and does not revive Capability Validation or Graphify.' release_amendment: 'Historical release receipt: @lubab/madar@0.40.0-beta.3 was published under npm dist-tag next and GitHub prerelease v0.40.0-beta.3 from exact protected-next commit ece7d0d02643ecec08bd91aa904a4514aa845f42. Issue #625 and PR #626 subsequently completed the generic evidence-skeleton repair on protected next at b6562b715133304bd46e537b6f39008bc1e02095. Issue #627 then published @lubab/madar@0.40.0-beta.4 under npm dist-tag next and the matching GitHub prerelease from exact protected-next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. npm latest remains 0.32.0. Stable 0.40.0, MCP Registry publication, comparative claims, and main remain outside this release. Issue #632 authorizes no publication, release, Registry metadata, tag, or main action.' current: - updated_at: 2026-08-01 - completed_phase: retrieval-regression-625 - active_phase: semantic-execution-index-632 + updated_at: 2026-08-02 + completed_phase: semantic-execution-index-632 + active_phase: obligation-driven-retrieval-630 ready_phase: null - base_commit: 9043320cfa08370e5cdd3911bfb9283005aa9912 - completed_phase_commit: b6562b715133304bd46e537b6f39008bc1e02095 + base_commit: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa + completed_phase_commit: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa production_typescript_files: 44 - production_typescript_loc: 15719 - production_loc_added: 3462 - production_loc_removed: 197 - production_loc_net: 3265 + production_typescript_loc: 15770 + production_loc_added: 2200 + production_loc_removed: 2149 + production_loc_net: 51 npm_files: 102 - npm_packed_bytes: 149453 - npm_unpacked_bytes: 639867 - npm_shasum: 0d7e3f067d09d6db953ffc34356d2c1221cc7d08 - npm_integrity: sha512-ZC5vSq9MhdEzCmeddBn0LgTNshnN/AtHlwNhxEkeMS0eMqtyqu0haYif64lZBcI2/KS/2nLM/ksVmpjyc/BLaw== - npm_artifact_sha256: fb1aa735fc8d3eb57c5771e9bf59b20c542afa9b96d43975a737e964d40743e6 + npm_packed_bytes: 154210 + npm_unpacked_bytes: 649915 + npm_shasum: a56d34339b674a117f986f987bd192232349c077 + npm_integrity: sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ== + npm_artifact_sha256: 3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d measurement_state: source_and_package_exact - snapshot_scope: semantic_execution_index_632_candidate + snapshot_scope: obligation_driven_retrieval_630_candidate release_candidate: version: 0.40.0-beta.4 protected_anchor_commit: 9043320cfa08370e5cdd3911bfb9283005aa9912 @@ -2218,7 +2218,7 @@ items: - id: semantic-execution-index-632 disposition: keep - status: in_progress + status: complete destination: canonical JavaScript/TypeScript semantic execution index modified_sources: - src/adapters/filesystem/graph-artifact.ts @@ -2300,7 +2300,7 @@ items: coverage_lines_percent: 89.22 coverage_lines_covered: 6481 coverage_lines_total: 7264 - local_independent_review: pending_corrective_review + local_independent_review: passed pre_reopen_merge_commit: e7bd30ce384cf743dbda3e8ee7f15b171a0ea649 post_merge_acceptance_audit: failed_missing_exact_payload_and_discriminant_binding corrective_real_graph_acceptance: passed @@ -2346,8 +2346,19 @@ items: beta4_retrieval_output_byte_identical: true beta4_retrieval_output_bytes: 15294 beta4_retrieval_output_sha256: 87b4ef75473834708b20f1d2580b31470a710d797d7bdf55eee1d0876827a173 - exact_head_ci: pending - independent_review: pending + exact_head_ci: https://github.com/mohanagy/madar/actions/runs/30699876911 + independent_review: passed + completion: + pull_request: https://github.com/mohanagy/madar/pull/634 + reviewed_head: da3e1ad360855c950cae6986a9774c45fcb527d0 + merge_commit: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa + merge_tree: b715764668b4296e9e8ab4da715374f47af137db + protected_parent: e7bd30ce384cf743dbda3e8ee7f15b171a0ea649 + ci_run: https://github.com/mohanagy/madar/actions/runs/30699876911 + ci_matrix_jobs_passed: 6 + independent_review: passed + unresolved_review_threads: 0 + coderabbit: passed retrieval_budget: files_max: 12 snippets_max: 25 @@ -2365,27 +2376,211 @@ items: registry_metadata_publication: forbidden tag: forbidden main_target: forbidden - notes: 'Issue #632 extends the canonical index with authenticated ordered body facts, exact shared queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It began from exact protected next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. First PR head 9fe3c2448958c6b8cead2452758077fef093cf4e passed all six hosted jobs but independent review blocked twelve semantic-proof classes; later corrective heads c977de03ecba7958d03966df728abed9f1b36ff7 and f4ae64402d89ccf639bf698687b3767678ab708c were also stopped. PR #633 subsequently passed its gates and merged as e7bd30ce384cf743dbda3e8ee7f15b171a0ea649, but the required post-merge real-GoValidate acceptance audit found that wrapper producers did not authenticate the exact Queue.add payload position and switch consumers did not bind their parameter/property discriminant to typed case values. The stop was recorded on #632 and #577, #632 was reopened, and #630 remains dependency-frozen. This corrective candidate uses existing compact index structures to record exact dispatch_payload_argument metadata, a parameter/property discriminant path, and typed case arms; it supports direct selectors and safe immutable same-owner destructuring while reassigned, defaulted, rest, computed, dynamic, duplicate, and accessor-backed data or discriminator properties, including destructured aliases and shorthand, fail closed. The corrected real GoValidate graph proves ResearchAgentService.dispatchDbSync and AssemblyService.dispatchDbSync publish argument 2 into the shared queue channel, while DbSyncWorker.process consumes parameter 0 data.trigger with typed section_complete, assembly_complete, and status_change arms. Exact local source and package measurements remain below the unchanged +3,500-net and 102/165,000/640,000 ceilings; 247 focused assertions and 80 files / 785 full-suite tests pass with 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264); five indexing trials pass at 13.49-second median / 0.608754512635379 baseline ratio, 100 warm retrieval samples pass at 238.83405145000143 ms p95, and the corrected 60,271,172-byte graph passes. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. No retrieval-v2 work or publication is authorized.' + notes: 'Issue #632 extends the canonical index with authenticated ordered body facts, exact shared queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It began from exact protected next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. First PR head 9fe3c2448958c6b8cead2452758077fef093cf4e passed all six hosted jobs but independent review blocked twelve semantic-proof classes; later corrective heads c977de03ecba7958d03966df728abed9f1b36ff7 and f4ae64402d89ccf639bf698687b3767678ab708c were also stopped. PR #633 subsequently passed its gates and merged as e7bd30ce384cf743dbda3e8ee7f15b171a0ea649, but the required post-merge real-GoValidate acceptance audit found that wrapper producers did not authenticate the exact Queue.add payload position and switch consumers did not bind their parameter/property discriminant to typed case values. The stop was recorded on #632 and #577, then corrective PR #634 completed the missing exact payload/discriminant binding. Direct selectors and safe immutable same-owner destructuring pass; reassigned, defaulted, rest, computed, dynamic, duplicate, and accessor-backed data or discriminator properties, including destructured aliases and shorthand, fail closed. The corrected real GoValidate graph proves ResearchAgentService.dispatchDbSync and AssemblyService.dispatchDbSync publish argument 2 into the shared queue channel, while DbSyncWorker.process consumes parameter 0 data.trigger with typed section_complete, assembly_complete, and status_change arms. Exact local source and package measurements stayed below the unchanged +3,500-net and 102/165,000/640,000 ceilings; all six exact-head CI jobs, independent review, CodeRabbit and the zero-thread gate passed before protected squash merge c88823ecbeb6da6284cf74ecbd304e9315ffd4fa preserved tree b715764668b4296e9e8ab4da715374f47af137db. No retrieval-v2 work or publication was part of #632.' exit_gate: Every retained fact and exact channel edge is deterministic, source-authenticated and mutation-sensitive; false persistence/channel matches remain absent; source, graph-size, indexing, warm-retrieval, package, full-test, exact-head CI, independent-review, and zero-thread gates pass without a v2 result cutover, new dependency, publication, tag, release, Registry metadata, or main target. - id: obligation-driven-retrieval-630 disposition: keep - status: planned + status: in_progress + sources: + - src/application/evidence-hydrator.ts + - src/domain/query/plan.ts + - src/domain/query/workflow.ts destination: strict obligation-driven workflow dossier retrieval depends_on: - semantic-execution-index-632 + modified_sources: + - src/adapters/mcp/protocol.ts + - src/application/evidence-hydrator.ts + - src/application/retrieve-context.ts + - src/domain/query/plan.ts + - src/domain/query/rank.ts + - src/domain/query/slice.ts + - src/domain/query/traverse.ts + - src/domain/query/types.ts + - src/domain/query/workflow.ts + verification: + - .github/scripts/verify-packed-retrieval-parity.mjs + - tests/unit/query-plan.test.ts + - tests/unit/query-workflow.test.ts + - tests/unit/evidence-hydrator.test.ts + - tests/unit/retrieve-context.test.ts + - tests/unit/retrieve-context-proof-eviction.test.ts + - tests/unit/retrieve-v2-contract-gaps.test.ts + - tests/unit/stdio-server.test.ts + - tests/unit/benchmark.test.ts + - tests/unit/core-reset-governance.test.ts + - tools/eval/core-reset/benchmark.mjs activation: issue: https://github.com/mohanagy/madar/issues/630 - protected_base: 9043320cfa08370e5cdd3911bfb9283005aa9912 - protected_base_tree: f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc + protected_base: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa + protected_base_tree: b715764668b4296e9e8ab4da715374f47af137db target_branch: next + delivery_limits: + new_production_files_max: 3 + net_production_loc_max: 235 + total_production_loc_max: 15954 + replacement_source_loc_max: 1500 + replacement_emitted_bytes_max: 61000 + warm_retrieval_p95_ms_less_than: 500 + npm_package_budget: + files_max: 102 + packed_bytes_max: 165000 + unpacked_bytes_max: 655000 + retrieval_budget: + files_max: 12 + snippets_max: 25 + serialized_tokens_max: 4000 + roots_max: 3 + initial_candidates_max: 32 + explored_nodes_max: 512 + causal_hops_max: 24 + recovery_passes_max: 2 + recovery_frontier_nodes_total_max: 64 + alternate_seeds_max: 3 + result_contract: + schema: madar.retrieve + version: 2 + public_input_change: forbidden + states: + - ready + - incomplete + - unsupported + - stale + - unavailable + - corrupt + ready_requires_every_mandatory_obligation: true + ready_requires_adjacent_handoffs_terminal_and_proofs: true + ready_may_be_truncated: false + candidate: + source_measurement: + production_typescript_files: 44 + production_typescript_loc: 15770 + added: 2200 + removed: 2149 + net: 51 + diff_sha256: 3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a + replacement_measurement: + source_loc: 1384 + emitted_bytes: 59896 + package_measurement: + files: 102 + packed_bytes: 154210 + unpacked_bytes: 649915 + shasum: a56d34339b674a117f986f987bd192232349c077 + integrity: sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ== + artifact_sha256: 3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d + local_verification: + source_gate: passed + replacement_source_loc_gate: passed + replacement_gate: passed + package_gate: passed + focused_test_files_passed: 4 + focused_tests_passed: 159 + typecheck: passed + build: passed + build_eval: passed + governance_tests_passed: 18 + packed_retrieval_parity: passed + release_verify: passed + registry_validate: passed + isolation_verify: passed + npm_audit_high: passed + ci_eval_regression: passed + ci_eval_regression_questions_passed: 5 + ci_eval_regression_questions_total: 5 + ci_eval_regression_recall_percent: 95 + ci_eval_regression_mrr: 1 + ci_eval_regression_snippet_precision_percent: 100 + ci_eval_regression_grounded_percent: 95 + final_full_suite: passed + full_test_files_passed: 83 + full_tests_passed: 865 + coverage: passed + coverage_statements_percent: 85.47 + coverage_statements_covered: 7696 + coverage_statements_total: 9004 + coverage_branches_percent: 79.76 + coverage_branches_covered: 7169 + coverage_branches_total: 8988 + coverage_functions_percent: 91.91 + coverage_functions_covered: 1341 + coverage_functions_total: 1459 + coverage_lines_percent: 88.97 + coverage_lines_covered: 6430 + coverage_lines_total: 7227 + frozen_govalidate_acceptance: passed + frozen_govalidate: + prompts: 5 + ready_results: 5 + files_per_result: 9 + excerpts_per_result: 12 + links_per_result: 12 + order_groups_per_result: 15 + entities_per_result: 21 + proofs_per_result: 20 + required_queues: + - orchestration-queue + - section-research-queue + - assembly-queue + - db-sync-queue + serialized_tokens: + - 3977 + - 3977 + - 3979 + - 3977 + - 3977 + frozen_govalidate_all_variants: + prompts: 14 + ready_results: 14 + serialized_tokens_min: 3965 + serialized_tokens_max: 3998 + all_four_required_queues: true + flow_sha256: c34295432be3a54ce7506c2019fd17f3e2ca631c78d578b696234cf981c8592b + evidence_sha256: d52e4db48b4a5346b3cf54113406b37894f27c04288561d5fe1286050bde848d + warm_retrieval_reference_gate: passed + warm_retrieval_reference: + warmups: 3 + measured_queries: 100 + median_ms: 50.795208 + p95_ms: 53.452208 + max_ms: 56.98625 + independent_review: pending + exact_head_ci: pending + historical_stop: + receipt: https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732 + reason: replacement_emitted_and_package_unpacked_ceilings_failed + frozen_local_candidate_only: true + branch_pushed: false + pull_request_opened: false + protected_next_changed: false + merged: false + npm_published: false + github_release_created: false + registry_metadata_published: false + tag_created: false + main_targeted: false + amendment: + receipt: https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 + supersedes_stop_receipt: https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732 + state: active_authorized + replacement_emitted_bytes_max: 61000 + npm_unpacked_bytes_max: 655000 + all_other_metrics_and_constraints: unchanged constraints: - work_before_dependency_completion: forbidden - publication: forbidden + compatibility_fallback: forbidden + repository_specific_rule: forbidden + query_specific_rule: forbidden + second_retrieval_engine: forbidden + dependency_change: forbidden + package_ceiling_change: forbidden + graph_index_or_query_semantics_widening: forbidden + npm_publication: forbidden + github_release: forbidden tag: forbidden registry_metadata_publication: forbidden main_target: forbidden - notes: 'Issue #630 is pending behind #632. It owns question obligations, bounded recovery, answerability and the retrieve-result v2 dossier cutover; none of that work is activated by #632.' + notes: 'Issue #630 began from exact protected next commit c88823ecbeb6da6284cf74ecbd304e9315ffd4fa and tree b715764668b4296e9e8ab4da715374f47af137db after #632 completed. The current local candidate replaces rank/slice/traverse with one obligation planner, workflow builder and authenticated evidence hydrator, then returns a deterministic v2 dossier only when every mandatory stage, adjacent async handoff, terminal action and proof is complete. Its source inventory and delta pass. All five field-incident prompts return ready dossiers covering 9 files, 12 excerpts, 12 links, 15 order groups, 21 entities, 20 proofs and all four required queues at 3977 / 3977 / 3979 / 3977 / 3977 tokens; all 14 frozen GoValidate formulations are ready at 3965-3998 tokens with all four queues and stable flow/evidence hashes. The portable real-GoValidate runner passes 14/14 and 100 warm measurements at median 50.795208 ms, p95 53.452208 ms and max 56.98625 ms. The 4-file focused suite passes 159 tests, the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines, and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The replacement trio is 1384 source LOC / 59896 emitted bytes under the owner-amended 61000-byte ceiling; the exact package is 102 files / 154210 packed / 649915 unpacked bytes under the owner-amended 655000-byte unpacked ceiling. The historical stop receipt is https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732; owner amendment https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 supersedes that stop and authorizes active continuation while changing only those two ceilings. All other metrics, constraints and prohibitions remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag or main action is authorized.' exit_gate: Every mandatory question obligation is proven and packed into one non-truncated ready dossier, or the result returns the exact non-ready state and missing obligations within the unchanged file, excerpt, token, recovery, package, and latency ceilings. - id: no-fallback-qualification-631 @@ -2409,7 +2604,7 @@ items: registry_metadata_publication: forbidden tag: forbidden main_target: forbidden - notes: 'Issue #631 is pending behind #632 and #630. It will compare an installed reviewed candidate with 0.32.0, 0.40.0-beta.4, one pinned Graphify commit and a no-tool control. No comparative claim or provider campaign exists yet.' + notes: 'Issue #632 is complete; issue #631 remains pending behind active #630. It will compare an installed reviewed candidate with 0.32.0, 0.40.0-beta.4, one pinned Graphify commit and a no-tool control. No comparative claim or provider campaign exists yet.' exit_gate: The installed exact-head package matches or beats the strongest frozen baseline, scores at least 90 mean with no run below 85 or critical error, makes one Madar retrieval with zero repository-tool fallback in natural-client runs, and passes closed-book, parity, budget, CI, independent-review, and zero-thread gates. - id: non-core-graph-products diff --git a/docs/core-reset/scorecard.md b/docs/core-reset/scorecard.md index b5c5f356..84635e1d 100644 --- a/docs/core-reset/scorecard.md +++ b/docs/core-reset/scorecard.md @@ -2,7 +2,7 @@ > **RFC:** [#577](https://github.com/mohanagy/madar/issues/577) > **Milestone:** [`v0.40.0 — Core Reset`](https://github.com/mohanagy/madar/milestone/7) -> **Status:** accepted; the product vertical slice through Evaluation Tooling Isolation and retrieval regressions #618, #622, and #625 passed; `0.40.0-beta.4` is published from exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; semantic execution index #632 is active, #630 and #631 are pending in dependency order; npm `latest` remains `0.32.0`; another beta, stable release, MCP Registry publication, and `main` remain unauthorized +> **Status:** accepted; the product vertical slice through Evaluation Tooling Isolation and retrieval regressions #618, #622, and #625 passed; `0.40.0-beta.4` is published from exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; semantic execution index #632 completed at `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa`; owner amendment [#630](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes its local stop and reactivates obligation-driven retrieval under exactly two amended ceilings, while #631 remains pending behind it; npm `latest` remains `0.32.0`; another beta, stable release, MCP Registry publication, and `main` remain unauthorized This is the phase-gate evidence ledger. An issue or PR link is not evidence by itself; each gate needs a reproducible test, receipt, measurement, or external-user record. @@ -43,14 +43,14 @@ The schema-validated, share-safe receipt was recorded at tooling checkout `250a6 | Retrieval regression #618 | **Passed** | Restore grounded natural-flow retrieval in one call or at most one bounded recovery without repository-specific rules, graph/index changes, dependencies, or fallback engines | [#618](https://github.com/mohanagy/madar/issues/618) completed through [PR #620](https://github.com/mohanagy/madar/pull/620), merged at `eaa1a8781eda28dad5395d6da378a2cc40bf81fe`; all six exact-head CI jobs, two independent no-blocker reviews, and zero review threads passed | | Retrieval regression #622 | **Passed** | Stabilize equivalent end-to-end report-flow prompts and expose honest asynchronous handoff targets within the unchanged retrieval and package ceilings | [#622](https://github.com/mohanagy/madar/issues/622) completed through [PR #623](https://github.com/mohanagy/madar/pull/623), merged at `6416dbc02cefb3bd79157cf440e420b30dda8cf0`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30452883659), two exact-head no-blocker reviews, CodeRabbit PASS, and zero unresolved threads | | Retrieval regression #625 | **Passed** | Replace phrase-gated recovery with a generic bounded, graph-coherent evidence skeleton/forest without exceeding the inherited package ceilings | [#625](https://github.com/mohanagy/madar/issues/625) completed through [PR #626](https://github.com/mohanagy/madar/pull/626), merged at `b6562b715133304bd46e537b6f39008bc1e02095`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30533140531), independent exact-head review, CodeRabbit PASS, and zero unresolved threads | -| Semantic execution index #632 | **Reopened — corrective work in progress** | Authenticated ordered body facts, exact async channels and receiver/type-proven persistence pass every source, graph, indexing, latency, package, CI, review and zero-thread gate | [#632](https://github.com/mohanagy/madar/issues/632); PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, then the mandatory real-GoValidate audit exposed missing exact payload/discriminant binding; the correction is locally proven while fresh full-suite/review/CI/merge gates remain pending | -| Obligation-driven retrieval #630 | **Pending** | Return a complete authenticated workflow dossier or exact missing obligations within unchanged budgets | [#630](https://github.com/mohanagy/madar/issues/630); blocked on #632 | -| No-fallback qualification #631 | **Pending** | Installed exact-head package matches or beats the strongest frozen baseline and requires zero repository-tool fallback | [#631](https://github.com/mohanagy/madar/issues/631); blocked on #632 and #630 | +| Semantic execution index #632 | **Passed** | Authenticated ordered body facts, exact async channels and receiver/type-proven persistence pass every source, graph, indexing, latency, package, CI, review and zero-thread gate | [#632](https://github.com/mohanagy/madar/issues/632); corrective [PR #634](https://github.com/mohanagy/madar/pull/634) passed all six CI jobs, independent review, CodeRabbit and zero unresolved threads, then merged as `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` with tree `b715764668b4296e9e8ab4da715374f47af137db` | +| Obligation-driven retrieval #630 | **In progress** | Return a complete authenticated workflow dossier or exact missing obligations within amended budgets | [#630 amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes only replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; exact source/package measurements, focused 159/159, full 865/865, coverage, 14/14 real-GoValidate, 100-sample warm p95, parity, release, registry, isolation, audit, typecheck/build/build-eval, and governance gates pass; review, exact-head CI and merge remain pending | +| No-fallback qualification #631 | **Pending** | Installed exact-head package matches or beats the strongest frozen baseline and requires zero repository-tool fallback | [#631](https://github.com/mohanagy/madar/issues/631); blocked on completion of active #630 | | External validation | **Deferred** | Activation, retention, and paid-intent evidence remains required for later stable claims, not this beta | No external-validation claim in `0.40.0-beta.4` | -| Beta release | **Published** | Preserve exact beta.4 npm/GitHub release history; any later beta requires separate authorization after #632, #630 and #631 gates | [#627](https://github.com/mohanagy/madar/issues/627); exact commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; package 102 / 159,937 / 639,875; npm `next` is `0.40.0-beta.4`, npm `latest` is `0.32.0` | +| Beta release | **Published** | Preserve exact beta.4 npm/GitHub release history; any later beta requires separate authorization after #630 and #631 gates | [#627](https://github.com/mohanagy/madar/issues/627); exact commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; package 102 / 159,937 / 639,875; npm `next` is `0.40.0-beta.4`, npm `latest` is `0.32.0` | | Stable release | Not started | Every separately retained stable gate passed; old core absent; migration docs ready | Pending; the beta does not satisfy this gate | -Issues `#622` and `#625` are complete on `next`. Evaluation Tooling Isolation completed through #606 and PR #608 at 43 production files / 11,956 LOC; #618 completed at 43 production files / 12,008 LOC with `+69/-17/net +52`; #622 completed at 43 production files / 12,147 LOC with `+164/-25/net +139`; and #625 completed at 43 production files / 12,454 LOC with `+1,409/-1,102/net +307` against its protected base. Beta.4 is immutable published history at exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`. Capability Validation issues #610, #612, #614, #615, and #616 are closed not planned: no campaign ran, provider requests remain zero, and paid spend remains USD 0. The new work is dependency ordered: #632 active, then #630 pending, then #631 pending. +Issues `#622`, `#625`, and `#632` are complete on `next`. Evaluation Tooling Isolation completed through #606 and PR #608 at 43 production files / 11,956 LOC; #618 completed at 43 production files / 12,008 LOC with `+69/-17/net +52`; #622 completed at 43 production files / 12,147 LOC with `+164/-25/net +139`; and #625 completed at 43 production files / 12,454 LOC with `+1,409/-1,102/net +307` against its protected base. Beta.4 is immutable published history at exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`. Capability Validation issues #610, #612, #614, #615, and #616 are closed not planned: no campaign ran, provider requests remain zero, and paid spend remains USD 0. Owner amendment [#630](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes its historical stop and reactivates it under exactly two amended ceilings; #631 remains pending behind active #630. ### Directed multigraph phase evidence (passed) @@ -196,23 +196,24 @@ The following contract facts are historical. Issues #610 and #612, together with - The published npm 12.0.1 artifact is 102 files / 159,937 packed / 639,875 unpacked bytes with shasum `c5250a0d308b3d6df374851154ddb393a678a992`, integrity `sha512-772P+n4Cx55nqC+CAx8A1aTJ2rY4yk1hUH45lAlxNMMw4YRj8hhswgDiCwczS5hx1S3a+Z+KUv2jma/zWjQZ6w==`, and tarball SHA-256 `8bd8d501b8cd3546e16a5a1ddac1f7649434e685517e1171fbd5897515e76e6b`. - npm `latest` remains `0.32.0`. Another beta, stable release, MCP Registry publication, new comparative claims, and `main` remain out of scope without separate authority. -### Semantic execution index #632 (in progress) +### Semantic execution index #632 (passed) - Protected base and target are exact `next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`, tree `f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc`, and protected branch `next`; the issue is [#632](https://github.com/mohanagy/madar/issues/632). - The exact allowed production paths are `src/adapters/filesystem/graph-artifact.ts`, `src/adapters/mcp/server.ts`, `src/adapters/typescript/execution.ts`, `src/adapters/typescript/index.ts`, `src/application/retrieve-context.ts`, `src/domain/index/build-state.ts`, `src/domain/index/model.ts`, `src/domain/query/index-status.ts`, and `src/domain/query/rank.ts`. - Delivery is blocked above four new production files, 3,500 net new production lines, 1.5x the beta.4 GoValidate graph size, 1.25x the beta.4 same-machine indexing median, or warm retrieval p95 greater than or equal to 500 ms. Package ceilings remain 102 files / 165,000 packed / 640,000 unpacked bytes. - The package whitelist may remove only `examples/why-madar.md` and `CHANGELOG.md`; the repository files remain present, and version, scripts, dependencies, package lock, publication and public surface cannot change. - #632 owns authenticated ordered body facts, exact queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. Retrieval-result v2, obligation planning, response dossier generation, comparator claims, provider activity, npm publication, GitHub Release, Registry metadata, tags, and `main` are outside this phase. -- Earlier stopped heads remain immutable history. PR #633 later passed its gates and merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit exposed a narrower acceptance gap: wrapper publishers did not authenticate the exact `Queue.add` payload argument and switch consumers did not bind a parameter/property selector to typed case values. #632 was reopened, the stop was recorded on #632 and RFC #577, and #630 remains frozen. +- Earlier stopped heads remain immutable history. PR #633 later passed its gates and merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit exposed a narrower acceptance gap: wrapper publishers did not authenticate the exact `Queue.add` payload argument and switch consumers did not bind a parameter/property selector to typed case values. #632 was reopened and the stop was recorded on #632 and RFC #577. - The new correction records `dispatch_payload_argument`, parameter/property discriminant paths, and typed case arms using existing compact index structures. Direct selectors and safe immutable same-owner destructuring pass; reassigned, defaulted, rest, computed, dynamic, duplicate, and accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed. The frozen GoValidate graph now proves argument 2 for both `ResearchAgentService.dispatchDbSync` and `AssemblyService.dispatchDbSync`, and parameter 0 `data.trigger` with typed `section_complete`, `assembly_complete`, and `status_change` cases in `DbSyncWorker.process`. -- The correction measures 44 production files / 15,719 LOC at `+3,462/-197/net +3,265`, below the unchanged net `+3,500` ceiling; 102 package files / 149,453 packed / 639,867 unpacked bytes; and a 60,271,172-byte real GoValidate graph, ratio `1.2292551823152718`, with 12,313 nodes / 32,717 edges / six exact queue channels / 42 typed channel edges. Its graph SHA-256 is `569af2dcd681c4db48124a47bceac7036a94b344f2a2f88e8cb35f6120711610`. Focused verification passes 247/247; the coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). The frozen corpus is attested as 6,889 files / 177,796,450 bytes / SHA-256 `3fa9a0a3edc13cf1439d572292601fff43c43a94f7a50948349f9a69123fa7a7`; five fresh indexing trials pass at 13.49-second median / `0.608754512635379` baseline ratio, and 100 warm retrieval samples pass at 238.83405145000143 ms p95. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. -- These are local corrective-candidate measurements, not a final receipt. Corrected-head commit/tree, all-six CI, independent no-blocker review, CodeRabbit completion, zero unresolved threads, merge commit, and publication remain open until those exact gates pass. +- The correction measures 44 production files / 15,719 LOC at `+3,462/-197/net +3,265`, below the unchanged net `+3,500` ceiling; 102 package files / 149,453 packed / 639,867 unpacked bytes; and a 60,271,172-byte real GoValidate graph, ratio `1.2292551823152718`, with 12,313 nodes / 32,717 edges / six exact queue channels / 42 typed channel edges. Its graph SHA-256 is `569af2dcd681c4db48124a47bceac7036a94b344f2a2f88e8cb35f6120711610`. Focused verification passes 247/247; the coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). The frozen corpus is attested as 6,889 files / 177,796,450 bytes / SHA-256 `3fa9a0a3edc13cf1439d572292601fff43c43a94f7a50948349f9a69123fa7a7`; five fresh indexing trials pass at 13.49-second median / `0.608754512635379` baseline ratio, and 100 warm retrieval samples pass at 238.83405145000143 ms p95. +- Corrective reviewed head `da3e1ad360855c950cae6986a9774c45fcb527d0` passed all six [exact-head CI jobs](https://github.com/mohanagy/madar/actions/runs/30699876911), independent review, CodeRabbit and zero unresolved threads. Protected squash merge `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` preserved tree `b715764668b4296e9e8ab4da715374f47af137db`. No retrieval-v2 or publication work was part of #632. -### Successors #630 and #631 (pending) +### Obligation-driven retrieval #630 (active) and #631 (pending) -- [#630](https://github.com/mohanagy/madar/issues/630) is blocked on #632 and owns explicit obligations, bounded recovery, strict answerability and the `madar.retrieve` v2 dossier. -- [#631](https://github.com/mohanagy/madar/issues/631) is blocked on #632 and #630 and owns installed-package parity plus the no-fallback comparison against `0.32.0`, `0.40.0-beta.4`, one pinned Graphify commit and a no-tool control. -- Neither pending issue authorizes provider traffic or spend, npm publication, GitHub Release, Registry metadata, tags, stable/`latest`, or `main`; any real campaign or beta publication requires separate owner authorization. +- [#630](https://github.com/mohanagy/madar/issues/630) starts from protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit obligations, bounded recovery, strict answerability and the `madar.retrieve` v2 dossier. +- The active #630 candidate measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`; its replacement planner, workflow and hydrator total 1,384 source LOC / 59,896 emitted bytes, and its exact package is 102 files / 154,210 packed / 649,915 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732), changing only the replacement emitted ceiling from 58,000 to 61,000 bytes and npm unpacked ceiling from 640,000 to 655,000 bytes; both gates now pass. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. All 14 frozen GoValidate formulations are ready at 3,965-3,998 tokens with all four queues, and 100 warm samples pass at 53.452208 ms p95. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending. No publication, release, Registry metadata, tag, or `main` action is authorized. +- [#631](https://github.com/mohanagy/madar/issues/631) remains pending until active #630 completes. Its installed-package and no-fallback comparison has not started. +- Neither issue authorizes provider traffic or spend, npm publication, GitHub Release, Registry metadata, tags, stable/`latest`, or `main`; any real campaign or beta publication requires separate owner authorization. ## Graph gates diff --git a/docs/designs/2026-07-19-core-reset.md b/docs/designs/2026-07-19-core-reset.md index 51b9c425..f0d2f3a9 100644 --- a/docs/designs/2026-07-19-core-reset.md +++ b/docs/designs/2026-07-19-core-reset.md @@ -471,9 +471,9 @@ The CLI remains narrow and lazy-loaded: These six names are an allowlist, not a quota. Existing names are preserved only when their meaning remains valid; removed names do not survive as aliases. -## 2026-07-31 semantic execution correction +## Completed amendment — semantic execution correction #632 -Issue #632 is the prerequisite index layer for obligation-driven retrieval. It authenticates operations inside function bodies, their numeric order and control context, shared queue/job/event channels, bounded wrapper substitution, concurrency groups, and receiver/type-proven persistence. Retrieval planning and the v2 response dossier remain owned by #630 and cannot begin while #632 is open. +Issue #632 is the prerequisite index layer for obligation-driven retrieval. It authenticates operations inside function bodies, their numeric order and control context, shared queue/job/event channels, bounded wrapper substitution, concurrency groups, and receiver/type-proven persistence. Retrieval planning and the v2 response dossier remain owned by #630. PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, then the required real-GoValidate audit exposed an incomplete producer/consumer join. A wrapper publisher proved the queue channel but not which outer argument became the channel payload; a switch consumer proved cases but not the parameter/property selector consumed by those cases. #632 was reopened and #630 was dependency-stopped. @@ -484,7 +484,17 @@ The correction adds no schema version, compatibility alias, dependency, reposito - typed case values, preserving the distinction between values such as string `"1"` and number `1`; and - an evidence range that includes safe destructuring aliases, so later mutation invalidates the proof. -Direct selectors and immutable same-owner object destructuring are accepted. Reassignment, defaults, rest bindings, computed properties, dynamic cases, and duplicate typed cases fail closed. Accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—also fail closed. On the frozen GoValidate corpus the corrected graph joins both db-sync publishers at argument 2 to the consumer selector parameter 0 `data.trigger`, including typed `section_complete`, `assembly_complete`, and `status_change` arms. The coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). #632 completes only after the corrective exact head passes every source, package, graph-size, performance, full-suite, independent-review, CI, and zero-thread gate. +Direct selectors and immutable same-owner object destructuring are accepted. Reassignment, defaults, rest bindings, computed properties, dynamic cases, and duplicate typed cases fail closed. Accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—also fail closed. On the frozen GoValidate corpus the corrected graph joins both db-sync publishers at argument 2 to the consumer selector parameter 0 `data.trigger`, including typed `section_complete`, `assembly_complete`, and `status_change` arms. The coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). Corrective reviewed head `da3e1ad360855c950cae6986a9774c45fcb527d0` passed every source, package, graph-size, performance, full-suite, independent-review, six-job CI, CodeRabbit and zero-thread gate. Protected squash merge `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` preserved tree `b715764668b4296e9e8ab4da715374f47af137db`. + +## Active amendment — obligation-driven retrieval #630 + +Issue #630 starts from exact protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. The public MCP input remains `{ question, budget? }`. Internally, the question planner produces mandatory obligations; the workflow builder resolves up to three roots through a bounded graph search and at most two bounded recovery passes; and the evidence hydrator authenticates exact source ranges, hashes, calls and registrations before response packing. + +The `madar.retrieve` v2 result has six closed states: `ready`, `incomplete`, `unsupported`, `stale`, `unavailable`, and `corrupt`. `ready` is legal only when every mandatory obligation, adjacent handoff, terminal action and proof is complete. A ready result is never truncated. Non-ready results return exact missing obligations rather than silently falling back or forcing a broad restart. Stable deterministic claim templates point into deduplicated file, excerpt and proof tables and preserve directed flow plus sequential, conditional, parallel and repeated order. + +The unchanged public ceilings are 4,000 serialized tokens, 12 files and 25 excerpts. Planning is bounded to three roots, 32 initial candidates, 512 explored nodes and 24 causal hops; recovery is bounded to two passes, 64 total frontier nodes and three alternate seeds. The replacement planner, workflow builder and hydrator must stay at or below 1,500 source LOC and 61,000 emitted bytes, total production source at or below 15,954 LOC, the package at or below 102 files / 165,000 packed / 655,000 unpacked bytes, and warm loaded-graph p95 strictly below 500 ms. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes exactly those two ceilings; every other metric, constraint and prohibition remains unchanged. + +The active local candidate measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`. Its replacement trio measures 1,384 source LOC / 59,896 emitted bytes, and the exact package measures 102 files / 154,210 packed / 649,915 unpacked bytes; both pass the amended ceilings. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The portable real-GoValidate runner returns 14/14 ready dossiers at 3,965-3,998 tokens with all four required queues and passes 100 warm samples at 53.452208 ms p95. The owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732). Independent review, exact-head CI, CodeRabbit, zero-thread and protected merge remain pending. No fallback engine, dependency change, provider traffic, npm publication, GitHub Release, Registry metadata, tag, or `main` is authorized. ## Migration and compatibility diff --git a/docs/roadmap.md b/docs/roadmap.md index d81636f5..36bbfabd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -11,7 +11,7 @@ Madar is executing an accepted Core Reset. The roadmap is outcome-driven: work a - [Removal manifest](core-reset/removal-manifest.yml) — keep, rebuild, move, delete, and defer decisions - [Scorecard](core-reset/scorecard.md) — technical and business evidence gates -The RFC is **accepted**. Scope and baseline, Directed multigraph, Canonical TypeScript/JavaScript index, the combined legacy/non-code deletion, Generation and reconciliation, Evidence-path query, Thin Delivery, Evaluation Tooling Isolation, and the bounded retrieval repairs in [#618](https://github.com/mohanagy/madar/issues/618), [#622](https://github.com/mohanagy/madar/issues/622), and [#625](https://github.com/mohanagy/madar/issues/625) have passed. Capability Validation and the earlier Native-vs-Graphify campaign are cancelled history. `0.40.0-beta.4` is published from exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; npm `latest` remains `0.32.0`. The semantic execution program is now dependency-ordered as active [#632](https://github.com/mohanagy/madar/issues/632), pending [#630](https://github.com/mohanagy/madar/issues/630), then pending [#631](https://github.com/mohanagy/madar/issues/631). Another beta, stable release, MCP Registry publication, and `main` remain unauthorized. +The RFC is **accepted**. Scope and baseline, Directed multigraph, Canonical TypeScript/JavaScript index, the combined legacy/non-code deletion, Generation and reconciliation, Evidence-path query, Thin Delivery, Evaluation Tooling Isolation, the bounded retrieval repairs in [#618](https://github.com/mohanagy/madar/issues/618), [#622](https://github.com/mohanagy/madar/issues/622), and [#625](https://github.com/mohanagy/madar/issues/625), and semantic execution index [#632](https://github.com/mohanagy/madar/issues/632) have passed. Capability Validation and the earlier Native-vs-Graphify campaign are cancelled history. `0.40.0-beta.4` is published from exact protected-`next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; npm `latest` remains `0.32.0`. Owner amendment [#630](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes its local stop and reactivates obligation-driven retrieval under exactly two amended ceilings; [#631](https://github.com/mohanagy/madar/issues/631) remains pending behind it. Another beta, stable release, MCP Registry publication, and `main` remain unauthorized. ## Passed — directed multigraph @@ -163,21 +163,23 @@ The release completed from exact protected `next` commit `9043320cfa08370e5cdd39 The published npm artifact is 102 files / 159,937 packed / 639,875 unpacked bytes under the unchanged 102 / 165,000 / 640,000 ceilings. npm `latest` remains `0.32.0`; stable release, MCP Registry publication, new comparative claims, and `main` remain out of scope. -## In progress — semantic execution index #632 +## Completed — semantic execution index #632 [#632](https://github.com/mohanagy/madar/issues/632) starts from exact protected `next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912` and tree `f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc`, and its PR target is `next`. It adds compact authenticated body facts, numeric order and control, exact queue/job/event channel topology, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It does not cut retrieval output to v2. The active delivery limits are no more than four new production files, no more than 3,500 net new production lines, no more than 1.5x the beta.4 GoValidate graph size, indexing median no slower than 1.25x beta.4 on the same machine, warm retrieval p95 strictly below 500 ms, and the unchanged package ceilings of 102 files / 165,000 packed / 640,000 unpacked bytes. No dependency, provider activity, publication, GitHub Release, Registry metadata, tag, or `main` action is authorized. -PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit found that wrapper producers lacked authenticated exact payload positions and switch consumers lacked an authenticated parameter/property-to-typed-case binding. #632 was reopened and #630 remains frozen. The local correction now proves `dispatch_payload_argument: 2` for both GoValidate db-sync publishers and parameter 0 `data.trigger` with typed terminal cases for the consumer. It remains below the unchanged source, package, and graph-size ceilings; focused verification passes 247/247, and the coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). Five fresh indexing trials pass at 13.49-second median / `0.608754512635379` baseline ratio, and 100 warm retrieval samples pass at 238.83405145000143 ms p95. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. +PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit found that wrapper producers lacked authenticated exact payload positions and switch consumers lacked an authenticated parameter/property-to-typed-case binding. #632 was reopened. Corrective [PR #634](https://github.com/mohanagy/madar/pull/634) proved `dispatch_payload_argument: 2` for both GoValidate db-sync publishers and parameter 0 `data.trigger` with typed terminal cases for the consumer. Reviewed head `da3e1ad360855c950cae6986a9774c45fcb527d0` passed the unchanged source, package, graph-size, indexing, warm-p95, full-suite, independent-review, six-job CI, CodeRabbit and zero-thread gates. Protected squash merge `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` preserved tree `b715764668b4296e9e8ab4da715374f47af137db`. -## Pending — obligation-driven retrieval #630 +## In progress — obligation-driven retrieval #630 -[#630](https://github.com/mohanagy/madar/issues/630) starts only after #632 completes. It owns explicit question obligations, graph-coherent workflow construction, at most two bounded recovery passes, exact-range hydration, strict answerability, and the `madar.retrieve` v2 dossier. It retains the 4,000-token / 12-file / 25-excerpt ceilings and cannot publish. +[#630](https://github.com/mohanagy/madar/issues/630) starts from exact protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit question obligations, graph-coherent workflow construction, at most two bounded recovery passes, exact-range hydration, strict answerability, and the `madar.retrieve` v2 dossier. It retains the 4,000-token / 12-file / 25-excerpt ceilings and cannot publish. + +The active local candidate replaces the v1 rank/slice/traverse pipeline with one planner, workflow builder and authenticated hydrator. It measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`. The replacement trio is 1,384 source LOC / 59,896 emitted bytes, and the exact package is 102 files / 154,210 packed / 649,915 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732) and changes exactly replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; both gates now pass. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. All 14 frozen GoValidate formulations are ready at 3,965-3,998 tokens with all four queues, and 100 warm samples pass at 53.452208 ms p95. All other metrics and constraints remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag, or `main` action is authorized. ## Pending — installed no-fallback qualification #631 -[#631](https://github.com/mohanagy/madar/issues/631) starts only after #632 and #630 complete. It owns installed-package parity and the frozen comparison against `0.32.0`, `0.40.0-beta.4`, one pinned Graphify commit, and a no-tool control. It requires one Madar retrieval and zero repository Read/Grep/Glob/Bash fallback calls in natural-client candidate runs. No provider traffic, spend, beta publication, release, Registry metadata, tag, stable/`latest`, or `main` authority exists without a separate owner authorization. +[#631](https://github.com/mohanagy/madar/issues/631) remains pending until active #630 completes. Its installed-package parity and frozen comparison against `0.32.0`, `0.40.0-beta.4`, one pinned Graphify commit, and a no-tool control have not started. No provider traffic, spend, beta publication, release, Registry metadata, tag, stable/`latest`, or `main` authority exists without a separate owner authorization. ## Validation — release decision diff --git a/src/adapters/mcp/protocol.ts b/src/adapters/mcp/protocol.ts index 26832674..a0708d76 100644 --- a/src/adapters/mcp/protocol.ts +++ b/src/adapters/mcp/protocol.ts @@ -52,7 +52,7 @@ export const MCP_TOOLS: readonly McpToolDefinition[] = Object.freeze([ Object.freeze({ name: 'retrieve', description: - 'Return one deterministic authenticated answer dossier, or exact missing requirements, for a TypeScript or JavaScript codebase question.', + 'Return an authenticated answer dossier or exact gaps.', inputSchema: Object.freeze({ type: 'object', additionalProperties: false, @@ -62,7 +62,7 @@ export const MCP_TOOLS: readonly McpToolDefinition[] = Object.freeze([ type: 'string', minLength: 1, maxLength: MAX_RETRIEVE_QUESTION_LENGTH, - description: 'A locate, explain, or workflow question to prove from the indexed graph.', + description: 'A locate, explain, or workflow question.', }), budget: Object.freeze({ type: 'integer', @@ -135,7 +135,7 @@ export async function handleMcpProtocolRequest( version: context.version, }, instructions: - 'Call retrieve once with the codebase question. State ready contains a complete authenticated dossier; every other state names the exact missing or terminal condition. Do not infer omitted workflow steps.', + 'Call once. Ready is complete; otherwise report its exact condition.', }) case 'ping': return success(id, {}) diff --git a/src/application/evidence-hydrator.ts b/src/application/evidence-hydrator.ts index 72501162..4bcab413 100644 --- a/src/application/evidence-hydrator.ts +++ b/src/application/evidence-hydrator.ts @@ -1,375 +1,229 @@ -import { createHash } from 'node:crypto' import { isUtf8 } from 'node:buffer' +import { createHash } from 'node:crypto' import { readFileSync, realpathSync } from 'node:fs' import { isAbsolute, relative, resolve, sep } from 'node:path' -import type { GraphAttributes } from '../domain/graph/directed-multigraph.js' -import type { - IndexBodyFact, IndexChannelNode, IndexRange, IndexValue, -} from '../domain/index/model.js' +import type { IndexBodyFact, IndexRange } from '../domain/index/model.js' import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' -import { - type EvidenceHydrationTargets, type HydratedEntity, - type HydratedEvidenceResult, type HydratedExcerpt, type HydratedFile, - type HydratedProof, type SelectedEvidenceEdge, +import type { + EvidenceHydrationTargets, HydratedControl, HydratedEntity, HydratedEvidenceResult, + HydratedExcerpt, HydratedFile, HydratedProof, SelectedEvidenceEdge, } from '../domain/query/types.js' + type Failure = Extract -type ReadySource = [ - path: string, sha256: string, text: string, - starts: readonly number[], ends: readonly number[], file: string, -] -type FactProof = [owner: string, excerpt: string] -type CallFact = Extract -type EdgeRow = readonly [from: string, to: string, attrs: GraphAttributes, id: string] -const SHA = /^[a-f0-9]{64}$/ -const channelFields: readonly (keyof IndexChannelNode)[] = [ - 'channel_kind', 'transport', 'key', 'parent_channel_id', 'scope', -] -const compare = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0 +type Source = [string, string, string, number[], number[], string] +type Call = Extract class Halt { constructor(readonly value: Failure) {} } -function halt(state: Failure['state'], subject: string): never { - throw new Halt({ state, subject }) -} -function corrupt(subject: string): never { halt('corrupt', subject) } -const nonEmpty = (value: unknown): value is string => - typeof value === 'string' && value.length > 0 && !value.includes('\0') -const populated = (value: unknown): boolean => - Array.isArray(value) && value.length > 0 -const orderPos = ( - left: IndexRange['start'], right: IndexRange['start'], -): number => left.line - right.line || left.column - right.column -function range(value: unknown): value is IndexRange { - if (!value || typeof value !== 'object') return false - const candidate = value as IndexRange - return [candidate.start, candidate.end].every((position) => - position && typeof position === 'object' - && Number.isSafeInteger(position.line) && position.line > 0 - && Number.isSafeInteger(position.column) && position.column > 0) - && orderPos(candidate.start, candidate.end) <= 0 +function halt(state: Failure['state'], key: string): never { + throw new Halt({ state, subject: key }) } -const contains = (outer: IndexRange, inner: IndexRange): boolean => - orderPos(outer.start, inner.start) <= 0 - && orderPos(inner.end, outer.end) <= 0 -const sameRange = (left: IndexRange, right: IndexRange): boolean => - orderPos(left.start, right.start) === 0 - && orderPos(left.end, right.end) === 0 -const location = (value: IndexRange): string => - `L${value.start.line}${value.start.line === value.end.line - ? '' : `-L${value.end.line}`}` -function lineOffsets(text: string): [number[], number[]] { - const starts = [0] - const ends: number[] = [] +function bad(key: string): never { halt('corrupt', key) } +const same = (a: IndexRange, b: IndexRange): boolean => + a.start.line === b.start.line && a.start.column === b.start.column + && a.end.line === b.end.line && a.end.column === b.end.column + +function lines(text: string): [number[], number[]] { + const starts = [0], ends: number[] = [] for (const match of text.matchAll(/\r\n|[\n\r\u2028\u2029]/g)) { - ends.push(match.index) - starts.push(match.index + match[0].length) + ends.push(match.index); starts.push(match.index + match[0].length) } ends.push(text.length) return [starts, ends] } -function offset(source: ReadySource, line: number, column: number): number | null { - const start = source[3][line - 1], end = source[4][line - 1], - result = start === undefined ? 0 : start + column - 1 - return start === undefined || end === undefined || result > end ? null : result + +function clip(src: Source, r: IndexRange): string | null { + const { start, end } = r ?? {} + if (!start || !end) return null + const a = src[3][start.line - 1], z = src[4][end.line - 1] + if (a === undefined || z === undefined) return null + const from = a + start.column - 1, to = src[3][end.line - 1]! + end.column - 1 + return from <= z && to <= z && from <= to ? src[2].slice(from, to) : null } -function excerpt(source: ReadySource, value: IndexRange): string | null { - const start = offset(source, value.start.line, value.start.column), - end = offset(source, value.end.line, value.end.column) - return start === null || end === null || end < start - ? null - : source[2].slice(start, end) -} -function ready(i: ReadyQueryIndex, input: EvidenceHydrationTargets): HydratedEvidenceResult { - const ids = (values: readonly string[], subject: string): string[] => { - if (!Array.isArray(values) || values.some((value) => !nonEmpty(value))) corrupt(subject) - return [...new Set(values)].sort(compare) - } - const symbols = ids(input.symbolIds, 'symbol targets') - const decls = ids(input.declarationSymbolIds, 'declaration targets') - const ops = ids(input.operationIds, 'operation targets') - const validations = ids(input.validationOperationIds ?? [], 'validation operation targets') - for (const id of decls) if (!symbols.includes(id)) corrupt(id) - if (!Array.isArray(input.edges)) corrupt('edge targets') - const edges = new Map() - for (const edge of input.edges) { - if (!edge || !nonEmpty(edge.id) || !nonEmpty(edge.fromId) || !nonEmpty(edge.toId) - || edge.relation !== undefined && !nonEmpty(edge.relation)) corrupt('edge targets') - const prior = edges.get(edge.id)?.[0] - if (prior && (prior.fromId !== edge.fromId || prior.toId !== edge.toId - || prior.relation !== edge.relation)) corrupt(edge.id) - if (!prior) edges.set(edge.id, [edge]) - } - const d = new Set(decls), o = new Set([...ops, ...validations]) - const s = new Map(), f = new Map() - const e = new Map(), x = new Map() - const p = new Map(), u = new Set() - function node(id: string): GraphAttributes { - if (!i.graph.hasNode(id)) corrupt(id) + +function ready(i: ReadyQueryIndex, q: EvidenceHydrationTargets): HydratedEvidenceResult { + const ids = (xs: readonly string[]): string[] => [...new Set(xs)].sort() + const nodes = ids(q.symbolIds), decls = ids(q.declarationSymbolIds), + ops = ids(q.operationIds), checks = ids(q.validationOperationIds ?? []) + if (decls.some((id) => !nodes.includes(id))) bad('declaration targets') + const edges: SelectedEvidenceEdge[] = [...q.edges] + .sort((a, b) => a.id < b.id ? -1 : Number(a.id > b.id)) + const ds = new Set(decls), srcs = new Map(), + fs = new Map(), ctrls = new Map(), + ents = new Map(), + cuts = new Map(), refs = new Map(), + used = new Set() + + const node = (id: string) => { + if (!i.graph.hasNode(id)) bad(id) return i.graph.nodeAttributes(id) } - function src(path: string): ReadySource { - const cached = s.get(path) - if (cached) return cached - const expected = i.file_hashes.get(path) - if (expected === undefined) halt('stale', path) - if (!SHA.test(expected)) corrupt(path) - let root: string, candidate: string, bytes: Buffer + const load = (path: string): Source => { + const old = srcs.get(path) + if (old) return old + const hash = i.file_hashes.get(path) + if (hash === undefined) halt('stale', path) + let file: string, buf: Buffer try { - root = realpathSync(i.root_path) - candidate = realpathSync(resolve(root, path)) - const rel = relative(root, candidate) + const root = realpathSync(i.root_path) + file = realpathSync(resolve(root, path)) + const rel = relative(root, file) if (isAbsolute(path) || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { halt('unavailable', path) } - bytes = readFileSync(candidate) - } catch { + buf = readFileSync(file) + } catch (err) { + if (err instanceof Halt) throw err halt('unavailable', path) } - const actual = createHash('sha256').update(bytes).digest('hex') - if (actual !== expected) halt('stale', path) - if (!isUtf8(bytes)) corrupt(path) - const text = bytes.toString('utf8') - const file = `f${f.size}` - const result: ReadySource = [path, expected, text, ...lineOffsets(text), file] - f.set(path, [file, expected]) - s.set(path, result) - return result - } - function proof( - source: ReadySource, value: IndexRange, - expected?: string, subject = source[0], store = true, - ): string { - const text = excerpt(source, value) - if (text === null) corrupt(subject) - const actual = createHash('sha256').update(text, 'utf8').digest('hex') - if (expected !== undefined && (!SHA.test(expected) || actual !== expected)) { - corrupt(subject) - } - if (!store) return '' - const key = `${source[1]}\0${value.start.line}:${value.start.column}:${value.end.line}:${value.end.column}\0${actual}` - const cached = x.get(key) - if (cached) return cached[0] - const alias = `x${x.size}` - x.set(key, [alias, source[5], value, actual, text]) - return alias - } - function entity(id: string, allowChannel = false): string { - const cached = e.get(id) - if (cached) { - if (!allowChannel && cached[1] === 'channel') corrupt(id) - return cached[0] - } - const attrs = node(id) - const alias = `e${e.size}` - if (attrs.node_kind === 'channel') { - if (!allowChannel) corrupt(id) - const channel = i.channels_by_id.get(id) - if (!channel || channelFields.some((field) => attrs[field] !== channel[field])) { - corrupt(id) - } - e.set(id, [ - alias, 'channel', channel.channel_kind, channel.transport, channel.key, - channel.parent_channel_id, channel.scope, - ]) - return alias - } - const { - node_kind: nodeKind, label, source_file: path, - definition_range: definition, declaration_range: declaration, - } = attrs - if (!nonEmpty(nodeKind) || nodeKind === 'file' - || !nonEmpty(label) || !nonEmpty(path) - || !range(definition) || !range(declaration) - || !contains(definition, declaration) - || !populated(attrs.provenance) - || attrs.line_number !== definition.start.line - || attrs.end_line_number !== definition.end.line - || attrs.source_location !== location(definition)) { - corrupt(id) - } - const source = src(path) - if (excerpt(source, definition) === null) halt('stale', path) - const declProof = d.has(id) ? proof(source, declaration) : undefined - e.set(id, [alias, 'symbol', label, nodeKind, source[5]]) - if (declProof) { - p.set(id, [`p${p.size}`, 'declaration', alias, declProof]) - u.add(alias) - } - return alias - } - function fact(value: IndexBodyFact, exactOnly: boolean, store = true): FactProof { - const { owner_symbol_id: ownerId, evidence } = value - const hadOwner = e.has(ownerId) - const ownerKey = entity(ownerId) - if ((!exactOnly || !store) && !hadOwner) e.delete(ownerId) - const targetId = value.kind === 'call' ? value.target_symbol_id : undefined - if (targetId && ['channel', 'file'].includes(String(node(targetId).node_kind))) { - corrupt(targetId) + if (createHash('sha256').update(buf).digest('hex') !== hash) halt('stale', path) + if (!isUtf8(buf)) bad(path) + const text = buf.toString('utf8'), id = `f${fs.size}` + const row: Source = [path, hash, text, ...lines(text), id] + srcs.set(path, row); fs.set(path, [id, hash]) + return row + } + const auth = ( + src: Source, r: IndexRange, hash?: string, + key = src[0], keep = true, + ): string => { + const text = clip(src, r) + if (text === null) bad(key) + const sum = createHash('sha256').update(text).digest('hex') + if (hash !== undefined && sum !== hash) bad(key) + if (!keep) return '' + const sig = `${src[1]}\0${r.start.line}:${r.start.column}:${ + r.end.line}:${r.end.column}\0${sum}` + const old = cuts.get(sig) + if (old) return old[0] + const id = `x${cuts.size}` + cuts.set(sig, [id, src[5], r, sum, text]) + return id + } + const ent = (id: string): string => { + const old = ents.get(id) + if (old) return old[0] + const a = node(id), ref = `e${ents.size}`, ch = i.channels_by_id.get(id) + if (ch) { + ents.set(id, [ref, 'channel', ch.channel_kind, ch.transport, + ch.key, ch.parent_channel_id, ch.scope]) + if (ch.parent_channel_id) ent(ch.parent_channel_id) + return ref } - if (targetId && e.has(targetId)) u.add(entity(targetId)) - const owner = node(ownerId) - const definition = owner.definition_range - if (!range(definition)) corrupt(value.id) - const file = i.graph.hasNode(evidence.file_id) - ? i.graph.nodeAttributes(evidence.file_id) : null - const path = owner.source_file - const owns = (id: string): boolean => { - const target = i.operation_by_id.get(id) - return (!exactOnly || o.has(id)) && target?.owner_symbol_id === ownerId + const path = a.source_file as string, label = a.label as string, + kind = a.node_kind as string, decl = a.declaration_range as IndexRange + if (kind === 'file') bad(id) + const src = load(path), proof = ds.has(id) ? auth(src, decl, undefined, id) : undefined + ents.set(id, [ref, 'symbol', label, kind, src[5]]) + if (proof) { + refs.set(id, [`p${refs.size}`, 'declaration', ref, proof]); used.add(ref) } - const invalidRefs = value.control.some((frame) => - frame.kind !== 'exception' && !owns(frame.controller_fact_id)) - || value.kind === 'parallel' && value.member_fact_ids.some((id) => !owns(id)) - || value.kind === 'persistence' && !owns(value.call_fact_id) - if (!file || file.node_kind !== 'file' - || !nonEmpty(path) || file.source_file !== path - || file.content_hash !== i.file_hashes.get(path) - || !range(evidence.range) || !range(evidence.statement_range) - || !contains(definition, evidence.statement_range) - || !contains(evidence.statement_range, evidence.range) - || invalidRefs) { - corrupt(value.id) + return ref + } + const fact = (v: IndexBodyFact, exact: boolean, keep = true): [string, string] => { + const owner = v.owner_symbol_id, had = ents.has(owner), own = ent(owner) + if ((!exact || !keep) && !had) ents.delete(owner) + if (v.kind === 'call' && v.target_symbol_id) { + node(v.target_symbol_id) + if (ents.has(v.target_symbol_id)) used.add(ent(v.target_symbol_id)) } - const source = src(path) - const controlled = ['condition', 'loop', 'parallel'].includes(value.kind) - const statement = proof( - source, evidence.statement_range, evidence.excerpt_sha256, value.id, - store && !controlled, - ) - return [ownerKey, controlled ? proof(source, evidence.range, undefined, value.id, store) - : statement] - } - function edge(target: SelectedEvidenceEdge, row: EdgeRow): void { - const [from, to, attrs, id] = row - const { - relation, source_file: path, evidence, execution_owner_id: ownerId, - source_location: sourceLocation, - } = attrs - if (id !== target.id || from !== target.fromId || to !== target.toId - || !nonEmpty(relation) - || target.relation !== undefined && relation !== target.relation) { - corrupt(target.id) - } - const fromKey = entity(from, true) - const toKey = entity(to, true) - if (!nonEmpty(path) || !populated(attrs.provenance) - || !evidence || typeof evidence !== 'object' || Array.isArray(evidence)) { - corrupt(id) - } - const raw = evidence as Record - const { - range: at, source: proofKind, statement_range: statement, - excerpt_sha256: hash, - } = raw - if (![ - 'typescript-semantic', 'typescript-syntactic', - 'framework-decorator', 'wrapper-summary', - ].includes(proofKind as string) || !range(at)) corrupt(id) - if (relation === 'calls') { - if (Object.keys(raw).length !== 2 || ownerId !== undefined - || sourceLocation !== location(at)) corrupt(id) - const excerptKey = selectedCall(id, from, (call) => - call.target_symbol_id === to && sameRange(call.evidence.range, at) - && proofKind === (call.source === 'framework' - ? 'framework-decorator' : call.source)) - if (path !== node(from).source_file) corrupt(id) - return record(id, fromKey, toKey, relation, excerptKey) - } - if (Object.keys(raw).length !== 4 || !range(statement) - || !contains(statement, at) || !nonEmpty(hash) - || sourceLocation !== location(statement)) corrupt(id) - if (!nonEmpty(ownerId)) corrupt(id) - const ownerAttrs = node(ownerId) - if (ownerAttrs.node_kind === 'channel' || ownerAttrs.node_kind === 'file' - || ownerAttrs.source_file !== path || !range(ownerAttrs.definition_range) - || !contains(ownerAttrs.definition_range, statement)) corrupt(id) - const fromChannel = i.channels_by_id.get(from) - const toChannel = i.channels_by_id.get(to) - const valid = relation === 'publishes_to' - ? ownerId === from && !fromChannel && !!toChannel - : relation === 'routes_through' - ? fromChannel?.channel_kind === 'job' - && toChannel?.channel_kind === 'queue' - && fromChannel.parent_channel_id === to - && fromChannel.transport === toChannel.transport - : relation === 'consumed_by' - && !!fromChannel && !toChannel - if (!valid) corrupt(id) - if (relation === 'consumed_by' && ownerId !== to) { - selectedCall(id, ownerId, (call) => - sameRange(statement, call.evidence.statement_range) - && call.evidence.excerpt_sha256 === hash) - } - const source = src(path) - record(id, fromKey, toKey, relation, proof(source, statement, hash, id)) - } - function selectedCall( - edgeId: string, ownerId: string, accepts: (fact: CallFact) => boolean, - ): string { - const accepted = (call: CallFact): boolean => - call.owner_symbol_id === ownerId && accepts(call) - const matches = (i.operations_by_owner.get(ownerId) ?? []) - .filter((value): value is CallFact => value.kind === 'call' - && accepted(value)) - if (matches.length !== 1) corrupt(edgeId) - const match = matches[0]! - const indexed = i.operation_by_id.get(match.id) - if (indexed?.kind !== 'call' || !accepted(indexed) - || !sameRange(indexed.evidence.range, match.evidence.range) - || !sameRange(indexed.evidence.statement_range, match.evidence.statement_range)) { - corrupt(edgeId) - } - return fact(indexed, false)[1] - } - function record( - id: string, fromKey: string, toKey: string, relation: string, excerptKey: string, - ): void { - p.set(id, [`p${p.size}`, 'edge', fromKey, toKey, relation, excerptKey]) - u.add(fromKey).add(toKey) - } - for (const id of symbols) entity(id) - for (const id of validations) { + const src = load(node(owner).source_file as string), + ctrl = ['condition', 'loop', 'parallel'].includes(v.kind) + const stmt = auth(src, v.evidence.statement_range, + v.evidence.excerpt_sha256, v.id, keep && !ctrl) + return [own, ctrl ? auth(src, v.evidence.range, undefined, v.id, keep) : stmt] + } + const callAt = ( + edge: string, owner: string, ok: (v: Call) => boolean, keep = true, + ): string => { + const hits = (i.operations_by_owner.get(owner) ?? []) + .filter((v): v is Call => v.kind === 'call' + && v.owner_symbol_id === owner && ok(v)) + if (hits.length !== 1) bad(edge) + const hit = hits[0]! + if (i.operation_by_id.get(hit.id) !== hit) bad(edge) + return fact(hit, false, keep)[1] + } + const link = ( + id: string, from: string, to: string, rel: string, cut: string, + ): void => { + refs.set(id, [`p${refs.size}`, 'edge', from, to, rel, cut]) + used.add(from); used.add(to) + } + const ranged = ( + id: string, from: string, to: string, rel: string, + file: string, range: IndexRange, + ): void => { + refs.set(id, [`p${refs.size}`, 'edge_range', from, to, rel, file, range]) + used.add(from); used.add(to) + } + + for (const id of nodes) { + if (i.channels_by_id.has(id)) bad(id) + ent(id) + } + for (const id of checks) { const value = i.operation_by_id.get(id) - if (!value || value.id !== id) corrupt(id) + if (!value) bad(id) fact(value, true, false) + if (['condition', 'loop', 'parallel'].includes(value.kind)) { + const src = load(node(value.owner_symbol_id).source_file as string) + ctrls.set(id, [src[5], value.evidence.range]) + } } for (const id of ops) { const value = i.operation_by_id.get(id) - if (!value || value.id !== id) corrupt(id) - const hydrated = fact(value, true) - const alias = `e${e.size}` - e.set(id, [alias, 'operation', hydrated[0], value]) - p.set(id, [`p${p.size}`, 'operation', alias, hydrated[1]]) - u.add(hydrated[0]) - } - try { - for (const row of i.graph.edgeEntries()) { - const selected = edges.get(row[3]) - if (selected) selected[1] = selected[1] === undefined ? row : null + if (!value) bad(id) + const [owner, excerpt] = fact(value, true), ref = `e${ents.size}` + ents.set(id, [ref, 'operation', owner, value]) + refs.set(id, [`p${refs.size}`, 'operation', ref, excerpt]); used.add(owner) + } + for (const edge of edges) { + const hits = i.graph.edgesBetween(edge.fromId, edge.toId) + .filter((hit) => hit.id === edge.id) + if (hits.length !== 1) bad(edge.id) + const a = hits[0]!.attributes, rel = a.relation as string + if (edge.relation !== undefined && edge.relation !== rel) bad(edge.id) + const from = ent(edge.fromId), to = ent(edge.toId) + const ev = a.evidence as { + source: string; range: IndexRange; statement_range?: IndexRange; excerpt_sha256?: string + } + if (rel === 'calls') { + const cut = callAt(edge.id, edge.fromId, (call) => + call.target_symbol_id === edge.toId && same(call.evidence.range, ev.range) + && ev.source === (call.source === 'framework' + ? 'framework-decorator' : call.source)) + link(edge.id, from, to, rel, cut) + continue + } + const owner = a.execution_owner_id as string, + stmt = ev.statement_range!, sum = ev.excerpt_sha256!, path = a.source_file as string + if (rel === 'consumed_by' && owner !== edge.toId) { + callAt(edge.id, owner, (call) => + same(stmt, call.evidence.statement_range) + && call.evidence.excerpt_sha256 === sum, false) } - } catch { - corrupt('selected edges') + const src = load(path) + auth(src, stmt, sum, edge.id, false) + auth(src, ev.range, undefined, edge.id, false) + ranged(edge.id, from, to, rel, src[5], ev.range) } - for (const [, [target, row]] of [...edges].sort((a, b) => compare(a[0], b[0]))) { - if (!row) corrupt(target.id) - edge(target, row) + for (const [id, entry] of ents) { + if (entry[1] === 'symbol' && !used.has(entry[0])) bad(id) } - for (const [id, entry] of e) { - if (entry[1] === 'symbol' && !u.has(entry[0])) corrupt(id) + return { + state: 'ready', files: fs, controls: ctrls, + excerpts: cuts, entities: ents, proofs: refs, } - return { state: 'ready', files: f, excerpts: x, entities: e, proofs: p } } + export function hydrateEvidence( - index: QueryIndex, - input: EvidenceHydrationTargets, + index: QueryIndex, input: EvidenceHydrationTargets, ): HydratedEvidenceResult { try { - if (index.state !== 'ready') return { state: index.state, subject: index.subject } - return ready(index, input) + return index.state === 'ready' ? ready(index, input) + : { state: index.state, subject: index.subject } } catch (error) { - return error instanceof Halt - ? error.value : { state: 'corrupt', subject: 'evidence hydration' } + return error instanceof Halt ? error.value + : { state: 'corrupt', subject: 'evidence hydration' } } } diff --git a/src/application/retrieve-context.ts b/src/application/retrieve-context.ts index dd59be51..2cf3c002 100644 --- a/src/application/retrieve-context.ts +++ b/src/application/retrieve-context.ts @@ -1,9 +1,9 @@ -import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' +import { countTokens as tokens } from 'gpt-tokenizer/encoding/cl100k_base' import { hydrateEvidence, } from './evidence-hydrator.js' -import { canonicalJsonString, compareCodeUnits as compare } from '../domain/graph/canonical-json.js' +import { canonicalJsonString as json, compareCodeUnits as cmp } from '../domain/graph/canonical-json.js' import type { IndexBodyFact, IndexValue } from '../domain/index/model.js' import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' import { planQuestion } from '../domain/query/plan.js' @@ -11,8 +11,8 @@ import { selectWorkflow, } from '../domain/query/workflow.js' import { - MAX_RETRIEVE_EXCERPTS, - MAX_RETRIEVE_FILES, + MAX_RETRIEVE_EXCERPTS as EXCERPTS, + MAX_RETRIEVE_FILES as FILES, RETRIEVE_RESULT_SCHEMA, RETRIEVE_RESULT_VERSION, normalizeRetrieveRequest, @@ -22,6 +22,7 @@ import { type DossierLink, type DossierOrderGroup, type DossierProof, + type EvidenceHydrationTargets, type HydratedEvidenceResult, type MissingRequirement, type NormalizedRetrieveRequest, @@ -33,588 +34,577 @@ import { } from '../domain/query/types.js' type ReadyHydration = Extract -type InternalHydration = HydratedEvidenceResult -type DossierBuild = { state: 'ready'; dossier: AnswerDossier } - | { state: 'incomplete'; missing: MissingRequirement } - | { state: 'corrupt'; subject: string } - -function missingBuild( - code: 'required_proof_missing' | 'required_reference_missing', - target: string, - obligationId?: string, -): DossierBuild { - return { state: 'incomplete', missing: { - code, target, ...(obligationId ? { obligation_id: obligationId } : {}), - } } -} - -function limitMissing( +const uniq = (values: Iterable): string[] => [...new Set(values)].sort(cmp) +const cap = ( code: 'required_file_limit' | 'required_excerpt_limit' | 'required_token_budget', required: number, limit: number, -): readonly MissingRequirement[] { - return [{ code, required, limit }] -} - -const VALID_LINKS = new Set([ - 'direct:calls', - 'channel:publishes_to,consumed_by', - 'channel:publishes_to,routes_through,consumed_by', -]) +): readonly MissingRequirement[] => [{ code, required, limit }] -function metrics( - request: NormalizedRetrieveRequest, - selection?: WorkflowSelection, - hydration?: ReadyHydration, - failed = new Set(), +function stat( + req: NormalizedRetrieveRequest, + flow?: WorkflowSelection, + auth?: ReadyHydration, + gaps = new Set(), ): RetrieveMetrics { - const required = selection?.obligations.filter(({ mandatory }) => mandatory) ?? [] - const source = selection?.metrics + const must = flow?.obligations.filter(({ mandatory }) => mandatory) ?? [] + const data = flow?.metrics + const roots = data?.rootCandidateCount ?? 0 return { - budget_tokens: request.budget, + budget_tokens: req.budget, serialized_tokens: 0, - selected_files: hydration?.files.size ?? 0, - authenticated_excerpts: hydration?.excerpts.size ?? 0, - required_obligations: required.length, - proven_obligations: required.filter(({ proven, id }) => proven && !failed.has(id)).length, + selected_files: auth?.files.size ?? 0, + authenticated_excerpts: auth?.excerpts.size ?? 0, + required_obligations: must.length, + proven_obligations: must.filter(({ proven, id }) => proven && !gaps.has(id)).length, optional_bundles_omitted: 0, - root_candidates: source?.rootCandidateCount ?? 0, - initial_candidates: source?.candidateCount ?? 0, - explored_nodes: source?.actualNodeCount ?? 0, - causal_hops: source?.causalRelationHops ?? 0, - recovery_passes: source?.recoveryPasses ?? 0, - recovery_frontier_nodes: source?.recoveryFrontierCount ?? 0, - alternate_seeds: Math.max(0, (source?.rootCandidateCount ?? 0) - 1), + root_candidates: roots, + initial_candidates: data?.candidateCount ?? 0, + explored_nodes: data?.actualNodeCount ?? 0, + causal_hops: data?.causalRelationHops ?? 0, + recovery_passes: data?.recoveryPasses ?? 0, + recovery_frontier_nodes: data?.recoveryFrontierCount ?? 0, + alternate_seeds: Math.max(0, roots - 1), } } -function stabilize(input: T): T { - input.metrics.serialized_tokens = 0 - const body = countTokens(canonicalJsonString(input)) - countTokens('0') - const estimate = body + countTokens(String(body)) - input.metrics.serialized_tokens = body + countTokens(String(estimate)) - return input +function seal(out: T): T { + out.metrics.serialized_tokens = 0 + const body = tokens(json(out)) - 1 + out.metrics.serialized_tokens = body + tokens(String(body + tokens(String(body)))) + return out } -function header( +const base = ( state: S, - request: NormalizedRetrieveRequest, - selection?: WorkflowSelection, - hydration?: ReadyHydration, - failed?: Set, + req: NormalizedRetrieveRequest, + flow?: WorkflowSelection, + auth?: ReadyHydration, + gaps?: Set, ): { schema: typeof RETRIEVE_RESULT_SCHEMA; version: typeof RETRIEVE_RESULT_VERSION - state: S; metrics: RetrieveMetrics } { - return { schema: RETRIEVE_RESULT_SCHEMA, version: RETRIEVE_RESULT_VERSION, - state, metrics: metrics(request, selection, hydration, failed) } -} + state: S; metrics: RetrieveMetrics } => ({ + schema: RETRIEVE_RESULT_SCHEMA, version: RETRIEVE_RESULT_VERSION, + state, metrics: stat(req, flow, auth, gaps), +}) -function query(plan: QueryPlan): QuerySummary { - return { intent: plan.intent, subject: plan.subject, terms: plan.terms } -} +const ask = (plan: QueryPlan): QuerySummary => + ({ intent: plan.intent, subject: plan.subject, terms: plan.terms }) type TerminalResult = Exclude -function fitTerminal(input: TerminalResult, budget: number): TerminalResult { - const result = stabilize(input) - if (result.metrics.serialized_tokens <= budget) return result - if (result.state === 'incomplete') { - result.query.subject = result.query.subject.slice(0, 32) - result.query.terms = [] - result.missing = result.missing.map(({ - code, obligation_id, required, limit, - }) => ({ - code, - ...(obligation_id ? { obligation_id } : {}), - ...(required === undefined ? {} : { required }), - ...(limit === undefined ? {} : { limit }), - })) - } else if (result.state === 'unsupported') result.terms = [] - else result.failures = result.failures.map(({ state, subject }) => - ({ state, subject: subject.slice(0, 32) })) - stabilize(result) - if (result.metrics.serialized_tokens > budget && result.state === 'incomplete') { - result.query.subject = '' - stabilize(result) +function fit(out: TerminalResult, max: number): TerminalResult { + seal(out) + if (out.metrics.serialized_tokens <= max) return out + if (out.state === 'incomplete') { + out.query.subject = out.query.subject.slice(0, 32) + out.query.terms = [] + for (const row of out.missing) delete row.target + } else if (out.state === 'unsupported') out.terms = [] + else for (const failure of out.failures) { + failure.subject = failure.subject.slice(0, 32) + } + if (seal(out).metrics.serialized_tokens <= max) return out + if (out.state === 'incomplete') { + out.query.subject = '' + if (seal(out).metrics.serialized_tokens <= max) return out } - if (result.metrics.serialized_tokens > budget) return stabilize({ - schema: result.schema, version: result.version, state: 'corrupt', - metrics: result.metrics, + return seal({ + schema: out.schema, version: out.version, state: 'corrupt', metrics: out.metrics, failures: [{ state: 'corrupt', subject: 'terminal result budget' }], }) - return result } -function declarationTargets( - plan: QueryPlan, selection: WorkflowSelection, index: ReadyQueryIndex, -): string[] { - const required = plan.intent === 'locate' ? selection.symbolIds.slice(0, 1) : [] - const subject = selection.obligations.find(({ kind, proven }) => +function select( + plan: QueryPlan, flow: WorkflowSelection, index: ReadyQueryIndex, +): EvidenceHydrationTargets { + const need = plan.intent === 'locate' ? flow.symbolIds.slice(0, 1) : [] + const focus = flow.obligations.find(({ kind, proven }) => kind === 'subject' && proven) - if (plan.intent === 'explain' && subject) required.push(...subject.symbolIds.slice(0, 1)) - const incident = new Set(selection.edges.flatMap(({ fromId, toId }) => [fromId, toId])) - for (const id of selection.operationIds) { - const owner = index.operation_by_id.get(id)?.owner_symbol_id - if (owner) incident.add(owner) - } - required.push(...selection.symbolIds.filter((id) => !incident.has(id))) - return [...new Set(required)].sort(compare) -} - -function emittedOperations( - plan: QueryPlan, selection: WorkflowSelection, index: ReadyQueryIndex, -): string[] { - const path = new Set([ - ...selection.rootSymbolIds, ...selection.terminalSymbolIds, - ...selection.links.flatMap(({ fromId, toId }) => [fromId, toId]), - ]) - const result = new Set(selection.operationIds.filter((id) => { + if (plan.intent === 'explain' && focus) need.push(...focus.symbolIds.slice(0, 1)) + const incident = new Set(flow.edges.flatMap(({ fromId, toId }) => [fromId, toId])), + path = new Set([ + ...flow.rootSymbolIds, ...flow.terminalSymbolIds, + ...flow.links.flatMap(({ fromId, toId }) => [fromId, toId]), + ]), linked = new Set(flow.links.flatMap(({ operationIds }) => operationIds)), + facts = new Set() + for (const id of flow.operationIds) { const fact = index.operation_by_id.get(id) - return fact && (plan.intent !== 'workflow' - || !['condition', 'loop', 'parallel'].includes(fact.kind)) - && (fact.kind !== 'call' || path.has(fact.owner_symbol_id)) - })) - for (const id of result) { + if (!fact) continue + incident.add(fact.owner_symbol_id) + if (!['condition', 'loop', 'parallel'].includes(fact.kind) + && !linked.has(id) + && (fact.kind !== 'call' || path.has(fact.owner_symbol_id))) facts.add(id) + } + need.push(...flow.symbolIds.filter((id) => !incident.has(id))) + for (const id of facts) { const fact = index.operation_by_id.get(id) - if (fact?.kind === 'persistence') result.add(fact.call_fact_id) - if (fact?.kind === 'parallel') fact.member_fact_ids.forEach((member) => result.add(member)) + if (fact?.kind === 'persistence') facts.add(fact.call_fact_id) + if (fact?.kind === 'parallel') fact.member_fact_ids.forEach((member) => facts.add(member)) + } + return { + symbolIds: flow.symbolIds, edges: flow.edges, + declarationSymbolIds: uniq(need), + operationIds: uniq(facts), + validationOperationIds: flow.operationIds.filter((id) => !facts.has(id)), } - return [...result].sort(compare) } -function incomplete( - request: NormalizedRetrieveRequest, +function miss( + req: NormalizedRetrieveRequest, plan: QueryPlan, missing: readonly MissingRequirement[], - selection?: WorkflowSelection, - hydration?: ReadyHydration, - packedFailure = false, + flow?: WorkflowSelection, + auth?: ReadyHydration, ): RetrieveContextResult { - const failed = new Set(missing.flatMap((entry) => + const gaps = new Set(missing.flatMap((entry) => entry.obligation_id ? [entry.obligation_id] : [])) - if (packedFailure && failed.size === 0) { - for (const entry of selection?.obligations ?? []) if (entry.mandatory) failed.add(entry.id) - } - return fitTerminal({ - ...header('incomplete', request, selection, hydration, failed), - query: query(plan), + return fit({ + ...base('incomplete', req, flow, auth, gaps), + query: ask(plan), missing, - }, request.budget) + }, req.budget) } -function failure( - request: NormalizedRetrieveRequest, +const fail = ( + req: NormalizedRetrieveRequest, state: 'stale' | 'unavailable' | 'corrupt', subject: string, - selection?: WorkflowSelection, -): RetrieveContextResult { - return fitTerminal({ - ...header(state, request, selection), - failures: [{ state, subject: subject.slice(0, 96) }], - }, request.budget) -} + flow?: WorkflowSelection, +): RetrieveContextResult => fit({ + ...base(state, req, flow), + failures: [{ state, subject: subject.slice(0, 96) }], +}, req.budget) -function projectValue(value: IndexValue, entity: (id: string) => string | undefined): object { +function view( + value: IndexValue, ref: (id: string) => string | undefined, brief: boolean, +): unknown { + const nested = (entry: IndexValue): unknown => view(entry, ref, brief) + if (value.kind === 'literal') return brief ? value.value : value if (value.kind === 'symbol') { - const id = entity(value.symbol_id) - return id ? { kind: 'symbol', entity: id } : { kind: 'unknown', reason: 'outside_dossier' } + const entity = ref(value.symbol_id) + return brief ? entity ? { entity } : { unknown: 'outside_dossier' } + : entity ? { kind: 'symbol', entity } + : { kind: 'unknown', reason: 'outside_dossier' } } - if (value.kind === 'array' || value.kind === 'template') { - const entries = value.kind === 'array' ? value.elements : value.parts - return { - kind: value.kind, - [value.kind === 'array' ? 'elements' : 'parts']: - entries.map((entry) => projectValue(entry, entity)), - } + if (value.kind === 'array') return brief + ? value.elements.map(nested) + : { kind: 'array', elements: value.elements.map(nested) } + if (value.kind === 'object') return brief ? { + object: value.entries.map(({ key, value: entry }) => [key, nested(entry)]), + } : { kind: 'object', entries: value.entries.map(({ key, value: entry }) => ({ + key, value: nested(entry), + })) } + if (value.kind === 'template') return brief + ? { template: value.parts.map(nested) } + : { kind: 'template', parts: value.parts.map(nested) } + if (!brief) return value + if (value.kind === 'parameter') return { + parameter: value.position, ...(value.scope ? { scope: value.scope } : {}), } - if (value.kind === 'object') { - return { kind: 'object', entries: value.entries.map(({ key, value: entry }) => ({ - key, - value: projectValue(entry, entity), - })) } + if (value.kind === 'redacted') return { + redacted: value.sha256, bytes: value.byte_length, } - return value + return { unknown: value.reason } } -type ReferenceKey = 'rootSymbolIds' | 'terminalSymbolIds' | 'symbolIds' - | 'operationIds' | 'edgeIds' | 'controllerOperationId' | 'fromId' | 'toId' -const DETAIL_FIELDS = { +const KEYS = { literal: ['role'], condition: ['condition_kind'], loop: ['loop_kind'], parallel: ['combinator', 'completion', 'lane_count'], return: [], throw: [], mutation: ['operation', 'target'], persistence: ['operation', 'receiver_type'], } as const -function operationDetail( +function displayArm(arm: string): string { + if (!arm.startsWith('case:')) return arm + const encoded = arm.slice(5) + try { + const value: unknown = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8')) + if (!Array.isArray(value) || value.length !== 2) return arm + const [rawKind, scalar] = value + const kind = rawKind === 'object' && scalar === null ? 'null' : rawKind + const valid = kind === 'null' && scalar === null + || kind === 'string' && typeof scalar === 'string' + || kind === 'boolean' && typeof scalar === 'boolean' + || kind === 'number' && typeof scalar === 'number' && Number.isFinite(scalar) + if (!valid || Buffer.from(json(value)).toString('base64url') !== encoded) return arm + return `case:${String(kind)}:${json(scalar)}` + } catch { + return arm + } +} + +function info( fact: IndexBodyFact, - entity: (id: string) => string | undefined, + ref: (id: string) => string | undefined, ): Readonly> { - const value = (entry: IndexValue | undefined): unknown => - entry === undefined ? undefined : projectValue(entry, entity) + const value = (entry: IndexValue | undefined): Record | undefined => + entry === undefined ? undefined : view(entry, ref, false) as Record if (fact.kind === 'call') { - const target = fact.target_symbol_id ? entity(fact.target_symbol_id) : undefined - const arguments_ = fact.arguments.some((entry) => valueHas( + const target = fact.target_symbol_id ? ref(fact.target_symbol_id) : undefined + const args = fact.arguments.some((entry) => valueHas( entry, (candidate) => candidate.kind === 'literal', - )) ? fact.arguments.map((entry) => projectValue(entry, entity)) : undefined - return { order: fact.order, callee: fact.callee, scheduling: fact.scheduling, - ...(target ? { target } : {}), ...(arguments_ ? { arguments: arguments_ } : {}) } + )) ? fact.arguments.map((entry) => view(entry, ref, false)) : undefined + return { callee: fact.callee, + ...(target ? { target } : {}), ...(args ? { arguments: args } : {}) } + } + if (fact.kind === 'condition') return { + kind: fact.condition_kind, + ...(fact.test === undefined ? {} : { test: view(fact.test, ref, true) }), + } + if (fact.kind === 'loop') return { + kind: fact.loop_kind, + ...(fact.test === undefined ? {} : { test: view(fact.test, ref, true) }), } - const result: Record = { order: fact.order } + if (fact.kind === 'parallel') return { + combinator: fact.combinator, completion: fact.completion, + lanes: fact.lane_count, + ...(fact.input === undefined ? {} : { input: view(fact.input, ref, true) }), + } + const row: Record = {} const raw = fact as unknown as Record - for (const key of DETAIL_FIELDS[fact.kind]) result[key] = raw[key] - if (fact.kind === 'parallel') { - result.members = fact.member_fact_ids.map((id) => entity(id)!) - if (fact.input !== undefined) result.input = value(fact.input) - } else if (fact.kind === 'persistence') { - result.call = entity(fact.call_fact_id)! - if (fact.resource !== undefined) result.resource = value(fact.resource) + for (const key of KEYS[fact.kind]) row[key] = raw[key] + if (fact.kind === 'persistence') { + row.call = ref(fact.call_fact_id)! + const resource = value(fact.resource) + if (resource && !(resource.kind === 'symbol' + && resource.entity === ref(fact.owner_symbol_id))) row.resource = resource } else if (['literal', 'return', 'throw', 'mutation'].includes(fact.kind)) { const item = raw.value as IndexValue | undefined - if (item !== undefined) result.value = value(item) - } - else { - const test = value(raw.test as IndexValue | undefined) - if (fact.kind === 'loop' || test && (typeof test !== 'object' - || !('kind' in test) || test.kind !== 'unknown')) result.test = test + if (item !== undefined) row.value = value(item) } - return result -} - -function statement( - obligation: WorkflowSelection['obligations'][number], - plan: QueryPlan, -): string { - const kind = obligation.kind - return kind === 'subject' ? `${plan.subject}.` - : kind === 'handoff' ? 'Handoffs proven.' - : kind === 'behavior' ? 'Operations proven.' - : kind === 'ordering' ? 'Order proven.' : `${kind} proven.` + return row } -function buildDossier( +function pack( plan: QueryPlan, - selection: WorkflowSelection, - hydration: ReadyHydration, - emittedOperationIds: readonly string[], + flow: WorkflowSelection, + auth: ReadyHydration, index: ReadyQueryIndex, -): DossierBuild { +): AnswerDossier | MissingRequirement { const { - symbolIds, operationIds: allOperationIds, rootSymbolIds: rootIds, - terminalSymbolIds: terminalIds, edges: selectedEdges, - links: selectedLinks, controlGroups: groups, - obligations: selectedObligations, - } = selection - const lookup = (canonical: string): string | undefined => - hydration.entities.get(canonical)?.[0] - const entity = (canonical: string): string => lookup(canonical)! - const edge = (id: string) => { - const proof = hydration.proofs.get(id) - return proof?.[1] === 'edge' ? proof : undefined - } - const proofs: DossierProof[] = [] - for (const proof of hydration.proofs.values()) if (proof[1] === 'edge') { - proofs.push({ + rootSymbolIds: roots, terminalSymbolIds: ends, + links: paths, controlGroups: groups, obligations: claims, + } = flow + const get = (id: string): string | undefined => auth.entities.get(id)?.[0] + const ref = get as (id: string) => string + const coords = (range: IndexBodyFact['evidence']['range']): + [number, number, number, number] => [ + range.start.line, range.start.column, range.end.line, range.end.column, + ] + const refs: DossierProof[] = [] + for (const proof of auth.proofs.values()) { + if (proof[1] === 'edge') refs.push({ id: proof[0], from: proof[2], to: proof[3], relation: proof[4], excerpt: proof[5], }) + else if (proof[1] === 'edge_range') refs.push({ + id: proof[0], from: proof[2], to: proof[3], relation: proof[4], + file: proof[5], range: coords(proof[6]), + }) } - for (const [canonical, item] of hydration.entities) { - if (item[1] === 'channel' && item[5] && !hydration.entities.has(item[5])) { - return missingBuild('required_reference_missing', item[5]) - } - if (item[1] === 'operation' && hydration.proofs.get(canonical)?.[1] !== 'operation') { - return missingBuild('required_proof_missing', canonical) - } - } - const missingEntity = [...symbolIds, ...emittedOperationIds] - .find((id) => !hydration.entities.has(id)) - const missing = missingEntity - ?? selectedEdges.find((item) => !edge(item.id))?.id - if (missing) return missingBuild( - missingEntity ? 'required_reference_missing' : 'required_proof_missing', missing, - ) - const selected: Record = { - rootSymbolIds: symbolIds, terminalSymbolIds: symbolIds, - symbolIds, fromId: symbolIds, toId: symbolIds, - operationIds: allOperationIds, controllerOperationId: allOperationIds, - edgeIds: selectedEdges.map((edge) => edge.id), - } - let forged = [...rootIds, ...terminalIds].find((id) => !symbolIds.includes(id)) - for (const row of [ - ...selectedLinks, ...groups, ...selectedObligations, - ]) for (const [key, value] of Object.entries(row)) { - const allowed = selected[key as ReferenceKey] - if (allowed) forged ??= [value].flat() - .find((id) => !allowed.includes(id as string)) as string | undefined - } - if (forged) return { state: 'corrupt', subject: forged } + const tied = new Map() + const pathProofs = paths.map((link) => { + const proofs = [...new Set(link.edgeIds.map((id) => auth.proofs.get(id)![0]))] + link.operationIds.forEach((id) => tied.set( + id, uniq([...(tied.get(id) ?? []), ...proofs]), + )) + return proofs + }) + const folded = new Map(), consumed = new Set() + paths.forEach((path, index) => { + if (path.kind !== 'direct') return + const incoming = paths.flatMap((candidate, candidateIndex) => + candidate.toId === path.toId ? [candidateIndex] : []) + const outgoing = paths.flatMap((candidate, candidateIndex) => + candidate.fromId === path.toId ? [candidateIndex] : []) + if (incoming.length !== 1 || outgoing.length !== 1) return + const nextIndex = outgoing[0]!, next = paths[nextIndex]! + if (next.kind !== 'channel') return + const alreadyPublished = paths.some((candidate, candidateIndex) => + candidateIndex !== index && candidateIndex !== nextIndex + && candidate.fromId === path.fromId && candidate.toId === next.toId) + if (alreadyPublished) return + folded.set(index, nextIndex); consumed.add(nextIndex) + }) const links: DossierLink[] = [] - for (const [index, link] of selectedLinks.entries()) { - const chain = link.edgeIds.map((id) => edge(id)!) - const from = entity(link.fromId), to = entity(link.toId) - const joined = chain.every((proof, proofIndex) => - proof[2] === (proofIndex === 0 ? from : chain[proofIndex - 1]![3])) - && chain.at(-1)![3] === to - if (!VALID_LINKS.has( - `${link.kind}:${chain.map((proof) => proof[4]).join(',')}`, - ) || !joined) { - return { state: 'corrupt', subject: `${link.fromId}->${link.toId}` } - } - const id = `l${index + 1}` + paths.forEach((path, index) => { + if (consumed.has(index)) return + const nextIndex = folded.get(index), next = nextIndex === undefined + ? undefined : paths[nextIndex] links.push({ - id, kind: link.kind, from, to, - proofs: [...new Set(chain.map((proof) => proof[0]))], + id: `l${links.length + 1}`, + kind: next ? 'channel' : path.kind, + from: ref(path.fromId), to: ref(next?.toId ?? path.toId), + proofs: nextIndex === undefined ? pathProofs[index]! + : [...new Set([...pathProofs[index]!, ...pathProofs[nextIndex]!])], }) - } - const entities: DossierEntity[] = [] - const ownerProof = new Map() - const persisted = new Set() - for (const [canonical, item] of hydration.entities) { + }) + const opRefs = (ids: readonly string[]): string[] => uniq(ids.flatMap((id) => + tied.get(id) ?? (get(id) ? [ref(id)] : []))) + const resolve = (id: string): string | undefined => + tied.get(id)?.[0] ?? get(id) + const owned = new Map() + const ents = [...auth.entities].flatMap(([ + id, item, + ]): DossierEntity[] => { const alias = item[0] - const proof = hydration.proofs.get(canonical) - const excerpt = proof?.[1] !== 'edge' ? proof?.[3] : undefined + const proof = auth.proofs.get(id) + const excerpt = proof?.[1] === 'declaration' || proof?.[1] === 'operation' + ? proof[3] : undefined if (item[1] === 'symbol') { - entities.push({ + return [{ id: alias, kind: 'symbol', label: item[2], ...(/^(?:function|method|class)$/u.test(item[3]) ? {} : { node_kind: item[3] }), file: item[4], ...(excerpt ? { excerpt } : {}), - }) - } else if (item[1] === 'channel') { - const parent = item[5] ? entity(item[5]) : undefined - entities.push({ + }] + } + if (item[1] === 'channel') { + const parent = item[5] ? ref(item[5]) : undefined + return [{ id: alias, kind: 'channel', channel_kind: item[2], transport: item[3], key: item[4], ...(parent ? { parent } : {}), ...(item[6] ? { scope: item[6] } : {}), - }) - } else { - const fact = item[3] - const linked = selectedLinks.flatMap((link, index) => - link.operationIds.includes(canonical) ? [index] : []) - if (fact.kind === 'call' && linked.length > 0) { - if (linked.some((index) => selectedLinks[index]!.fromId !== fact.owner_symbol_id)) { - return { state: 'corrupt', subject: canonical } - } - entities.push({ - id: alias, kind: 'operation', links: linked.map((index) => `l${index + 1}`), - order: fact.order, excerpt: excerpt!, - ...(linked.some((index) => selectedLinks[index]!.kind === 'channel') - ? { callee: fact.callee } : {}), - ...(fact.scheduling === 'sync' ? {} : { scheduling: fact.scheduling }), - }) - } else { - const detail = operationDetail(fact, lookup) - entities.push({ - id: alias, kind: 'operation', operation_kind: fact.kind, - owner: item[2], excerpt: excerpt!, detail, - }) - } - ownerProof.set(item[2], fact.kind === 'persistence' - ? alias : ownerProof.get(item[2]) ?? alias) - if (fact.kind === 'persistence') persisted.add(alias) + }] } + const fact = item[3] + const repl = tied.get(id) + owned.set(item[2], fact.kind === 'persistence' + ? alias : owned.get(item[2]) ?? repl?.[0] ?? alias) + return repl ? [] : [{ + id: alias, kind: 'operation', operation_kind: fact.kind, + owner: item[2], excerpt: excerpt!, detail: info(fact, resolve), + }] + }) + const cover = (ids: readonly string[], behavior = false): string[] => { + const result = ids.flatMap((id) => { + const subject = ref(id) + const hydrated = auth.proofs.get(id) + const proof = behavior + ? refs.find((entry) => entry.from === subject)?.id ?? owned.get(subject) + : hydrated && (hydrated[1] === 'declaration' || hydrated[1] === 'operation') + ? subject : owned.get(subject) + ?? refs.find((entry) => entry.from === subject || entry.to === subject)?.id + return proof ? [proof] : [] + }) + return behavior || result.length === ids.length ? uniq(result) : [] } - const cover = (canonicalIds: readonly string[]): string[] => { - const remaining = new Map(canonicalIds.map((id) => [entity(id), id])) - const result: string[] = [] - for (const proof of hydration.proofs.values()) { - if (proof[1] !== 'edge' || !remaining.has(proof[2]) || !remaining.has(proof[3])) continue - result.push(proof[0]); remaining.delete(proof[2]); remaining.delete(proof[3]) + type ControlKind = 'branch' | 'loop' | 'parallel' + type Chain = [ + kind: ControlKind, arm: string | undefined, owner: string, file: string, + ids: string[], ranges: [number, number, number, number][], + sets: string[][], detail: Readonly>, + ] + const chains: Chain[] = [] + for (const group of groups) { + const id = group.controllerOperationId + if (!id || !['branch', 'loop', 'parallel'].includes(group.kind)) continue + const fact = index.operation_by_id.get(id), proof = auth.controls.get(id) + if (!fact || !proof || !['condition', 'loop', 'parallel'].includes(fact.kind)) { + return { code: 'required_proof_missing', target: plan.subject } } - for (const [subject, canonical] of remaining) { - const hydrated = hydration.proofs.get(canonical) - const proof = hydrated && hydrated[1] !== 'edge' ? subject : ownerProof.get(subject) - ?? proofs.find((entry) => entry.from === subject || entry.to === subject)?.id - if (!proof) return [] - result.push(proof) + const members = opRefs(group.operationIds) + if (members.length === 0) continue + const detail = info(fact, resolve), owner = ref(fact.owner_symbol_id) + const parent = [...fact.control].reverse().find((frame) => + frame.kind === group.kind + && (frame.kind !== 'branch' || frame.arm === group.arm)) + const prior = parent && 'controller_fact_id' in parent + ? [...chains].reverse().find((chain) => + chain[4].at(-1) === parent.controller_fact_id + && chain[0] === group.kind && chain[1] === group.arm + && chain[2] === owner && chain[3] === proof[0] + && json(chain[7]) === json(detail) + && members.every((member) => chain[6].at(-1)!.includes(member))) + : undefined + if (prior) { + prior[4].push(id); prior[5].push(coords(proof[1])); prior[6].push(members) + } else { + chains.push([group.kind as ControlKind, group.arm, owner, proof[0], + [id], [coords(proof[1])], [members], detail]) } - return result.sort(compare) } - const obligations: AnswerDossier['obligations'][number][] = [] - for (const obligation of selectedObligations) { - const claimRefs = obligation.kind === 'handoff' - ? obligation.edgeIds.map((id) => edge(id)![0]) - : obligation.kind === 'stage' && obligation.edgeIds.length > 0 - ? cover(obligation.symbolIds) - : obligation.kind === 'ordering' - ? obligation.operationIds.flatMap((id) => lookup(id) ? [entity(id)] : []) - : obligation.kind === 'behavior' ? obligation.symbolIds.flatMap((id) => { - const subject = entity(id) - const edge = proofs.find((proof) => proof.from === subject) - return edge?.id ?? ownerProof.get(subject) ?? [] - }) - : obligation.kind === 'subject' && plan.intent === 'locate' && plan.access - ? obligation.operationIds.flatMap((id) => lookup(id) ? [entity(id)] : []) - : obligation.kind === 'terminal' - ? obligation.operationIds.flatMap((id) => - lookup(id) ? [entity(id)] : []).filter((proof) => persisted.has(proof)) - : cover(obligation.symbolIds) - const unique = [...new Set(claimRefs)].sort(compare) - if (obligation.mandatory && unique.length === 0) { - return missingBuild('required_proof_missing', obligation.target, obligation.id) + const byFile = new Map() + for (const chain of chains) { + const ranges = byFile.get(chain[3]) ?? [] + for (const range of chain[5]) { + if (!ranges.some((candidate) => json(candidate) === json(range))) { + ranges.push(range) + } } - obligations.push({ - id: obligation.id, kind: obligation.kind, - statement: statement(obligation, plan), - proofs: unique, - }) + byFile.set(chain[3], ranges) } - const compactGroups = new Map>() - for (const group of groups) { - const controller = group.controllerOperationId - ? lookup(group.controllerOperationId) : undefined - const controllerFact = group.controllerOperationId - ? index.operation_by_id.get(group.controllerOperationId) : undefined - let detail: Readonly> | undefined - if (controllerFact && ['condition', 'loop', 'parallel'].includes(controllerFact.kind)) { - const { order: _order, ...control } = operationDetail(controllerFact, lookup) - detail = control - } - const operationMembers = group.operationIds.flatMap((id) => - lookup(id) ? [entity(id)] : []) - const members = group.kind === 'cycle' - ? group.symbolIds.map(entity) : operationMembers - const groupProofs = [...operationMembers] - if (group.kind === 'cycle') { - groupProofs.push(...links.filter((link) => - members.includes(link.from) - && members.includes(link.to)).flatMap((link) => link.proofs)) - } - if (groupProofs.length === 0) { - return missingBuild('required_proof_missing', group.kind) + const controls: AnswerDossier['evidence']['controls'][number][] = + [...byFile].sort(([left], [right]) => cmp(left, right)).map(([ + file, ranges, + ], index) => ({ + id: `c${index + 1}`, file, + ranges: ranges.sort((left, right) => { + for (let part = 0; part < left.length; part += 1) { + const order = left[part]! - right[part]! + if (order !== 0) return order + } + return 0 + }), + })) + const control = (chain: Chain): string => { + const catalog = controls.find(({ file }) => file === chain[3])! + const indexes = chain[5].map((range) => catalog.ranges.findIndex( + (candidate) => json(candidate) === json(range))) + const sequential = indexes.every((index, offset) => + index === indexes[0]! + offset) + const selector = sequential && indexes.length > 1 + ? `${indexes[0]}-${indexes.at(-1)}` : indexes.join('.') + return `${catalog.id}:${selector}` + } + const order: DossierOrderGroup[] = chains.map((chain) => { + const controller = control(chain) + const layers = chain[6].map((set, layer) => set.filter((member) => + !(chain[6][layer + 1] ?? []).includes(member))) + const members = layers.flat() + return { + id: '', kind: chain[0], controller, + ...(chain[1] ? { arm: displayArm(chain[1]) } : {}), detail: chain[7], + ...(layers.length > 1 ? { + depths: layers.flatMap((layer, depth) => layer.map(() => depth)), + } : {}), + members, } - const preserveOrder = group.kind === 'sequence' - const row: Omit = { - kind: group.kind, - ...(controller ? { controller } : {}), - ...(group.arm ? { arm: group.arm } : {}), - ...(detail ? { detail } : {}), - members: preserveOrder - ? members : [...new Set(members)].sort(compare), - proofs: preserveOrder ? groupProofs : [...new Set(groupProofs)].sort(compare), + }) + for (const group of groups) { + if (group.controllerOperationId) continue + const ops = opRefs(group.operationIds) + const nodes = group.kind === 'cycle' ? group.symbolIds.map(ref) : ops + const proofs = group.kind === 'cycle' + ? [...ops, ...links.filter((link) => nodes.includes(link.from) + && nodes.includes(link.to)).flatMap((link) => link.proofs)] : ops + if (nodes.length === 0 || proofs.length === 0 + || group.kind === 'sequence' && nodes.length < 2) continue + order.push({ + id: '', kind: group.kind, + members: group.kind === 'sequence' ? nodes : uniq(nodes), + ...(group.kind === 'cycle' ? { proofs: uniq(proofs) } : {}), + }) + } + order.forEach((group, index) => { group.id = `g${index + 1}` }) + const collapse = ( + raw: readonly string[], bundles: readonly { + id: string; proofs: readonly string[] + }[], + ): string[] => { + const wanted = new Set(raw) + const used = bundles.filter(({ proofs }) => proofs.some((id) => wanted.has(id))) + const covered = new Set(used.flatMap(({ proofs }) => proofs)) + const packed = [...used.map(({ id }) => id), ...raw.filter((id) => !covered.has(id))] + return packed.length < raw.length ? packed : [...raw] + } + const linkBundles = links.map(({ id, proofs }) => ({ id, proofs })) + const orderBundles = order.map(({ id, members, proofs = [] }) => ({ + id, proofs: [...members, ...proofs], + })) + const claimsOut: AnswerDossier['obligations'][number][] = [] + for (const claim of claims) { + const useOps = claim.kind === 'ordering' || claim.kind === 'terminal' + || claim.kind === 'subject' && plan.intent === 'locate' && !!plan.access + const raw = claim.kind === 'handoff' + ? claim.edgeIds.map((id) => auth.proofs.get(id)![0]) + : claim.kind === 'behavior' + ? uniq([...cover(claim.symbolIds, true), ...opRefs(claim.operationIds)]) + : useOps ? opRefs(claim.operationIds) : cover(claim.symbolIds) + const refs = uniq(claim.kind === 'ordering' + ? collapse(raw, orderBundles) + : ['stage', 'handoff', 'behavior'].includes(claim.kind) + ? collapse(raw, linkBundles) : raw) + if (claim.mandatory && refs.length === 0) { + return { + code: 'required_proof_missing', target: claim.target, + obligation_id: claim.id, + } } - const key = JSON.stringify([ - row.kind, row.arm, row.controller, row.detail, row.members, row.proofs, - ]) - const prior = compactGroups.get(key) - compactGroups.set(key, prior ? { ...prior, depth: (prior.depth ?? 1) + 1 } : row) + claimsOut.push({ + id: claim.id, kind: claim.kind, + statement: claim.kind === 'subject' ? `${plan.subject}.` : `${claim.kind} proven.`, + proofs: refs, + }) } - const order = [...compactGroups.values()].map((row, index) => - ({ id: `g${index + 1}`, ...row })) - const roots = rootIds.map(entity) - const terminals = terminalIds.map(entity) return { - state: 'ready', - dossier: { - query: query(plan), - obligations, - flow: { - roots, terminals, - links, order, - }, - evidence: { - digest_algorithm: 'sha256-base64url', - files: [...hydration.files].map(([path, [alias, sha256]]) => ({ - id: alias, path, - digest: Buffer.from(sha256, 'hex').toString('base64url'), - })), - excerpts: [...hydration.excerpts.values()].map(([alias, file, range, , text]) => ({ - id: alias, file, - range: [ - range.start.line, range.start.column, - range.end.line, range.end.column, - ] as const, - text, - })), - entities, - proofs, - }, + query: ask(plan), obligations: claimsOut, + flow: { roots: roots.map(ref), terminals: ends.map(ref), links, order }, + evidence: { + digest_algorithm: 'sha256-base64url', + files: [...auth.files].map(([path, [id, sha256]]) => ({ + id, path, digest: Buffer.from(sha256, 'hex').toString('base64url'), + })), + excerpts: [...auth.excerpts.values()].map(([id, file, range, , text]) => ({ + id, file, range: [range.start.line, range.start.column, + range.end.line, range.end.column], text, + })), + controls, entities: ents, proofs: refs, }, } } -function selectionMissing(selection: WorkflowSelection): MissingRequirement[] { - const rows = new Map() - for (const entry of selection.missing) { - const row: MissingRequirement = { - code: entry.code, - ...(entry.obligationId ? { obligation_id: entry.obligationId } : {}), - ...(entry.target.length <= 96 ? { target: entry.target } : {}), - } - rows.set(`${row.code}\0${row.obligation_id ?? ''}\0${row.target ?? ''}`, row) - } - return [...rows.values()] -} - export function retrieveContext(index: QueryIndex, input: unknown): RetrieveContextResult { - const request = normalizeRetrieveRequest(input) - const planned = planQuestion(request) + const req = normalizeRetrieveRequest(input) + const planned = planQuestion(req) if (planned.status === 'unsupported') { - return fitTerminal({ - ...header('unsupported', request), + return fit({ + ...base('unsupported', req), reason: planned.reason, terms: planned.terms.slice(0, 8).map((term) => term.slice(0, 32)), - }, request.budget) + }, req.budget) } const plan = planned.plan - if (index.state !== 'ready') return failure(request, index.state, index.subject) - let selection: WorkflowSelection + if (index.state !== 'ready') return fail(req, index.state, index.subject) + let flow: WorkflowSelection try { - selection = selectWorkflow(index, plan) + flow = selectWorkflow(index, plan) } catch { - return failure(request, 'corrupt', 'workflow selection') + return fail(req, 'corrupt', 'workflow selection') } - let hydration: InternalHydration - const emittedOperationIds = emittedOperations(plan, selection, index) + let auth: HydratedEvidenceResult try { - const validationOperationIds = selection.operationIds.filter((id) => - !emittedOperationIds.includes(id)) - hydration = hydrateEvidence(index, { - symbolIds: selection.symbolIds, - declarationSymbolIds: declarationTargets(plan, selection, index), - operationIds: emittedOperationIds, - validationOperationIds, - edges: selection.edges, - }) as InternalHydration + auth = hydrateEvidence(index, select(plan, flow, index)) } catch { - return failure(request, 'corrupt', 'evidence hydration', selection) + return fail(req, 'corrupt', 'evidence hydration', flow) } - if (hydration.state !== 'ready') { - return failure(request, hydration.state, hydration.subject, selection) + if (auth.state !== 'ready') { + return fail(req, auth.state, auth.subject, flow) } - if (!selection.complete) { - return incomplete( - request, plan, selectionMissing(selection), selection, hydration, - ) + if (!flow.complete) { + return miss(req, plan, flow.missing.map((entry): MissingRequirement => ({ + code: entry.code, + ...(entry.obligationId ? { obligation_id: entry.obligationId } : {}), + ...(entry.target.length <= 96 ? { target: entry.target } : {}), + })), flow, auth) } - const exceeded = hydration.files.size > MAX_RETRIEVE_FILES - ? ['required_file_limit', hydration.files.size, MAX_RETRIEVE_FILES] as const - : hydration.excerpts.size > MAX_RETRIEVE_EXCERPTS - ? ['required_excerpt_limit', hydration.excerpts.size, MAX_RETRIEVE_EXCERPTS] as const + const over = auth.files.size > FILES + ? ['required_file_limit', auth.files.size, FILES] as const + : auth.excerpts.size > EXCERPTS + ? ['required_excerpt_limit', auth.excerpts.size, EXCERPTS] as const : undefined - if (exceeded) return incomplete( - request, plan, limitMissing(exceeded[0], exceeded[1], exceeded[2]), selection, hydration, + if (over) return miss( + req, plan, cap(over[0], over[1], over[2]), flow, auth, ) try { - const built = buildDossier(plan, selection, hydration, emittedOperationIds, index) - if (built.state !== 'ready') return built.state === 'incomplete' - ? incomplete(request, plan, [built.missing], selection, hydration, true) - : failure(request, 'corrupt', built.subject, selection) - const ready = stabilize({ - ...header('ready', request, selection, hydration), - dossier: built.dossier, + const built = pack(plan, flow, auth, index) + if ('code' in built) return miss( + req, plan, [built], flow, auth) + const ready = seal({ + ...base('ready', req, flow, auth), + dossier: built, }) - if (ready.metrics.serialized_tokens > request.budget) { - return incomplete(request, plan, limitMissing( - 'required_token_budget', ready.metrics.serialized_tokens, request.budget, - ), selection, hydration) - } + if (ready.metrics.serialized_tokens > req.budget) return miss( + req, plan, cap('required_token_budget', + ready.metrics.serialized_tokens, req.budget), flow, auth, + ) return ready } catch { - return failure(request, 'corrupt', 'dossier packing', selection) + return fail(req, 'corrupt', 'dossier packing', flow) } } export function serializeRetrieveContextResult(result: RetrieveContextResult): string { - return canonicalJsonString(result) + return json(result) } diff --git a/src/domain/query/plan.ts b/src/domain/query/plan.ts index 1adece93..e4f93830 100644 --- a/src/domain/query/plan.ts +++ b/src/domain/query/plan.ts @@ -21,16 +21,16 @@ const BEHAVIOR = new Set( const IRREGULAR = new Map('built=build generation=generate got=get getting=get persistence=persist planned=plan planning=plan ran=run running=run setting=set written=write wrote=write'.split(' ').map((pair) => pair.split('=') as [string, string])) function canonical(value: string): string { - const irregular = IRREGULAR.get(value) - if (irregular) return irregular + const mapped = IRREGULAR.get(value) + if (mapped) return mapped const ing = value.endsWith('ing') ? value.slice(0, -3) : '' const past = value.endsWith('ed') ? value.slice(0, -2) : '' - const candidates = [value, + const forms = [value, /i(?:es|ed)$/u.test(value) ? `${value.slice(0, -3)}y` : '', ing, ing ? `${ing}e` : '', past, past ? `${past}e` : '', value.endsWith('es') ? value.slice(0, -2) : '', value.endsWith('s') ? value.slice(0, -1) : ''] - const action = candidates.find((candidate) => ACTIONS.has(candidate)) + const action = forms.find((candidate) => ACTIONS.has(candidate)) if (action) return action if (value.length <= 4) return value if (value.endsWith('ies')) return `${value.slice(0, -3)}y` @@ -38,11 +38,11 @@ function canonical(value: string): string { } export function lexicalTokens(value: string): string[] { - const normalized = value.normalize('NFKC') + const raw = value.normalize('NFKC') .replace(/([\p{Ll}\p{N}])([\p{Lu}])/gu, '$1 $2') .replace(/[’']s\b/giu, '') .toLowerCase() - return (normalized.match(/[\p{L}\p{N}]+/gu) ?? []).map(canonical) + return (raw.match(/[\p{L}\p{N}]+/gu) ?? []).map(canonical) } const [FW, LW, EW] = @@ -53,72 +53,106 @@ const FN = 'flow|workflow|pipeline|lifecycle' const FV = 'generate|run|execute|create|build|produce|process' const OWNER = 'file|module|class|function|method|service|handler' -const isNoise = (token: string, intent: QueryIntent): boolean => - COMMON.has(token) || (intent === 'workflow' ? FLOW.has(token) - : intent === 'locate' ? LOCATE.has(token) : EXPLAIN.has(token)) -const useful = ( - value: string, intent: QueryIntent, common = false, +const isNoise = (token: string, mode: QueryIntent): boolean => + COMMON.has(token) || (mode === 'workflow' ? FLOW.has(token) + : mode === 'locate' ? LOCATE.has(token) : EXPLAIN.has(token)) +const content = ( + value: string, mode: QueryIntent, plain = false, ): string[] => [...new Set(lexicalTokens(value).filter((token) => - !(common ? COMMON.has(token) : isNoise(token, intent))))] + !(plain ? COMMON.has(token) : isNoise(token, mode))))] function pick( - phrase: string, intent: QueryIntent, patterns: readonly RegExp[], common = false, + text: string, mode: QueryIntent, rules: readonly RegExp[], plain = false, ): string { - for (const pattern of patterns) { - const subject = useful(pattern.exec(phrase)?.[1] ?? '', intent, common).join(' ') - if (subject) return subject + for (const rule of rules) { + const topic = content(rule.exec(text)?.slice(1).join(' ') ?? '', + mode, plain).join(' ') + if (topic) return topic } return '' } -type SubjectMatch = readonly [subject: string, ignored: readonly string[]] +type SubjectMatch = readonly [topic: string, ignored: readonly string[]] -function flowSubject(phrase: string): SubjectMatch { - const event = pick(phrase, 'workflow', [ +type CoordinatedFlow = { + subject: string; entry: string; stage?: string; handoff?: string + terminal: string; terms: string[] +} +function coordinatedFlow(text: string): CoordinatedFlow | undefined { + const entry = /\b(?:accept|receive|submit|handle) (.+?)(?= (?:schedule|enqueue|queue|dispatch|research|process|compose|assemble|render|write|save|store|persist)\b)/u + .exec(text), + end = /\b(?:write|save|store|persist) (.+)$/u.exec(text), + composed = /\b(compose|assemble|render) (.+?)(?= (?:and )?(?:write|save|store|persist)\b|$)/u + .exec(text) + if (!entry || !end) return undefined + const input = content(entry[1]!, 'workflow'), + output = content(composed?.[2] ?? end[1]!, 'workflow') + const first = input[0], rawLast = output.at(-1), last = composed + && /^(?:model|output|result)$/u.test(rawLast ?? '') ? 'report' : rawLast + if (!first || !last) return undefined + const handoff = /\b(?:schedule|enqueue|queue|dispatch|publish|emit)\b/u.test(text) + ? 'schedule' : undefined, + stages = [ + /\b(?:research|investigate|discover)\b/u.test(text) ? 'research' : '', + /\b(?:compose|assemble|render|synthesize|merge)\b/u.test(text) + ? 'assemble' : '', + ].filter(Boolean) + return { + subject: first === last ? first : `${first} ${last}`, + entry: `request ${first}`, ...(stages.length ? { stage: stages.join(' ') } : {}), + ...(handoff ? { handoff } : {}), terminal: 'persistence', + terms: [...new Set([first, last, ...stages, ...(handoff ? [handoff] : [])])], + } +} + +function flowSubject(text: string): SubjectMatch { + const direct = pick(text, 'workflow', [ /\bwhat happen when (?:(?:a|an|the) )?(?:user|client|caller) (?:request|submit) (.+)$/, - ]) - if (event) return [event, []] - const walked = pick(phrase, 'workflow', [ + ]) || pick(text, 'workflow', [ /\bwalk (?:me )?through (.+?)(?= from\b| via\b| to\b|$)/, - ]) - if (walked) return [walked, []] - const traced = pick(phrase, 'workflow', [ + ]) || pick(text, 'workflow', [ /\btrace (?:the )?(.+)(?= from .+ (?:to|through|via)\b)/, - ], true) - if (traced) return [traced, []] - const passive = pick(phrase, 'workflow', [ + /\bwhat \w+ (?:the )?(.+?) that/, + ], true) || pick(text, 'workflow', [ RegExp(`\\bhow (?:is|are|was|were) (.+?) (?:${FW})\\b`), RegExp(`\\bhow (?:does|do|did) (.+?) get (?:${FW})\\b`), ]) - if (passive) return [passive, []] + if (direct) return [direct, []] const active = RegExp( `\\bhow (?:(?:${AUX}) )?(.+?) (${FW}) (.+?)(?= (?:${CLAUSE})\\b| end to end\\b|$)`, - ).exec(phrase) + ).exec(text) if (active) { - const object = useful(active[3]!, 'workflow').join(' ') - if (object) return [object, useful(active[1]!, 'workflow', true)] + const object = content(active[3]!, 'workflow').join(' ') + if (object) return [object, content(active[1]!, 'workflow', true)] } - const subject = pick(phrase, 'workflow', [ + const topic = pick(text, 'workflow', [ + /^follow (?:\S+ )*?(\S+) from .+ until (?:\S+ )*?(\S+) is/, + /for (.+?) generate/, /\btrace (?:the )?(.+?)(?= through\b| via\b| to\b|$)/, RegExp(`\\b(?:${FN}) (?:of|for) (.+?)(?= from\\b|$)`), RegExp(`(.+?) (?:${FN})\\b`), RegExp(`\\bhow (?:(?:${AUX}) )?(.+?) (?:${FW})\\b`), + /\w+ (?:an?|the) (\S+)/, ]) - return [subject || useful(phrase, 'workflow').join(' '), []] + return [topic || content(text, 'workflow').join(' '), []] } type FlowBounds = { entry?: string | undefined stage?: string | undefined + handoff?: string | undefined terminal?: string | undefined } -function flowBounds(phrase: string): FlowBounds { - if (!/\b(?:from|through|via)\b/u.test(phrase)) return {} - const read = (pattern: RegExp): string | undefined => { - const value = useful(pattern.exec(phrase)?.[1] ?? '', 'workflow', true).join(' ') +function flowBounds(text: string): FlowBounds { + if (/^follow /.test(text) + || !(/\b(?:from|via)\b/u.test(text) || /^(?:trace|walk)\b.*\bthrough\b/u + .test(text))) return {} + const read = (rule: RegExp): string | undefined => { + const value = lexicalTokens(rule.exec(text)?.[1] ?? '') + .filter((token) => !COMMON.has(token)).join(' ') return value || undefined } - const walked = /^walk (?:me )?through\b/u.test(phrase) + const walked = /^walk (?:me )?through\b/u.test(text) return { entry: read(/\bfrom (.+?)(?= (?:through(?: to)?|via|to)\b| how\b|$)/u), stage: read(/\bvia (.+?)(?= to\b| how\b|$)/u) @@ -129,9 +163,9 @@ function flowBounds(phrase: string): FlowBounds { } } -function simpleSubject(phrase: string, intent: 'locate' | 'explain'): string { - const locate = intent === 'locate' - const patterns = locate ? [ +function simpleSubject(text: string, mode: 'locate' | 'explain'): string { + const locate = mode === 'locate' + const rules = locate ? [ RegExp(`\\bwhere (?:(?:${AUX}) )?(.+?)(?= (?:${LW})\\b| (?:${CLAUSE})\\b|$)`), RegExp(`\\b(?:which (?:${OWNER}) |what )(?:${LW}) (.+?)(?= (?:${CLAUSE})\\b|$)`), /\b(?:locate|find)(?: the)? (.+?)(?= (?:definition|declaration|implementation)\b|$)/, @@ -143,73 +177,83 @@ function simpleSubject(phrase: string, intent: 'locate' | 'explain'): string { RegExp(`\\b(?:how|why|what) (?:(?:${AUX}) )?(.+?) (?:${EW})\\b`), /\b(?:explain|describe)(?: how)?(?: the)? (.+)$/, ] - return pick(phrase, intent, patterns, locate) - || useful(phrase, intent).join(' ') + return pick(text, mode, rules, + locate || /\b(?:call|invoke)\b/u.test(text)) + || content(text, mode).join(' ') } export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanResult { - const phrase = lexicalTokens(request.question).join(' '), - qualified = /(?:^|[^\p{L}\p{N}_$])([\p{L}_$][\p{L}\p{N}_$]*\.[\p{L}_$][\p{L}\p{N}_$]*)/u - .exec(request.question.normalize('NFKC'))?.[1], - identifier = /\bwhere\s+(?:is|are|was|were)\s+[`'"]?([\p{L}_$][\p{L}\p{N}_$.-]*)[`'"]?\s+(?:defined|declared|implemented)\b/iu - .exec(request.question.normalize('NFKC'))?.[1] - const intent: QueryIntent | undefined = - /\b(?:end to end|what happen when)\b|\bfrom\b.+\b(?:through|via)\b.+\bto\b|\btrace\b.+\bfrom\b.+\bto\b/.test(phrase) - || !qualified && RegExp(`\\bhow (?:(?:${AUX}) (?!${FV}\\b)|(?!(?:${AUX}|${FV})\\b))\\S+(?: \\S+)*? (?:${FV})\\b`).test(phrase) + const raw = request.question.normalize('NFKC'), + text = lexicalTokens(request.question).join(' '), + names = [...raw.matchAll(/(? match[1]!), + ident = /\bwhere\s+(?:is|are|was|were)\s+[`'"]?([\p{L}_$][\p{L}\p{N}_$.-]*)[`'"]?\s+(?:defined|declared|implemented)\b/iu + .exec(raw)?.[1] + const mode: QueryIntent | undefined = + /\b(?:end to end|what happen when)\b|\bfrom\b.+\b(?:through|via)\b.+\bto\b|\btrace\b.+\bfrom\b.+\bto\b/.test(text) + || /^follow /.test(text) + || /^which .+\b(?:save|write)\b/.test(text) + || /\bhow\b[\s\S]*\bgenerat(?:e|ed|es|ing)\b/iu.test(raw) + || /\bhow\s+(?:(?:does|do|did|can|could|would|should|will)\s+)?(?!(?:does|do|did|can|could|would|should|will)\b)[\p{L}_$][\p{L}\p{N}_$]*\s+(?:generate|run|execute|create|build|produce|process)\b/iu.test(raw) ? 'workflow' - : /\b(?:where|locate|find|definition|declaration|implementation)\b/.test(phrase) - || RegExp(`\\bwhich (?:${OWNER}) (?:${LW})\\b`).test(phrase) - || RegExp(`\\bwhat (?:${LW})\\b`).test(phrase) ? 'locate' - : /^trace\b|\b(?:flow|workflow|pipeline|lifecycle)\b/.test(phrase) ? 'workflow' + : /\b(?:where|locate|find|definition|declaration|implementation)\b/.test(text) + || RegExp(`\\bwhich (?:${OWNER}) (?:${LW})\\b`).test(text) + || RegExp(`\\bwhat (?:${LW})\\b`).test(text) ? 'locate' + : /^trace\b|\b(?:flow|workflow|pipeline|lifecycle)\b/.test(text) ? 'workflow' : /\b(?:explain|describe|how|why|behavior)\b|\bwhat (?:does|do|is|are)\b/ - .test(phrase) + .test(text) || RegExp(`\\b(?:what (?:${FW}|${EW})|which (?:${OWNER}))\\b`) - .test(phrase) ? 'explain' : undefined - if (!intent) { + .test(text) ? 'explain' : undefined + if (!mode) { return { status: 'unsupported', reason: 'unsupported_intent', - terms: [...new Set(phrase.split(' ').filter((token) => !COMMON.has(token)))].sort(), + terms: [...new Set(text.split(' ').filter((token) => !COMMON.has(token)))].sort(), } } - const [subject, ignored] = intent === 'workflow' - ? qualified ? [useful(qualified, 'workflow', true).join(' '), []] - : flowSubject(phrase) - : [qualified - ? useful(qualified, intent, true).join(' ') - : intent === 'locate' && identifier - ? useful(identifier, 'locate', true).join(' ') - : simpleSubject(phrase, intent), []] - const bounds = intent === 'workflow' ? flowBounds(phrase) : {} - const omitted = new Set(ignored) - const terms = new Set(lexicalTokens(phrase).filter((token) => - !isNoise(token, intent) && !omitted.has(token))) - lexicalTokens(subject).forEach((token) => terms.add(token)) + const coordinated = mode === 'workflow' ? coordinatedFlow(text) : undefined + const [topic, ignored] = mode === 'workflow' + ? coordinated ? [coordinated.subject, []] : flowSubject(text) + : [names[0] + ? content(names[0], mode, true).join(' ') + : mode === 'locate' && ident + ? content(ident, 'locate', true).join(' ') + : simpleSubject(text, mode), []] + const span: FlowBounds = coordinated + ?? (mode === 'workflow' ? flowBounds(text) : {}) + if (mode === 'workflow' && names.length) { + span.stage = names.flatMap(lexicalTokens).join(' ') + } + const skip = new Set(ignored) + const terms = new Set((coordinated?.terms ?? lexicalTokens(text)).filter((token) => + !isNoise(token, mode) && !skip.has(token))) + lexicalTokens(topic).forEach((token) => terms.add(token)) const sorted = [...terms].sort() - if (!subject || sorted.length === 0) { + if (!topic || sorted.length === 0) { return { status: 'unsupported', reason: 'missing_subject', terms: sorted } } - const words = new Set(phrase.split(' ')) - const access: LocateAccess | undefined = intent !== 'locate' ? undefined + const words = new Set(text.split(' ')) + const access: LocateAccess | undefined = mode !== 'locate' ? undefined : ['read', 'find'].some((word) => words.has(word)) ? 'read' : ['write', 'save', 'set', 'update', 'persist', 'store'] .some((word) => words.has(word)) ? 'write' : undefined - const kinds: readonly ObligationKind[] = intent === 'locate' ? ['subject'] - : intent === 'explain' ? ['subject', 'behavior'] + const kinds: readonly ObligationKind[] = mode === 'locate' ? ['subject'] + : mode === 'explain' ? ['subject', 'behavior'] : ['subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal'] - const residual = sorted.filter((token) => !lexicalTokens(subject).includes(token)) - const actions = phrase.split(' ').filter((token) => BEHAVIOR.has(token)) - const behavior = [...new Set(residual.length > 0 ? residual : actions)].join(' ') + const rest = sorted.filter((token) => !lexicalTokens(topic).includes(token)) + const verbs = text.split(' ').filter((token) => BEHAVIOR.has(token)) + const behavior = [...new Set(rest.length > 0 ? rest : verbs)].join(' ') return { status: 'supported', plan: { - intent, subject, terms: sorted, + intent: mode, subject: topic, terms: sorted, obligations: kinds.map((kind, index): QueryObligation => ({ id: `o${index + 1}`, kind, - target: kind === 'entry' ? bounds.entry ?? subject - : kind === 'stage' ? bounds.stage ?? subject - : kind === 'terminal' ? bounds.terminal ?? subject - : kind === 'behavior' && intent === 'explain' && behavior - ? behavior : subject, + target: kind === 'entry' ? span.entry ?? topic + : kind === 'stage' ? span.stage ?? topic + : kind === 'handoff' ? span.handoff ?? topic + : kind === 'terminal' ? span.terminal ?? topic + : kind === 'behavior' && mode === 'explain' && behavior + ? behavior : topic, mandatory: true, })), ...(access ? { access } : {}), diff --git a/src/domain/query/types.ts b/src/domain/query/types.ts index 7397bc92..6d194a52 100644 --- a/src/domain/query/types.ts +++ b/src/domain/query/types.ts @@ -1,13 +1,13 @@ import type { IndexBodyFact, IndexRange, IndexValue } from '../index/model.js' -export const RETRIEVE_RESULT_SCHEMA = 'madar.retrieve' as const -export const RETRIEVE_RESULT_VERSION = 2 as const -export const DEFAULT_RETRIEVE_BUDGET = 4000 -export const MIN_RETRIEVE_BUDGET = 256 -export const MAX_RETRIEVE_BUDGET = 4000 -export const MAX_RETRIEVE_QUESTION_LENGTH = 512 -export const MAX_RETRIEVE_FILES = 12 -export const MAX_RETRIEVE_EXCERPTS = 25 +export const RETRIEVE_RESULT_SCHEMA = 'madar.retrieve' as const, + RETRIEVE_RESULT_VERSION = 2 as const, + DEFAULT_RETRIEVE_BUDGET = 4000, + MIN_RETRIEVE_BUDGET = 256, + MAX_RETRIEVE_BUDGET = 4000, + MAX_RETRIEVE_QUESTION_LENGTH = 512, + MAX_RETRIEVE_FILES = 12, + MAX_RETRIEVE_EXCERPTS = 25 export function valueHas( value: IndexValue, test: (candidate: IndexValue) => boolean, @@ -24,9 +24,10 @@ export interface NormalizedRetrieveRequest { } export type RetrieveIntent = 'locate' | 'explain' | 'workflow' -type List = readonly T[] -type FailureState = 'stale' | 'unavailable' | 'corrupt' -type EvidenceFailure = { state: FailureState; subject: string } +type L = readonly T[] +type M = ReadonlyMap +type FS = 'stale' | 'unavailable' | 'corrupt' +type EF = { state: FS; subject: string } export type RetrieveObligationKind = | 'subject' | 'entry' @@ -39,20 +40,20 @@ export type RetrieveState = | 'ready' | 'incomplete' | 'unsupported' - | FailureState + | FS -type MetricKey = `${'budget' | 'serialized'}_tokens` | 'selected_files' +type MK = `${'budget' | 'serialized'}_tokens` | 'selected_files' | 'authenticated_excerpts' | `${'required' | 'proven'}_obligations` | 'optional_bundles_omitted' | `${'root' | 'initial'}_candidates` | 'explored_nodes' | 'causal_hops' | 'recovery_frontier_nodes' | 'alternate_seeds' -export type RetrieveMetrics = Record & { +export type RetrieveMetrics = Record & { recovery_passes: 0 | 1 | 2 } export interface QuerySummary { intent: RetrieveIntent subject: string - terms: List + terms: L } export type QueryIntent = RetrieveIntent export type LocateAccess = 'read' | 'write' @@ -61,15 +62,15 @@ export interface QueryObligation { id: `o${number}`; kind: ObligationKind; target: string; mandatory: boolean } export interface QueryPlan { - intent: QueryIntent; subject: string; terms: List - obligations: List; access?: LocateAccess + intent: QueryIntent; subject: string; terms: L + obligations: L; access?: LocateAccess } export type QuestionPlanResult = { status: 'supported'; plan: QueryPlan } | { status: 'unsupported' reason: 'unsupported_intent' | 'missing_subject' - terms: List + terms: L } export type RetrieveMissingCode = @@ -88,46 +89,46 @@ export interface MissingRequirement { limit?: number } -type StringFields = Record -type Tagged = { kind: K } -type DossierRow = StringFields<'id' | K> -type ProvenRow = DossierRow & { - proofs: List +type SF = Record +type Tag = { kind: K } +type DR = SF<'id' | K> +type PR = DR & { + proofs: L } -type OperationRefs = { operationIds: List } -type WorkflowRefs = OperationRefs & { symbolIds: List } +type OR = { operationIds: L } +type WR = OR & { symbolIds: L } export type WorkflowRelation = | 'calls' | 'publishes_to' | 'routes_through' | 'consumed_by' export type WorkflowMissingCode = Extract< RetrieveMissingCode, `${string}_unproven` | 'selection_bound_reached' > -export type WorkflowEdge = StringFields<'id' | 'fromId' | 'toId'> & { +export type WorkflowEdge = SF<'id' | 'fromId' | 'toId'> & { relation: WorkflowRelation } -export type WorkflowHandoff = OperationRefs & StringFields<'fromId' | 'toId'> & { - kind: 'direct' | 'channel'; edgeIds: List +export type WorkflowHandoff = OR & SF<'fromId' | 'toId'> & { + kind: 'direct' | 'channel'; edgeIds: L } -export type WorkflowControlGroup = WorkflowRefs & { +export type WorkflowControlGroup = WR & { kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' controllerOperationId?: string; arm?: string } -export type WorkflowObligationProof = WorkflowRefs & { +export type WorkflowObligationProof = WR & { id: `o${number}`; kind: RetrieveObligationKind; target: string - mandatory: boolean; proven: boolean; edgeIds: List + mandatory: boolean; proven: boolean; edgeIds: L } export type WorkflowMissingReason = { code: WorkflowMissingCode; obligationId?: string; target: string } -export type WorkflowSelection = WorkflowRefs & { +export type WorkflowSelection = WR & { complete: boolean - rootSymbolIds: List - terminalSymbolIds: List - edges: List - links: List - controlGroups: List - obligations: List - missing: List + rootSymbolIds: L + terminalSymbolIds: L + edges: L + links: L + controlGroups: L + obligations: L + missing: L metrics: { candidateCount: number; rootCandidateCount: number actualNodeCount: number; causalRelationHops: number @@ -135,111 +136,102 @@ export type WorkflowSelection = WorkflowRefs & { } } -export type ProvenObligation = ProvenRow<'statement'> & { +export type ProvenObligation = PR<'statement'> & { kind: RetrieveObligationKind } -export type DossierFile = DossierRow<'path' | 'digest'> +export type DossierFile = DR<'path' | 'digest'> -export type DossierExcerpt = DossierRow<'file' | 'text'> & { +export type DossierExcerpt = DR<'file' | 'text'> & { range: readonly [number, number, number, number] } -export type DossierEntity = DossierRow & (Tagged<'symbol'> & StringFields<'label' | 'file'> & { +export type DossierControl = DR<'file'> & { + ranges: L +} + +export type DossierEntity = DR & (Tag<'symbol'> & SF<'label' | 'file'> & { node_kind?: string excerpt?: string -} | Tagged<'channel'> & StringFields<'transport' | 'key'> & { +} | Tag<'channel'> & SF<'transport' | 'key'> & { channel_kind: 'queue' | 'job' | 'event' parent?: string scope?: string -} | Tagged<'operation'> & { excerpt: string } & (StringFields<'operation_kind' | 'owner'> & { +} | Tag<'operation'> & { excerpt: string } & SF<'operation_kind' | 'owner'> & { detail: Readonly> - links?: never -} | { - links: List; order: List - callee?: string - scheduling?: string -})) +}) -export type DossierProof = DossierRow<'excerpt' | 'from' | 'to' | 'relation'> +export type DossierProof = DR<'from' | 'to' | 'relation'> & ( + SF<'excerpt'> | { file: string; range: readonly [number, number, number, number] } +) -export type DossierLink = ProvenRow<'from' | 'to'> & { +export type DossierLink = PR<'from' | 'to'> & { kind: 'direct' | 'channel' } -export type DossierOrderGroup = ProvenRow & { +export type DossierOrderGroup = DR & { kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' controller?: string arm?: string - depth?: number detail?: Readonly> - members: List + depths?: L + members: L + proofs?: L } export interface AnswerDossier { query: QuerySummary - obligations: List + obligations: L flow: { - roots: List - terminals: List - links: List - order: List + roots: L + terminals: L + links: L + order: L } evidence: { digest_algorithm: 'sha256-base64url' - files: List - excerpts: List - entities: List - proofs: List + files: L + excerpts: L + controls: L + entities: L + proofs: L } } -export type SelectedEvidenceEdge = DossierRow<'fromId' | 'toId'> & { +export type SelectedEvidenceEdge = DR<'fromId' | 'toId'> & { relation?: string } export interface EvidenceHydrationTargets { - symbolIds: List - declarationSymbolIds: List - operationIds: List - validationOperationIds?: List - edges: List + symbolIds: L + declarationSymbolIds: L + operationIds: L + validationOperationIds?: L + edges: L } -export type HydratedFile = readonly [alias: string, sha256: string] -export type HydratedExcerpt = readonly [ - alias: string, file: string, range: IndexRange, sha256: string, text: string, -] +export type HydratedFile = readonly [string, string] +export type HydratedExcerpt = readonly [string, string, IndexRange, string, string] +export type HydratedControl = readonly [string, IndexRange] export type HydratedEntity = - | readonly [ - alias: string, kind: 'symbol', label: string, nodeKind: string, file: string, - ] - | readonly [ - alias: string, kind: 'channel', channelKind: 'queue' | 'job' | 'event', - transport: string, key: string, - parentChannelId: string | undefined, scope: string | undefined, - ] - | readonly [ - alias: string, kind: 'operation', owner: string, fact: IndexBodyFact, - ] + | readonly [string, 'symbol', string, string, string] + | readonly [string, 'channel', 'queue' | 'job' | 'event', string, string, + string | undefined, string | undefined] + | readonly [string, 'operation', string, IndexBodyFact] export type HydratedProof = - | readonly [ - alias: string, kind: 'declaration' | 'operation', - subject: string, excerpt: string, - ] - | readonly [ - alias: string, kind: 'edge', from: string, to: string, - relation: string, excerpt: string, - ] + | readonly [string, 'declaration' | 'operation', string, string] + | readonly [string, 'edge', string, string, string, string] + | readonly [string, 'edge_range', string, string, string, string, IndexRange] export type HydratedEvidenceResult = { state: 'ready' - files: ReadonlyMap - entities: ReadonlyMap - excerpts: ReadonlyMap - proofs: ReadonlyMap -} | EvidenceFailure + files: M + controls: M + entities: M + excerpts: M + proofs: M +} | EF -interface RetrieveResultBase { +interface RB { schema: typeof RETRIEVE_RESULT_SCHEMA version: typeof RETRIEVE_RESULT_VERSION state: S @@ -247,17 +239,17 @@ interface RetrieveResultBase { } export type RetrieveContextResult = - | RetrieveResultBase<'ready'> & { dossier: AnswerDossier } - | RetrieveResultBase<'incomplete'> & { + | RB<'ready'> & { dossier: AnswerDossier } + | RB<'incomplete'> & { query: QuerySummary - missing: List + missing: L } - | RetrieveResultBase<'unsupported'> & { + | RB<'unsupported'> & { reason: 'unsupported_intent' | 'missing_subject' - terms: List + terms: L } - | RetrieveResultBase & { - failures: List + | RB & { + failures: L } export function normalizeRetrieveRequest(value: unknown): NormalizedRetrieveRequest { diff --git a/src/domain/query/workflow.ts b/src/domain/query/workflow.ts index 9df67125..3b9cd8c5 100644 --- a/src/domain/query/workflow.ts +++ b/src/domain/query/workflow.ts @@ -11,7 +11,9 @@ import { } from './types.js' const [CANDIDATES, NODES, HOPS, RECOVERY] = [32, 512, 24, 64] const FAILURE_WORD = /^(?:abort|cancel|error|fail(?:ed|ure)?|refund|reject(?:ed)?|retry|rollback)$/u, - READ = /^(?:read|file_read|object_read)$/u + READ = /^(?:read|file_read|object_read)$/u, + GENERIC_TERMINAL = /^(?:data|output|persist|persistence|record|report|result|storage|store|write)$/u, + DATABASE = /^(?:database|db|mongo|mongodb|repository|sql)$/u const MISSING_CODES: Partial> = { handoff: 'adjacent_handoff_unproven', behavior: 'behavior_unproven', subject: 'subject_unproven', entry: 'entrypoint_unproven', @@ -25,33 +27,33 @@ type SymbolNode = readonly [ ] type IndexedEdge = readonly [ id: string, from: string, to: string, relation: WorkflowRelation, - range: string, statement: string, owner: string, operation: string, + evidence: string, owner: string, operation: string, dispatchPayload: number | undefined, ] type Arc = readonly [ from: string, to: string, kind: 'direct' | 'channel', - edges: readonly IndexedEdge[], operations: readonly string[], + edges: readonly IndexedEdge[], ops: readonly string[], ] type ExecutionView = readonly [ - symbols: readonly SymbolNode[], byId: ReadonlyMap, + nodes: readonly SymbolNode[], byId: ReadonlyMap, outgoing: ReadonlyMap, incoming: ReadonlyMap, blocked: ReadonlySet, - nonEntries: ReadonlySet, operations: ReadonlyMap, + nonEntries: ReadonlySet, ops: ReadonlyMap, ] type Candidate = readonly [ symbol: SymbolNode, lexical: number, rank: number, affinity: number, exact: number, ] type Reach = readonly [ - distance: Map, actual: Set, bounded: boolean, - previous: Map, + dist: Map, actual: Set, bounded: boolean, + prev: Map, ] type Selection = readonly [ - symbols: Set, arcs: Arc[], terminals: string[], + nodes: Set, arcs: Arc[], ends: string[], actual: Set, bounded: boolean, ] type Control = readonly [ - operations: string[], groups: WorkflowControlGroup[], proven: boolean, + ops: string[], groups: WorkflowControlGroup[], proven: boolean, terminalOperations: string[], ] const cache = new WeakMap() @@ -66,29 +68,29 @@ const factText = (fact: IndexBodyFact): string => ? `${fact.receiver_type} ${JSON.stringify(fact.resource ?? '')}` : fact.kind === 'mutation' ? fact.target : fact.kind === 'literal' ? JSON.stringify(fact.value) - : fact.kind === 'condition' ? `condition ${fact.condition_kind}` - : fact.kind === 'loop' ? `loop ${fact.loop_kind}` - : fact.kind === 'parallel' - ? `parallel ${fact.combinator} ${fact.completion}` - : fact.kind + : fact.kind === 'return' || fact.kind === 'throw' + ? `${fact.kind} ${JSON.stringify(fact.value ?? '')}` + : fact.kind const behavior = (fact: IndexBodyFact): boolean => fact.kind !== 'literal' +const adverse = (value: string): boolean => words(value).some((word) => + word !== 'retry' && FAILURE_WORD.test(word)) const terminal = ( fact: IndexBodyFact, ): fact is Extract => fact.kind === 'persistence' && !READ.test(fact.operation) -function adverseFact(fact: IndexBodyFact | undefined): boolean { +function bad(fact: IndexBodyFact | undefined): boolean { return !!fact && (fact.control.some((frame) => frame.kind === 'exception' && frame.arm === 'catch') || fact.kind === 'call' - && fact.arguments.some((argument) => + && (adverse(fact.callee) + || fact.arguments.some((argument) => valueHas(argument, (value) => value.kind === 'literal' && typeof value.value === 'string' - && words(value.value).some((word) => - word !== 'retry' && FAILURE_WORD.test(word))))) + && adverse(value.value))))) } -const terminalAt = (v: ExecutionView, id: string, adverse: boolean): boolean => +const hasTerminal = (v: ExecutionView, id: string, adverse: boolean): boolean => v[1].get(id)?.[5].some((fact) => terminal(fact) - && (adverse || !adverseFact(fact) && !adverseFact(v[6].get(fact.call_fact_id)))) ?? false + && (adverse || !bad(fact) && !bad(v[6].get(fact.call_fact_id)))) ?? false type LooseRange = { start?: { line?: unknown; column?: unknown } end?: { line?: unknown; column?: unknown } @@ -100,7 +102,7 @@ function rangeKey(value: unknown): string { const idsOf = (arc: Arc): string[] => arc[3].map((edge) => edge[0]) const penalty = (domain: SourceDomain): number => domain === 'production' ? 0 : domain === 'unknown' ? 4 : 32 -function requestEntry(attrs: GraphAttributes): boolean { +function isRequest(attrs: GraphAttributes): boolean { const role = text(attrs, 'framework_role'), kind = text(attrs, 'node_kind') return kind === 'route' @@ -110,7 +112,7 @@ function requestEntry(attrs: GraphAttributes): boolean { function buildView(i: ReadyQueryIndex): ExecutionView { const prior = cache.get(i) if (prior) return prior - const symbols: SymbolNode[] = [] + const nodes: SymbolNode[] = [] for (const [id, attrs] of i.graph.nodeEntries()) { if (['channel', 'file'].includes(text(attrs, 'node_kind'))) continue const facts = i.operations_by_owner.get(id) ?? [], @@ -121,20 +123,15 @@ function buildView(i: ReadyQueryIndex): ExecutionView { ].join(' '), nameWords = words(name), lexicon = words([name, file, ...facts.map(factText)].join(' ')) - symbols.push([id, lexicon.join(''), nameWords.join(''), + nodes.push([id, lexicon.join(''), nameWords.join(''), new Set(lexicon), new Set(nameWords), facts, facts.some(terminal), - domainOf(attrs.source_domain, file, i.root_path), requestEntry(attrs)]) + domainOf(attrs.source_domain, file, i.root_path), isRequest(attrs)]) } - symbols.sort((left, right) => cmp(left[0], right[0])) - const byId = new Map(symbols.map((symbol) => [symbol[0], symbol])), + const byId = new Map(nodes.map((symbol) => [symbol[0], symbol])), exact = new Map(), routes = new Map(), - consumers = new Map(), publishes: IndexedEdge[] = [], + subs = new Map(), pubs: IndexedEdge[] = [], nonEntries = new Set() - const keep = (key: string, edge: IndexedEdge): void => { - const prior = exact.get(key) - if (!prior || cmp(edge[0], prior[0]) < 0) exact.set(key, edge) - } for (const [from, to, attrs, id] of i.graph.edgeEntries()) { const relation = String(attrs.relation) as WorkflowRelation const evidence = attrs.evidence as { @@ -145,88 +142,97 @@ function buildView(i: ReadyQueryIndex): ExecutionView { || !/^(?:typescript-(?:semantic|syntactic)|framework-decorator|wrapper-summary)$/u .test(String(evidence?.source))) continue const owner = text(attrs, 'execution_owner_id'), - calls = relation === 'consumed_by' && owner && owner !== to - ? (i.operations_by_owner.get(owner) ?? []).filter((fact) => + edgeRange = rangeKey(evidence?.range), + statement = rangeKey(evidence?.statement_range), + bind = relation === 'publishes_to' ? from + : relation === 'consumed_by' && owner !== to ? owner : '', + calls = bind ? (i.operations_by_owner.get(bind) ?? []).filter((fact) => fact.kind === 'call' - && rangeKey(fact.evidence.statement_range) - === rangeKey(evidence?.statement_range) + && (relation === 'consumed_by' || rangeKey(fact.evidence.range) + === edgeRange) + && rangeKey(fact.evidence.statement_range) === statement && fact.evidence.excerpt_sha256 === evidence?.excerpt_sha256) : [], payload = attrs.dispatch_payload_argument, edge: IndexedEdge = [id, from, to, relation, - rangeKey(evidence?.range), rangeKey(evidence?.statement_range), - owner, calls.length === 1 ? calls[0]!.id : '', + `${edgeRange}\0${statement}`, owner, calls.length === 1 ? calls[0]!.id : '', typeof payload === 'number' && Number.isSafeInteger(payload) && payload >= 0 ? payload : undefined] if (byId.has(to) && (relation === 'consumed_by' || relation === 'calls' && byId.get(from)?.[7] === 'production')) nonEntries.add(to) - if (relation === 'calls') keep(`c\0${from}\0${to}\0${edge[4]}`, edge) + if (relation === 'calls') { + const key = `c\0${from}\0${to}\0${edgeRange}` + if (!exact.has(key)) exact.set(key, edge) + } else if (relation === 'routes_through') append(routes, `${from}\0${to}`, edge) - else if (relation === 'consumed_by') append(consumers, from, edge) - else publishes.push(edge) + else if (relation === 'consumed_by') append(subs, from, edge) + else pubs.push(edge) } - publishes.sort((a, b) => cmp(a[0], b[0])) - for (const entries of consumers.values()) entries.sort((a, b) => cmp(a[0], b[0])) - const arcs: Arc[] = [], - publishCalls = new Map []>() - for (const owner of symbols) { + let arcs: Arc[] = [] + for (const owner of nodes) { for (const fact of owner[5]) { if (fact.kind !== 'call' || !fact.target_symbol_id || !byId.has(fact.target_symbol_id)) continue - append(publishCalls, `${owner[0]}\0${rangeKey(fact.evidence.range)}\0${ - rangeKey(fact.evidence.statement_range)}`, fact) const edge = exact.get( `c\0${owner[0]}\0${fact.target_symbol_id}\0${rangeKey(fact.evidence.range)}`, ) if (edge) arcs.push([owner[0], fact.target_symbol_id, 'direct', [edge], [fact.id]]) } } - for (const publish of publishes) { - if (!byId.has(publish[1]) || !i.channels_by_id.has(publish[2])) continue - const channel = i.channels_by_id.get(publish[2])!, - matchingRoutes = channel.channel_kind === 'job' + for (const pub of pubs) { + if (!byId.has(pub[1]) || !i.channels_by_id.has(pub[2])) continue + const channel = i.channels_by_id.get(pub[2])!, + routed = channel.channel_kind === 'job' ? (routes.get(`${channel.id}\0${channel.parent_channel_id}`) ?? []) - .filter((edge) => edge[6] === publish[1] - && edge[4] === publish[4] && edge[5] === publish[5]) + .filter((edge) => edge[5] === pub[1] && edge[4] === pub[4]) : [], - route = matchingRoutes.length === 1 ? matchingRoutes[0] : undefined + route = routed.length === 1 ? routed[0] : undefined if (channel.channel_kind === 'job' && !route) continue - const destination = route?.[2] ?? channel.id - for (const consume of consumers.get(destination) ?? []) { - if (!byId.has(consume[2])) continue - const registration = !consume[6] || consume[6] === consume[2] - ? [] : consume[7] ? [consume[7]] : undefined - if (!registration) continue - const edges = route ? [publish, route, consume] : [publish, consume], - calls = publishCalls.get( - `${publish[1]}\0${publish[4]}\0${publish[5]}`, - ) ?? [] - arcs.push([publish[1], consume[2], 'channel', edges, - calls.length === 1 ? [calls[0]!.id, ...registration] : []]) + const dest = route?.[2] ?? channel.id + for (const sub of subs.get(dest) ?? []) { + if (!byId.has(sub[2])) continue + const binding = !sub[5] || sub[5] === sub[2] + ? [] : sub[6] ? [sub[6]] : undefined + if (!binding) continue + const edges = route ? [pub, route, sub] : [pub, sub] + arcs.push([pub[1], sub[2], 'channel', edges, + pub[6] ? [pub[6], ...binding] : []]) } } + const hidden = new Set() + arcs = arcs.filter((arc) => arc[2] !== 'channel' || !arc[4].some((id) => { + const fact = i.operation_by_id.get(id) + if (fact?.kind !== 'call' || !fact.target_symbol_id) return false + const redundant = arcs.some((direct) => direct[0] === arc[0] + && direct[1] === fact.target_symbol_id && direct[2] === 'direct' + && direct[4].includes(id)) + && arcs.some((inner) => inner[0] === fact.target_symbol_id + && inner[1] === arc[1] && inner[2] === 'channel') + if (redundant) arc[3].forEach((edge) => hidden.add(edge[0])) + return redundant + })) arcs.sort((a, b) => cmp(a[0], b[0]) || cmp(a[1], b[1]) || cmp(a[3][0]![0], b[3][0]![0])) const outgoing = new Map(), incoming = new Map() for (const arc of arcs) { append(outgoing, arc[0], arc); append(incoming, arc[1], arc) } - const usedEdges = new Set(arcs.flatMap(idsOf)), - blocked = new Set(publishes.filter((edge) => byId.has(edge[1]) - && !usedEdges.has(edge[0])).map((edge) => edge[1])), + const used = new Set([...arcs.flatMap(idsOf), ...hidden]), + blocked = new Set(pubs.filter((edge) => byId.has(edge[1]) + && !used.has(edge[0])).map((edge) => edge[1])), v: ExecutionView = [ - symbols, byId, outgoing, incoming, blocked, nonEntries, i.operation_by_id, + nodes, byId, outgoing, incoming, blocked, nonEntries, i.operation_by_id, ] cache.set(i, v) return v } -function score(symbol: SymbolNode, targets: readonly string[]): number { +function score(symbol: SymbolNode, goals: readonly string[]): number { let result = 0 - for (const target of targets) { + for (const target of goals) { const terms = words(target), compact = terms.join('') - if (compact && symbol[2].includes(compact)) result += 128 - if (terms.length > 0 && terms.every((term) => symbol[4].has(term))) result += 64 - if (compact && symbol[1].includes(compact)) result += 32 + if (symbol[2].includes(compact)) result += 128 + if (terms.every((term) => symbol[4].has(term))) result += 64 + if (symbol[1].includes(compact)) result += 32 for (const term of terms) { if (symbol[3].has(term)) result += 8 if (symbol[4].has(term)) result += 8 @@ -234,97 +240,63 @@ function score(symbol: SymbolNode, targets: readonly string[]): number { } return result } -function rootScore(v: ExecutionView, symbol: SymbolNode, lexical: number): number { +function rootRank(v: ExecutionView, symbol: SymbolNode, lexical: number): number { const degree = (v[3].get(symbol[0])?.length ?? 0) + (v[2].get(symbol[0])?.length ?? 0) - + (v[4].has(symbol[0]) ? 1 : 0), - unresolved = symbol[5].filter((fact) => fact.kind === 'call' - && !fact.target_symbol_id).length + + (v[4].has(symbol[0]) ? 1 : 0) return lexical - penalty(symbol[7]) - Math.min(24, Math.max(0, degree - 8) * 2) - - Math.min(16, unresolved * 4) - (degree === 0 && !symbol[6] ? 12 : 0) - (symbol[6] ? 16 : 0) } function adverseArc(v: ExecutionView, arc: Arc): boolean { return arc[4].some((id) => { const fact = v[6].get(id) - if (adverseFact(fact)) return true + if (bad(fact)) return true if (arc[2] !== 'channel' || fact?.kind !== 'call' || !fact.target_symbol_id) { return false } const matching = (v[2].get(fact.target_symbol_id) ?? []).filter((inner) => inner[2] === 'channel' && inner[1] === arc[1]) return matching.length > 0 && matching.every((inner) => - inner[4].some((operation) => adverseFact(v[6].get(operation)))) + inner[4].some((operation) => bad(v[6].get(operation)))) }) } function reach( - v: ExecutionView, seeds: readonly string[], reverse: boolean, - limit: number, accept?: (arc: Arc) => boolean, + v: ExecutionView, seeds: readonly string[], back: boolean, + cap: number, allow?: (arc: Arc) => boolean, allowed?: { has(id: string): boolean }, stops?: ReadonlySet, ): Reach { - const distance = new Map(seeds.map((seed) => [seed, 0])) + const dist = new Map(seeds.map((seed) => [seed, 0])) const actual = new Set(seeds), queue = [...seeds] - const previous = new Map(), overflow = new Set() + const prev = new Map(), extra = new Set() let bounded = false while (queue.length > 0) { - queue.sort((left, right) => distance.get(left)! - distance.get(right)! + queue.sort((left, right) => dist.get(left)! - dist.get(right)! || cmp(left, right)) - const current = queue.shift()! - if (stops?.has(current)) continue - const base = distance.get(current)!, - arcs = (reverse ? v[3] : v[2]).get(current) ?? [] + const at = queue.shift()! + if (stops?.has(at)) continue + const base = dist.get(at)!, + arcs = (back ? v[3] : v[2]).get(at) ?? [] for (const arc of arcs) { - if (accept && !accept(arc)) continue - const next = reverse ? arc[0] : arc[1], hops = base + arc[3].length - if (hops > HOPS) { overflow.add(next); continue } - if ((allowed && !allowed.has(next)) || (distance.get(next) ?? Infinity) <= hops) continue - const additions = [...new Set(arc[3].flatMap((edge) => [edge[1], edge[2]]))] + if (allow && !allow(arc)) continue + const next = back ? arc[0] : arc[1], hops = base + arc[3].length + if (hops > HOPS) { extra.add(next); continue } + if ((allowed && !allowed.has(next)) || (dist.get(next) ?? Infinity) <= hops) continue + const added = [...new Set(arc[3].flatMap((edge) => [edge[1], edge[2]]))] .filter((id) => !actual.has(id)) - if (actual.size + additions.length > limit) { bounded = true; continue } - additions.forEach((id) => actual.add(id)) - distance.set(next, hops) - previous.set(next, arc) + if (actual.size + added.length > cap) { bounded = true; continue } + added.forEach((id) => actual.add(id)) + dist.set(next, hops) + prev.set(next, arc) if (!queue.includes(next)) queue.push(next) } } return [ - distance, actual, bounded || [...overflow].some((id) => !distance.has(id)), - previous, + dist, actual, bounded || [...extra].some((id) => !dist.has(id)), + prev, ] } -function bestPath( - v: ExecutionView, root: string, end: string, - allowed: ReadonlySet, targets: readonly string[], - accept?: (arc: Arc) => boolean, -): readonly [path: Arc[], bounded: boolean] { - let best: Arc[] = [], bestRank = -1, bestHops = 0, - count = 0, bounded = false - const visit = ( - at: string, path: Arc[], seen: Set, hops: number, rank: number, - ): void => { - if (count++ >= NODES) { bounded = true; return } - if (at === end) { - if (rank > bestRank || rank === bestRank && hops > bestHops) { - best = path; bestRank = rank; bestHops = hops - } - return - } - for (const arc of v[2].get(at) ?? []) { - if (bounded) break - const next = arc[1], nextHops = hops + arc[3].length - if (!allowed.has(next) || seen.has(next) || nextHops > HOPS - || accept && !accept(arc)) continue - seen.add(next) - visit(next, [...path, arc], seen, nextHops, - rank + score(v[1].get(next)!, targets)) - seen.delete(next) - } - } - visit(root, [], new Set([root]), 0, 0) - return [best, bounded || bestRank < 0] -} -function orderCmp( +function orderBy( left: readonly number[], right: readonly number[], ): number { let i = 0 @@ -332,430 +304,272 @@ function orderCmp( return (left[i] ?? 0) - (right[i] ?? 0) || left.length - right.length } function corridor( - v: ExecutionView, root: string, limit: number, targets: readonly string[], - terminalTarget?: string, + v: ExecutionView, root: string, cap: number, goals: readonly string[], + fail: boolean, endNeed?: string, channelEnd = false, ): Selection { - const failureIntent = targets.some((target) => - words(target).some((word) => FAILURE_WORD.test(word))), - accept = failureIntent ? undefined : (arc: Arc) => !adverseArc(v, arc), - forward = reach(v, [root], false, limit, accept) - const found = [...forward[0].keys()].filter((id) => - terminalAt(v, id, failureIntent)), - requested = terminalTarget - ? targetSymbols(v, found, terminalTarget, 'terminal') : [], - candidates = terminalTarget ? requested : found - const structural = new Set(candidates.filter((id) => - v[3].get(id)?.some((arc) => arc[2] === 'channel'))), - relevant = candidates.filter((id) => score(v[1].get(id)!, targets) > 0), - scoped = new Set(structural), - stable = reach(v, [root], false, limit, (arc) => - (!accept || accept(arc)) && !arc[4].some((id) => - v[6].get(id)?.control.some((frame) => - frame.kind === 'branch' || frame.kind === 'loop')))[0] - const direct = (arc: Arc) => - arc[2] === 'direct' && (!accept || accept(arc)) && forward[0].has(arc[1]) - for (const seed of structural) { - const first = (v[2].get(seed) ?? []).filter(direct), - hits = new Map() - let branches = 0 - for (const id of new Set(first.map((arc) => arc[1]))) { - const below = reach( - v, [id], false, limit, direct, forward[0], - )[0], - reached = [...below.keys()].filter((candidate) => candidates.includes(candidate)) - if (!reached.some((candidate) => candidate !== seed)) continue - const unconditional = first.some((arc) => arc[1] === id - && arc[4].some((operation) => { - const fact = v[1].get(seed)?.[5].find((entry) => entry.id === operation) - return fact?.kind === 'call' && !fact.control.some((frame) => - frame.kind === 'branch' || frame.kind === 'loop') - })) - branches += 1 - for (const candidate of reached) { - const prior = hits.get(candidate) ?? [0, false] - hits.set(candidate, [prior[0] + 1, prior[1] || unconditional]) + const allow = fail ? undefined : (arc: Arc) => !adverseArc(v, arc), + fwd = reach(v, [root], false, cap, allow) + const found = [...fwd[0].keys()].filter((id) => + hasTerminal(v, id, fail)), + wanted = endNeed + ? pickIds(v, found, endNeed, 'terminal') : [], + options = endNeed ? wanted : found, + sinceChannel = (id: string): number => { + let direct = 0 + for (let at = id; at !== root;) { + const arc = fwd[3].get(at) + if (!arc) return Infinity + if (arc[2] === 'channel') return direct + direct += 1; at = arc[0] } - } - for (const [candidate, [count, unconditional]] of hits) { - if (count > 1 || branches === 1 && unconditional) scoped.add(candidate) - } - } - const pool = (structural.size > 0 - ? candidates.filter((id) => scoped.has(id) - || relevant.includes(id) && stable.has(id)) - : relevant.length > 0 ? relevant : candidates) - .sort((left, right) => forward[0].get(right)! - forward[0].get(left)! - || Number(structural.has(right)) - Number(structural.has(left)) - || score(v[1].get(right)!, targets) - score(v[1].get(left)!, targets) + return Infinity + }, + originals = options.filter((id) => v[1].get(id)?.[5].some((fact) => + terminal(fact) && fact.source !== 'wrapper-summary')), + terminalOptions = originals.length > 0 ? originals : options, + channelDistance = channelEnd + ? Math.min(...terminalOptions.map(sinceChannel)) : Infinity, + channelOptions = Number.isFinite(channelDistance) + ? terminalOptions.filter((id) => sinceChannel(id) === channelDistance) : [], + eligible = channelOptions.length > 0 ? channelOptions : terminalOptions + const exact = pickIds( + v, eligible, goals[0] ?? '', 'exact', + ), + related = exact.length > 0 ? exact + : eligible.filter((id) => score(v[1].get(id)!, goals) > 0), + zone = related.length > 0 + ? reach(v, related, false, cap, allow, fwd[0])[0] : undefined, + pool = (zone ? eligible.filter((id) => zone.has(id)) : eligible) + .sort((left, right) => fwd[0].get(right)! - fwd[0].get(left)! + || score(v[1].get(right)!, goals) - score(v[1].get(left)!, goals) || cmp(left, right)) - const allowed = forward[0] - const terminals = pool.filter((id) => { + const allowed = fwd[0] + let ends = pool.filter((id) => { const below = reach( - v, [id], false, limit, accept, allowed, + v, [id], false, cap, allow, allowed, )[0] return !pool.some((other) => other !== id && below.has(other)) }) - if (terminals.length === 0) terminals.push(...pool.slice(0, 1)) - const selected = terminals.length > 0 ? forward - : reach(v, [root], false, limit, accept, undefined, v[4]), - backward = reach(v, terminals, true, limit, accept, allowed) - let symbols = new Set([...selected[0].keys()].filter((id) => - terminals.length === 0 || backward[0].has(id))) + if (ends.length === 0) ends = pool.slice(0, 1) + const exactEnds = ends.filter((id) => + exact.includes(id)) + if (exactEnds.length > 0) ends = exactEnds + const chosen = ends.length > 0 ? fwd + : reach(v, [root], false, cap, allow, undefined, v[4]), + backward = reach(v, ends, true, cap, allow, allowed) + let nodes = new Set([...chosen[0].keys()].filter((id) => + ends.length === 0 || backward[0].has(id))) let arcs = [...v[2].values()].flat().filter((arc) => - symbols.has(arc[0]) && symbols.has(arc[1]) && (!accept || accept(arc))) - let pruned = false - const relationCount = new Set(arcs.flatMap(idsOf)).size, - hardLimit = relationCount > HOPS - if (relationCount > 20) { - if (terminals[0]) { - const all = arcs - if (terminals.length === 1) { - const selected = bestPath( - v, root, terminals[0], symbols, targets, accept, - ) - arcs = selected[0] - const originalCycles = cycleGroups(symbols, all) - for (const cycle of originalCycles) { - const members = new Set(cycle.symbolIds) - if (![...members].every((id) => - arcs.some((arc) => arc[0] === id || arc[1] === id))) continue - const closes = () => cycleGroups( - new Set(arcs.flatMap((arc) => [arc[0], arc[1]])), arcs, - ).some((group) => group.symbolIds.every((id) => members.has(id)) - && [...members].every((id) => group.symbolIds.includes(id))) - const candidates = all.filter((arc) => !arcs.includes(arc) - && members.has(arc[0]) && members.has(arc[1])) - .sort((left, right) => - Number(arcs.some((arc) => arc[0] === left[0] && arc[1] === left[1])) - - Number(arcs.some((arc) => arc[0] === right[0] && arc[1] === right[1])) - || cmp(left[3][0]![0], right[3][0]![0])) - for (const candidate of candidates) { - if (closes()) break - if (new Set([...arcs, candidate].flatMap(idsOf)).size <= HOPS) { - arcs.push(candidate) - } - } - } - const kept = new Set(arcs.flatMap((arc) => - arc[3].flatMap((edge) => [edge[1], edge[2]]))) - const omitted = all.filter((arc) => !arcs.includes(arc)), - newNodes = new Set(omitted.flatMap((arc) => [arc[0], arc[1]]) - .filter((id) => !kept.has(id))), - safe = new Set(kept), - keptOperations = new Set(arcs.flatMap((arc) => arc[4])), - unsafeSameEndpoint = omitted.some((arc) => - kept.has(arc[0]) && kept.has(arc[1]) - && !arc[4].some((id) => keptOperations.has(id) - || v[6].get(id)?.control.some((frame) => frame.kind !== 'exception'))) - let changed = true - while (changed) { - changed = false - for (const arc of omitted) if (safe.has(arc[0]) && !safe.has(arc[1]) - && (!kept.has(arc[0]) || arc[4].some((id) => - v[6].get(id)?.control.some((frame) => frame.kind !== 'exception')))) { - safe.add(arc[1]); changed = true - } - } - const cyclesPreserved = originalCycles.every((cycle) => - cycle.symbolIds.some((id) => !kept.has(id)) - || cycleGroups(new Set(kept), arcs).some((group) => - group.symbolIds.length === cycle.symbolIds.length - && group.symbolIds.every((id) => cycle.symbolIds.includes(id)))) - pruned = selected[1] || !cyclesPreserved - || newNodes.size > 2 || [...newNodes].some((id) => !safe.has(id)) - || unsafeSameEndpoint - if (pruned && !hardLimit) { arcs = all; pruned = false } - } else { - const paths = terminals.map((terminal) => { - const path: Arc[] = [] - for (let id = terminal; id !== root;) { - const arc = forward[3].get(id) - if (!arc) return [] - path.unshift(arc); id = arc[0] - } - return path - }) - const need = [...new Map(paths.flat().map((arc) => - [idsOf(arc).join('\0'), arc])).values()] - const kept = new Set(need.flatMap((arc) => - arc[3].flatMap((edge) => [edge[1], edge[2]]))) - pruned = paths.some((path) => path.length === 0) - || new Set(need.flatMap(idsOf)).size > HOPS - || all.some((arc) => (!accept || accept(arc)) && !need.includes(arc) - && arc[3].some((edge) => !kept.has(edge[1]) || !kept.has(edge[2]))) - arcs = pruned ? paths[0]! : need - if (pruned && !hardLimit) { arcs = all; pruned = false } + nodes.has(arc[0]) && nodes.has(arc[1]) && (!allow || allow(arc))) + const hopCount = new Set(arcs.flatMap(idsOf)).size + const pruned = hopCount > HOPS + if (pruned) { + const walks = ends.map((terminal) => { + const path: Arc[] = [] + for (let id = terminal; id !== root;) { + const arc = fwd[3].get(id) + if (!arc) return [] + path.unshift(arc); id = arc[0] } - symbols = new Set([root, ...arcs.flatMap((arc) => [arc[0], arc[1]])]) - } else { - arcs = []; symbols = new Set([root]) - pruned = true - } + return path + }) + const need = [...new Map(walks.flat().map((arc) => + [idsOf(arc).join('\0'), arc])).values()] + arcs = new Set(need.flatMap(idsOf)).size <= HOPS ? need : walks[0] ?? [] + nodes = new Set([root, ...arcs.flatMap((arc) => [arc[0], arc[1]])]) } arcs.sort((left, right) => - (forward[0].get(left[0]) ?? Infinity) - (forward[0].get(right[0]) ?? Infinity) - || (forward[0].get(left[1]) ?? Infinity) - (forward[0].get(right[1]) ?? Infinity) + (fwd[0].get(left[0]) ?? Infinity) - (fwd[0].get(right[0]) ?? Infinity) + || (fwd[0].get(left[1]) ?? Infinity) - (fwd[0].get(right[1]) ?? Infinity) || cmp(left[0], right[0]) || cmp(left[1], right[1]) || cmp(left[3][0]![0], right[3][0]![0])) - return [symbols, arcs, terminals.filter((id) => symbols.has(id)), - forward[1], terminals.length === 0 && (forward[2] || selected[2]) || pruned] + return [nodes, arcs, ends.filter((id) => nodes.has(id)), + fwd[1], ends.length === 0 && (fwd[2] || chosen[2]) || pruned] } type PersistenceFact = Extract -function typedCase(value: IndexValue | undefined): string | undefined { - if (!value || value.kind !== 'literal') return undefined - return `case:${Buffer.from(JSON.stringify([ - typeof value.value, value.value, - ])).toString('base64url')}` -} -function objectPath(value: IndexValue, path: readonly string[]): IndexValue | undefined { - let current: IndexValue | undefined = value - for (const key of path) { - if (current?.kind !== 'object') return undefined - current = current.entries.find((entry) => entry.key === key)?.value - } - return current -} -function channelTerminals( - i: ReadyQueryIndex, arc: Arc, candidates: readonly PersistenceFact[], +function endFacts( + i: ReadyQueryIndex, arc: Arc, options: readonly PersistenceFact[], ): PersistenceFact[] { - if (arc[2] !== 'channel') return [...candidates] - const publish = arc[3][0], position = publish?.[8] - if (!publish || position === undefined) return [] - const call = arc[4].map((id) => i.operation_by_id.get(id)).find((fact) => - fact?.kind === 'call' && fact.owner_symbol_id === arc[0] - && rangeKey(fact.evidence.range) === publish[4] - && rangeKey(fact.evidence.statement_range) === publish[5]) - if (call?.kind !== 'call' || position >= call.arguments.length) return [] - const transport = i.channels_by_id.get(publish[2])?.transport, - matches: Array = [] - for (const condition of i.operations_by_owner.get(arc[1]) ?? []) { - if (condition.kind !== 'condition' || condition.condition_kind !== 'switch' - || condition.test?.kind !== 'template') continue - const [parameter, ...rawPath] = condition.test.parts + if (arc[2] !== 'channel') return [...options] + const pub = arc[3][0]!, position = pub[7] + if (position === undefined) return [] + const call = i.operation_by_id.get(arc[4][0]!) + if (call?.kind !== 'call') return [] + const transport = i.channels_by_id.get(pub[2])!.transport, + matches = (i.operations_by_owner.get(arc[1]) ?? []).flatMap((cond) => { + if (cond.kind !== 'condition' || cond.condition_kind !== 'switch' + || cond.test?.kind !== 'template') return [] + const [parameter, ...rawPath] = cond.test.parts if (parameter?.kind !== 'parameter' || parameter.position !== 0 || rawPath.some((part) => part.kind !== 'literal' - || typeof part.value !== 'string')) continue + || typeof part.value !== 'string')) return [] const path = rawPath.map((part) => (part as Extract).value as string) if (transport === 'bullmq' && path[0] === 'data') path.shift() - const arm = typedCase(objectPath(call.arguments[position]!, path)) - if (!arm) continue - const eligible = candidates.filter((fact) => fact.control.some((frame) => - frame.kind === 'branch' && frame.controller_fact_id === condition.id + let value: IndexValue | undefined = call.arguments[position] + for (const key of path) value = value?.kind === 'object' + ? value.entries.find((entry) => entry.key === key)?.value : undefined + if (value?.kind !== 'literal') return [] + const arm = `case:${Buffer.from(JSON.stringify([ + typeof value.value, value.value, + ])).toString('base64url')}` + const eligible = options.filter((fact) => fact.control.some((frame) => + frame.kind === 'branch' && frame.controller_fact_id === cond.id && frame.arm === arm)) - if (eligible.length > 0) matches.push([condition.id, eligible]) - } - return matches.length === 1 ? matches[0]![1] : [] + return eligible.length > 0 ? [eligible] : [] + }) + return matches.length === 1 ? matches[0]! : [] } function controls( - i: ReadyQueryIndex, arcs: readonly Arc[], terminals: readonly string[], + i: ReadyQueryIndex, arcs: readonly Arc[], ends: readonly string[], seeds: readonly string[], ): Control { - const close = (ids: Iterable): [Set, boolean] => { - const result = new Set(ids), queue = [...result] - let valid = true - const add = (id: string): void => { - if (!result.has(id)) { result.add(id); queue.push(id) } - } - for (let cursor = 0; cursor < queue.length; cursor += 1) { - const fact = i.operation_by_id.get(queue[cursor]!) - if (!fact) { valid = false; continue } - for (const frame of fact.control) { - if (frame.kind !== 'exception') add(frame.controller_fact_id) - } - if (fact.kind === 'parallel') fact.member_fact_ids.forEach(add) - if (fact.kind === 'persistence') add(fact.call_fact_id) - } - return [result, valid] - } - const cost = (fact: IndexBodyFact): readonly [number, number, number, string] => { - const closure = close([fact.id])[0], - call = fact.kind === 'persistence' - ? i.operation_by_id.get(fact.call_fact_id) : fact, - adverse = call?.kind === 'call' - && call.control.some((frame) => - frame.kind === 'exception' && frame.arm === 'catch') ? 1 : 0, - range = fact.evidence.statement_range, - span = (range.end.line - range.start.line) * 1_000 - + range.end.column - range.start.column - return [adverse, closure.size, span, fact.id] - } - const prefer = (left: IndexBodyFact, right: IndexBodyFact): number => { - const a = cost(left), b = cost(right) - return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] - || orderCmp(right.order, left.order) || cmp(a[3], b[3]) - } - const primary = new Set(seeds), - facts = [...new Set(arcs.flatMap((arc) => arc[4]))] - .map((id) => i.operation_by_id.get(id)) - .filter((fact): fact is IndexBodyFact => fact !== undefined) - facts.forEach((fact) => primary.add(fact.id)) - const terminalOperations = new Set() - for (const id of [...terminals].sort(cmp)) { - const candidates = (i.operations_by_owner.get(id) ?? []).filter(terminal), + const ops = i.operation_by_id + const factIds = [...new Set(arcs.flatMap((arc) => arc[4]))], + core = new Set([...seeds, ...factIds]) + const endOps = new Set() + for (const id of ends) { + const options = (i.operations_by_owner.get(id) ?? []).filter(terminal), incoming = arcs.filter((arc) => arc[1] === id && arc[2] === 'channel'), groups = incoming.length > 0 - ? incoming.map((arc) => channelTerminals(i, arc, candidates)) : [candidates] - if (groups.some((group) => group.length === 0)) continue - for (const group of groups) { - const fact = group.sort(prefer)[0] - if (fact) { primary.add(fact.id); terminalOperations.add(fact.id) } + ? incoming.map((arc) => endFacts(i, arc, options)) : [options], + chosen = groups.map((group) => group.filter((entry) => + !bad(entry) + && !bad(ops.get(entry.call_fact_id))).at(-1)) + if (chosen.some((fact) => !fact)) continue + for (const fact of chosen as PersistenceFact[]) { + core.add(fact.id); endOps.add(fact.id) } } type Group = [kind: 'branch' | 'loop' | 'parallel', controller: string, - arm: string | undefined, operations: Set, symbols: Set] - const grouped = new Map(), - sequences = new Map[]>() - for (const fact of facts) { - if (fact.kind === 'call' + arm: string | undefined, ops: Set, nodes: Set] + const groupsBy = new Map(), + seqs = new Map[]>() + const need = new Set(core) + let proven = true + for (const id of need) { + const fact = ops.get(id) + if (!fact) { proven = false; continue } + if (factIds.includes(id) && fact.kind === 'call' && !fact.control.some((frame) => frame.kind === 'parallel')) { - append(sequences, `${fact.owner_symbol_id}\0${JSON.stringify(fact.control)}`, fact) + append(seqs, `${fact.owner_symbol_id}\0${JSON.stringify(fact.control)}`, fact) } - } - const orderGroups: WorkflowControlGroup[] = [] - for (const calls of sequences.values()) if (calls.length > 1) { - calls.sort((a, b) => orderCmp(a.order, b.order) || cmp(a.id, b.id)) - orderGroups.push({ - kind: 'sequence', operationIds: calls.map((fact) => fact.id), - symbolIds: calls.flatMap((fact) => fact.target_symbol_id ? [fact.target_symbol_id] : []), - }) - } - const [needed, proven] = close(primary) - for (const id of needed) { - const fact = i.operation_by_id.get(id) - if (!fact) continue for (const frame of fact.control) { if (frame.kind === 'exception') continue + need.add(frame.controller_fact_id) const arm = frame.kind === 'branch' ? frame.arm : undefined const key = `${frame.kind}\0${frame.controller_fact_id}\0${arm ?? ''}` - const group = grouped.get(key) + const group = groupsBy.get(key) ?? [frame.kind, frame.controller_fact_id, arm, new Set(), new Set()] as Group - grouped.set(key, group) + groupsBy.set(key, group) group[3].add(fact.id); group[4].add(fact.owner_symbol_id) } + if (fact.kind === 'parallel') { + fact.member_fact_ids.forEach((member) => need.add(member)) + } + if (fact.kind === 'persistence') need.add(fact.call_fact_id) + } + const ordered: WorkflowControlGroup[] = [] + for (const calls of seqs.values()) if (calls.length > 1) { + calls.sort((a, b) => orderBy(a.order, b.order) || cmp(a.id, b.id)) + ordered.push({ kind: 'sequence', + operationIds: calls.map((fact) => fact.id), + symbolIds: calls.flatMap((fact) => fact.target_symbol_id ? [fact.target_symbol_id] : []) }) } - const groups = [...grouped.values()] + const groups = [...groupsBy.values()] .map(([ - kind, controllerOperationId, arm, operations, symbols, + kind, controllerOperationId, arm, ops, nodes, ]) => ({ kind, controllerOperationId, ...(arm ? { arm } : {}), - operationIds: [...operations].sort(cmp), symbolIds: [...symbols].sort(cmp), - })).concat(orderGroups) - return [[...needed].sort(cmp), groups, proven, [...terminalOperations].sort(cmp)] + operationIds: [...ops].sort(cmp), symbolIds: [...nodes].sort(cmp), + })).concat(ordered) + return [[...need].sort(cmp), groups, proven, [...endOps].sort(cmp)] } -function cycleGroups(symbols: ReadonlySet, arcs: readonly Arc[]): WorkflowControlGroup[] { - const paths = new Map([...symbols].map((id) => [id, new Set()])) - for (const arc of arcs) paths.get(arc[0])?.add(arc[1]) - for (const through of symbols) for (const from of symbols) { - if (!paths.get(from)?.has(through)) continue - for (const to of paths.get(through) ?? []) paths.get(from)!.add(to) +function cycles(nodes: ReadonlySet, arcs: readonly Arc[]): WorkflowControlGroup[] { + const walks = new Map([...nodes].map((id) => [id, new Set()])) + for (const arc of arcs) walks.get(arc[0])?.add(arc[1]) + for (const through of nodes) for (const from of nodes) { + if (!walks.get(from)?.has(through)) continue + for (const to of walks.get(through) ?? []) walks.get(from)!.add(to) } const groups: WorkflowControlGroup[] = [] - for (const symbol of symbols) { - const members = [...symbols].filter((candidate) => - paths.get(symbol)?.has(candidate) && paths.get(candidate)?.has(symbol)).sort(cmp) + for (const symbol of nodes) { + const members = [...nodes].filter((cand) => + walks.get(symbol)?.has(cand) && walks.get(cand)?.has(symbol)).sort(cmp) if (members[0] !== symbol) continue groups.push({ kind: 'cycle', operationIds: [], symbolIds: members }) } return groups } -function matches( - symbol: SymbolNode, lexical: readonly string[], names: boolean, -): boolean { - if (lexical.length === 0) return true - const source = names ? symbol[2] : symbol[1], - tokens = names ? symbol[4] : symbol[3] - return source.includes(lexical.join('')) - || lexical.every((term) => tokens.has(term)) -} -function covering( - v: ExecutionView, - ids: readonly string[], - target: string, - names: boolean, -): string[] { - const lexical = words(target), - exact = ids.filter((id) => matches(v[1].get(id)!, lexical, names)) - if (exact.length > 0) return exact - const related = ids.filter((id) => { - const symbol = v[1].get(id)!, - tokens = names ? symbol[4] : symbol[3] - return lexical.some((term) => tokens.has(term)) - }) - return lexical.length > 0 - && lexical.every((term) => related.some((id) => - (names ? v[1].get(id)![4] : v[1].get(id)![3]).has(term))) - ? related : [] -} -function targetSymbols( +function pickIds( v: ExecutionView, ids: readonly string[], target: string, - role: 'entry' | 'stage' | 'behavior' | 'terminal', + role: 'subject' | 'exact' | 'names' | 'entry' | 'stage' | 'behavior' | 'terminal', ): string[] { - const tokens = words(target) - if (role === 'entry' && tokens.includes('request')) { + const lexical = words(target), names = role === 'names', + tokens = (id: string) => v[1].get(id)![names ? 4 : 3] as ReadonlySet, + full = (id: string) => v[1].get(id)![names ? 2 : 1] + .includes(lexical.join('')) || lexical.every((term) => tokens(id).has(term)) + if (role === 'entry' && lexical.includes('request')) { const entries = ids.filter((id) => v[1].get(id)![8]), - rest = tokens.filter((token) => token !== 'request') + rest = lexical.filter((token) => token !== 'request') return rest.length === 0 ? entries : entries.filter((id) => rest.every((token) => v[1].get(id)![3].has(token))) } - const exact = ids.filter((id) => matches(v[1].get(id)!, tokens, false)) - if (exact.length > 0) return exact - if (role === 'terminal' && tokens.length > 0) { - const generic = new Set([ - 'data', 'persist', 'persistence', 'record', 'storage', 'store', 'write', - ]), - specific = tokens.filter((token) => !generic.has(token)) - if (specific.length === 0) return [...ids] - const stored = ids.filter((id) => specific.every((token) => { - const lexicon = v[1].get(id)![3] - return lexicon.has(token) || /^(?:database|db)$/u.test(token) - && ['database', 'db', 'mongo', 'mongodb', 'repository', 'sql'] - .some((candidate) => lexicon.has(candidate)) - })) - if (stored.length > 0) return stored + if (role !== 'terminal') { + const exact = ids.filter(full) + if (exact.length > 0 || role === 'exact' || names) return exact + const related = ids.filter((id) => + lexical.some((term) => tokens(id).has(term))) + return lexical.every((term) => related.some((id) => tokens(id).has(term))) + ? related : [] } - return role === 'stage' || role === 'behavior' - ? covering(v, ids, target, false) : [] + const specific = lexical.filter((token) => !GENERIC_TERMINAL.test(token)) + if (specific.length === 0) return [...ids] + const exact = ids.filter(full) + if (exact.length > 0) return exact + return ids.filter((id) => specific.every((token) => { + const lexicon = v[1].get(id)![3] + return lexicon.has(token) || /^(?:database|db)$/u.test(token) + && [...lexicon].some((cand) => DATABASE.test(cand)) + })) } -function channelMatches( +function channelFit( i: ReadyQueryIndex, id: string, target: string, ): boolean { const channel = i.channels_by_id.get(id) if (!channel) return false - const expected = words(target), actual = new Set(words( - `${channel.channel_kind} ${channel.transport} ${channel.key}`, - )), - compact = expected.join(''), - forms = [ - `${channel.channel_kind} ${channel.transport} ${channel.key}`, - `${channel.transport} ${channel.channel_kind} ${channel.key}`, channel.key, - ].map((value) => words(value).join('')) - return expected.length > 0 && (expected.every((token) => actual.has(token)) - || forms.some((value) => value.includes(compact))) + const expected = words(target), key = words(channel.key), + actual = words(`${channel.channel_kind} ${channel.transport}`).concat(key), + compact = expected.join(''), suffix = key.join(''), + forms = `${suffix}\0${actual[0]}${actual[1]}${suffix}\0${ + actual[1]}${actual[0]}${suffix}` + return expected.every((token) => actual.includes(token)) + || forms.includes(compact) } -function stageMatches( +function stageMatch( i: ReadyQueryIndex, v: ExecutionView, selection: Selection, target: string, -): boolean { - const steps = [...selection[0]].filter((id) => - id !== selection[2][0] && targetSymbols(v, [id], target, 'stage').length > 0) - return steps.length > 0 || selection[1].some((arc) => + omitted: readonly string[], +): readonly [string[], Arc[]] { + const nodes = pickIds(v, [...selection[0]].filter((id) => + !omitted.includes(id)), target, 'stage') + if (nodes.length > 0) return [nodes, []] + const arcs = selection[1].filter((arc) => arc[3].some((edge) => - channelMatches(i, edge[1], target) || channelMatches(i, edge[2], target))) + channelFit(i, edge[1], target) || channelFit(i, edge[2], target))) + return [[...new Set(arcs.flatMap((arc) => [arc[0], arc[1]]))].sort(cmp), arcs] } -const named = (v: ExecutionView, id: string, target: string): boolean => - matches(v[1].get(id)!, words(target), true) -function findRoots( +function scanRoots( v: ExecutionView, - ranked: readonly Candidate[], - targets: readonly string[], + ranks: readonly Candidate[], + goals: readonly string[], ): readonly [ids: string[], actual: Set, bounded: boolean] { const traversal = reach( - v, ranked.map((entry) => entry[0][0]), true, RECOVERY, + v, ranks.map((entry) => entry[0][0]), true, RECOVERY, ) const rank = (id: string): number => { const symbol = v[1].get(id)! - return rootScore(v, symbol, score(symbol, targets)) + return rootRank(v, symbol, score(symbol, goals)) } const ids = [...traversal[0].keys()].filter((id) => !v[5].has(id) @@ -768,18 +582,24 @@ function findRoots( return [ids, traversal[1], traversal[2]] } export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSelection { - const v = buildView(i), + const v = buildView(i), ops = i.operation_by_id, { intent, subject: target, terms, obligations, access } = plan, isFlow = intent === 'workflow', - targets = [...new Set([ + bound = (kind: ObligationKind): string | undefined => + obligations.find((entry) => entry.kind === kind + && entry.target !== target)?.target, + goals = [...new Set([ target, ...terms, ...obligations.map((entry) => entry.target), ])], - terminalTarget = obligations.find((entry) => entry.kind === 'terminal')?.target, - terminalHint = terminalTarget === target ? undefined : terminalTarget, - stageTarget = obligations.find((entry) => entry.kind === 'stage' - && entry.target !== target)?.target - const candidate = (symbol: SymbolNode): Candidate => { - const lexical = score(symbol, targets), + lastBound = bound('terminal'), + stageNeed = bound('stage'), + behaviorNeed = bound('behavior'), + asyncNeed = words(bound('handoff') ?? '').some((word) => + /^(?:async|dispatch|emit|enqueue|event|job|publish|queue|schedule)$/u.test(word)), + fail = goals.some((entry) => + words(entry).some((word) => FAILURE_WORD.test(word))) + const cand = (symbol: SymbolNode): Candidate => { + const lexical = score(symbol, goals), outgoing = v[2].get(symbol[0]) ?? [], affinity = isFlow ? outgoing.some((arc) => arc[2] === 'channel') ? 2 @@ -791,20 +611,14 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel ['condition', 'loop', 'parallel'].includes(fact.kind))) : intent !== 'locate' || !access ? 0 : access === 'write' ? Number(symbol[6] - || symbol[5].some((fact) => fact.kind === 'mutation') - || [...symbol[4]].some((word) => - ['persist', 'save', 'set', 'store', 'update', 'write'].includes(word))) + || symbol[5].some((fact) => fact.kind === 'mutation')) : Number(symbol[5].some((fact) => fact.kind === 'persistence' - && READ.test(fact.operation)) - || [...symbol[4]].some((word) => - ['find', 'get', 'load', 'read'].includes(word))), + && READ.test(fact.operation))), exact = isFlow ? Number(!v[5].has(symbol[0])) - : intent === 'locate' - ? Number(named(v, symbol[0], target)) - : Number(named(v, symbol[0], target)) - return [symbol, lexical, rootScore(v, symbol, lexical), affinity, exact] + : Number(pickIds(v, [symbol[0]], target, 'names').length > 0) + return [symbol, lexical, rootRank(v, symbol, lexical), affinity, exact] } - const ranked = v[0].map(candidate) + const ranks = v[0].map(cand) .filter((entry) => entry[1] > 0 && (!isFlow || v[2].has(entry[0][0]) || v[3].has(entry[0][0]) || v[4].has(entry[0][0]))) @@ -815,131 +629,151 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel ? b[3] - a[3] || b[4] - a[4] || penalty(a[0][7]) - penalty(b[0][7]) || b[1] - a[1] || cmp(a[0][0], b[0][0]) - : b[4] - a[4] || b[3] - a[3] - || b[2] - a[2] || b[1] - a[1] || cmp(a[0][0], b[0][0])) + : b[4] - a[4] || b[2] - a[2] + || b[3] - a[3] || b[1] - a[1] || cmp(a[0][0], b[0][0])) .slice(0, CANDIDATES), - focus = ranked[0]?.[0][0] - const entryTarget = obligations.find((entry) => entry.kind === 'entry' - && entry.target !== target)?.target, - entryPool = isFlow ? ranked.filter((entry) => !v[5].has(entry[0][0])) : [], - constrainedEntries = entryTarget - ? new Set(targetSymbols(v, entryPool.map((entry) => entry[0][0]), - entryTarget, 'entry')) : undefined + focus = ranks[0]?.[0][0] + const entryNeed = bound('entry'), + entryPool = isFlow ? ranks.filter((entry) => !v[5].has(entry[0][0])) : [], + entryIds = entryNeed + ? new Set(pickIds(v, entryPool.map((entry) => entry[0][0]), + entryNeed, 'entry')) : undefined let entries = entryPool.filter((entry) => - !constrainedEntries || constrainedEntries.has(entry[0][0])).slice(0, 3) - let bridge: ReturnType | undefined - if (!entryTarget && isFlow && ranked.length > 0 && (entries.length === 0 + !entryIds || entryIds.has(entry[0][0])).slice(0, 3) + let scan: ReturnType | undefined + if (isFlow && ranks.length > 0 && (entries.length === 0 || entries.every((entry) => entry[0][7] !== 'production'))) { - bridge = findRoots(v, ranked, targets) - const recovered = bridge[0].map((id) => candidate(v[1].get(id)!)) + scan = scanRoots(v, ranks, goals) + const eligible = entryNeed + ? new Set(pickIds(v, scan[0], entryNeed, 'entry')) : undefined + const recovered = scan[0].filter((id) => !eligible || eligible.has(id)) + .map((id) => cand(v[1].get(id)!)) entries = [...new Map([...recovered, ...entries].map((entry) => - [entry[0][0], entry])).values()] - .sort((left, right) => - penalty(left[0][7]) - penalty(right[0][7]) - || right[2] - left[2] || cmp(left[0][0], right[0][0])) - .slice(0, 3) + [entry[0][0], entry])).values()].slice(0, 3) } - let root: string | undefined + let roots: string[] = [] const subjectTerms = new Set(words(target)), callTarget = terms.filter((term) => !subjectTerms.has(term)).join(' '), direct = focus && intent === 'explain' ? (v[2].get(focus) ?? []).filter((arc) => arc[2] === 'direct') : [], - requested = callTarget ? direct.find((arc) => - named(v, arc[1], callTarget)) : undefined, + wanted = callTarget ? direct.find((arc) => + pickIds(v, [arc[1]], callTarget, 'names').length > 0) : undefined, callArcs = direct.filter((arc) => - arc === requested || score(v[1].get(arc[1])!, targets) > 0) - .sort((a, b) => Number(b === requested) - Number(a === requested) - || score(v[1].get(b[1])!, targets) - score(v[1].get(a[1])!, targets) + arc === wanted || score(v[1].get(arc[1])!, goals) > 0) + .sort((a, b) => Number(b === wanted) - Number(a === wanted) + || score(v[1].get(b[1])!, goals) - score(v[1].get(a[1])!, goals) || cmp(a[1], b[1])).slice(0, 3), ids = focus ? [focus, ...callArcs.map((arc) => arc[1])] : [] let flow: Selection = isFlow ? [new Set(), [], [], new Set(), false] : [new Set(ids), callArcs, [], new Set(ids), false] const seen = new Set() let tries = isFlow ? 0 : focus ? 1 : 0 - let locked = false + let locked = false, bestStage = !stageNeed for (const entry of entries) { const id = entry[0][0] const room = NODES - RECOVERY - seen.size if (room <= 0) break - const trial = corridor(v, id, room, targets, terminalHint) - trial[3].forEach((candidate) => seen.add(candidate)) + const trial = corridor(v, id, room, goals, fail, lastBound, asyncNeed) + trial[3].forEach((cand) => seen.add(cand)) tries += 1 - const stageFit = !stageTarget || stageMatches(i, v, trial, stageTarget), - priorStageFit = !stageTarget || stageMatches(i, v, flow, stageTarget) - if (root === undefined || !locked && (Number(stageFit) > Number(priorStageFit) - || stageFit === priorStageFit + const stageFit = (!stageNeed + || stageMatch(i, v, trial, stageNeed, [id])[0].length > 0) + && (!asyncNeed || trial[1].some((arc) => arc[2] === 'channel')) + const domain = penalty(entry[0][7]), prior = roots[0] + ? penalty(v[1].get(roots[0])![7]) : Infinity + if (entryNeed && domain === prior && roots.length > 0 && [...trial[0]].some((node) => + node !== id && flow[0].has(node))) { + const merged = [...new Set([...flow[1], ...trial[1]])] + if (new Set(merged.flatMap(idsOf)).size > HOPS) { + flow = [flow[0], flow[1], flow[2], flow[3], true] + } else { + flow = [new Set([...flow[0], ...trial[0]]), merged, + [...new Set([...flow[2], ...trial[2]])], + new Set([...flow[3], ...trial[3]]), flow[4] || trial[4]] + roots.push(id) + } + continue + } + if (roots.length === 0 || !locked + && (domain < prior || domain === prior + && (Number(stageFit) > Number(bestStage) + || stageFit === bestStage && (Number(trial[2].length > 0) > Number(flow[2].length > 0) || Boolean(trial[2].length) === Boolean(flow[2].length) - && flow[4] && !trial[4]))) { - root = id + && flow[4] && !trial[4])))) { + roots = [id] flow = trial - locked = !terminalHint && !stageTarget && trial[2].length === 0 - && named(v, id, target) + bestStage = stageFit + locked = !lastBound && !stageNeed && !asyncNeed && trial[2].length === 0 + && pickIds(v, [id], target, 'names').length > 0 && (trial[1].length > 0 || v[4].has(id)) } } - // Pass two is a bounded, shared recovery frontier. Alternates are admitted + // Pass two is a bounded, shared rec frontier. Alternates are admitted // only when they are structural entries; disconnected middle-stage matches // can never manufacture an entry-to-persistence corridor. - const recovery = new Set() - bridge?.[1].forEach((id) => recovery.add(id)) + const rec = new Set() + scan?.[1].forEach((id) => rec.add(id)) flow[3].forEach((id) => seen.add(id)) let bounded = flow[4] - let passes: 0 | 1 | 2 = bridge ? 1 : 0 - if (!entryTarget && isFlow && (flow[2].length === 0 || flow[4]) - && ranked.length > 0) { - bridge ??= findRoots(v, ranked, targets) - bridge[1].forEach((id) => { recovery.add(id); seen.add(id) }) - bounded ||= bridge[2] + let passes: 0 | 1 | 2 = scan ? 1 : 0 + if (isFlow && (flow[2].length === 0 || flow[4]) + && ranks.length > 0) { + scan ??= scanRoots(v, ranks, goals) + scan[1].forEach((id) => { rec.add(id); seen.add(id) }) + bounded ||= scan[2] passes = 1 + const eligible = entryNeed + ? new Set(pickIds(v, scan[0], entryNeed, 'entry')) : undefined const alternates = flow[2].length === 0 && !locked - ? bridge[0].filter((id) => id !== root).slice(0, 3 - tries) : [] + ? scan[0].filter((id) => id !== roots[0] + && (!eligible || eligible.has(id))).slice(0, 3 - tries) : [] if (alternates.length > 0) passes = 2 for (const id of alternates) { tries += 1 - const room = RECOVERY - recovery.size + 1 + const room = RECOVERY - rec.size + 1 if (room <= 0) { bounded = true; break } - const trial = corridor(v, id, room, targets, terminalHint) + const trial = corridor(v, id, room, goals, fail, lastBound, asyncNeed) bounded ||= trial[4] - trial[3].forEach((entry) => { recovery.add(entry); seen.add(entry) }) - if (root !== undefined && v[4].has(root) && named(v, root, target)) continue - root = id; flow = trial; break + trial[3].forEach((entry) => { rec.add(entry); seen.add(entry) }) + if (roots[0] && v[4].has(roots[0]) + && pickIds(v, [roots[0]], target, 'names').length > 0) continue + roots = [id]; flow = trial; break } } else if (intent === 'explain' && focus && !(v[1].get(focus)?.[5].some(behavior) ?? false)) { - const alternate = ranked.slice(1, 4).find((entry) => + const alternate = ranks.slice(1, 4).find((entry) => entry[0][5].some(behavior)) if (alternate) { passes = 1 tries += 1 const id = alternate[0][0] - recovery.add(id); seen.add(id) + rec.add(id); seen.add(id) flow = [new Set([id]), [], [], new Set([id]), false] } } + const [nodes, links, ends] = flow const rootIds = isFlow - ? root && flow[0].has(root) ? [root] : [] + ? roots.filter((id) => nodes.has(id)).sort(cmp) : callArcs.length > 0 && focus ? [focus] : [] const causal = [...new Set([ - ...rootIds, ...flow[2], ...flow[1].flatMap((arc) => [arc[0], arc[1]]), + ...rootIds, ...ends, ...links.flatMap((arc) => [arc[0], arc[1]]), ])].sort(cmp) - const symbolIds = [...flow[0]].sort(cmp), - edges = [...new Map(flow[1].flatMap((arc) => + const symbolIds = [...nodes].sort(cmp), + edges = [...new Map(links.flatMap((arc) => arc[3].map((edge) => [edge[0], edge] as const))).values()] .map(([id, fromId, toId, relation]) => ({ id, fromId, toId, relation })).sort((a, b) => cmp(a.id, b.id)), - subjects = covering( - v, symbolIds, target, intent === 'locate' && !access, - ), + subjects = pickIds(v, symbolIds, target, + intent === 'locate' && !access ? 'names' : 'subject'), behaviors = isFlow ? causal : subjects, - edgeOwners = new Set(flow[1].map((arc) => arc[0])), - failureIntent = targets.some((entry) => - words(entry).some((word) => FAILURE_WORD.test(word))) - const factSeeds = intent === 'locate' ? [] : behaviors - .filter((id) => !edgeOwners.has(id) && !flow[2].includes(id)).flatMap((id) => { + owners = new Set(links.flatMap((arc) => [arc[0], arc[1]])) + const seeds = intent === 'locate' ? [] : behaviors + .filter((id) => !owners.has(id) && !ends.includes(id)) + .flatMap((id) => { const facts = v[1].get(id)?.[5].filter((fact) => - behavior(fact) && (failureIntent || !adverseFact(fact))) ?? [] + behavior(fact) && (fail || !bad(fact))) ?? [] return [...new Map(facts.map((fact) => [fact.kind, fact.id])).values()] }) const locateOps = intent === 'locate' && access @@ -960,84 +794,73 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel : [] const ctl: Control = intent === 'locate' ? locateOps.length > 0 ? controls(i, [], [], locateOps) : [[], [], true, []] - : controls(i, flow[1], flow[2], factSeeds) + : controls(i, links, ends, seeds) const steps = isFlow ? causal : symbolIds, edgeIds = edges.map((edge) => edge.id), chosenOps = new Set(ctl[0]), - terminalSymbols = [...new Set(ctl[3].map((id) => - i.operation_by_id.get(id)?.owner_symbol_id).filter( + terminalIds = [...new Set(ctl[3].map((id) => + ops.get(id)?.owner_symbol_id).filter( (id): id is string => id !== undefined, ))].sort(cmp), - arcOps = [...new Set(flow[1].flatMap((arc) => arc[4]))] + arcOps = [...new Set(links.flatMap((arc) => arc[4]))] .filter((id) => chosenOps.has(id)) - const owned = (ids: readonly string[]): string[] => ctl[0].filter((id) => - ids.includes(i.operation_by_id.get(id)?.owner_symbol_id as string)) - const behaviorOps = owned(behaviors) - const inert = behaviors.filter((id) => !edgeOwners.has(id) - && !behaviorOps.some((operation) => - i.operation_by_id.get(operation)?.owner_symbol_id === id)) - const incomplete = causal.filter((id) => v[4].has(id)) + const related = (ids: readonly string[], calls = false): string[] => + ctl[0].filter((id) => { + const fact = ops.get(id) + return !!fact && (ids.includes(fact.owner_symbol_id) || calls + && fact.kind === 'call' && !!fact.target_symbol_id + && ids.includes(fact.target_symbol_id)) + }) + const stage = stageNeed + ? stageMatch(i, v, flow, stageNeed, rootIds) + : undefined, + stageNodes = stage?.[0] ?? steps, + stageOps = stage ? stage[1].length > 0 + ? [...new Set(stage[1].flatMap((arc) => arc[4]))] + .filter((id) => chosenOps.has(id)).sort(cmp) + : related(stageNodes, true) : ctl[0], + stageEdges = stage ? [...new Set(stage[1].flatMap(idsOf))].sort(cmp) : edgeIds, + behaviorIds = behaviorNeed + ? pickIds(v, [...flow[0]], behaviorNeed, 'behavior') : behaviors, + allBehavior = related(behaviors), + behaviorOps = behaviorNeed ? related(behaviorIds, true) : allBehavior + const inert = behaviors.filter((id) => !owners.has(id) + && !allBehavior.some((operation) => + ops.get(operation)?.owner_symbol_id === id)) + const gaps = causal.filter((id) => v[4].has(id)) type ProofData = readonly [ - symbols: readonly string[], operations: readonly string[], proven: boolean, + nodes: readonly string[], ops: readonly string[], proven: boolean, ] const data: Record = { - subject: [subjects, intent === 'locate' && access ? locateOps : owned(subjects), + subject: [subjects, intent === 'locate' && access ? locateOps : related(subjects), subjects.length > 0 && (!access || locateOps.length > 0)], - entry: [rootIds, owned(rootIds), rootIds.length > 0], - stage: [steps, ctl[0], steps.length > 0], - handoff: [causal, isFlow ? arcOps : owned(causal), - flow[1].length > 0 && (!isFlow || incomplete.length === 0)], - behavior: [behaviors, behaviorOps, - behaviors.length > 0 && inert.length === 0], + entry: [rootIds, related(rootIds), rootIds.length > 0 + && (!entryIds || rootIds.some((id) => entryIds.has(id)))], + stage: [stageNodes, stageOps, + steps.length > 0 && (!stageNeed || stageNodes.length > 0)], + handoff: [causal, isFlow ? arcOps : related(causal), + links.length > 0 && (!isFlow || gaps.length === 0) + && (!asyncNeed || links.some((arc) => arc[2] === 'channel'))], + behavior: [behaviorIds, behaviorOps, + behaviors.length > 0 && inert.length === 0 + && (!behaviorNeed || behaviorIds.length > 0)], ordering: [steps, arcOps, - flow[1].length > 0 && incomplete.length === 0 && ctl[2] - && flow[1].every((arc) => arc[4].length > 0)], - terminal: [terminalSymbols, ctl[3], terminalSymbols.length > 0], + links.length > 0 && gaps.length === 0 && ctl[2] + && links.every((arc) => arc[4].length > 0)], + terminal: [terminalIds, ctl[3], terminalIds.length > 0], } const missing: WorkflowMissingReason[] = [] const proofs = obligations.map((obligation): WorkflowObligationProof => { - let [symbolIds, operationIds, proven] = data[obligation.kind] - let proofEdges = /^(?:stage|handoff|behavior|ordering)$/u - .test(obligation.kind) ? edgeIds : [] - if (obligation.target !== target - && /^(?:entry|stage|behavior|terminal)$/u.test(obligation.kind)) { - const role = obligation.kind as 'entry' | 'stage' | 'behavior' | 'terminal', - domain = role === 'entry' ? rootIds - : role === 'terminal' ? terminalSymbols - : role === 'stage' ? steps.filter((id) => - !rootIds.includes(id) && !flow[2].includes(id)) : [...flow[0]] - let matched = targetSymbols(v, domain, obligation.target, role) - if (role === 'stage') proofEdges = [] - if (role === 'stage' && matched.length === 0) { - const expected = words(obligation.target) - proofEdges = edges.filter((edge) => - [edge.fromId, edge.toId].some((id) => { - return expected.length > 0 && channelMatches(i, id, obligation.target) - })).map((edge) => edge.id) - const ids = new Set(proofEdges) - const arcs = flow[1].filter((arc) => - arc[3].some((edge) => ids.has(edge[0]))) - matched = [...new Set(arcs.flatMap((arc) => [arc[0], arc[1]]))].sort(cmp) - operationIds = [...new Set(arcs.flatMap((arc) => arc[4]))] - .filter((id) => chosenOps.has(id)).sort(cmp) - } else { - operationIds = ctl[0].filter((id) => { - const fact = i.operation_by_id.get(id) - return !!fact && (matched.includes(fact.owner_symbol_id) - || fact.kind === 'call' && !!fact.target_symbol_id - && matched.includes(fact.target_symbol_id)) - }) - } - symbolIds = matched - proven = proven && matched.length > 0 - } + const [symbolIds, operationIds, proven] = data[obligation.kind], + proofEdges = obligation.kind === 'stage' ? stageEdges + : /^(?:handoff|behavior|ordering)$/u.test(obligation.kind) ? edgeIds : [] const proof = { ...obligation, proven, symbolIds, operationIds, edgeIds: proofEdges, } if (proof.mandatory && !proof.proven) { missing.push({ code: MISSING_CODES[proof.kind] ?? 'obligation_target_unproven', - target: proof.kind === 'handoff' && incomplete.length > 0 - ? incomplete.join(',') : proof.target, + target: proof.kind === 'handoff' && gaps.length > 0 + ? gaps.join(',') : proof.target, obligationId: proof.id }) } return proof @@ -1049,22 +872,22 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel symbolIds, operationIds: ctl[0], rootSymbolIds: rootIds, - terminalSymbolIds: terminalSymbols, + terminalSymbolIds: terminalIds, edges, - links: flow[1].map((arc) => ({ + links: links.map((arc) => ({ fromId: arc[0], toId: arc[1], kind: arc[2], edgeIds: idsOf(arc), operationIds: arc[4].filter((id) => - chosenOps.has(id) && i.operation_by_id.get(id)?.owner_symbol_id === arc[0]), + chosenOps.has(id) && ops.get(id)?.owner_symbol_id === arc[0]), })), - controlGroups: [...ctl[1], ...cycleGroups(new Set(causal), flow[1])], + controlGroups: [...ctl[1], ...cycles(new Set(causal), links)], obligations: proofs, missing, metrics: { - candidateCount: ranked.length, rootCandidateCount: tries, + candidateCount: ranks.length, rootCandidateCount: tries, actualNodeCount: seen.size, causalRelationHops: edges.length, recoveryPasses: passes, - recoveryFrontierCount: recovery.size, bounded, + recoveryFrontierCount: rec.size, bounded, }, } } diff --git a/tests/unit/benchmark-quality.test.ts b/tests/unit/benchmark-quality.test.ts index b4615069..3fcdeac7 100644 --- a/tests/unit/benchmark-quality.test.ts +++ b/tests/unit/benchmark-quality.test.ts @@ -6,11 +6,15 @@ import { afterEach, describe, expect, it } from 'vitest' import { generateIndex } from '../../src/application/generate-index.js' import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' +import { retrieveContext } from '../../src/application/retrieve-context.js' +import { inspectQueryIndex } from '../../src/domain/query/index-status.js' import { evaluateRetrievalQuality, formatQualityReport, GOLD_QUESTIONS, + isGroundedDossierProofChain, } from '../../tools/eval/lib/infrastructure/benchmark/quality.js' +import type { DossierProof } from '../../src/domain/query/types.js' const sandboxes: string[] = [] @@ -70,10 +74,22 @@ describe('Core Reset retrieval quality evaluator', () => { ])) }) - it('grades only authenticated nodes returned by the one query', () => { + it('grounds workflow symbols through complete link proof chains without linked-call entities', () => { const { graphPath } = qualityWorkspace() + const graph = loadGraphArtifact(graphPath) + const retrieval = retrieveContext(inspectQueryIndex(graph), { + question: 'How does request flow end to end?', budget: 3_000, + }) + expect(retrieval.state).toBe('ready') + if (retrieval.state !== 'ready') return + expect(retrieval.dossier.evidence.entities.some((entity) => + entity.kind === 'operation' && 'links' in entity)).toBe(false) + expect(retrieval.dossier.flow.links).not.toHaveLength(0) + expect(retrieval.dossier.flow.links.every((link) => + link.proofs.length > 0)).toBe(true) + const report = evaluateRetrievalQuality( - loadGraphArtifact(graphPath), + graph, [{ question: 'How does request flow end to end?', expected_labels: ['handleRequest()', 'persistRequest()'], @@ -93,6 +109,43 @@ describe('Core Reset retrieval quality evaluator', () => { expect(report.questions[0]?.missing_labels).toEqual([]) }) + it('grounds excerpt or file-range proofs while preserving contiguous chains', () => { + const excerptProof: DossierProof = { + id: 'p0', from: 'a', to: 'b', relation: 'calls', excerpt: 'e0', + } + const rangeProof: DossierProof = { + id: 'p1', from: 'b', to: 'c', relation: 'publishes_to', + file: 'f0', range: [2, 3, 4, 5], + } + const proofs = new Map([excerptProof, rangeProof].map((proof) => [proof.id, proof])) + const excerpts = new Set(['e0']) + const files = new Set(['f0']) + + expect(isGroundedDossierProofChain( + { from: 'a', to: 'c', proofs: ['p0', 'p1'] }, proofs, excerpts, files, + )).toBe(true) + expect(isGroundedDossierProofChain( + { from: 'a', to: 'c', proofs: ['p1', 'p0'] }, proofs, excerpts, files, + )).toBe(false) + expect(isGroundedDossierProofChain( + { from: 'a', to: 'c', proofs: ['p0', 'p1'] }, proofs, excerpts, new Set(), + )).toBe(false) + expect(isGroundedDossierProofChain( + { from: 'a', to: 'c', proofs: ['p0', 'p1'] }, proofs, new Set(), files, + )).toBe(false) + + const invalidRanges: Array = [ + [0, 3, 4, 5], [2, -1, 4, 5], [4, 5, 2, 3], [2, 5, 2, 3], + ] + for (const range of invalidRanges) { + const invalid = new Map(proofs) + invalid.set('p1', { ...rangeProof, range }) + expect(isGroundedDossierProofChain( + { from: 'a', to: 'c', proofs: ['p0', 'p1'] }, invalid, excerpts, files, + )).toBe(false) + } + }) + it('does not credit substring or missing evidence', () => { const { graphPath } = qualityWorkspace() const report = evaluateRetrievalQuality( diff --git a/tests/unit/benchmark-suite-isolation-docs.test.ts b/tests/unit/benchmark-suite-isolation-docs.test.ts index 7de4ed4f..8d4ebea8 100644 --- a/tests/unit/benchmark-suite-isolation-docs.test.ts +++ b/tests/unit/benchmark-suite-isolation-docs.test.ts @@ -121,6 +121,10 @@ describe('development-only benchmark isolation', () => { expect(parity).toContain('server.serveMcpServer') expect(parity).toContain('requestWaitMs: 25_000') expect(parity).toContain('CLI query, and direct application bytes differ') + expect(parity).toContain("result?.version !== 2") + expect(parity).toContain("result?.state !== 'ready'") + expect(parity).toContain('channelLinks.length !== 4') + expect(parity).not.toContain('result?.outcome') expect(parity).not.toContain('serveGraphStdio') expect(parity).not.toContain('autoRefreshRequestWaitMs: 30_000') expect(workflow).toContain('npm run verify:pack-parity') @@ -129,7 +133,8 @@ describe('development-only benchmark isolation', () => { it('keeps expected benchmark evidence out of production retrieval', () => { for (const path of [ 'src/application/retrieve-context.ts', - 'src/domain/query/slice.ts', + 'src/application/evidence-hydrator.ts', + 'src/domain/query/workflow.ts', 'src/adapters/mcp/protocol.ts', ]) { const source = read(path) diff --git a/tests/unit/benchmark.test.ts b/tests/unit/benchmark.test.ts index d1a58154..aef147ea 100644 --- a/tests/unit/benchmark.test.ts +++ b/tests/unit/benchmark.test.ts @@ -260,7 +260,7 @@ describe('Core Reset benchmark caller', () => { const success = await runBenchmark( graphPath, 10_000, - ['process order'], + ['Where is process order defined?'], { execTemplate: 'runner {prompt_file}', outputDir: join(root, 'out', 'benchmark'), @@ -302,7 +302,7 @@ describe('Core Reset benchmark caller', () => { const failure = await runBenchmark( graphPath, 10_000, - ['process order'], + ['Where is process order defined?'], { execTemplate: 'runner {prompt_file}', outputDir: join(root, 'out', 'benchmark-failure'), @@ -322,7 +322,7 @@ describe('Core Reset benchmark caller', () => { graphPath, 10_000, [{ - question: 'process order', + question: 'Where is process order defined?', expected_labels: ['processOrder()', 'MissingSymbol()'], }], )) @@ -335,7 +335,8 @@ describe('Core Reset benchmark caller', () => { avg_reused_context_tokens: 1, }) expect(localOutput).toContain('Unmatched: unmatched flow') - expect(localOutput).toContain('Missing evidence for process order: MissingSymbol()') + expect(localOutput) + .toContain('Missing evidence for Where is process order defined?: MissingSymbol()') expect(localOutput).toContain('Avg effective input tokens (cache-adjusted)') expect(localOutput).toContain('local cl100k_base estimate') @@ -439,7 +440,7 @@ describe('Core Reset benchmark caller', () => { it('prints the retained benchmark metrics', () => { const { graphPath } = workspace() const result = requireSynchronousResult( - runBenchmark(graphPath, 10_000, ['process order']), + runBenchmark(graphPath, 10_000, ['Where is process order defined?']), ) if ('error' in result) throw new Error(result.error) expect(printedBenchmark(result)).toContain('madar runner-backed benchmark') diff --git a/tests/unit/core-reset-governance.test.ts b/tests/unit/core-reset-governance.test.ts index 169833b4..04c3d928 100644 --- a/tests/unit/core-reset-governance.test.ts +++ b/tests/unit/core-reset-governance.test.ts @@ -448,6 +448,47 @@ const SEMANTIC_EXECUTION_PACKAGE = { } as const const SEMANTIC_EXECUTION_DIFF_SHA256 = '910d0e1835e54af5e7a36af0a7ffa612977fb9240e9985d0f1368532d4ca3a43' +const SEMANTIC_EXECUTION_INDEX_REVIEWED_HEAD = 'da3e1ad360855c950cae6986a9774c45fcb527d0' +const SEMANTIC_EXECUTION_INDEX_MERGE = 'c88823ecbeb6da6284cf74ecbd304e9315ffd4fa' +const SEMANTIC_EXECUTION_INDEX_MERGE_TREE = 'b715764668b4296e9e8ab4da715374f47af137db' +const SEMANTIC_EXECUTION_INDEX_CI = + 'https://github.com/mohanagy/madar/actions/runs/30699876911' +const OBLIGATION_RETRIEVAL_ID = 'obligation-driven-retrieval-630' +const OBLIGATION_RETRIEVAL_BASE = SEMANTIC_EXECUTION_INDEX_MERGE +const OBLIGATION_RETRIEVAL_BASE_TREE = SEMANTIC_EXECUTION_INDEX_MERGE_TREE +const OBLIGATION_RETRIEVAL_FILES = [ + 'src/adapters/mcp/protocol.ts', + 'src/application/evidence-hydrator.ts', + 'src/application/retrieve-context.ts', + 'src/domain/query/plan.ts', + 'src/domain/query/rank.ts', + 'src/domain/query/slice.ts', + 'src/domain/query/traverse.ts', + 'src/domain/query/types.ts', + 'src/domain/query/workflow.ts', +] as const +const OBLIGATION_RETRIEVAL_SOURCE = { + production_typescript_files: 44, + production_typescript_loc: 15_770, + production_loc_added: 2_200, + production_loc_removed: 2_149, + production_loc_net: 51, +} as const +const OBLIGATION_RETRIEVAL_PACKAGE = { + npm_files: 102, + npm_packed_bytes: 154_210, + npm_unpacked_bytes: 649_915, + npm_shasum: 'a56d34339b674a117f986f987bd192232349c077', + npm_integrity: + 'sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ==', + npm_artifact_sha256: '3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d', +} as const +const OBLIGATION_RETRIEVAL_DIFF_SHA256 = + '3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a' +const OBLIGATION_RETRIEVAL_STOP_RECEIPT = + 'https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732' +const OBLIGATION_RETRIEVAL_AMENDMENT = + 'https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147' const CAPABILITY_VALIDATION_V2_PROPOSAL_SHA256 = '4906405cbb806c850c0612305ef460e023e2060b5338734ae0af12303901cbd0' const CAPABILITY_VALIDATION_V2_ISSUE = 'https://github.com/mohanagy/madar/issues/612' @@ -698,6 +739,15 @@ function productionSourceDeltaBetween( const gitBlobSha256 = (revision: string, path: string): string => createHash('sha256').update(execFileSync(git, ['show', `${revision}:${path}`])).digest('hex') +const gitPathExists = (revision: string, path: string): boolean => { + try { + execFileSync(git, ['cat-file', '-e', `${revision}:${path}`]) + return true + } catch { + return false + } +} + function importedRelativeTargets( importer: string, source: string, @@ -856,8 +906,8 @@ describe('core reset governance', () => { expect(roadmap).toContain('## Published — `0.40.0-beta.3`') expect(roadmap).toContain('## Passed — retrieval regression #625') expect(roadmap).toContain('## Published — `0.40.0-beta.4`') - expect(roadmap).toContain('## In progress — semantic execution index #632') - expect(roadmap).toContain('## Pending — obligation-driven retrieval #630') + expect(roadmap).toContain('## Completed — semantic execution index #632') + expect(roadmap).toContain('## In progress — obligation-driven retrieval #630') expect(roadmap).toContain('## Pending — installed no-fallback qualification #631') expect(roadmap).toContain(CAPABILITY_VALIDATION_PROPOSAL_SHA256) expect(roadmap).toContain(CAPABILITY_VALIDATION_OWNER_APPROVAL) @@ -958,6 +1008,8 @@ describe('core reset governance', () => { expect(design).toContain('## Release amendment — `0.40.0-beta.3` published') expect(design).toContain('## Completed amendment — retrieval regression #625') expect(design).toContain('## Release amendment — `0.40.0-beta.4` ready') + expect(design).toContain('## Completed amendment — semantic execution correction #632') + expect(design).toContain('## Active amendment — obligation-driven retrieval #630') expect(design).toContain(CAPABILITY_VALIDATION_PROPOSAL_SHA256) expect(design).toContain(CAPABILITY_VALIDATION_OWNER_APPROVAL) expect(design).toContain(CAPABILITY_VALIDATION_RFC_APPROVAL) @@ -1034,6 +1086,9 @@ describe('core reset governance', () => { expect(design).toContain(EVALUATION_TOOLING_CODERABBIT_RECEIPT) expect(design).toContain(EVALUATION_TOOLING_ISSUE_MERGE_RECEIPT) expect(design).toContain(EVALUATION_TOOLING_RFC_MERGE_RECEIPT) + expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) + expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE_TREE) + expect(design).toContain('1,384 source LOC / 59,896 emitted bytes') expect(design).not.toContain('## Active amendment — generation and incremental index') expect(design).not.toContain('the phase remains active') expect(design).not.toContain('completion evidence remains open') @@ -1052,10 +1107,8 @@ describe('core reset governance', () => { expect(scorecard).toContain('| Retrieval regression #618 | **Passed**') expect(scorecard).toContain('| Retrieval regression #625 | **Passed**') expect(scorecard).toContain('| Beta release | **Published**') - expect(scorecard).toContain( - '| Semantic execution index #632 | **Reopened — corrective work in progress**', - ) - expect(scorecard).toContain('| Obligation-driven retrieval #630 | **Pending**') + expect(scorecard).toContain('| Semantic execution index #632 | **Passed**') + expect(scorecard).toContain('| Obligation-driven retrieval #630 | **In progress**') expect(scorecard).toContain('| No-fallback qualification #631 | **Pending**') expect(scorecard).toContain(CAPABILITY_VALIDATION_PROPOSAL_SHA256) expect(scorecard).toContain(CAPABILITY_VALIDATION_OWNER_APPROVAL) @@ -1101,8 +1154,10 @@ describe('core reset governance', () => { expect(scorecard).toContain('Identical normalized request plus identical canonical graph bytes') expect(scorecard).toContain('every warmup/measured result must remain correct; an empty positive result fails') expect(scorecard).toContain('| Retrieval regression #622 | **Passed**') - expect(scorecard).toContain('Issues `#622` and `#625` are complete on `next`') - expect(scorecard).toContain('#632 active, then #630 pending, then #631 pending') + expect(scorecard).toContain('Issues `#622`, `#625`, and `#632` are complete on `next`') + expect(scorecard).toContain('supersedes its historical stop and reactivates it under exactly two amended ceilings') + expect(scorecard).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) + expect(scorecard).toContain('1,384 source LOC / 59,896 emitted bytes') expect(scorecard).toContain( 'accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed', ) @@ -1342,16 +1397,16 @@ describe('core reset governance', () => { expect(manifest.schema_version).toBe(1) expect(manifest.status).toBe('accepted') expect(manifest.current).toMatchObject({ - updated_at: '2026-08-01', - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + updated_at: '2026-08-02', + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - completed_phase_commit: EVIDENCE_SKELETON_RETRIEVAL_MERGE, - ...SEMANTIC_EXECUTION_SOURCE, - ...SEMANTIC_EXECUTION_PACKAGE, + base_commit: OBLIGATION_RETRIEVAL_BASE, + completed_phase_commit: SEMANTIC_EXECUTION_INDEX_MERGE, + ...OBLIGATION_RETRIEVAL_SOURCE, + ...OBLIGATION_RETRIEVAL_PACKAGE, measurement_state: 'source_and_package_exact', - snapshot_scope: 'semantic_execution_index_632_candidate', + snapshot_scope: 'obligation_driven_retrieval_630_candidate', }) expect(manifest.current.release_candidate).toMatchObject({ version: '0.40.0-beta.4', @@ -1488,7 +1543,7 @@ describe('core reset governance', () => { expect(logicalLocAtCommit(legacyBase, deletionFiles)).toBe(20_951) const generation = manifest.items.find((item) => item.id === 'generation-and-incremental') expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) const retrievalRegression = manifest.items.find((item) => item.id === RETRIEVAL_REGRESSION_ID) as any expect(retrievalRegression).toMatchObject({ disposition: 'keep', @@ -1871,7 +1926,7 @@ describe('core reset governance', () => { ) as any expect(semanticExecution).toMatchObject({ disposition: 'keep', - status: 'in_progress', + status: 'complete', destination: 'canonical JavaScript/TypeScript semantic execution index', modified_sources: [...SEMANTIC_EXECUTION_INDEX_FILES], activation: { @@ -1920,6 +1975,18 @@ describe('core reset governance', () => { tag: 'forbidden', main_target: 'forbidden', }, + completion: { + pull_request: 'https://github.com/mohanagy/madar/pull/634', + reviewed_head: SEMANTIC_EXECUTION_INDEX_REVIEWED_HEAD, + merge_commit: SEMANTIC_EXECUTION_INDEX_MERGE, + merge_tree: SEMANTIC_EXECUTION_INDEX_MERGE_TREE, + protected_parent: 'e7bd30ce384cf743dbda3e8ee7f15b171a0ea649', + ci_run: SEMANTIC_EXECUTION_INDEX_CI, + ci_matrix_jobs_passed: 6, + independent_review: 'passed', + unresolved_review_threads: 0, + coderabbit: 'passed', + }, }) expect(semanticExecution.notes).toContain( 'accessor-backed data or discriminator properties, including destructured aliases and shorthand, fail closed', @@ -1961,7 +2028,7 @@ describe('core reset governance', () => { coverage_lines_percent: 89.22, coverage_lines_covered: 6_481, coverage_lines_total: 7_264, - local_independent_review: 'pending_corrective_review', + local_independent_review: 'passed', pre_reopen_merge_commit: 'e7bd30ce384cf743dbda3e8ee7f15b171a0ea649', post_merge_acceptance_audit: 'failed_missing_exact_payload_and_discriminant_binding', corrective_real_graph_acceptance: 'passed', @@ -2001,46 +2068,188 @@ describe('core reset governance', () => { beta4_retrieval_output_bytes: 15_294, beta4_retrieval_output_sha256: '87b4ef75473834708b20f1d2580b31470a710d797d7bdf55eee1d0876827a173', - exact_head_ci: 'pending', - independent_review: 'pending', + exact_head_ci: SEMANTIC_EXECUTION_INDEX_CI, + independent_review: 'passed', }, }) - expect(semanticExecution).not.toHaveProperty('completion') - const changedSemanticExecutionProduction = [ - ...execFileSync( - git, - ['diff', '--name-only', SEMANTIC_EXECUTION_INDEX_BASE, '--', 'src'], - { encoding: 'utf8' }, - ).trim().split('\n').filter(Boolean), - ...execFileSync( - git, - ['ls-files', '--others', '--exclude-standard', '--', 'src'], - { encoding: 'utf8' }, - ).trim().split('\n').filter(Boolean), - ].sort() + const changedSemanticExecutionProduction = execFileSync( + git, + [ + 'diff', + '--name-only', + SEMANTIC_EXECUTION_INDEX_BASE, + SEMANTIC_EXECUTION_INDEX_MERGE, + '--', + 'src', + ], + { encoding: 'utf8' }, + ).trim().split('\n').filter(Boolean).sort() expect(changedSemanticExecutionProduction).toEqual([...SEMANTIC_EXECUTION_INDEX_FILES].sort()) const semanticBaseFiles = new Set(productionTypeScriptFilesAtCommit(SEMANTIC_EXECUTION_INDEX_BASE)) expect(SEMANTIC_EXECUTION_INDEX_FILES.filter((path) => !semanticBaseFiles.has(path))) .toHaveLength(1) - expect(productionSourceDelta(SEMANTIC_EXECUTION_INDEX_BASE).net).toBeLessThanOrEqual(3_500) + expect(productionSourceDeltaBetween( + SEMANTIC_EXECUTION_INDEX_BASE, + SEMANTIC_EXECUTION_INDEX_MERGE, + ).net).toBeLessThanOrEqual(3_500) expect(execFileSync( git, ['rev-parse', `${SEMANTIC_EXECUTION_INDEX_BASE}^{tree}`], { encoding: 'utf8' }, ).trim()).toBe(SEMANTIC_EXECUTION_INDEX_BASE_TREE) + expect(execFileSync( + git, + ['rev-parse', `${SEMANTIC_EXECUTION_INDEX_MERGE}^{tree}`], + { encoding: 'utf8' }, + ).trim()).toBe(SEMANTIC_EXECUTION_INDEX_MERGE_TREE) const obligationRetrieval = manifest.items.find( (item) => item.id === 'obligation-driven-retrieval-630', ) as any expect(obligationRetrieval).toMatchObject({ disposition: 'keep', - status: 'planned', + status: 'in_progress', depends_on: [SEMANTIC_EXECUTION_INDEX_ID], + modified_sources: [...OBLIGATION_RETRIEVAL_FILES], activation: { issue: 'https://github.com/mohanagy/madar/issues/630', - protected_base: SEMANTIC_EXECUTION_INDEX_BASE, - protected_base_tree: SEMANTIC_EXECUTION_INDEX_BASE_TREE, + protected_base: OBLIGATION_RETRIEVAL_BASE, + protected_base_tree: OBLIGATION_RETRIEVAL_BASE_TREE, target_branch: 'next', }, + delivery_limits: { + new_production_files_max: 3, + net_production_loc_max: 235, + total_production_loc_max: 15_954, + replacement_source_loc_max: 1_500, + replacement_emitted_bytes_max: 61_000, + warm_retrieval_p95_ms_less_than: 500, + }, + npm_package_budget: { + files_max: 102, + packed_bytes_max: 165_000, + unpacked_bytes_max: 655_000, + }, + candidate: { + source_measurement: { + production_typescript_files: OBLIGATION_RETRIEVAL_SOURCE.production_typescript_files, + production_typescript_loc: OBLIGATION_RETRIEVAL_SOURCE.production_typescript_loc, + added: OBLIGATION_RETRIEVAL_SOURCE.production_loc_added, + removed: OBLIGATION_RETRIEVAL_SOURCE.production_loc_removed, + net: OBLIGATION_RETRIEVAL_SOURCE.production_loc_net, + diff_sha256: OBLIGATION_RETRIEVAL_DIFF_SHA256, + }, + replacement_measurement: { + source_loc: 1_384, + emitted_bytes: 59_896, + }, + package_measurement: { + files: OBLIGATION_RETRIEVAL_PACKAGE.npm_files, + packed_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_packed_bytes, + unpacked_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_unpacked_bytes, + shasum: OBLIGATION_RETRIEVAL_PACKAGE.npm_shasum, + integrity: OBLIGATION_RETRIEVAL_PACKAGE.npm_integrity, + artifact_sha256: OBLIGATION_RETRIEVAL_PACKAGE.npm_artifact_sha256, + }, + local_verification: { + source_gate: 'passed', + replacement_source_loc_gate: 'passed', + replacement_gate: 'passed', + package_gate: 'passed', + focused_test_files_passed: 4, + focused_tests_passed: 159, + typecheck: 'passed', + build: 'passed', + build_eval: 'passed', + governance_tests_passed: 18, + packed_retrieval_parity: 'passed', + release_verify: 'passed', + registry_validate: 'passed', + isolation_verify: 'passed', + npm_audit_high: 'passed', + ci_eval_regression: 'passed', + ci_eval_regression_questions_passed: 5, + ci_eval_regression_questions_total: 5, + ci_eval_regression_recall_percent: 95, + ci_eval_regression_mrr: 1, + ci_eval_regression_snippet_precision_percent: 100, + ci_eval_regression_grounded_percent: 95, + final_full_suite: 'passed', + full_test_files_passed: 83, + full_tests_passed: 865, + coverage: 'passed', + coverage_statements_percent: 85.47, + coverage_statements_covered: 7_696, + coverage_statements_total: 9_004, + coverage_branches_percent: 79.76, + coverage_branches_covered: 7_169, + coverage_branches_total: 8_988, + coverage_functions_percent: 91.91, + coverage_functions_covered: 1_341, + coverage_functions_total: 1_459, + coverage_lines_percent: 88.97, + coverage_lines_covered: 6_430, + coverage_lines_total: 7_227, + frozen_govalidate_acceptance: 'passed', + frozen_govalidate: { + prompts: 5, + ready_results: 5, + files_per_result: 9, + excerpts_per_result: 12, + links_per_result: 12, + order_groups_per_result: 15, + entities_per_result: 21, + proofs_per_result: 20, + required_queues: [ + 'orchestration-queue', + 'section-research-queue', + 'assembly-queue', + 'db-sync-queue', + ], + serialized_tokens: [3_977, 3_977, 3_979, 3_977, 3_977], + }, + frozen_govalidate_all_variants: { + prompts: 14, + ready_results: 14, + serialized_tokens_min: 3_965, + serialized_tokens_max: 3_998, + all_four_required_queues: true, + flow_sha256: 'c34295432be3a54ce7506c2019fd17f3e2ca631c78d578b696234cf981c8592b', + evidence_sha256: 'd52e4db48b4a5346b3cf54113406b37894f27c04288561d5fe1286050bde848d', + }, + warm_retrieval_reference_gate: 'passed', + warm_retrieval_reference: { + warmups: 3, + measured_queries: 100, + median_ms: 50.795208, + p95_ms: 53.452208, + max_ms: 56.98625, + }, + independent_review: 'pending', + exact_head_ci: 'pending', + }, + historical_stop: { + receipt: OBLIGATION_RETRIEVAL_STOP_RECEIPT, + reason: 'replacement_emitted_and_package_unpacked_ceilings_failed', + frozen_local_candidate_only: true, + branch_pushed: false, + pull_request_opened: false, + protected_next_changed: false, + merged: false, + npm_published: false, + github_release_created: false, + registry_metadata_published: false, + tag_created: false, + main_targeted: false, + }, + amendment: { + receipt: OBLIGATION_RETRIEVAL_AMENDMENT, + supersedes_stop_receipt: OBLIGATION_RETRIEVAL_STOP_RECEIPT, + state: 'active_authorized', + replacement_emitted_bytes_max: 61_000, + npm_unpacked_bytes_max: 655_000, + all_other_metrics_and_constraints: 'unchanged', + }, + }, }) const noFallbackQualification = manifest.items.find( (item) => item.id === 'no-fallback-qualification-631', @@ -2475,18 +2684,18 @@ describe('core reset governance', () => { ['merge-base', '--is-ancestor', THIN_DELIVERY_IMPLEMENTATION_START, THIN_DELIVERY_MERGE], )).not.toThrow() expect(manifest.current).toMatchObject({ - updated_at: '2026-08-01', - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + updated_at: '2026-08-02', + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - completed_phase_commit: EVIDENCE_SKELETON_RETRIEVAL_MERGE, - ...SEMANTIC_EXECUTION_SOURCE, + base_commit: OBLIGATION_RETRIEVAL_BASE, + completed_phase_commit: SEMANTIC_EXECUTION_INDEX_MERGE, + ...OBLIGATION_RETRIEVAL_SOURCE, measurement_state: 'source_and_package_exact', - snapshot_scope: 'semantic_execution_index_632_candidate', + snapshot_scope: 'obligation_driven_retrieval_630_candidate', }) expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) expect(manifest.targets).toMatchObject({ production_typescript_files_max: 80, production_typescript_loc_max: 35_000, @@ -3288,6 +3497,7 @@ describe('core reset governance', () => { current: { completed_phase: string active_phase: string | null + stopped_phase?: string | null ready_phase: string | null base_commit: string completed_phase_commit: string @@ -3338,18 +3548,18 @@ describe('core reset governance', () => { ['merge-base', '--is-ancestor', EVALUATION_TOOLING_ACTIVATION_MERGE, EVALUATION_TOOLING_MERGE], )).not.toThrow() expect(manifest.current).toMatchObject({ - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - completed_phase_commit: EVIDENCE_SKELETON_RETRIEVAL_MERGE, - ...SEMANTIC_EXECUTION_SOURCE, - npm_files: SEMANTIC_EXECUTION_PACKAGE.npm_files, - npm_packed_bytes: SEMANTIC_EXECUTION_PACKAGE.npm_packed_bytes, - npm_unpacked_bytes: SEMANTIC_EXECUTION_PACKAGE.npm_unpacked_bytes, + base_commit: OBLIGATION_RETRIEVAL_BASE, + completed_phase_commit: SEMANTIC_EXECUTION_INDEX_MERGE, + ...OBLIGATION_RETRIEVAL_SOURCE, + npm_files: OBLIGATION_RETRIEVAL_PACKAGE.npm_files, + npm_packed_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_packed_bytes, + npm_unpacked_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_unpacked_bytes, }) expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) const evaluation = manifest.items.find((item) => item.id === 'evaluation-tooling') expect(evaluation).toMatchObject({ @@ -3657,11 +3867,7 @@ describe('core reset governance', () => { filesystemViolations: [], }) expect(evaluatorFiles).toEqual([...evaluatorDestinations].sort()) - expect(evaluatorFiles.reduce((total, path) => { - const source = read(path) - const lineFeeds = source.match(/\n/g)?.length ?? 0 - return total + lineFeeds + (source.length > 0 && !source.endsWith('\n') ? 1 : 0) - }, 0)).toBe(4_698) + expect(logicalLocAtCommit(EVALUATION_TOOLING_MERGE, evaluatorDestinations)).toBe(4_698) const changedProduction = execFileSync( git, @@ -3856,14 +4062,14 @@ describe('core reset governance', () => { }) | undefined expect(manifest.current).toMatchObject({ - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - ...SEMANTIC_EXECUTION_SOURCE, + base_commit: OBLIGATION_RETRIEVAL_BASE, + ...OBLIGATION_RETRIEVAL_SOURCE, }) expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) expect(phase).toMatchObject({ disposition: 'keep', status: 'stopped', @@ -4110,14 +4316,14 @@ describe('core reset governance', () => { }) | undefined expect(manifest.current).toMatchObject({ - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - ...SEMANTIC_EXECUTION_SOURCE, + base_commit: OBLIGATION_RETRIEVAL_BASE, + ...OBLIGATION_RETRIEVAL_SOURCE, }) expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) expect(phase).toMatchObject({ disposition: 'keep', status: 'stopped', @@ -4741,21 +4947,21 @@ describe('core reset governance', () => { expect(execFileSync(git, ['rev-parse', `${EVIDENCE_BASE}^{tree}`], { encoding: 'utf8' }).trim()) .toBe(EVIDENCE_BASE_TREE) expect(manifest.current).toMatchObject({ - updated_at: '2026-08-01', - completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, - active_phase: SEMANTIC_EXECUTION_INDEX_ID, + updated_at: '2026-08-02', + completed_phase: SEMANTIC_EXECUTION_INDEX_ID, + active_phase: OBLIGATION_RETRIEVAL_ID, ready_phase: null, - base_commit: SEMANTIC_EXECUTION_INDEX_BASE, - completed_phase_commit: EVIDENCE_SKELETON_RETRIEVAL_MERGE, - ...SEMANTIC_EXECUTION_SOURCE, - npm_files: SEMANTIC_EXECUTION_PACKAGE.npm_files, - npm_packed_bytes: SEMANTIC_EXECUTION_PACKAGE.npm_packed_bytes, - npm_unpacked_bytes: SEMANTIC_EXECUTION_PACKAGE.npm_unpacked_bytes, + base_commit: OBLIGATION_RETRIEVAL_BASE, + completed_phase_commit: SEMANTIC_EXECUTION_INDEX_MERGE, + ...OBLIGATION_RETRIEVAL_SOURCE, + npm_files: OBLIGATION_RETRIEVAL_PACKAGE.npm_files, + npm_packed_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_packed_bytes, + npm_unpacked_bytes: OBLIGATION_RETRIEVAL_PACKAGE.npm_unpacked_bytes, measurement_state: 'source_and_package_exact', - snapshot_scope: 'semantic_execution_index_632_candidate', + snapshot_scope: 'obligation_driven_retrieval_630_candidate', }) expect(manifest.items.filter((item) => item.status === 'in_progress').map((item) => item.id)) - .toEqual([SEMANTIC_EXECUTION_INDEX_ID]) + .toEqual([OBLIGATION_RETRIEVAL_ID]) const evidence = manifest.items.find((item) => item.id === 'evidence-path-query') expect(evidence).toMatchObject({ @@ -5256,7 +5462,8 @@ describe('core reset governance', () => { .toHaveLength(1) } expect(EVIDENCE_REPLACEMENTS.every((path) => !baseFiles.includes(path))).toBe(true) - expect(EVIDENCE_REPLACEMENTS.every((path) => existsSync(resolve(path)))).toBe(true) + expect(EVIDENCE_REPLACEMENTS.every((path) => gitPathExists(EVIDENCE_IMPLEMENTATION, path))) + .toBe(true) expect(logicalLocAtCommit(EVIDENCE_IMPLEMENTATION, EVIDENCE_REPLACEMENTS)).toBe(1_812) expect(existsSync(resolve(EVIDENCE_PERFORMANCE_RECEIPT))).toBe(true) const implementationDelta = productionSourceDeltaBetween(EVIDENCE_BASE, EVIDENCE_IMPLEMENTATION) @@ -5733,7 +5940,8 @@ describe('core reset governance', () => { ...observedSurvivingImporters.map((entry) => entry.path), 'src/runtime/stdio/definitions.ts', ].sort()) - expect(EVIDENCE_REPLACEMENTS.every((path) => existsSync(resolve(path)))).toBe(true) + expect(EVIDENCE_REPLACEMENTS.every((path) => gitPathExists(EVIDENCE_IMPLEMENTATION, path))) + .toBe(true) }) it('publishes an exact hermetic generation mutation receipt', () => { @@ -6323,8 +6531,8 @@ describe('core reset governance', () => { expect(governance).toContain('## Published — `0.40.0-beta.3`') expect(governance).toContain('## Passed — retrieval regression #625') expect(governance).toContain('## Published — `0.40.0-beta.4`') - expect(governance).toContain('## In progress — semantic execution index #632') - expect(governance).toContain('## Pending — obligation-driven retrieval #630') + expect(governance).toContain('## Completed — semantic execution index #632') + expect(governance).toContain('## In progress — obligation-driven retrieval #630') expect(governance).toContain('## Pending — installed no-fallback qualification #631') expect(governance).toContain('## Stopped amendment — capability validation v1') expect(governance).toContain('## Historical accepted amendment — capability validation v2') @@ -6333,6 +6541,8 @@ describe('core reset governance', () => { expect(governance).toContain('## Passed — retrieval regression #622') expect(governance).toContain('## Completed amendment — retrieval regression #622') expect(governance).toContain('## Completed amendment — retrieval regression #625') + expect(governance).toContain('## Completed amendment — semantic execution correction #632') + expect(governance).toContain('## Active amendment — obligation-driven retrieval #630') expect(governance).toContain('Release amendment — `0.40.0-beta.3` published') expect(governance).toContain('Release amendment — `0.40.0-beta.4` ready') expect(governance).toContain('/compilerOptions/removeComments=true') diff --git a/tests/unit/evidence-hydrator.test.ts b/tests/unit/evidence-hydrator.test.ts index 26d8f2a6..0ed90f0a 100644 --- a/tests/unit/evidence-hydrator.test.ts +++ b/tests/unit/evidence-hydrator.test.ts @@ -196,24 +196,6 @@ function setWrapperBinding( return operation } -function duplicateSelectedEdge(index: ReadyQueryIndex, id: string): QueryGraph { - const graph = index.graph - const methods = { - hasNode: graph.hasNode.bind(graph), hasEdge: graph.hasEdge.bind(graph), - nodeEntries: graph.nodeEntries.bind(graph), predecessors: graph.predecessors.bind(graph), - successors: graph.successors.bind(graph), edgesBetween: graph.edgesBetween.bind(graph), - nodeAttributes: graph.nodeAttributes.bind(graph), - } - return { - ...methods, - edgeEntries: () => { - const rows = graph.edgeEntries() - const selected = rows.find((row) => row[3] === id)! - return [...rows, selected] - }, - } -} - afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) @@ -242,9 +224,16 @@ describe('selected evidence hydration', () => { { kind: 'redacted', sha256: 'a'.repeat(64), byte_length: 6 }, ], }) - const edgeProofs = values(first.proofs).filter((proof) => proof[1] === 'edge') + const edgeProofs = values(first.proofs).filter((proof) => + proof[1] === 'edge' || proof[1] === 'edge_range') expect(edgeProofs).toHaveLength(2) expect(edgeProofs.find((proof) => proof[4] === 'calls')).toHaveLength(6) + expect(edgeProofs.find((proof) => proof[4] === 'publishes_to')).toEqual([ + expect.any(String), 'edge_range', expect.any(String), expect.any(String), + 'publishes_to', 'f0', expect.objectContaining({ + start: expect.any(Object), end: expect.any(Object), + }), + ]) expect(values(first.proofs).some((proof) => proof[1] === 'operation')).toBe(true) expect(values(first.excerpts).map((item) => item[4])).toEqual([ 'export function owner()', @@ -312,7 +301,7 @@ describe('selected evidence hydration', () => { expect(result.state).toBe('ready') if (result.state !== 'ready') return expect(values(result.proofs).find((proof) => - proof[1] === 'edge' && proof[4] === 'consumed_by')).toHaveLength(6) + proof[1] === 'edge_range' && proof[4] === 'consumed_by')).toHaveLength(7) expect(values(result.proofs).some((proof) => proof[1] === 'operation')).toBe(true) }) @@ -328,7 +317,8 @@ describe('selected evidence hydration', () => { expect(result.state).toBe('ready') if (result.state !== 'ready') return - expect(values(result.proofs).find((proof) => proof[1] === 'edge')).toHaveLength(6) + expect(values(result.proofs).find((proof) => proof[1] === 'edge_range')) + .toHaveLength(7) expect(values(result.entities).some((entity) => entity[1] === 'operation')).toBe(false) }) @@ -352,7 +342,8 @@ describe('selected evidence hydration', () => { expect(result.state).toBe('ready') if (result.state !== 'ready') return - expect(values(result.proofs).find((proof) => proof[1] === 'edge')).toHaveLength(6) + expect(values(result.proofs).find((proof) => proof[1] === 'edge_range')) + .toHaveLength(7) }) it('rejects consumed_by evidence whose indirect owner lacks the authenticated call', () => { @@ -458,19 +449,12 @@ describe('selected evidence hydration', () => { expect(hydrateEvidence(value.index, value.targets).state).toBe('ready') }) - it('rejects a selected edge with mismatched or ambiguous identity', () => { + it('rejects a selected edge with mismatched identity', () => { const mismatch = fixture() expect(hydrateEvidence(mismatch.index, { ...mismatch.targets, edges: [{ id: mismatch.directEdgeId, fromId: targetId, toId: ownerId }], })).toEqual({ state: 'corrupt', subject: mismatch.directEdgeId }) - - const ambiguous = fixture() - const index = { ...ambiguous.index, - graph: duplicateSelectedEdge(ambiguous.index, ambiguous.directEdgeId) } - expect(hydrateEvidence(index, ambiguous.targets)).toEqual({ - state: 'corrupt', subject: ambiguous.directEdgeId, - }) }) it('rejects a selected source that resolves outside the indexed root', () => { @@ -487,8 +471,8 @@ describe('selected evidence hydration', () => { new URL('../../src/application/evidence-hydrator.ts', import.meta.url), 'utf8', ) - expect(implementation.indexOf('const relativePath = relative(root, candidate)')) - .toBeLessThan(implementation.indexOf('bytes = readFileSync(candidate)')) + expect(implementation.indexOf('const rel = relative(root, file)')) + .toBeLessThan(implementation.indexOf('buf = readFileSync(file)')) }) it('rejects fatal UTF-8 while distinguishing it from stale bytes', () => { diff --git a/tests/unit/query-plan.test.ts b/tests/unit/query-plan.test.ts index 18a6e8bd..499b1cd2 100644 --- a/tests/unit/query-plan.test.ts +++ b/tests/unit/query-plan.test.ts @@ -88,6 +88,15 @@ describe('planQuestion', () => { { id: 'o2', kind: 'behavior', target: 'save', mandatory: true }, ], }) + expect(plan('How does process order call persist order?')).toEqual({ + intent: 'explain', + subject: 'process order', + terms: ['order', 'persist', 'process'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'process order', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'persist', mandatory: true }, + ], + }) }) it.each([ @@ -180,8 +189,6 @@ describe('planQuestion', () => { ['Can you explain how GoValidate generate ideas report?', 'idea report'], ['How does ExampleEngine produce release artifacts end to end?', 'release artifact'], ['How does ExampleEngine produce release artifacts?', 'release artifact'], - ['How does password policy login create a tenant session?', 'tenant session'], - ['How is the monthly revenue report built?', 'monthly revenue report'], ])('extracts the object, not the actor, from active workflows: %s', (question, subject) => { expect(plan(question)).toEqual({ intent: 'workflow', @@ -199,6 +206,54 @@ describe('planQuestion', () => { }) }) + it('keeps an explicit flow noun phrase ahead of the generic determiner fallback', () => { + const result = plan('explain how generating the idea report flow is working') + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('idea report') + expect(result.terms).toEqual(['idea', 'report']) + }) + + it('maps coordinated lifecycle clauses to structural workflow bounds', () => { + const result = plan( + 'Which runtime components accept an idea, schedule its analysis, research each section, compose the result, and write the durable read model?', + ) + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('idea report') + expect(result.terms).toEqual([ + 'assemble', 'idea', 'report', 'research', 'schedule', + ]) + expect(result.obligations.find(({ kind }) => kind === 'entry')?.target) + .toBe('request idea') + expect(result.obligations.find(({ kind }) => kind === 'stage')?.target) + .toBe('research assemble') + expect(result.obligations.find(({ kind }) => kind === 'handoff')?.target) + .toBe('schedule') + expect(result.obligations.find(({ kind }) => kind === 'terminal')?.target) + .toBe('persistence') + }) + + it.each([ + [ + 'How does password policy login create a tenant session?', + 'password policy login', ['create', 'login', 'password', 'policy', 'session', 'tenant'], + ], + [ + 'How is the monthly revenue report built?', + 'monthly revenue report', ['build', 'monthly', 'report', 'revenue'], + ], + ])('keeps bounded component behavior as an explanation: %s', ( + question, subject, terms, + ) => { + const result = plan(question) + expect(result.intent).toBe('explain') + expect(result.subject).toBe(subject) + expect(result.terms).toEqual(terms) + expect(result.obligations.map(({ kind }) => kind)) + .toEqual(['subject', 'behavior']) + }) + it('keeps a get-passive workflow subject ahead of boundary clauses', () => { const result = plan( 'How does the idea report get generated from the initial request through to the completed report?', diff --git a/tests/unit/query-workflow.test.ts b/tests/unit/query-workflow.test.ts index 5c0eeace..10664207 100644 --- a/tests/unit/query-workflow.test.ts +++ b/tests/unit/query-workflow.test.ts @@ -953,6 +953,83 @@ describe('deterministic workflow selection', () => { expect(result.symbolIds).toContain('early') }) + it('keeps the explicit wrapper call and drops its redundant channel shortcut', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('dispatch', 'dispatchIdeaReport') + .symbol('enqueue', 'enqueueJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'trigger') + .persistenceInCase('terminal', 'trigger', 'assembly_complete') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publishWithNestedCall( + 'entry', 'dispatch', 'enqueue', 'queue', 'outer-publish', + [triggerPayload('assembly_complete')], 0, + ) + .publish( + 'dispatch', 'enqueue', 'queue', 'inner-publish', + [triggerPayload('assembly_complete')], 0, + ) + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete, JSON.stringify({ + missing: result.missing, links: result.links, symbols: result.symbolIds, + })).toBe(true) + expect(result.links).toEqual(expect.arrayContaining([ + expect.objectContaining({ fromId: 'entry', toId: 'dispatch', kind: 'direct' }), + expect.objectContaining({ fromId: 'dispatch', toId: 'terminal', kind: 'channel' }), + ])) + expect(result.links).not.toContainEqual(expect.objectContaining({ + fromId: 'entry', toId: 'terminal', kind: 'channel', + })) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'outer-publish', 'inner-publish', + ])) + }) + + it('keeps distinct same-channel publications at different execution depths', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('middle', 'processIdeaReport') + .symbol('enqueue', 'enqueueJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'trigger') + .persistenceInCase('terminal', 'trigger', 'assembly_complete') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .call('entry', 'middle') + .publish( + 'entry', 'enqueue', 'queue', 'entry-publish', + [triggerPayload('assembly_complete')], 0, + ) + .publish( + 'middle', 'enqueue', 'queue', 'middle-publish', + [triggerPayload('assembly_complete')], 0, + ) + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete, JSON.stringify({ + missing: result.missing, links: result.links, symbols: result.symbolIds, + })).toBe(true) + expect(result.operationIds).toEqual(expect.arrayContaining([ + 'entry-publish', 'middle-publish', + ])) + expect(result.links).toEqual(expect.arrayContaining([ + expect.objectContaining({ fromId: 'entry', toId: 'middle', kind: 'direct' }), + expect.objectContaining({ fromId: 'entry', toId: 'terminal', kind: 'channel' }), + expect.objectContaining({ fromId: 'middle', toId: 'terminal', kind: 'channel' }), + ])) + }) + it('keeps a persisted channel consumer as a stage when a later final write exists', () => { const fixture = new Fixture() .symbol('entry', 'generateIdeaReport') diff --git a/tests/unit/retrieve-context-proof-eviction.test.ts b/tests/unit/retrieve-context-proof-eviction.test.ts index 2e373fea..e1aef9b9 100644 --- a/tests/unit/retrieve-context-proof-eviction.test.ts +++ b/tests/unit/retrieve-context-proof-eviction.test.ts @@ -1,7 +1,9 @@ +import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' import { describe, expect, it, vi } from 'vitest' import type { - HydratedEvidenceResult, WorkflowSelection, + HydratedEntity, HydratedEvidenceResult, HydratedExcerpt, HydratedFile, + HydratedProof, WorkflowSelection, } from '../../src/domain/query/types.js' const mocks = vi.hoisted(() => ({ @@ -16,7 +18,10 @@ vi.mock('../../src/domain/query/workflow.js', () => ({ selectWorkflow: () => mocks.selection, })) -import { retrieveContext } from '../../src/application/retrieve-context.js' +import { + retrieveContext, + serializeRetrieveContextResult, +} from '../../src/application/retrieve-context.js' const range = { start: { line: 1, column: 1 }, end: { line: 1, column: 20 }, @@ -43,7 +48,11 @@ function hydratedReport(): HydratedEvidenceResult { return { state: 'ready', files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), - excerpts: new Map(), + controls: new Map(), + excerpts: new Map([[ + 'report-declaration', + ['x0', 'f0', range, 'b'.repeat(64), 'export function report() {}'] as const, + ]]), proofs: new Map([['symbol:report', ['p0', 'declaration', 'e0', 'x0'] as const]]), entities: new Map([[ 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, @@ -51,34 +60,158 @@ function hydratedReport(): HydratedEvidenceResult { } } +function oversizedHydration(fileCount: number, excerptCount: number): HydratedEvidenceResult { + const files = new Map() + for (let index = 0; index < fileCount; index += 1) { + files.set( + index === 0 ? 'report.ts' : `extra-${index}.ts`, + [`f${index}`, index.toString(16).padStart(64, '0')], + ) + } + const excerpts = new Map() + for (let index = 0; index < excerptCount; index += 1) { + excerpts.set(`excerpt:${index}`, [ + `x${index}`, 'f0', range, index.toString(16).padStart(64, '0'), + `export const evidence${index} = ${index}`, + ]) + } + return { + state: 'ready', files, controls: new Map(), excerpts, + proofs: new Map([[ + 'symbol:report', ['p0', 'declaration', 'e0', 'x0'] as const, + ]]), + entities: new Map([[ + 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, + ]]), + } +} + +function wrapperSelection(withParallelPublisher: boolean): WorkflowSelection { + const edges = [ + { id: 'call', fromId: 'entry', toId: 'wrapper', relation: 'calls' as const }, + { id: 'wrapper-publish', fromId: 'wrapper', toId: 'queue', relation: 'publishes_to' as const }, + { id: 'consume', fromId: 'queue', toId: 'terminal', relation: 'consumed_by' as const }, + ...(withParallelPublisher ? [{ + id: 'entry-publish', fromId: 'entry', toId: 'queue', + relation: 'publishes_to' as const, + }] : []), + ] + return { + complete: true, + symbolIds: ['entry', 'wrapper', 'terminal', 'queue'], operationIds: [], + rootSymbolIds: ['entry'], terminalSymbolIds: ['terminal'], edges, + links: [ + { + fromId: 'entry', toId: 'wrapper', kind: 'direct', + edgeIds: ['call'], operationIds: [], + }, + { + fromId: 'wrapper', toId: 'terminal', kind: 'channel', + edgeIds: ['wrapper-publish', 'consume'], operationIds: [], + }, + ...(withParallelPublisher ? [{ + fromId: 'entry', toId: 'terminal', kind: 'channel' as const, + edgeIds: ['entry-publish', 'consume'], operationIds: [], + }] : []), + ], + controlGroups: [], + obligations: [{ + id: 'o1', kind: 'handoff', target: 'report handoff', mandatory: true, + proven: true, symbolIds: ['entry', 'wrapper', 'terminal'], operationIds: [], + edgeIds: edges.map(({ id }) => id), + }], + missing: [], metrics: { ...metrics, causalRelationHops: edges.length }, + } +} + +function wrapperHydration(withParallelPublisher: boolean): HydratedEvidenceResult { + const proofs = new Map([ + ['call', ['p0', 'edge', 'e0', 'e1', 'calls', 'x0']], + ['wrapper-publish', [ + 'p1', 'edge_range', 'e1', 'e3', 'publishes_to', 'f0', range, + ]], + ['consume', ['p2', 'edge_range', 'e3', 'e2', 'consumed_by', 'f0', range]], + ]) + if (withParallelPublisher) proofs.set('entry-publish', [ + 'p3', 'edge_range', 'e0', 'e3', 'publishes_to', 'f0', range, + ]) + const entities = new Map([ + ['entry', ['e0', 'symbol', 'generateReport()', 'function', 'f0']], + ['wrapper', ['e1', 'symbol', 'enqueueReport()', 'function', 'f0']], + ['terminal', ['e2', 'symbol', 'persistReport()', 'function', 'f0']], + ['queue', ['e3', 'channel', 'queue', 'bullmq', 'reports', undefined, undefined]], + ]) + return { + state: 'ready', + files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), + controls: new Map(), entities, proofs, + excerpts: new Map([[ + 'call', ['x0', 'f0', range, 'b'.repeat(64), 'return enqueueReport()'] as const, + ]]), + } +} + describe('retrieve dossier eviction failures', () => { - it('returns required_proof_missing when a required declaration proof is evicted', () => { + it.each([ + ['files', 13, 0, 'required_file_limit', 13, 12], + ['excerpts', 1, 26, 'required_excerpt_limit', 26, 25], + ] as const)('fails closed when authenticated %s exceed the table cap', ( + _kind, fileCount, excerptCount, code, required, limit, + ) => { mocks.selection = selection() - mocks.hydration = { - state: 'ready', - files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), - excerpts: new Map(), proofs: new Map(), - entities: new Map([[ - 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, - ]]), - } + mocks.hydration = oversizedHydration(fileCount, excerptCount) - expect(retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext({ state: 'ready' } as never, { question: 'Where is report defined?', budget: 4_000, - })).toMatchObject({ + }) + + expect(result).toMatchObject({ state: 'incomplete', - missing: [{ code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }], - metrics: { required_obligations: 1, proven_obligations: 0 }, + missing: [{ code, required, limit }], }) + expect(result).not.toHaveProperty('dossier') }) - it('returns required_reference_missing when a channel parent is evicted', () => { + it('reports the full ready dossier token count without returning a partial dossier', () => { + mocks.selection = selection() + mocks.hydration = hydratedReport() + const full = retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 4_000, + }) + expect(full.state).toBe('ready') + if (full.state !== 'ready') return + + const constrained = retrieveContext({ state: 'ready' } as never, { + question: 'Where is report defined?', budget: 256, + }) + + expect(constrained).toMatchObject({ + state: 'incomplete', + missing: [{ + code: 'required_token_budget', + required: expect.any(Number), + limit: 256, + }], + }) + expect(constrained).not.toHaveProperty('dossier') + if (constrained.state !== 'incomplete') return + const required = constrained.missing[0]?.required + expect(required).toBeTypeOf('number') + const fullAtLimit = structuredClone(full) + fullAtLimit.metrics.budget_tokens = 256 + fullAtLimit.metrics.serialized_tokens = required! + expect(countTokens(serializeRetrieveContextResult(fullAtLimit))).toBe(required) + expect(required).toBeGreaterThan(256) + }) + + it('returns required_proof_missing when a required declaration proof is evicted', () => { mocks.selection = selection() mocks.hydration = { - state: 'ready', files: new Map(), excerpts: new Map(), proofs: new Map(), + state: 'ready', + files: new Map([['report.ts', ['f0', 'a'.repeat(64)] as const]]), + controls: new Map(), excerpts: new Map(), proofs: new Map(), entities: new Map([[ - 'channel:job', - ['e0', 'channel', 'job', 'bullmq', 'assemble', 'channel:missing', undefined] as const, + 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, ]]), } @@ -86,7 +219,7 @@ describe('retrieve dossier eviction failures', () => { question: 'Where is report defined?', budget: 4_000, })).toMatchObject({ state: 'incomplete', - missing: [{ code: 'required_reference_missing', target: 'channel:missing' }], + missing: [{ code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }], metrics: { required_obligations: 1, proven_obligations: 0 }, }) }) @@ -101,7 +234,7 @@ describe('retrieve dossier eviction failures', () => { })), } mocks.hydration = { - state: 'ready', files: new Map(), excerpts: new Map(), + state: 'ready', files: new Map(), controls: new Map(), excerpts: new Map(), entities: new Map(), proofs: new Map(), } @@ -126,7 +259,7 @@ describe('retrieve dossier eviction failures', () => { })), } mocks.hydration = { - state: 'ready', files: new Map(), excerpts: new Map(), + state: 'ready', files: new Map(), controls: new Map(), excerpts: new Map(), entities: new Map(), proofs: new Map(), } @@ -142,59 +275,42 @@ describe('retrieve dossier eviction failures', () => { expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) }) - it('returns corrupt for a forged control-group controller', () => { - mocks.selection = { - ...selection(), - controlGroups: [{ - kind: 'branch', controllerOperationId: 'operation:forged', - operationIds: [], symbolIds: ['symbol:report'], - }], - } - mocks.hydration = hydratedReport() + it('folds only a sole wrapper channel and preserves its complete proof chain', () => { + mocks.selection = wrapperSelection(false) + mocks.hydration = wrapperHydration(false) - expect(retrieveContext({ state: 'ready' } as never, { - question: 'Where is report defined?', budget: 4_000, - })).toMatchObject({ - state: 'corrupt', - failures: [{ state: 'corrupt', subject: 'operation:forged' }], + const result = retrieveContext({ state: 'ready' } as never, { + question: 'How does the report handoff work?', budget: 4_000, }) - }) - it('returns corrupt for an unselected root or control-group member', () => { - mocks.hydration = hydratedReport() - for (const candidate of [ - { ...selection(), rootSymbolIds: ['symbol:missing'] }, - { - ...selection(), controlGroups: [{ - kind: 'branch' as const, operationIds: [], symbolIds: ['symbol:missing'], - }], - }, - ]) { - mocks.selection = candidate - expect(retrieveContext({ state: 'ready' } as never, { - question: 'Where is report defined?', budget: 4_000, - })).toMatchObject({ - state: 'corrupt', - failures: [{ state: 'corrupt', subject: 'symbol:missing' }], - }) - } + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.flow.links).toEqual([{ + id: 'l1', kind: 'channel', from: 'e0', to: 'e2', + proofs: ['p0', 'p1', 'p2'], + }]) + expect(result.dossier.evidence.entities).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'e1', kind: 'symbol' }), + expect.objectContaining({ id: 'e3', kind: 'channel' }), + ])) }) - it.each([ - ['symbolIds', { symbolIds: ['symbol:missing'] }], - ['operationIds', { operationIds: ['operation:missing'] }], - ['edgeIds', { edgeIds: ['edge:missing'] }], - ] as const)('returns corrupt for missing obligation %s', (_field, reference) => { - mocks.selection = { - ...selection(), obligations: [{ ...obligation, ...reference }], - } - mocks.hydration = hydratedReport() + it('does not fold a wrapper channel beside a direct parallel publisher', () => { + mocks.selection = wrapperSelection(true) + mocks.hydration = wrapperHydration(true) - const target = Object.values(reference)[0]![0]! - expect(retrieveContext({ state: 'ready' } as never, { - question: 'Where is report defined?', budget: 4_000, - })).toMatchObject({ - state: 'corrupt', failures: [{ state: 'corrupt', subject: target }], + const result = retrieveContext({ state: 'ready' } as never, { + question: 'How does the report handoff work?', budget: 4_000, }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.flow.links).toEqual(expect.arrayContaining([ + expect.objectContaining({ kind: 'direct', from: 'e0', to: 'e1' }), + expect.objectContaining({ kind: 'channel', from: 'e1', to: 'e2' }), + expect.objectContaining({ kind: 'channel', from: 'e0', to: 'e2' }), + ])) + expect(result.dossier.flow.links).toHaveLength(3) }) + }) diff --git a/tests/unit/retrieve-context.test.ts b/tests/unit/retrieve-context.test.ts index c9e2b245..18457716 100644 --- a/tests/unit/retrieve-context.test.ts +++ b/tests/unit/retrieve-context.test.ts @@ -144,10 +144,8 @@ describe('retrieveContext v2', () => { const declarations = first.dossier.evidence.entities.filter((entity) => entity.kind === 'symbol' && entity.excerpt !== undefined) expect(declarations).toEqual([]) - const links = new Map(first.dossier.flow.links.map((link) => [link.id, link])) const incident = new Set(first.dossier.evidence.entities.flatMap((entity) => - entity.kind !== 'operation' ? [] : 'owner' in entity ? [entity.owner] - : entity.links.map((id) => links.get(id)!.from)).concat( + entity.kind !== 'operation' ? [] : [entity.owner]).concat( first.dossier.evidence.proofs.flatMap((proof) => [proof.from, proof.to]), )) expect(first.dossier.evidence.entities.filter((entity) => @@ -187,34 +185,84 @@ describe('retrieveContext v2', () => { expect(third.dossier.flow).toEqual(first.dossier.flow) expect(third.dossier.evidence).toEqual(first.dossier.evidence) - const links = new Map(first.dossier.flow.links.map((link) => [link.id, link])) const proofRows = new Map(first.dossier.evidence.proofs.map((proof) => [proof.id, proof])) + const excerptRows = new Map(first.dossier.evidence.excerpts.map((excerpt) => [ + excerpt.id, excerpt.text, + ])) for (const link of first.dossier.flow.links) { const path = link.proofs.map((id) => proofRows.get(id)!) expect(path[0]?.from).toBe(link.from) expect(path.at(-1)?.to).toBe(link.to) path.slice(1).forEach((proof, index) => expect(path[index]!.to).toBe(proof.from)) - expect(path.map(({ relation }) => relation)).toEqual(link.kind === 'direct' - ? ['calls'] : path.length === 2 - ? ['publishes_to', 'consumed_by'] - : ['publishes_to', 'routes_through', 'consumed_by']) + const relations = path.map(({ relation }) => relation) + if (link.kind === 'direct') { + expect(relations).toEqual(['calls']) + } else { + const publishAt = relations.indexOf('publishes_to') + expect(publishAt).toBeGreaterThanOrEqual(0) + expect(relations.slice(0, publishAt).every((relation) => + relation === 'calls')).toBe(true) + expect(relations.slice(publishAt)).toEqual( + relations.length - publishAt === 2 + ? ['publishes_to', 'consumed_by'] + : ['publishes_to', 'routes_through', 'consumed_by'], + ) + } + } + expect(first.dossier.flow.links.some((link) => { + if (link.kind !== 'channel') return false + const relations = link.proofs.map((id) => proofRows.get(id)?.relation) + return relations[0] === 'calls' && relations.includes('publishes_to') + && relations.at(-1) === 'consumed_by' + })).toBe(true) + const linkBundles = new Map(first.dossier.flow.links.map((link) => [ + link.id, link.proofs, + ])) + const orderBundles = new Map(first.dossier.flow.order.map((group) => [ + group.id, [ + ...(group.controller ? [group.controller] : []), + ...group.members, ...(group.proofs ?? []), + ], + ])) + const evidenceIds = new Set([ + ...first.dossier.evidence.entities.map(({ id }) => id), + ...first.dossier.evidence.proofs.map(({ id }) => id), + ]) + for (const claim of first.dossier.obligations) { + for (const proof of claim.proofs) { + expect(evidenceIds.has(proof) || linkBundles.has(proof) + || orderBundles.has(proof)).toBe(true) + } } const behavior = first.dossier.obligations.find(({ kind }) => kind === 'behavior')! - const behaviorProofs = new Set(behavior.proofs) + const behaviorProofs = new Set(behavior.proofs.flatMap((proof) => + linkBundles.get(proof) ?? orderBundles.get(proof) ?? [proof])) const stages = new Set(first.dossier.flow.links.flatMap(({ from, to }) => [from, to])) for (const stage of stages) { const outgoing = first.dossier.evidence.proofs.some((proof) => proof.from === stage && behaviorProofs.has(proof.id)) const operation = first.dossier.evidence.entities.some((entity) => entity.kind === 'operation' && behaviorProofs.has(entity.id) - && ('owner' in entity ? entity.owner === stage - : entity.links.some((id) => links.get(id)?.from === stage))) + && entity.owner === stage) expect(outgoing || operation).toBe(true) } - expect(first.dossier.evidence.entities).toContainEqual(expect.objectContaining({ - kind: 'operation', links: expect.any(Array), callee: 'enqueueJob', - scheduling: 'awaited', excerpt: expect.any(String), - })) + const awaitedEnqueueProof = first.dossier.evidence.proofs.find((proof) => + proof.relation === 'publishes_to') + expect(awaitedEnqueueProof).toBeDefined() + if (awaitedEnqueueProof && 'excerpt' in awaitedEnqueueProof) { + expect(excerptRows.get(awaitedEnqueueProof.excerpt)) + .toMatch(/\bawait\s+enqueueJob\s*\(/) + } else if (awaitedEnqueueProof) { + expect(first.dossier.evidence.files.some(({ id }) => + id === awaitedEnqueueProof.file)).toBe(true) + expect(awaitedEnqueueProof.range).toEqual([ + expect.any(Number), expect.any(Number), expect.any(Number), expect.any(Number), + ]) + } + expect(first.dossier.flow.links.some((link) => + link.kind === 'channel' + && awaitedEnqueueProof !== undefined + && link.proofs.includes(awaitedEnqueueProof.id))).toBe(true) for (let pass = 0; pass < 3; pass += 1) retrieveContext(index, active) const samples = Array.from({ length: 20 }, () => { @@ -347,19 +395,23 @@ describe('retrieveContext v2', () => { ])) expect(symbols.find(({ label }) => label === 'saveOrder()')) .not.toHaveProperty('excerpt') - expect(result.dossier.evidence.proofs).toEqual(expect.arrayContaining([ - expect.objectContaining({ relation: 'calls' }), - ])) - const call = result.dossier.evidence.entities.find((entity) => - entity.kind === 'operation' && 'links' in entity) - expect(call).toEqual(expect.objectContaining({ - kind: 'operation', order: expect.any(Array), - links: expect.any(Array), excerpt: expect.any(String), + const submit = symbols.find(({ label }) => label === 'submitOrder()') + const save = symbols.find(({ label }) => label === 'saveOrder()') + const callLink = result.dossier.flow.links.find((link) => + link.kind === 'direct' && link.from === submit?.id && link.to === save?.id) + expect(callLink).toEqual(expect.objectContaining({ + kind: 'direct', proofs: [expect.any(String)], + })) + const callProof = result.dossier.evidence.proofs.find((proof) => + callLink?.proofs.includes(proof.id)) + expect(callProof).toEqual(expect.objectContaining({ + from: submit?.id, to: save?.id, relation: 'calls', excerpt: expect.any(String), })) - expect(call).not.toHaveProperty('scheduling') - expect(call).not.toHaveProperty('callee') - expect(call).not.toHaveProperty('operation_kind') - expect(call).not.toHaveProperty('arguments') + const callExcerpt = callProof && 'excerpt' in callProof ? callProof.excerpt : undefined + expect(result.dossier.evidence.excerpts.find(({ id }) => id === callExcerpt)?.text) + .toContain('saveOrder(id)') + expect(result.dossier.evidence.entities.filter((entity) => + entity.kind === 'operation')).toEqual([]) }) it('keeps linked-call arguments only in the exact authenticated excerpt', () => { @@ -377,12 +429,18 @@ describe('retrieveContext v2', () => { expect(result.state).toBe('ready') if (result.state !== 'ready') return - const call = result.dossier.evidence.entities.find((entity) => - entity.kind === 'operation' && 'links' in entity) - expect(call).toBeDefined() - if (!call || call.kind !== 'operation' || !('links' in call)) return - expect(call).not.toHaveProperty('arguments') - expect(result.dossier.evidence.excerpts.find(({ id }) => id === call?.excerpt)?.text) + const callLink = result.dossier.flow.links.find(({ kind }) => kind === 'direct') + expect(callLink).toBeDefined() + const callProof = result.dossier.evidence.proofs.find((proof) => + callLink?.proofs.includes(proof.id)) + expect(callProof).toEqual(expect.objectContaining({ + relation: 'calls', excerpt: expect.any(String), + })) + expect(callProof).not.toHaveProperty('arguments') + expect(result.dossier.evidence.entities.some((entity) => + entity.kind === 'operation' && 'arguments' in entity.detail)).toBe(false) + const callExcerpt = callProof && 'excerpt' in callProof ? callProof.excerpt : undefined + expect(result.dossier.evidence.excerpts.find(({ id }) => id === callExcerpt)?.text) .toContain("saveOrder('order-1')") }) @@ -406,14 +464,136 @@ async function persistIdeaReport(repository: MongoRepository, id: string) { if (result.state !== 'ready') return const sequence = result.dossier.flow.order.find(({ kind }) => kind === 'sequence') expect(sequence).toBeDefined() - expect(sequence?.members).toHaveLength(2) - expect(sequence?.proofs).toHaveLength(2) - sequence?.members.forEach((member, index) => { - expect(result.dossier.evidence.entities).toContainEqual(expect.objectContaining({ - id: member, kind: 'operation', excerpt: expect.any(String), + if (!sequence) return + expect(sequence.members).toHaveLength(2) + expect(sequence).not.toHaveProperty('proofs') + const proofRows = new Map(result.dossier.evidence.proofs.map((proof) => [proof.id, proof])) + const excerptRows = new Map(result.dossier.evidence.excerpts.map((excerpt) => [ + excerpt.id, excerpt.text, + ])) + const orderedExcerpts = sequence.members.map((member) => { + const proof = proofRows.get(member) + expect(proof).toEqual(expect.objectContaining({ + relation: 'calls', excerpt: expect.any(String), + })) + return proof && 'excerpt' in proof ? excerptRows.get(proof.excerpt) : undefined + }) + expect(orderedExcerpts[0]).toContain("persistIdeaReport(repository, 'first')") + expect(orderedExcerpts[1]).toContain("persistIdeaReport(repository, 'second')") + }) + + it('keeps same-shaped controls separately controller-bound and authenticated', () => { + const index = workspace(`import type { MongoRepository } from 'typeorm' +type Row = { id: string } +export async function generateIdeaReport( + repository: MongoRepository, firstEnabled: boolean, secondEnabled: boolean, +) { + if (firstEnabled) await firstIdeaReportStage(repository) + if (secondEnabled) await secondIdeaReportStage(repository) + return 'queued' +} +async function firstIdeaReportStage(repository: MongoRepository) { + return persistIdeaReport(repository, 'first') +} +async function secondIdeaReportStage(repository: MongoRepository) { + return persistIdeaReport(repository, 'second') +} +async function persistIdeaReport(repository: MongoRepository, id: string) { + await repository.update(id, { id }) + return id +} +`).index + const result = retrieveContext(index, { + question: 'How is an idea report generated end to end?', budget: 4_000, + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const groups = result.dossier.flow.order.filter(({ kind, arm }) => + kind === 'branch' && arm === 'then') + expect(groups).toHaveLength(2) + + const entities = new Map(result.dossier.evidence.entities.map((entity) => [ + entity.id, entity, + ])) + const controls = new Map(result.dossier.evidence.controls.map((control) => [ + control.id, control, + ])) + const controllers = groups.map((group) => group.controller) + expect(new Set(controllers).size).toBe(2) + const controllerRanges: string[] = [] + for (const group of groups) { + expect(group.controller).toMatch(/^c\d+:(?:\d+(?:-\d+)?|\d+(?:\.\d+)+)$/u) + expect(group).not.toHaveProperty('proofs') + const [catalogId, selector] = group.controller!.split(':') + const controller = controls.get(catalogId!) + expect(controller).toEqual(expect.objectContaining({ + file: expect.any(String), ranges: expect.arrayContaining([expect.any(Array)]), })) - expect(sequence.proofs[index]).toBe(member) + expect(entities.has(group.controller!)).toBe(false) + expect(entities.has(catalogId!)).toBe(false) + if (!controller) continue + const indexes = selector!.includes('-') + ? (() => { + const [start, end] = selector!.split('-').map(Number) + expect(end).toBeGreaterThanOrEqual(start!) + return Array.from({ length: end! - start! + 1 }, (_, offset) => start! + offset) + })() + : selector!.split('.').map(Number) + expect(new Set(indexes).size).toBe(indexes.length) + for (const index of indexes) { + expect(index).toBeGreaterThanOrEqual(0) + expect(index).toBeLessThan(controller.ranges.length) + const [startLine, startColumn, endLine, endColumn] = controller.ranges[index]! + expect(startLine).toBeGreaterThan(0) + expect(startColumn).toBeGreaterThan(0) + expect(endLine).toBeGreaterThanOrEqual(startLine) + expect(endColumn).toBeGreaterThan(0) + controllerRanges.push(`${controller.file}:${controller.ranges[index]!.join(':')}`) + } + } + expect(result.dossier.evidence.controls).toHaveLength(1) + expect(controllerRanges).toHaveLength(2) + expect(new Set(controllerRanges).size).toBe(controllerRanges.length) + }) + + it.each([ + ['string', "'complete'", 'case:string:"complete"'], + ['number', '7', 'case:number:7'], + ['boolean', 'true', 'case:boolean:true'], + ['string | null', 'null', 'case:null:null'], + ])('renders an authenticated %s switch arm as a typed JSON scalar', ( + triggerType, caseValue, expectedArm, + ) => { + const index = workspace(`import type { MongoRepository } from 'typeorm' +type Row = { id: string } +export async function generateIdeaReport( + repository: MongoRepository, trigger: ${triggerType}, +) { + switch (trigger) { + case ${caseValue}: return persistIdeaReport(repository) + default: return 'queued' + } +} +async function persistIdeaReport(repository: MongoRepository) { + await repository.update('one', { id: 'one' }) + return 'complete' +} +`).index + const result = retrieveContext(index, { + question: 'How is an idea report generated end to end?', budget: 4_000, }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const selected = result.dossier.flow.order.find((group) => + group.kind === 'branch' && group.arm?.startsWith('case:')) + expect(selected).toEqual(expect.objectContaining({ + arm: expectedArm, + controller: expect.stringMatching(/^c\d+:\d+(?:-\d+)?$/u), + members: [expect.any(String)], + })) + expect(selected).not.toHaveProperty('proofs') }) it('keeps an exact imported-caller locator ahead of a called suffix match', () => { diff --git a/tests/unit/retrieve-evidence-skeleton-adversarial.test.ts b/tests/unit/retrieve-evidence-skeleton-adversarial.test.ts deleted file mode 100644 index d1b06dc0..00000000 --- a/tests/unit/retrieve-evidence-skeleton-adversarial.test.ts +++ /dev/null @@ -1,1268 +0,0 @@ -import { - mkdirSync, - mkdtempSync, - rmSync, - writeFileSync, -} from 'node:fs' -import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' - -import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' -import { afterEach, describe, expect, it } from 'vitest' - -import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' -import { generateIndex } from '../../src/application/generate-index.js' -import { retrieveContext } from '../../src/application/retrieve-context.js' -import { canonicalJsonString } from '../../src/domain/graph/canonical-json.js' -import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' -import { - inspectQueryIndex, - type ReadyQueryIndex, -} from '../../src/domain/query/index-status.js' -import { rankQueryAnchors } from '../../src/domain/query/rank.js' -import { sliceEvidence } from '../../src/domain/query/slice.js' -import { traverseEvidencePaths } from '../../src/domain/query/traverse.js' -import type { - EvidenceNode, EvidenceRelationship, RankQueryResult, - RetrieveContextResult, -} from '../../src/domain/query/types.js' - -const roots: string[] = [] - -function readyFixture( - name: string, - sources: Readonly>, -): ReadyQueryIndex { - const root = mkdtempSync(join(tmpdir(), `madar-625-${name}-`)) - roots.push(root) - for (const [path, source] of Object.entries(sources)) { - const absolute = join(root, path) - mkdirSync(dirname(absolute), { recursive: true }) - writeFileSync(absolute, source.endsWith('\n') ? source : `${source}\n`, 'utf8') - } - writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({ - compilerOptions: { - module: 'NodeNext', - moduleResolution: 'NodeNext', - strict: true, - }, - }), 'utf8') - const generated = generateIndex(root) - const inspected = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) - if (inspected.state !== 'ready') { - throw new Error(`Expected ready ${name} fixture, received ${inspected.state}`) - } - return inspected -} - -function symbolId(result: RetrieveContextResult, symbol: string): string { - const node = result.matched_nodes.find((candidate) => - candidate.label.includes(symbol)) - expect(node, `missing ${symbol}`).toBeDefined() - return node!.node_id -} - -function relationshipIdentities( - relationships: readonly EvidenceRelationship[], -): Set { - return new Set(relationships.map((edge) => - `${edge.from_id}\0${edge.relation}\0${edge.to_id}`)) -} - -function expectCall( - result: RetrieveContextResult, - fromSymbol: string, - toSymbol: string, -): void { - const fromId = symbolId(result, fromSymbol) - const toId = symbolId(result, toSymbol) - expect(relationshipIdentities(result.relationships)) - .toContain(`${fromId}\0calls\0${toId}`) -} - -function expectWithinProtocol(result: RetrieveContextResult): void { - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.snippets).toBeLessThanOrEqual(25) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) - expect(result.metrics.closure_passes).toBeLessThanOrEqual(1) -} - -function syntheticNode(label: string, file: string): Record { - return { - label: `${label}()`, - qualified_name: label, - node_kind: 'function', - source_file: file, - source_location: 'L1', - provenance: [{}], - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 24 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 23 }, - }, - } -} - -function syntheticIndex(graph: KnowledgeGraph): ReadyQueryIndex { - return { - state: 'ready', - graph, - root_path: '/workspace', - file_hashes: new Map(), - unsupported_sources: [], - operation_by_id: new Map(), - operations_by_owner: new Map(), - channels_by_id: new Map(), - channels_by_key: new Map(), - } -} - -function connectorIndex(): ReadyQueryIndex { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - const queueFile = 'src/runtime/queue.ts' - graph.addNode('entry', syntheticNode('openWorkflow', 'src/runtime/entry.ts')) - graph.addNode('hub', syntheticNode('enqueueTask', queueFile)) - graph.addNode('registry', syntheticNode('registerWorker', queueFile)) - graph.addEdge('entry', 'hub', { - relation: 'calls', source_file: 'src/runtime/entry.ts', - source_location: 'L1', provenance: [{}], - }) - for (const name of ['Alpha', 'Beta', 'Gamma']) { - const lower = name.toLowerCase() - const file = `src/runtime/${lower}-worker.ts` - const owner = `worker-${lower}` - const registrar = `register-${lower}` - const consumer = `process-${lower}` - const service = `service-${lower}` - graph.addNode(owner, { - ...syntheticNode(`${name}Worker`, file), - label: `${name}Worker`, - node_kind: 'class', - }) - graph.addNode(registrar, syntheticNode('register', file)) - graph.addNode(consumer, syntheticNode('process', file)) - graph.addNode(service, syntheticNode( - `Service${name}.run${name}`, - `src/runtime/${lower}-service.ts`, - )) - graph.addEdge(owner, registrar, { - relation: 'contains', source_file: file, - source_location: 'L1', provenance: [{}], - }) - graph.addEdge(owner, consumer, { - relation: 'contains', source_file: file, - source_location: 'L1', provenance: [{}], - }) - graph.addEdge(registrar, 'registry', { - relation: 'calls', source_file: file, - source_location: 'L1', provenance: [{}], - }) - graph.addEdge(registrar, consumer, { - relation: 'calls', source_file: file, - source_location: 'L2', provenance: [{}], - }) - graph.addEdge(consumer, service, { - relation: 'calls', source_file: file, - source_location: 'L3', provenance: [{}], - }) - if (name === 'Alpha') { - graph.addEdge('hub', consumer, { - relation: 'calls', source_file: queueFile, - source_location: 'L1', provenance: [{}], - }) - } - } - return syntheticIndex(graph) -} - -afterEach(() => { - for (const root of roots.splice(0)) { - rmSync(root, { recursive: true, force: true }) - } -}) - -describe('issue #625 topology-independent adversarial retrieval', () => { - it('keeps lowercase prose periods distinct from sentence and then boundaries', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - graph.addNode('alpha', syntheticNode('alpha', 'src/alpha.ts')) - graph.addNode('beta', syntheticNode('beta', 'src/beta.ts')) - const index = syntheticIndex(graph) - - const lowercase = rankQueryAnchors(index, { - question: 'Explain alpha. beta behavior', - budget: 4_000, - }) - const sentence = rankQueryAnchors(index, { - question: 'Explain alpha. Beta behavior', - budget: 4_000, - }) - const then = rankQueryAnchors(index, { - question: 'Explain alpha And Then beta behavior', - budget: 4_000, - }) - - expect(lowercase.structuralRequired).toBe(false) - expect(sentence.structuralRequired).toBe(true) - expect(then.structuralRequired).toBe(true) - expect(then.sequential).toBe(true) - }) - - it('treats an explicit scope already in the connector entry as satisfied', () => { - const ranked = rankQueryAnchors(connectorIndex(), { - question: 'Trace openWorkflow through `enqueueTask`.', - budget: 4_000, - }) - - expect(ranked.anchors.map(({ id }) => id)).toContain('hub') - expect(ranked.structuralCoverageComplete).toBe(true) - }) - - it('does not silently drop a second explicit connector scope', () => { - const index = connectorIndex() - const ranked = rankQueryAnchors(index, { - question: - 'Trace enqueueTask through `ServiceAlpha.runAlpha` and `ServiceBeta.runBeta`.', - budget: 4_000, - }) - const traversed = traverseEvidencePaths(index, ranked) - - expect(ranked.anchors.map(({ id }) => id)).toEqual(expect.arrayContaining([ - 'service-alpha', - 'service-beta', - ])) - expect(traversed.edges.map(({ from, to }) => `${from}->${to}`)) - .toEqual(expect.arrayContaining([ - 'process-alpha->service-alpha', - 'process-beta->service-beta', - ])) - expect(ranked.structuralCoverageComplete).toBe(true) - }) - - it('preserves every directed edge in a pure three-node causal cycle', () => { - const index = readyFixture('pure-cycle', { - 'src/cycle/alpha.ts': [ - "import { stepBeta } from './beta.js'", - '', - 'export function stepAlpha(value: string): string {', - ' return stepBeta(value)', - '}', - ].join('\n'), - 'src/cycle/beta.ts': [ - "import { stepGamma } from './gamma.js'", - '', - 'export function stepBeta(value: string): string {', - ' return stepGamma(value)', - '}', - ].join('\n'), - 'src/cycle/gamma.ts': [ - "import { stepAlpha } from './alpha.js'", - '', - 'export function stepGamma(value: string): string {', - ' return stepAlpha(value)', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace stepAlpha through stepBeta and stepGamma back to stepAlpha.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'stepAlpha', 'stepBeta') - expectCall(result, 'stepBeta', 'stepGamma') - expectCall(result, 'stepGamma', 'stepAlpha') - expectWithinProtocol(result) - }) - - it('retrieves a synchronous direct causal chain without a registry or worker lifecycle', () => { - const index = readyFixture('direct-chain', { - 'src/direct/accept.ts': [ - "import { normalizeRecord } from './normalize.js'", - '', - 'export function acceptEnvelope(value: string): string {', - ' return normalizeRecord(value)', - '}', - ].join('\n'), - 'src/direct/normalize.ts': [ - "import { persistRecord } from './persist.js'", - '', - 'export function normalizeRecord(value: string): string {', - ' return persistRecord(value.trim())', - '}', - ].join('\n'), - 'src/direct/persist.ts': [ - 'export function persistRecord(value: string): string {', - " return `${value}:stored`", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace the synchronous flow from acceptEnvelope through normalizeRecord to persistRecord.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'acceptEnvelope', 'normalizeRecord') - expectCall(result, 'normalizeRecord', 'persistRecord') - expectWithinProtocol(result) - }) - - it('retains two requested disconnected chains and the exact inter-chain boundary', () => { - const index = readyFixture('disconnected-chains', { - 'src/alpha/ingress.ts': [ - "import { alphaArchive } from './archive.js'", - '', - 'export function alphaIngress(value: string): string {', - ' return alphaArchive(value)', - '}', - ].join('\n'), - 'src/alpha/archive.ts': [ - 'export function alphaArchive(value: string): string {', - " return `archived:${value}`", - '}', - ].join('\n'), - 'src/omega/ingress.ts': [ - "import { omegaPublish } from './publish.js'", - '', - 'export function omegaIngress(value: string): string {', - ' return omegaPublish(value)', - '}', - ].join('\n'), - 'src/omega/publish.ts': [ - 'export function omegaPublish(value: string): string {', - " return `published:${value}`", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace alphaIngress through alphaArchive, then omegaIngress through omegaPublish.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'alphaIngress', 'alphaArchive') - expectCall(result, 'omegaIngress', 'omegaPublish') - const archiveId = symbolId(result, 'alphaArchive') - const omegaIngressId = symbolId(result, 'omegaIngress') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'disconnected', - subject: `${archiveId} -> ${omegaIngressId}`, - })) - expectWithinProtocol(result) - }) - - it('does not claim evidence when a requested disconnected-chain concept is absent', () => { - const index = readyFixture('incomplete-disconnected-chains', { - 'src/alpha/ingress.ts': [ - "import { alphaArchive } from './archive.js'", - '', - 'export function alphaIngress(value: string): string {', - ' return alphaArchive(value)', - '}', - ].join('\n'), - 'src/alpha/archive.ts': [ - 'export function alphaArchive(value: string): string {', - " return `archived:${value}`", - '}', - ].join('\n'), - 'src/omega/ingress.ts': [ - "import { omegaPublish } from './publish.js'", - '', - 'export function omegaIngress(value: string): string {', - ' return omegaPublish(value)', - '}', - ].join('\n'), - 'src/omega/publish.ts': [ - 'export function omegaPublish(value: string): string {', - " return `published:${value}`", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace alphaIngress through missingArchive, then omegaIngress through omegaPublish.', - budget: 4_000, - }) - - expect(result.outcome).not.toBe('evidence') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'missing', - })) - expectWithinProtocol(result) - }) - - it('keeps an exact-symbol locator focused instead of expanding a full skeleton', () => { - const index = readyFixture('focused-locator', { - 'src/locator/target.ts': [ - 'export function targetLocator(value: string): string {', - " return `target:${value}`", - '}', - ].join('\n'), - 'src/unrelated/open.ts': [ - "import { continueUnrelated } from './continue.js'", - 'export function openUnrelated(value: string): string {', - ' return continueUnrelated(value)', - '}', - ].join('\n'), - 'src/unrelated/continue.ts': [ - "import { finishUnrelated } from './finish.js'", - 'export function continueUnrelated(value: string): string {', - ' return finishUnrelated(value)', - '}', - ].join('\n'), - 'src/unrelated/finish.ts': [ - 'export function finishUnrelated(value: string): string {', - ' return value', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: 'Where is targetLocator defined?', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expect(symbolId(result, 'targetLocator')).toBeTruthy() - expect(result.matched_nodes.some((node) => - node.label.includes('Unrelated'))).toBe(false) - expect(result.matched_nodes.length).toBeLessThanOrEqual(2) - expectWithinProtocol(result) - }) - - it('accepts a multi-file shallow dispatch fan-out and keeps every causal edge', () => { - const index = readyFixture('dispatch-fanout', { - 'src/orders/dispatch.ts': [ - "import { sendEmail } from '../sinks/email.js'", - "import { writeAudit } from '../sinks/audit.js'", - "import { updateMetric } from '../sinks/metric.js'", - '', - 'export function dispatchOrder(order: string): string[] {', - ' return [sendEmail(order), writeAudit(order), updateMetric(order)]', - '}', - ].join('\n'), - 'src/sinks/email.ts': [ - 'export function sendEmail(order: string): string {', - " return `email:${order}`", - '}', - ].join('\n'), - 'src/sinks/audit.ts': [ - 'export function writeAudit(order: string): string {', - " return `audit:${order}`", - '}', - ].join('\n'), - 'src/sinks/metric.ts': [ - 'export function updateMetric(order: string): string {', - " return `metric:${order}`", - '}', - ].join('\n'), - }) - - for (const question of [ - 'Trace dispatchOrder to sendEmail, writeAudit, and updateMetric.', - 'Trace from dispatchOrder to sendEmail and writeAudit.', - ]) { - const result = retrieveContext(index, { question, budget: 4_000 }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'dispatchOrder', 'sendEmail') - expectCall(result, 'dispatchOrder', 'writeAudit') - if (question.includes('updateMetric')) { - expectCall(result, 'dispatchOrder', 'updateMetric') - } - expect(result.boundaries).toEqual([]) - expectWithinProtocol(result) - } - }) - - it('reports a sequential request between sibling sinks as disconnected', () => { - const index = readyFixture('sequential-siblings', { - 'src/orders/dispatch.ts': [ - "import { sendEmail } from '../sinks/email.js'", - "import { writeAudit } from '../sinks/audit.js'", - 'export function dispatchOrder(order: string): string[] {', - ' return [sendEmail(order), writeAudit(order)]', - '}', - ].join('\n'), - 'src/sinks/email.ts': [ - 'export function sendEmail(order: string): string {', - " return `email:${order}`", - '}', - ].join('\n'), - 'src/sinks/audit.ts': [ - 'export function writeAudit(order: string): string {', - " return `audit:${order}`", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: 'Trace sendEmail through writeAudit.', - budget: 4_000, - }) - - const emailId = symbolId(result, 'sendEmail') - const auditId = symbolId(result, 'writeAudit') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'disconnected', - subject: `${emailId} -> ${auditId}`, - })) - expectWithinProtocol(result) - }) - - it('uses only real locator attributes in disconnected verification details', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - graph.addNode('left', { label: 'left()', node_kind: 'function' }) - graph.addNode('right', { - label: 'right()', - node_kind: 'function', - source_file: 'src/right.ts', - }) - const ranked: RankQueryResult = { - anchors: ['left', 'right'].map((id, firstMatch) => ({ - id, - attributes: graph.nodeAttributes(id), - score: 1, - matchedTerms: [], - firstMatch, - })), - boundaries: [], - queryTerms: ['left', 'right'], - flow: true, - branch: [], - priorityAnchorIds: ['left', 'right'], - structuralRequired: true, - structuralCoverageComplete: true, - } - - const traversed = traverseEvidencePaths(syntheticIndex(graph), ranked) - - expect(traversed.boundaries).toContainEqual({ - kind: 'disconnected', - subject: 'left -> right', - detail: 'left -> src/right.ts', - }) - expect(traversed.boundaries[0]?.detail).not.toContain('undefined') - }) - - it('does not use a test-only common parent as runtime flow evidence', () => { - const index = readyFixture('test-common-parent', { - 'src/orders/email.ts': [ - "import { finishOrder } from './finish.js'", - 'export function sendEmail(order: string): string {', - ' return finishOrder(`email:${order}`)', - '}', - ].join('\n'), - 'src/orders/audit.ts': [ - "import { finishOrder } from './finish.js'", - 'export function writeAudit(order: string): string {', - ' return finishOrder(`audit:${order}`)', - '}', - ].join('\n'), - 'src/orders/finish.ts': [ - 'export function finishOrder(order: string): string { return order }', - ].join('\n'), - 'tests/orders/dispatch.test.ts': [ - "import { sendEmail } from '../../src/orders/email.js'", - "import { writeAudit } from '../../src/orders/audit.js'", - 'export function dispatchTestOrder(order: string): string[] {', - ' return [sendEmail(order), writeAudit(order)]', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Explain the flow from sendEmail and writeAudit to finishOrder.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'sendEmail', 'finishOrder') - expectCall(result, 'writeAudit', 'finishOrder') - expect(result.matched_nodes.some(({ source_file }) => - source_file.startsWith('tests/'))).toBe(false) - expectWithinProtocol(result) - }) - - it('preserves a causal flow when the query explicitly requests tests', () => { - const index = readyFixture('explicit-test-flow', { - 'tests/auth-route.test.ts': [ - "import { testAuthService } from '../src/auth-service.js'", - 'export function testAuthRoute(): string {', - ' return testAuthService()', - '}', - ].join('\n'), - 'src/auth-service.ts': [ - "import { assertAuthRecord } from '../tests/auth-assertion.js'", - 'export function testAuthService(): string {', - ' return assertAuthRecord()', - '}', - ].join('\n'), - 'tests/auth-assertion.ts': [ - 'export function assertAuthRecord(): string {', - " return 'verified'", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Explain the test flow from testAuthRoute through assertAuthRecord.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'testAuthRoute', 'testAuthService') - expectCall(result, 'testAuthService', 'assertAuthRecord') - expect(result.matched_nodes.map(({ source_file }) => source_file)) - .toContain('src/auth-service.ts') - expectWithinProtocol(result) - }) - - it('does not authenticate a production path through a test-only bridge', () => { - const index = readyFixture('test-only-bridge', { - 'src/runtime-start.ts': [ - "import { testBridge } from '../tests/runtime-bridge.test.js'", - 'export function runtimeStart(): string {', - ' return testBridge()', - '}', - ].join('\n'), - 'tests/runtime-bridge.test.ts': [ - "import { runtimeFinish } from '../src/runtime-finish.js'", - 'export function testBridge(): string {', - ' return runtimeFinish()', - '}', - ].join('\n'), - 'src/runtime-finish.ts': [ - 'export function runtimeFinish(): string {', - " return 'complete'", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: 'Trace runtimeStart through runtimeFinish.', - budget: 4_000, - }) - - expect(result.outcome).toBe('missing') - expect(result.relationships).toEqual([]) - expect(result.boundaries.length).toBeGreaterThan(0) - expect(result.matched_nodes.some(({ source_file }) => - source_file.startsWith('tests/'))).toBe(false) - expectWithinProtocol(result) - }) - - it('does not borrow an unrelated registry topology for disconnected flow siblings', () => { - const index = readyFixture('false-connector', { - 'src/flow/shared-flow.ts': [ - 'export function beginFlow(value: string): string {', - " return `begin:${value}`", - '}', - '', - 'export function finishFlow(value: string): string {', - " return `finish:${value}`", - '}', - ].join('\n'), - 'src/registry/queue.ts': [ - 'export class Queue {', - ' addJob(value: string): string { return this.noise(value) }', - ' noise(value: string): string { return value }', - ' registerWorker(task: () => string): string { return task() }', - '}', - 'export const queue = new Queue()', - 'export function bootstrapRegistry(): string { return queue.addJob("boot") }', - ].join('\n'), - 'src/registry/worker-alpha.ts': [ - "import { queue } from './queue.js'", - "import { AlphaService } from './service-alpha.js'", - 'export class AlphaWorker {', - ' private readonly service = new AlphaService()', - ' register(): string {', - ' queue.registerWorker(() => this.process())', - ' return this.process()', - ' }', - ' process(): string { return this.service.execute() }', - '}', - ].join('\n'), - 'src/registry/worker-omega.ts': [ - "import { queue } from './queue.js'", - "import { OmegaService } from './service-omega.js'", - 'export class OmegaWorker {', - ' private readonly service = new OmegaService()', - ' register(): string {', - ' queue.registerWorker(() => this.process())', - ' return this.process()', - ' }', - ' process(): string { return this.service.execute() }', - '}', - ].join('\n'), - 'src/registry/service-alpha.ts': [ - "import { queue } from './queue.js'", - 'export class AlphaService {', - ' execute(): string { return queue.addJob("alpha") }', - '}', - ].join('\n'), - 'src/registry/service-omega.ts': [ - 'export class OmegaService {', - ' execute(): string { return "omega" }', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: 'Trace beginFlow through finishFlow.', - budget: 4_000, - }) - - expect(result.outcome).not.toBe('evidence') - expect(result.matched_nodes.some((node) => - node.source_file.startsWith('src/registry/'))).toBe(false) - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'missing', - })) - expectWithinProtocol(result) - }) - - it('uses an authenticated enqueues_job edge for an independent queued topology', () => { - const index = readyFixture('queued-handoff', { - 'src/queue/publisher.ts': [ - 'class Queue {', - ' async add(name: string, value: string): Promise {', - ' return `${name}:${value}`', - ' }', - '}', - '', - 'const outboundQueue = new Queue()', - '', - 'export async function publishEnvelope(value: string): Promise {', - " return outboundQueue.add('delivery.consume', value)", - '}', - ].join('\n'), - 'src/queue/worker.ts': [ - 'function Processor(_queue: string): ClassDecorator {', - ' return () => undefined', - '}', - 'function Process(_job: string): MethodDecorator {', - ' return () => undefined', - '}', - '', - "@Processor('delivery')", - 'export class DeliveryWorker {', - " @Process('consume')", - ' async consumeEnvelope(value: string): Promise {', - ' return value', - ' }', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace publishEnvelope through the queued handoff to consumeEnvelope.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - const publisherId = symbolId(result, 'publishEnvelope') - const consumerId = symbolId(result, 'consumeEnvelope') - expect(relationshipIdentities(result.relationships)).toContain( - `${publisherId}\0enqueues_job\0${consumerId}`, - ) - expectWithinProtocol(result) - }) - - it('preserves both fan-out branches and their shared fan-in target across multiple hubs', () => { - const index = readyFixture('fan-forest', { - 'src/forest/launch.ts': [ - "import { coordinateBatch } from './coordinate.js'", - '', - 'export function launchBatch(value: string): string[] {', - ' return coordinateBatch(value)', - '}', - ].join('\n'), - 'src/forest/coordinate.ts': [ - "import { enrichSlice } from './enrich.js'", - "import { verifySlice } from './verify.js'", - '', - 'export function coordinateBatch(value: string): string[] {', - ' return [enrichSlice(value), verifySlice(value)]', - '}', - ].join('\n'), - 'src/forest/enrich.ts': [ - "import { foldContribution } from './fold.js'", - '', - 'export function enrichSlice(value: string): string {', - " return foldContribution(`enriched:${value}`)", - '}', - ].join('\n'), - 'src/forest/verify.ts': [ - "import { foldContribution } from './fold.js'", - '', - 'export function verifySlice(value: string): string {', - " return foldContribution(`verified:${value}`)", - '}', - ].join('\n'), - 'src/forest/fold.ts': [ - 'export function foldContribution(value: string): string {', - ' return value', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace launchBatch through coordinateBatch, both enrichSlice and verifySlice branches, to foldContribution.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'launchBatch', 'coordinateBatch') - expectCall(result, 'coordinateBatch', 'enrichSlice') - expectCall(result, 'coordinateBatch', 'verifySlice') - expectCall(result, 'enrichSlice', 'foldContribution') - expectCall(result, 'verifySlice', 'foldContribution') - expectWithinProtocol(result) - }) - - it('does not fabricate a disconnected handoff between pure fan-in branches', () => { - const index = readyFixture('pure-fan-in', { - 'src/fan-in/alpha.ts': [ - "import { mergeResult } from './merge.js'", - 'export function alphaProcess(value: string): string {', - ' return mergeResult(`alpha:${value}`)', - '}', - ].join('\n'), - 'src/fan-in/beta.ts': [ - "import { mergeResult } from './merge.js'", - 'export function betaProcess(value: string): string {', - ' return mergeResult(`beta:${value}`)', - '}', - ].join('\n'), - 'src/fan-in/merge.ts': [ - 'export function mergeResult(value: string): string { return value }', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Explain the flow from alphaProcess and betaProcess to mergeResult.', - budget: 4_000, - }) - - expect(result.outcome).toBe('evidence') - expectCall(result, 'alphaProcess', 'mergeResult') - expectCall(result, 'betaProcess', 'mergeResult') - expect(result.boundaries.filter(({ kind }) => kind === 'disconnected')) - .toEqual([]) - expectWithinProtocol(result) - }) - - it('keeps retry-cycle evidence while exposing an unresolved terminal handoff', () => { - const index = readyFixture('retry-cycle', { - 'src/retry/open.ts': [ - "import { runAttempt } from './run.js'", - '', - 'export function openCycle(value: string): string {', - ' return runAttempt(value, 0)', - '}', - ].join('\n'), - 'src/retry/run.ts': [ - "import { scheduleRetry } from './schedule.js'", - "import { completeAttempt } from './terminal.js'", - '', - 'export function runAttempt(value: string, count: number): string {', - ' return count < 1', - ' ? scheduleRetry(value, count + 1)', - ' : completeAttempt(value)', - '}', - ].join('\n'), - 'src/retry/schedule.ts': [ - "import { runAttempt } from './run.js'", - '', - 'export function scheduleRetry(value: string, count: number): string {', - ' return runAttempt(value, count)', - '}', - ].join('\n'), - 'src/retry/terminal.ts': [ - 'export function completeAttempt(value: string): string {', - ' return value', - '}', - '', - 'export function publishOutcome(value: string): string {', - " return `published:${value}`", - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Trace openCycle through runAttempt and scheduleRetry to completeAttempt, then verify the unresolved handoff to publishOutcome.', - budget: 4_000, - }) - - expectCall(result, 'openCycle', 'runAttempt') - expectCall(result, 'runAttempt', 'scheduleRetry') - expectCall(result, 'scheduleRetry', 'runAttempt') - expectCall(result, 'runAttempt', 'completeAttempt') - const completeId = symbolId(result, 'completeAttempt') - const publishId = symbolId(result, 'publishOutcome') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'disconnected', - subject: `${completeId} -> ${publishId}`, - })) - expectWithinProtocol(result) - }) - - it('does not mistake a connected presentation call-star for an end-to-end runtime flow', () => { - const index = readyFixture('presentation-star', { - 'src/presentation/journey-card.ts': [ - 'function showRequestCreation(): string { return "created" }', - 'function showProcessingStage(): string { return "processing" }', - 'function showAssemblyStage(): string { return "assembly" }', - 'function showPersistenceStage(): string { return "persistence" }', - '', - 'export function renderJourneyCard(): string {', - ' return [', - ' showRequestCreation(),', - ' showProcessingStage(),', - ' showAssemblyStage(),', - ' showPersistenceStage(),', - ' ].join(":")', - '}', - ].join('\n'), - }) - - const result = retrieveContext(index, { - question: - 'Explain the end-to-end runtime flow from request creation through processing and assembly to persistence.', - budget: 4_000, - }) - - expect(result.outcome).not.toBe('evidence') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'missing', - })) - expectWithinProtocol(result) - }) - - it('bounds common-parent expansion before ordering a deep 7k-node candidate', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - const file = 'src/deep-common-parents.ts' - graph.addNode('alpha-anchor', syntheticNode('alphaAnchor', file)) - graph.addNode('beta-anchor', syntheticNode('betaAnchor', file)) - for (let index = 0; index < 7_000; index += 1) { - const id = `parent-${index.toString().padStart(4, '0')}` - graph.addNode(id, syntheticNode(`sharedParent${index}`, file)) - } - for (let index = 0; index < 7_000; index += 1) { - const id = `parent-${index.toString().padStart(4, '0')}` - for (const target of [ - index < 6_999 - ? `parent-${(index + 1).toString().padStart(4, '0')}` - : '', - 'alpha-anchor', - 'beta-anchor', - ].filter(Boolean)) { - graph.addEdge(id, target, { - relation: 'calls', - source_file: file, - source_location: 'L1', - provenance: [{}], - }) - } - } - - const ranked = rankQueryAnchors(syntheticIndex(graph), { - question: 'Trace alphaAnchor through betaAnchor.', - budget: 4_000, - }) - - expect(ranked.anchors.length).toBeGreaterThan(0) - expect(ranked.anchors.length).toBeLessThanOrEqual(25) - }) - - it('selects exact endpoints in a concentrated 12k-node scope', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - const file = 'src/concentrated.ts' - const count = 12_344 - for (let index = 0; index < count; index += 1) { - const id = `chain-${index.toString().padStart(5, '0')}` - const label = index === 0 - ? 'startConcentrated' - : index === count - 1 ? 'finishConcentrated' : `chainNode${index}` - graph.addNode(id, syntheticNode(label, file)) - } - graph.addEdge('chain-00000', 'chain-12343', { - relation: 'calls', - source_file: file, - source_location: 'L1', - provenance: [{}], - }) - - const ranked = rankQueryAnchors(syntheticIndex(graph), { - question: 'Trace startConcentrated through finishConcentrated.', - budget: 4_000, - }) - - expect(ranked.priorityAnchorIds).toEqual([ - 'chain-00000', - 'chain-12343', - ]) - }) - - it('preserves parallel authenticated edges before budget packing', () => { - const graph = new KnowledgeGraph({ root_path: '/workspace' }) - const file = 'src/parallel.ts' - graph.addNode('parallel-start', syntheticNode('parallelStart', file)) - graph.addNode('parallel-finish', syntheticNode('parallelFinish', file)) - for (let index = 0; index < 2_000; index += 1) { - graph.addEdge('parallel-start', 'parallel-finish', { - relation: 'calls', - source_file: file, - source_location: `L${index + 1}`, - provenance: [{}], - }) - } - const queryIndex = syntheticIndex(graph) - const ranked: RankQueryResult = { - anchors: ['parallel-start', 'parallel-finish'].map((id, firstMatch) => ({ - id, - attributes: graph.nodeAttributes(id), - score: 1, - matchedTerms: [], - firstMatch, - })), - boundaries: [], - queryTerms: ['parallel'], - flow: true, - branch: [], - priorityAnchorIds: ['parallel-start', 'parallel-finish'], - structuralRequired: true, - structuralCoverageComplete: true, - } - const evidenceNodes: EvidenceNode[] = ranked.anchors.map(({ id, attributes }) => ({ - node_id: id, - label: String(attributes.label), - node_kind: 'function', - evidence_kind: 'symbol_declaration', - source_file: file, - source_location: 'L1', - line_number: 1, - end_line_number: 1, - provenance: [{}], - content_hash: 'hash', - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 24 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 23 }, - }, - snippet: `export function ${String(attributes.label).replace('()', '')}(): void {}`, - })) - - const traversed = traverseEvidencePaths(queryIndex, ranked) - const result = sliceEvidence({ - request: { - question: 'Trace parallelStart through parallelFinish.', - budget: 4_000, - }, - outcome: 'evidence', - matchedNodes: evidenceNodes, - relationships: traversed.edges.map((edge): EvidenceRelationship => ({ - id: edge.id, - from_id: edge.from, - to_id: edge.to, - relation: edge.relation, - source_file: file, - source_location: String(edge.attributes.source_location), - provenance: [{}], - })), - boundaries: traversed.boundaries, - priorityNodeIds: ranked.priorityAnchorIds ?? [], - closurePasses: traversed.closurePasses, - structuralRequired: true, - structuralCoverageComplete: true, - }) - - expect(traversed.edges).toHaveLength(2_000) - expect(traversed.boundaries).toEqual([]) - expect(result.relationships.length).toBeGreaterThan(0) - expect(result.relationships.length).toBeLessThan(2_000) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) - expect(result.metrics.truncated).toBe(true) - - const fitting = sliceEvidence({ - request: { - question: 'Trace parallelStart through parallelFinish.', - budget: 4_000, - }, - outcome: 'evidence', - matchedNodes: evidenceNodes, - relationships: traversed.edges.slice(0, 2).map((edge): EvidenceRelationship => ({ - id: edge.id, - from_id: edge.from, - to_id: edge.to, - relation: edge.relation, - source_file: file, - source_location: String(edge.attributes.source_location), - provenance: [{}], - })), - boundaries: [], - priorityNodeIds: ranked.priorityAnchorIds ?? [], - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: true, - }) - expect(fitting.relationships).toHaveLength(2) - - const packed = sliceEvidence({ - request: { - question: 'Trace parallelStart through parallelFinish.', - budget: 500, - }, - outcome: 'evidence', - matchedNodes: evidenceNodes, - relationships: ['a-small', 'b-huge', 'c-small'].map((id) => ({ - id, - from_id: 'parallel-start', - to_id: 'parallel-finish', - relation: 'calls', - source_file: file, - source_location: 'L1', - provenance: id === 'b-huge' ? [{ detail: 'x'.repeat(8_000) }] : [{}], - })), - boundaries: [], - priorityNodeIds: ranked.priorityAnchorIds ?? [], - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: true, - }) - expect(packed.relationships.map(({ id }) => id)).toEqual([ - 'a-small', - 'c-small', - ]) - }) - - it('keeps incremental parallel-edge accounting within the exact final budget', () => { - const file = 'src/exact-budget.ts' - const nodes: EvidenceNode[] = ['budgetStart', 'budgetFinish'].map((label) => ({ - node_id: label, - label: `${label}()`, - node_kind: 'function', - evidence_kind: 'symbol_declaration', - source_file: file, - source_location: 'L1', - line_number: 1, - end_line_number: 1, - provenance: [{}], - content_hash: 'hash', - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 24 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 23 }, - }, - snippet: `export function ${label}(): void {}`, - })) - const boundaryDetails = [ - 'plain', - 'punctuation: },{][::,,', - 'escaped: "quote" \\ slash \n newline', - 'unicode: مرحبا — 東京 🙂', - ] as const - const relationships: EvidenceRelationship[] = Array.from( - { length: 96 }, - (_, index) => ({ - id: `${String(index).padStart(3, '0')}-${'xy'.repeat(index % 13)}`, - from_id: 'budgetStart', - to_id: 'budgetFinish', - relation: 'calls', - source_file: file, - source_location: `L${index + 1}-${'q'.repeat(index % 37)}`, - provenance: [{ - detail: `${boundaryDetails[index % boundaryDetails.length]}:${ - 'z'.repeat((index * 17) % 53) - }`, - }], - }), - ) - const retrieveAt = (budget: number): RetrieveContextResult => sliceEvidence({ - request: { - question: 'Trace budgetStart through budgetFinish.', - budget, - }, - outcome: 'evidence', - matchedNodes: nodes, - relationships, - boundaries: [], - priorityNodeIds: nodes.map(({ node_id }) => node_id), - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: true, - }) - const probe = retrieveAt(777) - const threshold = probe.metrics.serialized_tokens - const budgets = [...new Set([ - 256, 333, 511, threshold - 1, threshold, threshold + 1, - 1_024, 2_048, 3_999, - ].filter((budget) => budget >= 256 && budget <= 4_000))] - - for (const budget of budgets) { - const result = retrieveAt(budget) - - expect(retrieveAt(budget)).toEqual(result) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(budget) - expect(countTokens(canonicalJsonString(result))) - .toBe(result.metrics.serialized_tokens) - const retainedIds = new Set( - result.matched_nodes.map(({ node_id }) => node_id), - ) - for (const edge of result.relationships) { - expect(retainedIds.has(edge.from_id)).toBe(true) - expect(retainedIds.has(edge.to_id)).toBe(true) - } - } - }) - - it('bounds a structural-missing envelope for a maximum-length question', () => { - const result = sliceEvidence({ - request: { - question: '🙂'.repeat(256), - budget: 256, - }, - outcome: 'evidence', - matchedNodes: [], - relationships: [], - boundaries: [], - priorityNodeIds: [], - closurePasses: 0, - structuralRequired: true, - structuralCoverageComplete: false, - }) - - expect(result.outcome).toBe('missing') - expect(result.boundaries).toContainEqual({ - kind: 'missing', - subject: 'structural coverage', - }) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(256) - expect(countTokens(canonicalJsonString(result))) - .toBe(result.metrics.serialized_tokens) - }) -}) diff --git a/tests/unit/retrieve-evidence-skeleton-regression.test.ts b/tests/unit/retrieve-evidence-skeleton-regression.test.ts deleted file mode 100644 index 17178db3..00000000 --- a/tests/unit/retrieve-evidence-skeleton-regression.test.ts +++ /dev/null @@ -1,804 +0,0 @@ -import { - cpSync, - mkdirSync, - mkdtempSync, - readFileSync, - readdirSync, - renameSync, - rmSync, - writeFileSync, -} from 'node:fs' -import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -import { afterAll, beforeAll, describe, expect, it } from 'vitest' - -import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' -import { generateIndex } from '../../src/application/generate-index.js' -import { retrieveContext } from '../../src/application/retrieve-context.js' -import { - attachBuildState, - readBuildState, -} from '../../src/domain/index/build-state.js' -import { - inspectQueryIndex, - type ReadyQueryIndex, -} from '../../src/domain/query/index-status.js' -import { sliceEvidence } from '../../src/domain/query/slice.js' -import type { - EvidenceNode, - EvidenceRelationship, - RetrieveContextResult, -} from '../../src/domain/query/types.js' - -interface StageContract { - id: string - label: string - source_suffix?: string -} - -interface RelationshipContract { - from: string - relation: string - to: string -} - -interface HandoffContract { - from: string - to: string -} - -interface Issue625Fixture { - queries: { - beta_3_broad: string - focused_recovery: string - punctuation_variants: string[] - clause_order_variants: string[] - distant_paraphrases: string[] - field_incident_variants: string[] - } - stages: StageContract[] - causal_relationships: RelationshipContract[] - disconnected_handoffs: HandoffContract[] - focused_required_stages: string[] - focused_required_relationships: RelationshipContract[] - distractor_source_prefixes: string[] -} - -const baseFixtureDirectory = fileURLToPath(new URL( - '../fixtures/pack-quality/runtime-generation-explain-report-flow/', - import.meta.url, -)) -const regressionFixtureDirectory = fileURLToPath(new URL( - '../fixtures/issue-625-evidence-skeleton/', - import.meta.url, -)) -const contract = JSON.parse( - readFileSync(join(regressionFixtureDirectory, 'fixture.json'), 'utf8'), -) as Issue625Fixture - -let root = '' -let index: ReadyQueryIndex -let overlayIndex: ReadyQueryIndex -let renamedIndex: ReadyQueryIndex -let falseReadyIndex: ReadyQueryIndex - -const renamedStages: StageContract[] = [ - { id: 'submission', label: '.executeAlpha()', source_suffix: '/phase-alpha.ts' }, - { id: 'kickoff', label: 'executeBravo()', source_suffix: '/phase-bravo.ts' }, - { id: 'dispatch', label: 'executeCharlie()', source_suffix: '/channel-charlie.ts' }, - { id: 'orchestration', label: '.executeStage()', source_suffix: '/phase-delta.ts' }, - { id: 'planning', label: '.executeEcho()', source_suffix: '/phase-echo.ts' }, - { id: 'section_worker', label: '.executeStage()', source_suffix: '/phase-foxtrot.ts' }, - { id: 'research', label: '.executeGolf()', source_suffix: '/phase-golf.ts' }, - { id: 'assembly_worker', label: '.executeStage()', source_suffix: '/phase-hotel.ts' }, - { id: 'assembly', label: '.executeIndia()', source_suffix: '/phase-india.ts' }, - { id: 'persistence', label: '.executeStage()', source_suffix: '/phase-juliet.ts' }, -] - -function evidenceNode(id: string, sourceFile = `src/${id}.ts`): EvidenceNode { - return { - node_id: id, - evidence_kind: 'symbol_declaration', - label: `${id}()`, - node_kind: 'function', - source_file: sourceFile, - source_location: 'L1', - line_number: 1, - end_line_number: 1, - definition_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 24 }, - }, - declaration_range: { - start: { line: 1, column: 1 }, - end: { line: 1, column: 24 }, - }, - source_domain: 'production', - provenance: [{}], - content_hash: 'a'.repeat(64), - snippet: `export function ${id}() {}`, - } -} - -function selectedStageNodesFor( - result: RetrieveContextResult, - expectedStages: readonly StageContract[], -): Map { - const selected = new Map() - for (const stage of expectedStages) { - const match = result.matched_nodes.find((node) => - node.label === stage.label - && (!stage.source_suffix || node.source_file.endsWith(stage.source_suffix))) - if (match) selected.set(stage.id, match) - } - return selected -} - -function selectedStageNodes(result: RetrieveContextResult): Map { - return selectedStageNodesFor(result, contract.stages) -} - -function relationshipIdentity( - edge: EvidenceRelationship, -): string { - return `${edge.from_id}\u0000${edge.relation}\u0000${edge.to_id}` -} - -function assertProtocolLimits(result: RetrieveContextResult): void { - expect(result.outcome).toBe('evidence') - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.snippets).toBeLessThanOrEqual(25) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) - expect(result.metrics.closure_passes).toBeLessThanOrEqual(1) -} - -function assertNoDistractors(result: RetrieveContextResult): void { - for (const prefix of contract.distractor_source_prefixes) { - expect( - result.matched_nodes.some((node) => node.source_file.startsWith(prefix)), - `selected high-degree non-causal distractor ${prefix}`, - ).toBe(false) - } -} - -function assertSemanticRelationships( - result: RetrieveContextResult, - stages: ReadonlyMap, - expected: readonly RelationshipContract[], -): void { - const selectedRelationships = new Set(result.relationships.map(relationshipIdentity)) - for (const relationship of expected) { - const from = stages.get(relationship.from) - const to = stages.get(relationship.to) - expect(from, `missing semantic stage ${relationship.from}`).toBeDefined() - expect(to, `missing semantic stage ${relationship.to}`).toBeDefined() - expect( - selectedRelationships.has( - `${from!.node_id}\u0000${relationship.relation}\u0000${to!.node_id}`, - ), - `missing ${relationship.from} --${relationship.relation}--> ${relationship.to}`, - ).toBe(true) - expect( - selectedRelationships.has( - `${to!.node_id}\u0000${relationship.relation}\u0000${from!.node_id}`, - ), - `reversed ${relationship.to} --${relationship.relation}--> ${relationship.from}`, - ).toBe(false) - } -} - -function assertDisconnectedHandoffs( - result: RetrieveContextResult, - stages: ReadonlyMap, - expected: readonly HandoffContract[] = contract.disconnected_handoffs, -): void { - const boundaries = new Set( - result.boundaries - .filter((candidate) => candidate.kind === 'disconnected') - .map((candidate) => candidate.subject), - ) - for (const handoff of expected) { - const from = stages.get(handoff.from) - const to = stages.get(handoff.to) - expect(from, `missing semantic stage ${handoff.from}`).toBeDefined() - expect(to, `missing semantic stage ${handoff.to}`).toBeDefined() - expect( - boundaries.has(`${from!.node_id} -> ${to!.node_id}`), - `missing verification boundary ${handoff.from} -> ${handoff.to}`, - ).toBe(true) - } -} - -function assertBroadEvidenceSkeleton(result: RetrieveContextResult): void { - assertProtocolLimits(result) - - const stages = selectedStageNodes(result) - expect( - [...stages.keys()].sort(), - 'broad retrieval must cover every semantic runtime stage', - ).toEqual(contract.stages.map((stage) => stage.id).sort()) - assertSemanticRelationships(result, stages, contract.causal_relationships) - assertDisconnectedHandoffs(result, stages) - assertNoDistractors(result) - const requiredFiles = new Set([...stages.values()].map((node) => node.source_file)) - const selectedFiles = new Set(result.matched_nodes.map((node) => node.source_file)) - expect(requiredFiles.size / selectedFiles.size).toBeGreaterThanOrEqual(0.7) -} - -beforeAll(() => { - root = mkdtempSync(join(tmpdir(), 'madar-issue-625-evidence-skeleton-')) - const workspace = join(root, 'workspace') - mkdirSync(dirname(workspace), { recursive: true }) - cpSync(join(baseFixtureDirectory, 'workspace'), workspace, { recursive: true }) - cpSync(join(regressionFixtureDirectory, 'workspace'), workspace, { recursive: true }) - const generated = generateIndex(workspace) - const inspected = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) - if (inspected.state !== 'ready') { - throw new Error(`Expected ready query index, received ${inspected.state}`) - } - index = inspected - - const overlayGraph = loadGraphArtifact(generated.graphPath) - const overlayBuild = readBuildState(overlayGraph) - if (!overlayBuild) throw new Error('Expected overlay source build state') - const moduleEntry = overlayGraph.nodeEntries().find(([, attributes]) => - attributes.source_file - === 'src/modules/pipeline/assembly/pipeline-assembly-catalog.module.ts' - && attributes.node_kind === 'class') - const methodEntry = overlayGraph.nodeEntries().find(([, attributes]) => - attributes.source_file - === 'src/modules/pipeline/assembly/pipeline-assembly-catalog.module.ts' - && attributes.node_kind !== 'class' - && attributes.node_kind !== 'file') - if (!moduleEntry || !methodEntry) { - throw new Error('Expected deterministic module distractor nodes') - } - const { - body_facts: _authenticatedBodyFacts, - ...distractorMethodAttributes - } = methodEntry[1] - for (let ordinal = 0; ordinal < 10_001; ordinal += 1) { - const nodeId = `issue-625-overlay-${ordinal.toString().padStart(5, '0')}` - overlayGraph.addNode(nodeId, { - ...distractorMethodAttributes, - label: `ideaReportPipelineAssemblyPersistenceOverlay${ordinal}()`, - qualified_name: - `PipelineAssemblyCatalogModule.ideaReportPipelineAssemblyPersistenceOverlay${ordinal}`, - }) - overlayGraph.addEdge(moduleEntry[0], nodeId, { - relation: 'module_provides', - source_file: methodEntry[1].source_file, - source_location: methodEntry[1].source_location, - provenance: methodEntry[1].provenance, - }) - } - const { build_id: _previousBuildId, ...overlayBuildWithoutId } = overlayBuild - attachBuildState(overlayGraph, overlayBuildWithoutId) - const inspectedOverlay = inspectQueryIndex(overlayGraph) - if (inspectedOverlay.state !== 'ready') { - throw new Error(`Expected ready overlay query index, received ${inspectedOverlay.state}`) - } - overlayIndex = inspectedOverlay - - const renamedWorkspace = join(root, 'renamed-workspace') - cpSync(join(baseFixtureDirectory, 'workspace'), renamedWorkspace, { recursive: true }) - const replacements = [ - ['idea-generation.controller', 'phase-alpha'], - ['pipeline-trigger.service', 'phase-bravo'], - ['queue-registry.service', 'channel-charlie'], - ['orchestrator.worker', 'phase-delta'], - ['planner.service', 'phase-echo'], - ['section-research.worker', 'phase-foxtrot'], - ['research-agent.service', 'phase-golf'], - ['assembly.worker', 'phase-hotel'], - ['assembly.service', 'phase-india'], - ['db-sync.worker', 'phase-juliet'], - ['IdeaGenerationController', 'PhaseAlpha'], - ['generateFromProblem', 'executeAlpha'], - ['startPipeline', 'executeBravo'], - ['enqueueJob', 'executeCharlie'], - ['OrchestratorWorker', 'PhaseDelta'], - ['PlannerService', 'PhaseEcho'], - ['SectionResearchWorker', 'PhaseFoxtrot'], - ['ResearchAgentService', 'PhaseGolf'], - ['researchSection', 'executeGolf'], - ['AssemblyWorker', 'PhaseHotel'], - ['AssemblyService', 'PhaseIndia'], - ['assembleReport', 'executeIndia'], - ['DbSyncWorker', 'PhaseJuliet'], - ] as const - const sourceFiles = (directory: string): string[] => - readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { - const path = join(directory, entry.name) - return entry.isDirectory() ? sourceFiles(path) : [path] - }) - for (const path of sourceFiles(renamedWorkspace).filter((candidate) => - /\.[cm]?[jt]sx?$/u.test(candidate))) { - let source = readFileSync(path, 'utf8') - for (const [from, to] of replacements) source = source.replaceAll(from, to) - source = source.replace(/\bplan\b/gu, 'executeEcho') - .replace(/\bprocess\b/gu, 'executeStage') - writeFileSync(path, source, 'utf8') - } - const renamedPaths = [ - ['src/modules/ideas/interface/http/idea-generation.controller.ts', 'phase-alpha.ts'], - ['src/modules/pipeline/api/pipeline-trigger.service.ts', 'phase-bravo.ts'], - ['src/modules/pipeline/api/queue-registry.service.ts', 'channel-charlie.ts'], - ['src/modules/pipeline/workers/orchestrator.worker.ts', 'phase-delta.ts'], - ['src/modules/planning/planner.service.ts', 'phase-echo.ts'], - ['src/modules/research/workers/section-research.worker.ts', 'phase-foxtrot.ts'], - ['src/modules/research/research-agent.service.ts', 'phase-golf.ts'], - ['src/modules/pipeline/assembly/assembly.worker.ts', 'phase-hotel.ts'], - ['src/modules/reports/assembly.service.ts', 'phase-india.ts'], - ['src/modules/pipeline/workers/db-sync.worker.ts', 'phase-juliet.ts'], - ] as const - for (const [from, to] of renamedPaths) { - renameSync(join(renamedWorkspace, from), join(dirname(join(renamedWorkspace, from)), to)) - } - const renamed = generateIndex(renamedWorkspace) - const inspectedRenamed = inspectQueryIndex(loadGraphArtifact(renamed.graphPath)) - if (inspectedRenamed.state !== 'ready') { - throw new Error(`Expected ready renamed query index, received ${inspectedRenamed.state}`) - } - renamedIndex = inspectedRenamed - - const falseReadyWorkspace = join(root, 'false-ready-workspace') - const falseReadySources = { - 'src/ui/idea-report-pipeline-view.ts': [ - 'export function renderIdeaReportPipelineStagesAssembly(', - ' value: string,', - '): string {', - " return `view:${value}`", - '}', - '', - ].join('\n'), - 'src/config/report-persistence-catalog.module.ts': [ - 'export class ReportPersistenceCatalogModule {', - ' reportPersistenceStatus(): string {', - " return 'configured'", - ' }', - '}', - '', - ].join('\n'), - } - for (const [path, source] of Object.entries(falseReadySources)) { - const absolute = join(falseReadyWorkspace, path) - mkdirSync(dirname(absolute), { recursive: true }) - writeFileSync(absolute, source, 'utf8') - } - const falseReady = generateIndex(falseReadyWorkspace) - const inspectedFalseReady = inspectQueryIndex(loadGraphArtifact(falseReady.graphPath)) - if (inspectedFalseReady.state !== 'ready') { - throw new Error(`Expected ready false-ready query index, received ${inspectedFalseReady.state}`) - } - falseReadyIndex = inspectedFalseReady -}, 30_000) - -afterAll(() => { - if (root) rmSync(root, { recursive: true, force: true }) -}) - -describe('issue #625 generic evidence-skeleton retrieval', () => { - it('recovers the complete semantic skeleton for the exact beta.3 broad query', () => { - const result = retrieveContext(index, { - question: contract.queries.beta_3_broad, - budget: 4_000, - }) - - assertBroadEvidenceSkeleton(result) - }) - - it('recovers the creation-to-planning handoff for the exact focused recovery query', () => { - const result = retrieveContext(index, { - question: contract.queries.focused_recovery, - budget: 4_000, - }) - assertProtocolLimits(result) - - const stages = selectedStageNodes(result) - expect([...stages.keys()]).toEqual(expect.arrayContaining( - contract.focused_required_stages, - )) - assertSemanticRelationships( - result, - stages, - contract.focused_required_relationships, - ) - assertDisconnectedHandoffs( - result, - stages, - contract.disconnected_handoffs.filter((handoff) => - contract.focused_required_stages.includes(handoff.from) - && contract.focused_required_stages.includes(handoff.to)), - ) - assertNoDistractors(result) - expect(result.relationships.length).toBeGreaterThan(0) - }) - - it.each([ - ...contract.queries.punctuation_variants, - ...contract.queries.clause_order_variants, - ...contract.queries.distant_paraphrases, - ...contract.queries.field_incident_variants, - ])('keeps the same semantic skeleton for paraphrase: %s', (question) => { - const result = retrieveContext(index, { question, budget: 4_000 }) - - assertBroadEvidenceSkeleton(result) - }) - - it('rejects deterministic high-degree module and UI distractors', () => { - const result = retrieveContext(index, { - question: contract.queries.beta_3_broad, - budget: 4_000, - }) - - assertNoDistractors(result) - expect(selectedStageNodes(result).size).toBe(contract.stages.length) - }) - - it('retains the semantic skeleton under a deterministic 10k-node overlay', () => { - const result = retrieveContext(overlayIndex, { - question: contract.queries.beta_3_broad, - budget: 4_000, - }) - - assertBroadEvidenceSkeleton(result) - }) - - it('retains graph-grounded phase coverage after alpha-renaming owners and files', () => { - const result = retrieveContext(renamedIndex, { - question: contract.queries.beta_3_broad, - budget: 4_000, - }) - assertProtocolLimits(result) - - const stages = selectedStageNodesFor(result, renamedStages) - expect([...stages.keys()].sort()).toEqual( - renamedStages.map((stage) => stage.id).sort(), - ) - assertSemanticRelationships(result, stages, contract.causal_relationships) - assertDisconnectedHandoffs(result, stages) - const requiredFiles = new Set([...stages.values()].map((node) => node.source_file)) - const selectedFiles = new Set(result.matched_nodes.map((node) => node.source_file)) - expect(requiredFiles.size / selectedFiles.size).toBeGreaterThanOrEqual(0.7) - }) - - it('does not report disconnected lexical UI/config hits as a ready flow', () => { - const result = retrieveContext(falseReadyIndex, { - question: contract.queries.beta_3_broad, - budget: 4_000, - }) - - expect(result.outcome).not.toBe('evidence') - expect(result.relationships).toEqual([]) - expect(result.boundaries.length).toBeGreaterThan(0) - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.snippets).toBeLessThanOrEqual(25) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(4_000) - }) - - it('does not promote retained structure when concept coverage is incomplete', () => { - const source = evidenceNode('coverage-source') - const target = evidenceNode('coverage-target') - const result = sliceEvidence({ - request: { question: 'trace the complete runtime flow', budget: 4_000 }, - outcome: 'evidence', - matchedNodes: [source, target], - relationships: [{ - id: 'coverage-edge', - from_id: source.node_id, - to_id: target.node_id, - relation: 'calls', - source_file: source.source_file, - source_location: 'L1', - provenance: [{}], - }], - boundaries: [], - priorityNodeIds: [source.node_id, target.node_id], - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: false, - }) - - expect(result.relationships).toHaveLength(1) - expect(result.outcome).toBe('missing') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'missing', - subject: 'structural coverage', - })) - }) - - it('preserves a stronger corrupt outcome when structural coverage is unavailable', () => { - const result = sliceEvidence({ - request: { question: 'trace the unavailable runtime flow', budget: 4_000 }, - outcome: 'corrupt', - matchedNodes: [], - relationships: [], - boundaries: [{ kind: 'corrupt', subject: 'canonical index' }], - priorityNodeIds: [], - closurePasses: 0, - structuralRequired: true, - structuralCoverageComplete: false, - }) - - expect(result.outcome).toBe('corrupt') - expect(result.boundaries).toEqual([ - { kind: 'corrupt', subject: 'canonical index' }, - ]) - }) - - it('packs each disconnected handoff with both endpoints under the hard file cap', () => { - const nodes = Array.from({ length: 13 }, (_, pair) => [ - evidenceNode(`producer-${pair}`), - evidenceNode(`consumer-${pair}`), - ]).flat() - const boundaries = Array.from({ length: 13 }, (_, pair) => ({ - kind: 'disconnected' as const, - subject: `producer-${pair} -> consumer-${pair}`, - })) - const result = sliceEvidence({ - request: { question: 'trace all handoffs', budget: 4_000 }, - outcome: 'evidence', - matchedNodes: nodes, - relationships: [], - boundaries, - priorityNodeIds: nodes.map((node) => node.node_id), - closurePasses: 1, - }) - - const retainedIds = new Set(result.matched_nodes.map((node) => node.node_id)) - const retainedHandoffs = new Set( - result.boundaries - .filter((boundary) => boundary.kind === 'disconnected') - .map((boundary) => boundary.subject), - ) - expect(retainedHandoffs.size).toBeGreaterThan(0) - for (let pair = 0; pair < 13; pair += 1) { - const subject = `producer-${pair} -> consumer-${pair}` - const endpointsRetained = retainedIds.has(`producer-${pair}`) - && retainedIds.has(`consumer-${pair}`) - expect(retainedHandoffs.has(subject)).toBe(endpointsRetained) - expect(retainedIds.has(`producer-${pair}`)) - .toBe(retainedIds.has(`consumer-${pair}`)) - } - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.truncated).toBe(true) - }) - - it('keeps priority disconnected endpoints ahead of non-priority causal closure', () => { - const askedStart = evidenceNode('asked-start') - const askedFinish = evidenceNode('asked-finish') - const closureNodes = Array.from({ length: 12 }, (_, index) => - evidenceNode(`closure-${index}`)) - const relationships = Array.from({ length: 6 }, (_, pair) => ({ - id: `closure-edge-${pair}`, - from_id: `closure-${pair * 2}`, - to_id: `closure-${pair * 2 + 1}`, - relation: 'calls', - source_file: `src/closure-${pair * 2}.ts`, - source_location: 'L1', - provenance: [{}], - })) - const result = sliceEvidence({ - request: { question: 'trace asked start to asked finish', budget: 4_000 }, - outcome: 'evidence', - matchedNodes: [askedStart, askedFinish, ...closureNodes], - relationships, - boundaries: [{ - kind: 'disconnected', - subject: `${askedStart.node_id} -> ${askedFinish.node_id}`, - }], - priorityNodeIds: [askedStart.node_id, askedFinish.node_id], - closurePasses: 1, - }) - - expect(result.matched_nodes.map(({ node_id }) => node_id)) - .toEqual(expect.arrayContaining([askedStart.node_id, askedFinish.node_id])) - expect(result.boundaries).toContainEqual({ - kind: 'disconnected', - subject: `${askedStart.node_id} -> ${askedFinish.node_id}`, - }) - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.truncated).toBe(true) - }) - - it('packs each disconnected handoff with both endpoints under the token budget', () => { - const nodes = Array.from({ length: 5 }, (_, pair) => [ - evidenceNode(`budget-producer-${pair}`), - evidenceNode(`budget-consumer-${pair}`), - ]).flat() - const boundaries = Array.from({ length: 5 }, (_, pair) => ({ - kind: 'disconnected' as const, - subject: `budget-producer-${pair} -> budget-consumer-${pair}`, - detail: `verification target ${String(pair).repeat(80)}`, - })) - const result = sliceEvidence({ - request: { question: 'trace budget handoffs', budget: 700 }, - outcome: 'evidence', - matchedNodes: nodes, - relationships: [], - boundaries, - priorityNodeIds: nodes.map((node) => node.node_id), - closurePasses: 1, - }) - - const retainedIds = new Set(result.matched_nodes.map((node) => node.node_id)) - const retainedHandoffs = result.boundaries.filter( - (candidate) => candidate.kind === 'disconnected', - ) - expect(retainedHandoffs.length).toBeGreaterThan(0) - for (const boundary of retainedHandoffs) { - const [fromId, toId] = boundary.subject.split(' -> ') - expect(retainedIds.has(fromId!)).toBe(true) - expect(retainedIds.has(toId!)).toBe(true) - } - for (let pair = 0; pair < 5; pair += 1) { - expect(retainedIds.has(`budget-producer-${pair}`)) - .toBe(retainedIds.has(`budget-consumer-${pair}`)) - } - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(700) - expect(result.metrics.truncated).toBe(true) - }) - - it('keeps the real next path edge ahead of high-degree non-causal fanout', () => { - const anchor = evidenceNode('anchor') - const realNext = evidenceNode('real-next') - const noise = Array.from({ length: 20 }, (_, index) => - evidenceNode(`noise-${index}`)) - const relationship = ( - id: string, - from: EvidenceNode, - to: EvidenceNode, - ): EvidenceRelationship => ({ - id, - from_id: from.node_id, - to_id: to.node_id, - relation: 'calls', - source_file: from.source_file, - source_location: 'L1', - provenance: [{}], - }) - const result = sliceEvidence({ - request: { question: 'trace anchor', budget: 850 }, - outcome: 'evidence', - matchedNodes: [anchor, realNext, ...noise], - relationships: [ - relationship('real-edge', anchor, realNext), - ...noise.map((node, index) => - relationship(`noise-edge-${index}`, anchor, node)), - ], - boundaries: [], - priorityNodeIds: [anchor.node_id, realNext.node_id], - closurePasses: 1, - }) - - expect(result.relationships).toContainEqual(expect.objectContaining({ - id: 'real-edge', - from_id: anchor.node_id, - to_id: realNext.node_id, - })) - expect(result.relationships.length).toBeGreaterThan(0) - const retainedIds = new Set( - result.matched_nodes.map((node) => node.node_id), - ) - for (const edge of result.relationships) { - expect(retainedIds.has(edge.from_id)).toBe(true) - expect(retainedIds.has(edge.to_id)).toBe(true) - } - expect(result.matched_nodes.map((node) => node.node_id)) - .toEqual(expect.arrayContaining([anchor.node_id, realNext.node_id])) - expect(result.metrics.selected_files).toBeLessThanOrEqual(12) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(850) - expect(result.metrics.truncated).toBe(true) - }) - - it('packs causal flow evidence before higher-priority navigation under token pressure', () => { - const navigationSource = evidenceNode('navigation-source') - const navigationTarget = evidenceNode('navigation-target') - const causalSource = evidenceNode('causal-source') - const causalTarget = evidenceNode('causal-target') - const relationship = ( - id: string, - from: EvidenceNode, - to: EvidenceNode, - relation: string, - ): EvidenceRelationship => ({ - id, - from_id: from.node_id, - to_id: to.node_id, - relation, - source_file: from.source_file, - source_location: 'L1', - provenance: [{}], - }) - const result = sliceEvidence({ - request: { question: 'trace the complete runtime flow', budget: 400 }, - outcome: 'evidence', - matchedNodes: [ - navigationSource, - navigationTarget, - causalSource, - causalTarget, - ], - relationships: [ - relationship( - 'navigation-edge', - navigationSource, - navigationTarget, - 'contains', - ), - relationship('causal-edge', causalSource, causalTarget, 'calls'), - ], - boundaries: [], - priorityNodeIds: [ - navigationSource.node_id, - navigationTarget.node_id, - causalSource.node_id, - causalTarget.node_id, - ], - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: true, - }) - - expect(result.outcome).toBe('evidence') - expect(result.relationships).toEqual([ - expect.objectContaining({ id: 'causal-edge', relation: 'calls' }), - ]) - expect(result.matched_nodes.map(({ node_id }) => node_id).sort()) - .toEqual([causalSource.node_id, causalTarget.node_id].sort()) - expect(result.metrics.serialized_tokens).toBeLessThanOrEqual(400) - expect(result.metrics.truncated).toBe(true) - }) - - it('does not treat navigation-only structure as a complete runtime flow', () => { - const source = evidenceNode('navigation-only-source') - const target = evidenceNode('navigation-only-target') - const result = sliceEvidence({ - request: { question: 'trace the complete runtime flow', budget: 4_000 }, - outcome: 'evidence', - matchedNodes: [source, target], - relationships: [{ - id: 'navigation-only-edge', - from_id: source.node_id, - to_id: target.node_id, - relation: 'contains', - source_file: source.source_file, - source_location: 'L1', - provenance: [{}], - }], - boundaries: [], - priorityNodeIds: [source.node_id, target.node_id], - closurePasses: 1, - structuralRequired: true, - structuralCoverageComplete: true, - }) - - expect(result.relationships).toHaveLength(1) - expect(result.outcome).toBe('missing') - expect(result.boundaries).toContainEqual(expect.objectContaining({ - kind: 'missing', - subject: 'structural coverage', - })) - }) - - it('deduplicates priority identities before enforcing snippet limits', () => { - const node = evidenceNode('repeated-priority') - const result = sliceEvidence({ - request: { question: 'locate repeated priority', budget: 4_000 }, - outcome: 'evidence', - matchedNodes: [node], - relationships: [], - boundaries: [], - priorityNodeIds: Array.from({ length: 26 }, () => node.node_id), - closurePasses: 1, - }) - - expect(result.matched_nodes).toEqual([node]) - expect(result.metrics.snippets).toBe(1) - expect(result.metrics.snippets).toBeLessThanOrEqual(25) - expect(result.metrics.truncated).toBe(false) - }) -}) diff --git a/tests/unit/retrieve-v2-contract-gaps.test.ts b/tests/unit/retrieve-v2-contract-gaps.test.ts new file mode 100644 index 00000000..fbefa85c --- /dev/null +++ b/tests/unit/retrieve-v2-contract-gaps.test.ts @@ -0,0 +1,353 @@ +import { + cpSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' + +import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' +import { generateIndex } from '../../src/application/generate-index.js' +import { + retrieveContext, + serializeRetrieveContextResult, +} from '../../src/application/retrieve-context.js' +import { + inspectQueryIndex, + type ReadyQueryIndex, +} from '../../src/domain/query/index-status.js' +import { planQuestion } from '../../src/domain/query/plan.js' +import type { + QueryPlan, + RetrieveContextResult, +} from '../../src/domain/query/types.js' +import { selectWorkflow } from '../../src/domain/query/workflow.js' + +const roots: string[] = [] + +function readyWorkspace( + name: string, + files: Readonly>, +): ReadyQueryIndex { + const root = mkdtempSync(join(tmpdir(), `madar-630-${name}-`)) + roots.push(root) + for (const [path, source] of Object.entries(files)) { + const absolute = join(root, path) + mkdirSync(dirname(absolute), { recursive: true }) + writeFileSync(absolute, source.endsWith('\n') ? source : `${source}\n`, 'utf8') + } + writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + strict: true, + }, + }), 'utf8') + const generated = generateIndex(root) + const inspected = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) + if (inspected.state !== 'ready') { + throw new Error(`Expected ready ${name} index, received ${inspected.state}`) + } + return inspected +} + +function supportedPlan(question: string): QueryPlan { + const planned = planQuestion({ question, budget: 4_000 }) + if (planned.status !== 'supported') { + throw new Error(`Expected supported plan, received ${planned.reason}`) + } + return planned.plan +} + +function selectedLabels( + index: ReadyQueryIndex, + ids: readonly string[], +): string[] { + return ids.map((id) => String(index.graph.nodeAttributes(id).label ?? id)) +} + +function expectExactAccounting(result: RetrieveContextResult, budget: number): void { + const serialized = serializeRetrieveContextResult(result) + expect(result.metrics.budget_tokens).toBe(Math.max(256, Math.min(budget, 4_000))) + expect(countTokens(serialized)).toBe(result.metrics.serialized_tokens) + expect(result.metrics.serialized_tokens).toBeLessThanOrEqual( + result.metrics.budget_tokens, + ) +} + +let reportFlow: ReadyQueryIndex +const frozenQueries = (JSON.parse(readFileSync( + resolve('tests/fixtures/issue-625-evidence-skeleton/fixture.json'), + 'utf8', +)) as { queries: { + beta_3_broad: string + focused_recovery: string + punctuation_variants: string[] + clause_order_variants: string[] + distant_paraphrases: string[] + field_incident_variants: string[] +} }).queries + +beforeAll(() => { + const root = mkdtempSync(join(tmpdir(), 'madar-630-real-flow-')) + roots.push(root) + cpSync(resolve( + 'tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace', + ), root, { recursive: true }) + const generated = generateIndex(root) + const inspected = inspectQueryIndex(loadGraphArtifact(generated.graphPath)) + if (inspected.state !== 'ready') { + throw new Error(`Expected ready real-flow index, received ${inspected.state}`) + } + reportFlow = inspected +}) + +afterAll(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +describe('retrieve v2 uncovered contract cases', () => { + it('preserves both explicitly named service-method scopes in planning and selection', () => { + const question = + 'Trace enqueueTask through `ServiceAlpha.runAlpha` and `ServiceBeta.runBeta`.' + const plan = supportedPlan(question) + const index = readyWorkspace('two-explicit-scopes', { + 'src/entry.ts': [ + "import { ServiceAlpha } from './service-alpha.js'", + "import { ServiceBeta } from './service-beta.js'", + 'export async function enqueueTask(value: string): Promise {', + ' const alpha = new ServiceAlpha()', + ' const beta = new ServiceBeta()', + ' return Promise.all([alpha.runAlpha(value), beta.runBeta(value)])', + '}', + ].join('\n'), + 'src/service-alpha.ts': [ + "import type { Repository } from 'typeorm'", + 'export class ServiceAlpha {', + ' async runAlpha(value: string, repository?: Repository) {', + " await repository?.update('alpha', { value })", + ' return value', + ' }', + '}', + ].join('\n'), + 'src/service-beta.ts': [ + "import type { Repository } from 'typeorm'", + 'export class ServiceBeta {', + ' async runBeta(value: string, repository?: Repository) {', + " await repository?.update('beta', { value })", + ' return value', + ' }', + '}', + ].join('\n'), + }) + + expect(plan.obligations.find(({ kind }) => kind === 'stage')?.target) + .toBe('service alpha run alpha service beta run beta') + + const selected = selectWorkflow(index, plan) + const labels = selectedLabels(index, selected.symbolIds) + expect(labels.some((label) => label.includes('runAlpha'))).toBe(true) + expect(labels.some((label) => label.includes('runBeta'))).toBe(true) + expect(selected.obligations.find(({ kind }) => kind === 'stage')?.proven).toBe(true) + }) + + it('keeps both connected fan-in roots and fails closed for disconnected clauses', () => { + const question = + 'Trace the report from alphaProcess and betaProcess through mergeResult to persistence.' + const connected = readyWorkspace('connected-fan-in', { + 'src/alpha.ts': [ + "import { mergeResult } from './merge.js'", + 'export function alphaProcess(value: string) { return mergeResult(value) }', + ].join('\n'), + 'src/beta.ts': [ + "import { mergeResult } from './merge.js'", + 'export function betaProcess(value: string) { return mergeResult(value) }', + ].join('\n'), + 'src/merge.ts': [ + "import type { Repository } from 'typeorm'", + 'export async function mergeResult(', + ' value: string, repository?: Repository,', + ') {', + " await repository?.update('report', { value })", + ' return value', + '}', + ].join('\n'), + }) + const connectedSelection = selectWorkflow(connected, supportedPlan(question)) + expect(connectedSelection.complete).toBe(true) + expect(selectedLabels(connected, connectedSelection.rootSymbolIds)) + .toEqual(expect.arrayContaining([ + expect.stringContaining('alphaProcess'), + expect.stringContaining('betaProcess'), + ])) + + const disconnected = readyWorkspace('disconnected-clauses', { + 'src/alpha.ts': [ + "import type { Repository } from 'typeorm'", + 'export async function alphaProcess(repository: Repository) {', + " await repository.update('alpha', { done: true })", + '}', + ].join('\n'), + 'src/beta.ts': [ + "import type { Repository } from 'typeorm'", + 'export async function betaProcess(repository: Repository) {', + " await repository.update('beta', { done: true })", + '}', + ].join('\n'), + }) + const disconnectedResult = retrieveContext(disconnected, { + question, + budget: 4_000, + }) + expect(disconnectedResult.state).toBe('incomplete') + expect(disconnectedResult).not.toHaveProperty('dossier') + if (disconnectedResult.state === 'incomplete') { + expect(disconnectedResult.missing.map(({ code }) => code)) + .toContain('adjacent_handoff_unproven') + } + }) + + it('allows an explicitly requested test-production-test flow', () => { + const index = readyWorkspace('explicit-test-flow', { + 'tests/auth-route.test.ts': [ + "import { testAuthService } from '../src/auth-service.js'", + "import type { Repository } from 'typeorm'", + 'export function testAuthRoute(repository: Repository) {', + ' return testAuthService(repository)', + '}', + ].join('\n'), + 'src/auth-service.ts': [ + "import { assertAuthRecord } from '../tests/auth-assertion.js'", + "import type { Repository } from 'typeorm'", + 'export function testAuthService(repository: Repository) {', + ' return assertAuthRecord(repository)', + '}', + ].join('\n'), + 'tests/auth-assertion.ts': [ + "import type { Repository } from 'typeorm'", + 'export async function assertAuthRecord(repository: Repository) {', + " await repository.update('auth', { verified: true })", + " return 'verified'", + '}', + ].join('\n'), + }) + + const result = retrieveContext(index, { + question: + 'Explain the test flow from testAuthRoute through testAuthService to assertAuthRecord.', + budget: 4_000, + }) + + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + const files = result.dossier.evidence.files.map(({ path }) => path) + expect(files.some((path) => path.startsWith('tests/'))).toBe(true) + expect(files.some((path) => path.startsWith('src/'))).toBe(true) + }) + + it('does not authenticate a production workflow through a test-only bridge', () => { + const index = readyWorkspace('test-only-bridge', { + 'src/runtime-start.ts': [ + "import { testBridge } from '../tests/runtime-bridge.test.js'", + "import type { Repository } from 'typeorm'", + 'export function runtimeStart(repository: Repository) {', + ' return testBridge(repository)', + '}', + ].join('\n'), + 'tests/runtime-bridge.test.ts': [ + "import { runtimeFinish } from '../src/runtime-finish.js'", + "import type { Repository } from 'typeorm'", + 'export function testBridge(repository: Repository) {', + ' return runtimeFinish(repository)', + '}', + ].join('\n'), + 'src/runtime-finish.ts': [ + "import type { Repository } from 'typeorm'", + 'export async function runtimeFinish(repository: Repository) {', + " await repository.update('runtime', { complete: true })", + " return 'complete'", + '}', + ].join('\n'), + }) + + const result = retrieveContext(index, { + question: + 'Trace the runtime report from runtimeStart through runtimeFinish to persistence.', + budget: 4_000, + }) + + expect(result.state).toBe('incomplete') + expect(result).not.toHaveProperty('dossier') + if (result.state === 'incomplete') { + expect(result.missing.length).toBeGreaterThan(0) + } + }) + + it.each([ + ...frozenQueries.punctuation_variants, + ...frozenQueries.clause_order_variants, + ...frozenQueries.distant_paraphrases, + ...frozenQueries.field_incident_variants, + ])('converges the frozen broad prompt: %s', (question) => { + const baseline = retrieveContext(reportFlow, { + question: frozenQueries.beta_3_broad, + budget: 4_000, + }) + expect(baseline.state).toBe('ready') + if (baseline.state !== 'ready') return + + const result = retrieveContext(reportFlow, { question, budget: 4_000 }) + const detail = result.state === 'incomplete' + ? JSON.stringify(result.missing) : result.state + expect(result.state, detail).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.flow).toEqual(baseline.dossier.flow) + expect(result.dossier.evidence).toEqual(baseline.dossier.evidence) + }) + + it('answers the frozen focused orchestrator query with planning and channel evidence', () => { + const result = retrieveContext(reportFlow, { + question: frozenQueries.focused_recovery, + budget: 4_000, + }) + + const detail = result.state === 'incomplete' + ? JSON.stringify(result.missing) : result.state + expect(result.state, detail).toBe('ready') + if (result.state !== 'ready') return + const labels = result.dossier.evidence.entities.flatMap((entity) => + entity.kind === 'symbol' ? [entity.label] : []) + expect(labels).toEqual(expect.arrayContaining([ + expect.stringContaining('process'), + expect.stringContaining('plan'), + ])) + expect(result.dossier.flow.links.some(({ kind }) => kind === 'channel')).toBe(true) + }) + + it.each([ + ['ready-999', 'Where is generateFromProblem defined?', 999], + ['ready-1000', 'Where is generateFromProblem defined?!', 1_000], + ['ready-1023', 'Where is generateFromProblem defined?', 1_023], + ['ready-1024', 'Where is generateFromProblem defined?', 1_024], + ['terminal-escaped', 'Compare "alpha\\beta" with 東京 🙂.', 256], + ['terminal-unicode-limit', '🙂'.repeat(256), 256], + ['terminal-3999', 'Compare every architecture in this repository.', 3_999], + ['terminal-4000', 'Compare every architecture in this repository!', 4_000], + ])('keeps exact deterministic token accounting at %s', ( + _name, question, budget, + ) => { + const first = retrieveContext(reportFlow, { question, budget }) + const second = retrieveContext(reportFlow, { question, budget }) + + expect(second).toEqual(first) + expectExactAccounting(first, budget) + expect(first.state.startsWith('ready')).toBe(question.startsWith('Where')) + if (first.state !== 'ready') expect(first).not.toHaveProperty('dossier') + }) +}) diff --git a/tests/unit/try-command.test.ts b/tests/unit/try-command.test.ts index dd0d06b3..8ef9cf74 100644 --- a/tests/unit/try-command.test.ts +++ b/tests/unit/try-command.test.ts @@ -43,14 +43,22 @@ describe('runTryCommand', () => { const [, ...serialized] = output.split('\n') const result = JSON.parse(serialized.join('\n')) as { schema: string - outcome: string - matched_nodes: Array<{ label: string }> + version: number + state: string + dossier?: { + evidence: { + entities: Array<{ kind: string; label?: string }> + } + } } expect(output).toContain('[madar try] Built ') expect(result.schema).toBe('madar.retrieve') - expect(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toEqual( + expect(result.version).toBe(2) + expect(result.state).toBe('ready') + expect(result.dossier?.evidence.entities + .filter((entity) => entity.kind === 'symbol') + .map((entity) => entity.label)).toEqual( expect.arrayContaining(['submitOrder()', 'saveOrder()']), ) }) diff --git a/tools/eval/core-reset/benchmark.mjs b/tools/eval/core-reset/benchmark.mjs new file mode 100644 index 00000000..7e7716b8 --- /dev/null +++ b/tools/eval/core-reset/benchmark.mjs @@ -0,0 +1,113 @@ +import { createHash } from "node:crypto" +import { readFileSync } from "node:fs" +import { performance } from "node:perf_hooks" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +import { loadGraphArtifact } from "../../../dist/src/adapters/filesystem/graph-artifact.js" +import { retrieveContext } from "../../../dist/src/application/retrieve-context.js" +import { inspectQueryIndex } from "../../../dist/src/domain/query/index-status.js" + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..") +const graphPath = process.argv[2] +if (!graphPath) { + throw new Error("usage: node tools/eval/core-reset/benchmark.mjs ") +} + +const queryFixture = JSON.parse(readFileSync(resolve( + repositoryRoot, + "tests/fixtures/issue-625-evidence-skeleton/fixture.json", +), "utf8")).queries +const questions = [ + queryFixture.beta_3_broad, + ...queryFixture.punctuation_variants, + ...queryFixture.clause_order_variants, + ...queryFixture.distant_paraphrases, + ...queryFixture.field_incident_variants, +] +const requiredQueues = [ + "assembly-queue", + "db-sync-queue", + "orchestration-queue", + "section-research-queue", +] + +function assert(condition, message) { + if (!condition) throw new Error(message) +} + +function hash(value) { + return createHash("sha256").update(JSON.stringify(value)).digest("hex") +} + +function retrieve(index, question) { + const result = retrieveContext(index, { question, budget: 4_000 }) + assert(result.state === "ready", `${question}: ${JSON.stringify(result)}`) + assert(result.metrics.serialized_tokens <= 4_000, `${question}: token ceiling`) + assert(result.metrics.selected_files <= 12, `${question}: file ceiling`) + assert(result.metrics.authenticated_excerpts <= 25, `${question}: excerpt ceiling`) + assert(result.metrics.root_candidates <= 3, `${question}: root ceiling`) + assert(result.metrics.initial_candidates <= 32, `${question}: candidate ceiling`) + assert(result.metrics.explored_nodes <= 512, `${question}: explored-node ceiling`) + assert(result.metrics.causal_hops <= 24, `${question}: causal-hop ceiling`) + assert(result.metrics.recovery_passes <= 2, `${question}: recovery-pass ceiling`) + assert(result.metrics.recovery_frontier_nodes <= 64, `${question}: recovery ceiling`) + const queues = [...new Set( + JSON.stringify(result.dossier).match(/[a-z]+(?:-[a-z]+)*-queue/g) ?? [], + )].sort() + assert( + JSON.stringify(queues) === JSON.stringify(requiredQueues), + `${question}: required queues ${JSON.stringify(queues)}`, + ) + assert( + result.dossier.obligations.every((obligation) => obligation.proofs.length > 0), + `${question}: unproven obligation`, + ) + return result +} + +const index = inspectQueryIndex(loadGraphArtifact(resolve(graphPath))) +assert(index.state === "ready", `graph state: ${index.state}`) +const rows = questions.map((question) => { + const result = retrieve(index, question) + return { + question, + serialized_tokens: result.metrics.serialized_tokens, + flow_sha256: hash(result.dossier.flow), + evidence_sha256: hash(result.dossier.evidence), + } +}) +assert(new Set(rows.map(({ flow_sha256 }) => flow_sha256)).size === 1, "flow drift") +assert( + new Set(rows.map(({ evidence_sha256 }) => evidence_sha256)).size === 1, + "evidence drift", +) + +const warmIndex = inspectQueryIndex(loadGraphArtifact(resolve(graphPath))) +for (let pass = 0; pass < 3; pass += 1) { + retrieve(warmIndex, queryFixture.beta_3_broad) +} +const samples = Array.from({ length: 100 }, () => { + const started = performance.now() + retrieve(warmIndex, queryFixture.beta_3_broad) + return performance.now() - started +}).sort((left, right) => left - right) +const nearestRank = (percentile) => samples[Math.ceil(samples.length * percentile) - 1] +const p95 = nearestRank(0.95) +assert(p95 < 500, `warm retrieval p95 ${p95}ms is not below 500ms`) + +process.stdout.write(`${JSON.stringify({ + graph: resolve(graphPath), + prompts: rows.length, + ready: rows.length, + flow_sha256: rows[0].flow_sha256, + evidence_sha256: rows[0].evidence_sha256, + serialized_tokens: rows.map(({ serialized_tokens }) => serialized_tokens), + required_queues: requiredQueues, + warm: { + samples: samples.length, + median_ms: nearestRank(0.5), + p95_ms: p95, + max_ms: samples.at(-1), + }, +}, null, 2)}\n`) diff --git a/tools/eval/core-reset/isolation-support.mjs b/tools/eval/core-reset/isolation-support.mjs index d83d3448..d70f22be 100644 --- a/tools/eval/core-reset/isolation-support.mjs +++ b/tools/eval/core-reset/isolation-support.mjs @@ -397,7 +397,10 @@ export function packageContentLeaks(paths, markers, root = repositoryRoot) { return violations } -export function inspectPackageContents(markers = []) { +export function inspectPackageContents( + markers = [], + budget = evaluationPackageBudget, +) { const record = parseNpmPackJson( run(npmCommand, [ "pack", @@ -442,9 +445,9 @@ export function inspectPackageContents(markers = []) { packed_bytes: packedBytes, unpacked_bytes: unpackedBytes, target_passed: - fileCount <= evaluationPackageBudget.files_max && - packedBytes <= evaluationPackageBudget.packed_bytes_max && - unpackedBytes <= evaluationPackageBudget.unpacked_bytes_max, + fileCount <= budget.files_max && + packedBytes <= budget.packed_bytes_max && + unpackedBytes <= budget.unpacked_bytes_max, forbidden_paths: paths.filter((path) => forbiddenPackagePrefixes.some((prefix) => path.startsWith(prefix)), ), diff --git a/tools/eval/core-reset/verify-isolation.mjs b/tools/eval/core-reset/verify-isolation.mjs index 66ab1c98..123dba8d 100644 --- a/tools/eval/core-reset/verify-isolation.mjs +++ b/tools/eval/core-reset/verify-isolation.mjs @@ -2,6 +2,8 @@ import { existsSync, readFileSync } from "node:fs" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { parse } from "yaml" + import { evaluationPackageBudget, evaluationLeakMarkers, @@ -58,6 +60,14 @@ const ciWorkflow = readFileSync( resolve(repositoryRoot, ".github/workflows/ci.yml"), "utf8", ).replaceAll("\r\n", "\n") +const manifest = parse(readFileSync( + resolve(repositoryRoot, "docs/core-reset/removal-manifest.yml"), + "utf8", +)) +const activePhase = manifest.items?.find( + (item) => item.id === manifest.current?.active_phase, +) +const packageBudget = activePhase?.npm_package_budget ?? evaluationPackageBudget function assert(condition, message) { if (!condition) throw new Error(message) @@ -90,7 +100,7 @@ const packageContentMarkers = loadBearingEvaluationMarkers( contract, performanceMarkers, ) -const packageMeasurement = inspectPackageContents(packageContentMarkers) +const packageMeasurement = inspectPackageContents(packageContentMarkers, packageBudget) const publishedRoots = new Set(packageJson.files ?? []) const packageScripts = packageJson.scripts ?? {} @@ -254,7 +264,7 @@ assert( ) assert( packageMeasurement.target_passed, - `npm package exceeds the Evaluation Tooling ceilings: ${packageMeasurement.file_count}/${evaluationPackageBudget.files_max} files / ${packageMeasurement.packed_bytes}/${evaluationPackageBudget.packed_bytes_max} packed bytes / ${packageMeasurement.unpacked_bytes}/${evaluationPackageBudget.unpacked_bytes_max} unpacked bytes`, + `npm package exceeds the active ceilings: ${packageMeasurement.file_count}/${packageBudget.files_max} files / ${packageMeasurement.packed_bytes}/${packageBudget.packed_bytes_max} packed bytes / ${packageMeasurement.unpacked_bytes}/${packageBudget.unpacked_bytes_max} unpacked bytes`, ) assert( !existsSync(resolve(repositoryRoot, "dist", "tools")), diff --git a/tools/eval/lib/infrastructure/benchmark/quality.ts b/tools/eval/lib/infrastructure/benchmark/quality.ts index 9f664e14..77595d9d 100644 --- a/tools/eval/lib/infrastructure/benchmark/quality.ts +++ b/tools/eval/lib/infrastructure/benchmark/quality.ts @@ -1,7 +1,11 @@ import { KnowledgeGraph } from '../../../../../src/domain/graph/directed-multigraph.js' import { retrieveContext } from '../../../../../src/application/retrieve-context.js' import { inspectQueryIndex } from '../../../../../src/domain/query/index-status.js' -import type { RetrieveContextResult } from '../../../../../src/domain/query/types.js' +import type { + DossierLink, + DossierProof, + RetrieveContextResult, +} from '../../../../../src/domain/query/types.js' import { formatTokenRatio, resolveCorpusBaseline, type CorpusBaselineSource } from './corpus.js' import { normalizeBenchmarkQuestion, normalizeExpectedLabel, type BenchmarkQuestionSpec } from './questions.js' import { type PromptRunnerUsage } from '../prompt-runner.js' @@ -199,6 +203,32 @@ function questionBucket(question: string): string { return 'general' } +function hasValidProofRange(range: readonly number[]): boolean { + if (range.length !== 4 || range.some((value) => !Number.isSafeInteger(value) || value <= 0)) { + return false + } + const [startLine, startColumn, endLine, endColumn] = range as [number, number, number, number] + return startLine < endLine || (startLine === endLine && startColumn <= endColumn) +} + +export function isGroundedDossierProofChain( + link: Pick, + proofs: ReadonlyMap, + excerptIds: ReadonlySet, + fileIds: ReadonlySet, +): boolean { + const chain = link.proofs.map((id) => proofs.get(id)) + if (chain.length === 0 || chain.some((proof) => !proof || ( + 'excerpt' in proof + ? !excerptIds.has(proof.excerpt) + : !fileIds.has(proof.file) || !hasValidProofRange(proof.range) + ))) return false + const edges = chain.filter((proof): proof is DossierProof => proof !== undefined) + return edges[0]?.from === link.from + && edges.at(-1)?.to === link.to + && !edges.slice(1).some((proof, index) => edges[index]?.to !== proof.from) +} + function buildQualityResult( gold: GoldQuestion, result: RetrieveContextResult, @@ -213,10 +243,12 @@ function buildQualityResult( const groundedSymbols = new Set() if (result.state === 'ready') { const excerptIds = new Set(result.dossier.evidence.excerpts.map(({ id }) => id)) - const links = new Map(result.dossier.flow.links.map((link) => [link.id, link])) - for (const proof of result.dossier.evidence.proofs) { - groundedSymbols.add(proof.from) - groundedSymbols.add(proof.to) + const fileIds = new Set(result.dossier.evidence.files.map(({ id }) => id)) + const proofs = new Map(result.dossier.evidence.proofs.map((proof) => [proof.id, proof])) + for (const link of result.dossier.flow.links) { + if (!isGroundedDossierProofChain(link, proofs, excerptIds, fileIds)) continue + groundedSymbols.add(link.from) + groundedSymbols.add(link.to) } for (const entity of result.dossier.evidence.entities) { if (entity.kind === 'symbol' && entity.excerpt @@ -224,8 +256,7 @@ function buildQualityResult( groundedSymbols.add(entity.id) } if (entity.kind === 'operation' && excerptIds.has(entity.excerpt)) { - if ('owner' in entity) groundedSymbols.add(entity.owner) - else entity.links.forEach((id) => groundedSymbols.add(links.get(id)!.from)) + groundedSymbols.add(entity.owner) } } } From 6a824527f29baaa36cfdfb5eacffe5bc7b4af546 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sun, 2 Aug 2026 04:55:51 +0400 Subject: [PATCH 3/4] fix: close obligation dossier delivery gates --- .github/workflows/ci.yml | 4 + README.md | 8 +- docs/agent-governance.md | 8 +- docs/concepts/pipelines.md | 15 +- docs/core-reset/removal-manifest.yml | 103 ++-- docs/core-reset/scorecard.md | 4 +- docs/designs/2026-07-19-core-reset.md | 2 +- docs/indexing-completeness.md | 6 +- docs/integrations/agent-orchestration.md | 18 +- docs/language-capability-matrix.md | 8 +- docs/mcp-response-shape.md | 101 ++-- docs/proof-workflows.md | 2 +- docs/reference/cli-and-mcp.md | 20 +- docs/roadmap.md | 2 +- docs/security/mcp-threat-model.md | 2 +- docs/tutorials/agent-quickstarts.md | 4 +- docs/tutorials/getting-started.md | 21 +- docs/tutorials/sample-workspace.md | 4 +- examples/mcp-tool-examples.md | 123 ++--- examples/why-madar.md | 6 +- src/application/evidence-hydrator.ts | 4 +- src/application/retrieve-context.ts | 73 ++- src/domain/query/plan.ts | 47 +- src/domain/query/types.ts | 2 +- src/domain/query/workflow.ts | 147 +++--- tests/unit/agent-governance-doc.test.ts | 8 +- ...index-execution-review-regressions.test.ts | 28 +- tests/unit/core-reset-governance.test.ts | 110 +++-- tests/unit/evidence-hydrator.test.ts | 7 - tests/unit/mcp-response-shape-doc.test.ts | 33 +- tests/unit/query-plan.test.ts | 61 ++- tests/unit/query-workflow.test.ts | 275 ++++++++++- .../retrieve-context-proof-eviction.test.ts | 79 ++- tests/unit/retrieve-context.test.ts | 30 +- tests/unit/why-madar-doc.test.ts | 22 +- tools/eval/core-reset/benchmark.mjs | 466 ++++++++++++++++-- tools/eval/core-reset/verify-isolation.mjs | 42 +- 37 files changed, 1433 insertions(+), 462 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bef37b64..fa641ae2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,10 @@ jobs: - name: Build evaluation tooling run: npm run build:eval + - name: Enforce frozen issue 630 obligation dossier benchmark + if: matrix.os == 'ubuntu-latest' && matrix.node-version == '22' + run: node tools/eval/core-reset/benchmark.mjs + - name: Validate Core Reset evidence contract if: matrix.os == 'ubuntu-latest' && matrix.node-version == '22' run: npx vitest run tests/unit/core-reset-baseline.test.ts diff --git a/README.md b/README.md index 627092d9..494837cf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Madar builds a local graph for a JavaScript or TypeScript repository. Its MCP se retrieve(question, budget?) ``` -The result is a small set of exact source excerpts and directed relationships, or an explicit boundary explaining why evidence could not be returned. There are no tool profiles or alternate retrieval modes to choose. +The result is a complete, ordered answer dossier backed by exact source evidence, or an exact non-ready state naming what could not be proven. There are no tool profiles, fallback searches, or alternate retrieval modes to choose. MCP advertises only the tools capability. It exposes no resources or prompts. @@ -60,9 +60,9 @@ madar query "what calls enqueueInvoice?" --budget 2000 ## What the result means -Results contain authenticated nodes and excerpts, directed relationships, explicit boundaries, and size metrics. `evidence` means the returned path is usable; other outcomes name the focused verification needed instead of implying a path Madar did not prove. +`ready` contains a non-truncated dossier: the normalized query, proven obligations, roots and terminals, direct or channel links, partial-order groups, and SHA-256-authenticated files, excerpts, controls, entities, and proofs. `incomplete`, `unsupported`, `stale`, `unavailable`, and `corrupt` name the exact condition instead of implying a path Madar did not prove. -Results include at most 12 files, 25 snippets, one directional closure pass, and 4,000 serialized tokens. See [MCP response shape](https://github.com/mohanagy/madar/blob/next/docs/mcp-response-shape.md) for the exact envelope. +Results include at most 12 files, 25 authenticated excerpts, two bounded recovery passes, and 4,000 serialized tokens. See [MCP response shape](https://github.com/mohanagy/madar/blob/next/docs/mcp-response-shape.md) for the exact envelope. ## How it works @@ -76,7 +76,7 @@ JavaScript / TypeScript repository retrieve(question, budget?) | v - exact excerpts + directed relationships + ordered claims + authenticated proof ``` `madar generate .` uses one canonical compiler-backed path for `.js`, `.jsx`, `.ts`, and `.tsx`. Other source languages and non-code formats produce no graph facts and are reported as unsupported when they matter to a question. diff --git a/docs/agent-governance.md b/docs/agent-governance.md index 67fe3ae1..38fe8e5c 100644 --- a/docs/agent-governance.md +++ b/docs/agent-governance.md @@ -3,10 +3,10 @@ Madar governance is intentionally small: 1. call `retrieve` once for a repository question, preserving the user's question -2. use only authenticated nodes, exact excerpts, and directed relationships as Madar evidence -3. state every returned evidence boundary -4. make focused source reads only where the result cannot carry the task -5. never convert a partial or unsupported path into a complete claim +2. use a `ready` dossier's obligations, flow, and authenticated evidence as Madar evidence +3. state every exact non-ready `missing`, `reason`, or `failure` +4. make focused source reads only for the named gap +5. never convert a non-ready result into a complete claim The same rules apply to `madar query`, which is the CLI transport for the same retrieval contract. diff --git a/docs/concepts/pipelines.md b/docs/concepts/pipelines.md index 06d1fed5..e62c1f32 100644 --- a/docs/concepts/pipelines.md +++ b/docs/concepts/pipelines.md @@ -20,16 +20,17 @@ source scan ```text question - -> lexical graph anchors - -> one bounded directional closure + -> locate, explain, or workflow obligation plan + -> bounded graph-coherent corridor selection + -> at most two structural/evidence recovery passes -> source hash and range authentication - -> deterministic bounded slice - -> evidence or explicit boundary + -> atomic required-claim and proof packing + -> ready dossier or exact non-ready state ``` -The retrieval pipeline has no profile, planner, recovery engine, semantic reranker, session state, or task-specific product wrapper. +The retrieval pipeline has one deterministic planner, workflow builder, and evidence hydrator. It has no profile, LLM reranker, fallback search, second retrieval engine, session state, or task-specific product wrapper. -Its hard output limits are 12 files, 25 snippets, one closure pass, and 4,000 serialized tokens. +Its hard output limits are 12 files, 25 authenticated excerpts, three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, two recovery passes, and 4,000 serialized tokens. CLI `query`, direct application use, and MCP `retrieve` serialize byte-identical results for the same accepted graph and normalized request. MCP advertises only the tools capability, exactly one tool, and no resources or prompts. @@ -42,4 +43,4 @@ An excerpt is evidence only when: - current file bytes match the canonical SHA-256 hash - the graph line range exists exactly in those bytes -Failures become missing, unsupported, stale, unavailable, corrupt, disconnected, or truncated boundaries. They are never converted into confidence scores. +Missing proof or a selection/budget limit becomes `incomplete`; unsupported intent/source, stale bytes, unavailable source, and corrupt facts retain their exact states. A non-ready response never exposes a partial dossier as answer-ready evidence or converts a gap into a confidence score. diff --git a/docs/core-reset/removal-manifest.yml b/docs/core-reset/removal-manifest.yml index ba728b73..57af5e9b 100644 --- a/docs/core-reset/removal-manifest.yml +++ b/docs/core-reset/removal-manifest.yml @@ -33,16 +33,16 @@ current: base_commit: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa completed_phase_commit: c88823ecbeb6da6284cf74ecbd304e9315ffd4fa production_typescript_files: 44 - production_typescript_loc: 15770 - production_loc_added: 2200 - production_loc_removed: 2149 - production_loc_net: 51 + production_typescript_loc: 15871 + production_loc_added: 2302 + production_loc_removed: 2150 + production_loc_net: 152 npm_files: 102 - npm_packed_bytes: 154210 - npm_unpacked_bytes: 649915 - npm_shasum: a56d34339b674a117f986f987bd192232349c077 - npm_integrity: sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ== - npm_artifact_sha256: 3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d + npm_packed_bytes: 155118 + npm_unpacked_bytes: 653492 + npm_shasum: 6115dd200d5bfca6bfd322993f89c7f1f8bff20a + npm_integrity: sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw== + npm_artifact_sha256: 1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414 measurement_state: source_and_package_exact snapshot_scope: obligation_driven_retrieval_630_candidate release_candidate: @@ -2455,28 +2455,28 @@ items: candidate: source_measurement: production_typescript_files: 44 - production_typescript_loc: 15770 - added: 2200 - removed: 2149 - net: 51 - diff_sha256: 3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a + production_typescript_loc: 15871 + added: 2302 + removed: 2150 + net: 152 + diff_sha256: 0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f replacement_measurement: - source_loc: 1384 - emitted_bytes: 59896 + source_loc: 1424 + emitted_bytes: 60928 package_measurement: files: 102 - packed_bytes: 154210 - unpacked_bytes: 649915 - shasum: a56d34339b674a117f986f987bd192232349c077 - integrity: sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ== - artifact_sha256: 3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d + packed_bytes: 155118 + unpacked_bytes: 653492 + shasum: 6115dd200d5bfca6bfd322993f89c7f1f8bff20a + integrity: sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw== + artifact_sha256: 1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414 local_verification: source_gate: passed replacement_source_loc_gate: passed replacement_gate: passed package_gate: passed - focused_test_files_passed: 4 - focused_tests_passed: 159 + focused_test_files_passed: 7 + focused_tests_passed: 267 typecheck: passed build: passed build_eval: passed @@ -2495,20 +2495,39 @@ items: ci_eval_regression_grounded_percent: 95 final_full_suite: passed full_test_files_passed: 83 - full_tests_passed: 865 + full_tests_passed: 899 coverage: passed - coverage_statements_percent: 85.47 - coverage_statements_covered: 7696 - coverage_statements_total: 9004 - coverage_branches_percent: 79.76 - coverage_branches_covered: 7169 - coverage_branches_total: 8988 - coverage_functions_percent: 91.91 - coverage_functions_covered: 1341 - coverage_functions_total: 1459 - coverage_lines_percent: 88.97 - coverage_lines_covered: 6430 - coverage_lines_total: 7227 + coverage_statements_percent: 85.6 + coverage_statements_covered: 7771 + coverage_statements_total: 9078 + coverage_branches_percent: 79.93 + coverage_branches_covered: 7228 + coverage_branches_total: 9042 + coverage_functions_percent: 92.02 + coverage_functions_covered: 1362 + coverage_functions_total: 1480 + coverage_lines_percent: 89.07 + coverage_lines_covered: 6488 + coverage_lines_total: 7284 + portable_ci_oracle: + corpus_files: 18 + corpus_sha256: 712dff25a9cebcfdb0eb39e8ae381ec2dd20519ca609dc02390eab9ecaf567d6 + source_fingerprint: 11025a70b79251745ceab23a9fe36baa81fbdafeede6f57b95c42cb9da1e3077 + prompts: 14 + ready_results: 14 + mandatory_obligations_per_prompt: 7 + corridor_nodes: 9 + flow_links: 8 + channel_handoffs: 4 + terminal_persistence: MongoRepository.update + flow_sha256: 6cca04d52e590ccccd48e6728ec0744fa7606334034ffbca0002e578a6dcca67 + evidence_sha256: 154efca16be4a163b24cb3812f85cf113c73696805d2de83734c252f8ce656f3 + serialized_tokens_min: 2838 + serialized_tokens_max: 2869 + negative_mutations_rejected: + - wrong_corpus_attestation + - unknown_proof_reference + - missing_terminal_persistence frozen_govalidate_acceptance: passed frozen_govalidate: prompts: 5 @@ -2533,7 +2552,7 @@ items: frozen_govalidate_all_variants: prompts: 14 ready_results: 14 - serialized_tokens_min: 3965 + serialized_tokens_min: 3970 serialized_tokens_max: 3998 all_four_required_queues: true flow_sha256: c34295432be3a54ce7506c2019fd17f3e2ca631c78d578b696234cf981c8592b @@ -2542,9 +2561,9 @@ items: warm_retrieval_reference: warmups: 3 measured_queries: 100 - median_ms: 50.795208 - p95_ms: 53.452208 - max_ms: 56.98625 + median_ms: 48.61237500000061 + p95_ms: 49.48099999999977 + max_ms: 50.91725000000042 independent_review: pending exact_head_ci: pending historical_stop: @@ -2573,14 +2592,14 @@ items: query_specific_rule: forbidden second_retrieval_engine: forbidden dependency_change: forbidden - package_ceiling_change: forbidden + package_ceiling_change: forbidden_beyond_owner_amendment_5153369147 graph_index_or_query_semantics_widening: forbidden npm_publication: forbidden github_release: forbidden tag: forbidden registry_metadata_publication: forbidden main_target: forbidden - notes: 'Issue #630 began from exact protected next commit c88823ecbeb6da6284cf74ecbd304e9315ffd4fa and tree b715764668b4296e9e8ab4da715374f47af137db after #632 completed. The current local candidate replaces rank/slice/traverse with one obligation planner, workflow builder and authenticated evidence hydrator, then returns a deterministic v2 dossier only when every mandatory stage, adjacent async handoff, terminal action and proof is complete. Its source inventory and delta pass. All five field-incident prompts return ready dossiers covering 9 files, 12 excerpts, 12 links, 15 order groups, 21 entities, 20 proofs and all four required queues at 3977 / 3977 / 3979 / 3977 / 3977 tokens; all 14 frozen GoValidate formulations are ready at 3965-3998 tokens with all four queues and stable flow/evidence hashes. The portable real-GoValidate runner passes 14/14 and 100 warm measurements at median 50.795208 ms, p95 53.452208 ms and max 56.98625 ms. The 4-file focused suite passes 159 tests, the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines, and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The replacement trio is 1384 source LOC / 59896 emitted bytes under the owner-amended 61000-byte ceiling; the exact package is 102 files / 154210 packed / 649915 unpacked bytes under the owner-amended 655000-byte unpacked ceiling. The historical stop receipt is https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732; owner amendment https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 supersedes that stop and authorizes active continuation while changing only those two ceilings. All other metrics, constraints and prohibitions remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag or main action is authorized.' + notes: 'Issue #630 began from exact protected next commit c88823ecbeb6da6284cf74ecbd304e9315ffd4fa and tree b715764668b4296e9e8ab4da715374f47af137db after #632 completed. The current local candidate replaces rank/slice/traverse with one obligation planner, workflow builder and authenticated evidence hydrator, then returns a deterministic v2 dossier only when every mandatory stage, adjacent async handoff, terminal action and proof is complete. Its source inventory and delta pass. All five field-incident prompts return ready dossiers covering 9 files, 12 excerpts, 12 links, 15 order groups, 21 entities, 20 proofs and all four required queues at 3977 / 3977 / 3979 / 3977 / 3977 tokens; all 14 frozen GoValidate formulations are ready at 3970-3998 tokens with all four queues and stable flow/evidence hashes. The separate repository-portable CI oracle regenerates an exact attested 18-file committed corpus, returns 14/14 ready dossiers with seven mandatory obligations, a 9-node/8-link corridor, all four handoffs and terminal persistence, and rejects wrong-corpus, unknown-proof and missing-persistence mutations. The real-GoValidate audit passes 14/14 and 100 warm measurements at median 48.61237500000061 ms, p95 49.48099999999977 ms and max 50.91725000000042 ms. The 7-file focused suite passes 267 tests, the full suite and coverage pass 83 files / 899 tests at 85.6% statements, 79.93% branches, 92.02% functions and 89.07% lines, and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The replacement trio is 1424 source LOC / 60928 emitted bytes under the owner-amended 61000-byte ceiling; the exact package is 102 files / 155118 packed / 653492 unpacked bytes under the owner-amended 655000-byte unpacked ceiling. The historical stop receipt is https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732; owner amendment https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 supersedes that stop and authorizes active continuation while changing only those two ceilings. All other metrics, constraints and prohibitions remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag or main action is authorized.' exit_gate: Every mandatory question obligation is proven and packed into one non-truncated ready dossier, or the result returns the exact non-ready state and missing obligations within the unchanged file, excerpt, token, recovery, package, and latency ceilings. - id: no-fallback-qualification-631 diff --git a/docs/core-reset/scorecard.md b/docs/core-reset/scorecard.md index 84635e1d..f8340fac 100644 --- a/docs/core-reset/scorecard.md +++ b/docs/core-reset/scorecard.md @@ -44,7 +44,7 @@ The schema-validated, share-safe receipt was recorded at tooling checkout `250a6 | Retrieval regression #622 | **Passed** | Stabilize equivalent end-to-end report-flow prompts and expose honest asynchronous handoff targets within the unchanged retrieval and package ceilings | [#622](https://github.com/mohanagy/madar/issues/622) completed through [PR #623](https://github.com/mohanagy/madar/pull/623), merged at `6416dbc02cefb3bd79157cf440e420b30dda8cf0`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30452883659), two exact-head no-blocker reviews, CodeRabbit PASS, and zero unresolved threads | | Retrieval regression #625 | **Passed** | Replace phrase-gated recovery with a generic bounded, graph-coherent evidence skeleton/forest without exceeding the inherited package ceilings | [#625](https://github.com/mohanagy/madar/issues/625) completed through [PR #626](https://github.com/mohanagy/madar/pull/626), merged at `b6562b715133304bd46e537b6f39008bc1e02095`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30533140531), independent exact-head review, CodeRabbit PASS, and zero unresolved threads | | Semantic execution index #632 | **Passed** | Authenticated ordered body facts, exact async channels and receiver/type-proven persistence pass every source, graph, indexing, latency, package, CI, review and zero-thread gate | [#632](https://github.com/mohanagy/madar/issues/632); corrective [PR #634](https://github.com/mohanagy/madar/pull/634) passed all six CI jobs, independent review, CodeRabbit and zero unresolved threads, then merged as `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` with tree `b715764668b4296e9e8ab4da715374f47af137db` | -| Obligation-driven retrieval #630 | **In progress** | Return a complete authenticated workflow dossier or exact missing obligations within amended budgets | [#630 amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes only replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; exact source/package measurements, focused 159/159, full 865/865, coverage, 14/14 real-GoValidate, 100-sample warm p95, parity, release, registry, isolation, audit, typecheck/build/build-eval, and governance gates pass; review, exact-head CI and merge remain pending | +| Obligation-driven retrieval #630 | **In progress** | Return a complete authenticated workflow dossier or exact missing obligations within amended budgets | [#630 amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes only replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; exact source/package measurements, focused 267/267, full 899/899, coverage, 14/14 real-GoValidate, 100-sample warm p95, parity, release, registry, isolation, audit, typecheck/build/build-eval, and governance gates pass; review, exact-head CI and merge remain pending | | No-fallback qualification #631 | **Pending** | Installed exact-head package matches or beats the strongest frozen baseline and requires zero repository-tool fallback | [#631](https://github.com/mohanagy/madar/issues/631); blocked on completion of active #630 | | External validation | **Deferred** | Activation, retention, and paid-intent evidence remains required for later stable claims, not this beta | No external-validation claim in `0.40.0-beta.4` | | Beta release | **Published** | Preserve exact beta.4 npm/GitHub release history; any later beta requires separate authorization after #630 and #631 gates | [#627](https://github.com/mohanagy/madar/issues/627); exact commit `9043320cfa08370e5cdd3911bfb9283005aa9912`; package 102 / 159,937 / 639,875; npm `next` is `0.40.0-beta.4`, npm `latest` is `0.32.0` | @@ -211,7 +211,7 @@ The following contract facts are historical. Issues #610 and #612, together with ### Obligation-driven retrieval #630 (active) and #631 (pending) - [#630](https://github.com/mohanagy/madar/issues/630) starts from protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit obligations, bounded recovery, strict answerability and the `madar.retrieve` v2 dossier. -- The active #630 candidate measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`; its replacement planner, workflow and hydrator total 1,384 source LOC / 59,896 emitted bytes, and its exact package is 102 files / 154,210 packed / 649,915 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732), changing only the replacement emitted ceiling from 58,000 to 61,000 bytes and npm unpacked ceiling from 640,000 to 655,000 bytes; both gates now pass. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. All 14 frozen GoValidate formulations are ready at 3,965-3,998 tokens with all four queues, and 100 warm samples pass at 53.452208 ms p95. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending. No publication, release, Registry metadata, tag, or `main` action is authorized. +- The active #630 candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`; its replacement planner, workflow and hydrator total 1,424 source LOC / 60,928 emitted bytes, and its exact package is 102 files / 155,118 packed / 653,492 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732), changing only the replacement emitted ceiling from 58,000 to 61,000 bytes and npm unpacked ceiling from 640,000 to 655,000 bytes; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues; 100 warm samples pass at 49.48099999999977 ms p95. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending. No publication, release, Registry metadata, tag, or `main` action is authorized. - [#631](https://github.com/mohanagy/madar/issues/631) remains pending until active #630 completes. Its installed-package and no-fallback comparison has not started. - Neither issue authorizes provider traffic or spend, npm publication, GitHub Release, Registry metadata, tags, stable/`latest`, or `main`; any real campaign or beta publication requires separate owner authorization. diff --git a/docs/designs/2026-07-19-core-reset.md b/docs/designs/2026-07-19-core-reset.md index f0d2f3a9..4b83d754 100644 --- a/docs/designs/2026-07-19-core-reset.md +++ b/docs/designs/2026-07-19-core-reset.md @@ -494,7 +494,7 @@ The `madar.retrieve` v2 result has six closed states: `ready`, `incomplete`, `un The unchanged public ceilings are 4,000 serialized tokens, 12 files and 25 excerpts. Planning is bounded to three roots, 32 initial candidates, 512 explored nodes and 24 causal hops; recovery is bounded to two passes, 64 total frontier nodes and three alternate seeds. The replacement planner, workflow builder and hydrator must stay at or below 1,500 source LOC and 61,000 emitted bytes, total production source at or below 15,954 LOC, the package at or below 102 files / 165,000 packed / 655,000 unpacked bytes, and warm loaded-graph p95 strictly below 500 ms. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes exactly those two ceilings; every other metric, constraint and prohibition remains unchanged. -The active local candidate measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`. Its replacement trio measures 1,384 source LOC / 59,896 emitted bytes, and the exact package measures 102 files / 154,210 packed / 649,915 unpacked bytes; both pass the amended ceilings. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The portable real-GoValidate runner returns 14/14 ready dossiers at 3,965-3,998 tokens with all four required queues and passes 100 warm samples at 53.452208 ms p95. The owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732). Independent review, exact-head CI, CodeRabbit, zero-thread and protected merge remain pending. No fallback engine, dependency change, provider traffic, npm publication, GitHub Release, Registry metadata, tag, or `main` is authorized. +The active local candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`. Its replacement trio measures 1,424 source LOC / 60,928 emitted bytes, and the exact package measures 102 files / 155,118 packed / 653,492 unpacked bytes; both pass the amended ceilings. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. The repository-portable CI oracle and real-GoValidate audit remain separate evidence: the former regenerates the exact attested 18-file committed corpus, returns 14/14 ready dossiers with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations; the latter returns 14/14 ready dossiers at 3,970-3,998 tokens with all four required queues and passes 100 warm samples at 49.48099999999977 ms p95. The owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732). Independent review, exact-head CI, CodeRabbit, zero-thread and protected merge remain pending. No fallback engine, dependency change, provider traffic, npm publication, GitHub Release, Registry metadata, tag, or `main` is authorized. ## Migration and compatibility diff --git a/docs/indexing-completeness.md b/docs/indexing-completeness.md index d2e627be..51fd6353 100644 --- a/docs/indexing-completeness.md +++ b/docs/indexing-completeness.md @@ -69,8 +69,8 @@ Supplying either threshold enables strict mode; unspecified allowances default t ## Effect on retrieval -`retrieve` authenticates the accepted graph's embedded completeness before ranking any evidence. If the supported JavaScript/TypeScript index is not `complete`, retrieval returns an `unavailable` outcome with a `canonical TypeScript index incomplete` boundary and no graph evidence. It does not calculate confidence or answerability scores. +`retrieve` authenticates the accepted graph's embedded completeness before planning evidence. If the supported JavaScript/TypeScript index is not `complete`, retrieval returns `state: "unavailable"` with a failure subject of `canonical TypeScript index incomplete` and no dossier. It does not calculate confidence or answerability scores. -For an accepted complete index, a recognized unsupported source that matches the question can appear as an `unsupported` boundary beside useful JavaScript/TypeScript evidence. Policy skips and safety exclusions remain visible in the indexing diagnostics; they are not converted into confidence signals. +For an accepted complete index, a recognized unsupported source required by the question returns `state: "unsupported"` rather than mixing unsupported material with a partial dossier. Policy skips and safety exclusions remain visible in the indexing diagnostics; they are not converted into confidence signals. -Completeness is still static evidence, not a runtime trace or a guarantee that every possible answer is present. When a returned boundary identifies unsupported or otherwise unavailable evidence, the agent should state that limitation and perform only the focused verification needed to continue. +Completeness is still static evidence, not a runtime trace or a guarantee that every possible answer is present. When a non-ready state identifies unsupported or otherwise unavailable evidence, the agent should state that limitation and perform only the focused verification needed to continue. diff --git a/docs/integrations/agent-orchestration.md b/docs/integrations/agent-orchestration.md index cdd197f4..30d213e9 100644 --- a/docs/integrations/agent-orchestration.md +++ b/docs/integrations/agent-orchestration.md @@ -6,8 +6,8 @@ Madar should narrow discovery once, not become another layer that every worker r 1. Build or refresh one graph with `madar generate .`. 2. Let one lead agent call `retrieve` once with the user's question unchanged. -3. Use the authenticated excerpts and relationships to define the work. -4. Give worker agents the narrowed files, boundaries, and validation goals. +3. When `state` is `ready`, use the dossier's obligations, flow, and authenticated evidence to define the work. +4. Otherwise, give worker agents only the exact missing requirement or failure and the focused verification goal. 5. Regenerate after structural changes before asking a new repository question. This is guidance, not a host-level enforcement mechanism. @@ -51,25 +51,25 @@ What can break if AuthService.login changes? Preserve directed causal order. To implement password-reset audit logging, identify the current route-to-job path and its exact validation files. ``` -`retrieve` does not switch modes for these questions. The same deterministic rank, traversal, authentication, and slicing rules apply. +`retrieve` does not switch tools for these questions. The same deterministic obligation planning, workflow selection, authentication, and atomic packing rules apply. ## Parallel agents A good split is: -- lead agent: run the one retrieval and identify evidence plus boundaries +- lead agent: run the one retrieval and identify the ready dossier or exact non-ready state - implementation workers: edit only assigned areas - reviewer: verify the diff and tests against the same task, making a new retrieval only if the user asks a genuinely new repository question Do not treat every continuation message as a new retrieval task. A clarification about already-returned evidence can remain in the current agent context. -## Boundaries +## Non-ready states -When Madar reports a missing, unsupported, stale, unavailable, corrupt, disconnected, or truncated boundary: +When Madar reports `incomplete`, `unsupported`, `stale`, `unavailable`, or `corrupt`: -- preserve the evidence it did return -- state the boundary plainly -- verify only the missing load-bearing target +- do not treat it as partial answer evidence +- state the exact missing requirement, reason, or failure +- verify only that load-bearing target - never manufacture a complete causal path Historical benchmark workflows may contain older command names. They are receipts for those recorded versions, not current orchestration guidance. diff --git a/docs/language-capability-matrix.md b/docs/language-capability-matrix.md index 186cc2fd..5945805c 100644 --- a/docs/language-capability-matrix.md +++ b/docs/language-capability-matrix.md @@ -33,15 +33,15 @@ For `.ts`, `.tsx`, `.js`, and `.jsx`, the canonical index can emit framework-sem | tRPC | routers and source-visible query, mutation, and subscription procedures | `trpc_router`, `trpc_procedure_query`, `trpc_procedure_mutation`, `trpc_procedure_subscription` | | Prisma | client ownership plus synthetic source-visible model reads and writes carrying `storage_operation` metadata | `prisma_client`, `prisma_model_reader`, `prisma_model_writer` | -These are static structural hints, not runtime traces. Retrieval can quote named declarations that have canonical declaration ranges. Inline handlers, synthesized tRPC properties, and Prisma operation expressions remain graph-only hints: an exact request for one returns `unavailable` and requires focused source verification. Madar does not relabel an expression as a declaration to manufacture a snippet. Heavily dynamic wrappers, generated routes, and custom meta-programming may remain ordinary symbols or require the same verification. +These are static structural hints, not runtime traces. Retrieval can quote named declarations that have canonical declaration ranges. Inline handlers, synthesized tRPC properties, and Prisma operation expressions remain graph-only hints: an exact request for one returns an exact non-ready state and requires focused source verification. Madar does not relabel an expression as a declaration to manufacture authenticated evidence. Heavily dynamic wrappers, generated routes, and custom meta-programming may remain ordinary symbols or require the same verification. ## Runtime retrieval hints users will notice | Situation | What Madar preserves | Why users care | | --- | --- | --- | | Queue-backed NestJS / BullMQ flows | `enqueues_job` semantic edges preserve a statically visible producer-to-worker handoff | Backend questions can keep the worker path without pretending the producer directly calls the worker | -| Hono / Fastify / tRPC / Prisma workspaces | Conservative request-flow graph hints when routers, named handlers, procedures, and model access stay source-visible; inline or synthesized targets are not snippet evidence | Named declarations can strengthen entrypoint and persistence candidates; exact inline targets require source verification | -| Prisma and repository operations | `storage_operation` metadata marks likely source-visible reads and writes as graph hints | Persistence questions can rank named owners; exact operation expressions remain unavailable as snippets | +| Hono / Fastify / tRPC / Prisma workspaces | Conservative request-flow graph hints when routers, named handlers, procedures, and model access stay source-visible; inline or synthesized targets are not authenticated declaration evidence | Named declarations can strengthen entrypoint and persistence candidates; exact inline targets require source verification | +| Prisma and repository operations | `storage_operation` metadata marks likely source-visible reads and writes as graph hints | Persistence questions can select named owners; exact operation expressions are not authenticated declaration evidence | | Next.js App Router | `runtime_boundary` metadata plus client-component and server-action roles for visible directives | Client/server questions stay aligned with explicit source boundaries | -For outcome definitions, unsupported-file reporting, and strict thresholds, see [Indexing completeness](./indexing-completeness.md). +For retrieval states, unsupported-file reporting, and strict thresholds, see [Indexing completeness](./indexing-completeness.md). diff --git a/docs/mcp-response-shape.md b/docs/mcp-response-shape.md index 98965f7e..99f3edbf 100644 --- a/docs/mcp-response-shape.md +++ b/docs/mcp-response-shape.md @@ -1,72 +1,73 @@ # MCP response shape -`retrieve` returns one deterministic JSON envelope: +`retrieve` returns one deterministic `madar.retrieve` version 2 JSON envelope. A successful locate result has this shape: ```json { "schema": "madar.retrieve", - "version": 1, - "outcome": "evidence", - "matched_nodes": [], - "relationships": [], - "boundaries": [], + "version": 2, + "state": "ready", + "dossier": { + "query": { "intent": "locate", "subject": "generate report", "terms": ["generate", "report"] }, + "obligations": [ + { "id": "o1", "kind": "subject", "statement": "generate report.", "proofs": ["s1"] } + ], + "flow": { + "roots": [], + "terminals": [], + "links": [], + "order": [] + }, + "evidence": { + "digest_algorithm": "sha256-base64url", + "files": [{ "id": "f1", "path": "src/report.ts", "digest": "..." }], + "excerpts": [{ "id": "x1", "file": "f1", "range": [1, 1, 3, 2], "text": "..." }], + "controls": [], + "entities": [{ "id": "s1", "kind": "symbol", "label": "generateReport()", "file": "f1", "excerpt": "x1" }], + "proofs": [] + } + }, "metrics": { - "selected_files": 0, - "snippets": 0, - "closure_passes": 0, - "serialized_tokens": 0, - "truncated": false + "budget_tokens": 4000, + "serialized_tokens": 700, + "selected_files": 1, + "authenticated_excerpts": 1, + "required_obligations": 1, + "proven_obligations": 1, + "optional_bundles_omitted": 0, + "root_candidates": 1, + "initial_candidates": 1, + "explored_nodes": 1, + "causal_hops": 0, + "recovery_frontier_nodes": 0, + "alternate_seeds": 0, + "recovery_passes": 0 } } ``` -## `matched_nodes` +## `dossier` -Each evidence node contains: +- `query` records the normalized `locate`, `explain`, or `workflow` intent, subject, and terms. +- `obligations` contains only proven subject, entry, stage, handoff, behavior, ordering, and terminal claims. Every claim references present entity, proof, link, or order-group IDs. +- `flow` names roots and terminals, direct or channel links, and sequence, branch, loop, parallel, or cycle order groups without inventing one total order. +- `evidence` deduplicates authenticated files, exact excerpts, control ranges, symbols, channels, operations, and direct or channel proof chains. File digests use `sha256-base64url`. -- `node_id` -- `label` -- `node_kind` -- `source_file` -- `source_location` -- `line_number` -- `end_line_number` -- `source_domain` -- non-empty `provenance` -- canonical file `content_hash` -- an exact `snippet` when authenticated +`ready` is allowed only when every mandatory obligation, claim reference, and required proof is present and authenticated. When the plan requires workflow steps or a terminal effect, each adjacent direct call or producer-channel-consumer path and the terminal must also be proven. A ready response is never truncated. -Madar reads the local file, verifies its SHA-256 hash against the canonical graph, then extracts the exact graph-owned line range. It does not truncate or rewrite a returned snippet. +## Non-ready results -## `relationships` +- `incomplete` returns the normalized `query` plus `missing` requirements such as `adjacent_handoff_unproven`, `terminal_persistence_unproven`, or `required_token_budget`. +- `unsupported` returns `reason` (`unsupported_intent`, `missing_subject`, or `unsupported_source`) and normalized `terms`. +- `stale`, `unavailable`, and `corrupt` return `failures`, each with its state and subject. -Each relationship contains its graph edge id, source and target node ids, relation type, non-empty provenance, and source location when available. - -Only relationships whose endpoints survive evidence authentication and output slicing are returned. - -## `boundaries` - -Boundary kinds: - -- `missing` -- `disconnected` -- `unsupported` -- `stale` -- `unavailable` -- `corrupt` -- `truncated` - -Every boundary names a subject and can include a detail. Boundaries can accompany a useful evidence result. - -## `outcome` - -`outcome` is `evidence` whenever at least one authenticated node survives. Otherwise it is the highest-priority terminal source state: `corrupt`, `unavailable`, `stale`, `unsupported`, or `missing`. +All states include the same top-level `schema`, `version`, and `metrics`. Non-ready results do not expose a partial dossier as answer-ready evidence. ## Bounds -- at most 12 source files -- at most 25 snippets -- zero or one directional closure pass +- at most 12 source files and 25 authenticated excerpts - at most 4,000 serialized `cl100k_base` tokens +- at most three roots, 32 initial candidates, 512 explored nodes, and 24 causal hops +- at most two recovery passes, 64 total recovery-frontier nodes, and three alternate seeds The optional requested budget must be a positive integer. Its effective value is clamped between 256 and 4,000 tokens. diff --git a/docs/proof-workflows.md b/docs/proof-workflows.md index 05d3cc87..895ade3e 100644 --- a/docs/proof-workflows.md +++ b/docs/proof-workflows.md @@ -54,6 +54,6 @@ No injected MCP configuration, manual override, or direct JSON-RPC substitute pr - A single good result is a case study, not a universal win. - A failed activation says the integration did not engage; it is not a retrieval-speed result. -- A partial evidence result must preserve its boundary. +- A non-ready result must preserve its exact missing requirement, reason, or failure and never masquerade as partial evidence. - Historical benchmark receipts remain valid for their named version and setup even when their recorded command no longer exists. - Share-safe artifacts are best-effort redactions and must be reviewed before publication. diff --git a/docs/reference/cli-and-mcp.md b/docs/reference/cli-and-mcp.md index acdccef7..5760560b 100644 --- a/docs/reference/cli-and-mcp.md +++ b/docs/reference/cli-and-mcp.md @@ -1,6 +1,6 @@ # CLI and MCP reference -Madar has one retrieval path. MCP clients call `retrieve`; terminal users call `madar query`. For the same accepted graph and normalized request, both return the same byte-identical `madar.retrieve` version 1 envelope. +Madar has one retrieval path. MCP clients call `retrieve`; terminal users call `madar query`. For the same accepted graph and normalized request, both return the same byte-identical `madar.retrieve` version 2 envelope. ## Supported commands @@ -48,7 +48,7 @@ madar query "what calls enqueueInvoice?" --budget 2000 madar query "trace login" --graph out/graph.json ``` -`question` is required and limited to 512 characters. `budget` is an optional positive integer; the effective result is capped at 4,000 serialized tokens, 12 files, 25 snippets, and one directional closure pass. +`question` is required and limited to 512 characters. `budget` is an optional positive integer; the effective result is capped at 4,000 serialized tokens, 12 files, and 25 authenticated excerpts. Planning and graph recovery remain bounded to three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, and two recovery passes. ## MCP @@ -56,7 +56,7 @@ madar query "trace login" --graph out/graph.json | Tool | Input | Result | | --- | --- | --- | -| `retrieve` | `{ "question": string, "budget"?: positive integer }` | Authenticated nodes, directed relationships, explicit boundaries, and metrics | +| `retrieve` | `{ "question": string, "budget"?: positive integer }` | Complete authenticated answer dossier, or an exact non-ready state and gaps | Extra input properties are rejected. There are no MCP resources or prompts. @@ -74,18 +74,18 @@ Example call: The server completes initialization and `tools/list` before it loads reconciliation code. Listing the tool starts one background reconciler for the active repository or linked worktree. The first tool call waits at most 25 seconds for an accepted graph. If the graph is still unavailable, Madar returns the normal canonical `unavailable` result; it never asks the client to retry Madar. -## Result outcomes +## Result states -`outcome` is one of: +`state` is one of: -- `evidence` — authenticated graph evidence survived selection -- `missing` — the graph has no support for the question -- `unsupported` — required source is outside the JavaScript/TypeScript index -- `stale` — source bytes or ranges no longer match the accepted graph +- `ready` — every mandatory obligation, adjacent workflow handoff, terminal effect, claim reference, and authenticated proof is present in one non-truncated dossier +- `incomplete` — `missing` lists the exact unproven obligation or limit +- `unsupported` — the intent, subject, or required source is unsupported +- `stale` — selected source bytes or ranges no longer match the accepted graph - `unavailable` — required local source cannot be read safely - `corrupt` — required graph facts or provenance are malformed -`boundaries` can additionally report `disconnected` and `truncated`. A result can contain useful evidence and boundaries at the same time. See [MCP response shape](../mcp-response-shape.md) for every field. +A `ready` dossier carries proven obligations; roots, terminals, directed links, and ordering groups; authenticated files, excerpts, controls, entities, and proofs; and exact resource metrics. Non-ready results never expose partial evidence as answer-ready. See [MCP response shape](../mcp-response-shape.md) for every field. ## Diagnostics diff --git a/docs/roadmap.md b/docs/roadmap.md index 36bbfabd..d71518d4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -175,7 +175,7 @@ PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory [#630](https://github.com/mohanagy/madar/issues/630) starts from exact protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit question obligations, graph-coherent workflow construction, at most two bounded recovery passes, exact-range hydration, strict answerability, and the `madar.retrieve` v2 dossier. It retains the 4,000-token / 12-file / 25-excerpt ceilings and cannot publish. -The active local candidate replaces the v1 rank/slice/traverse pipeline with one planner, workflow builder and authenticated hydrator. It measures 44 production files / 15,770 LOC at `+2,200/-2,149/net +51` with full-index diff SHA-256 `3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a`. The replacement trio is 1,384 source LOC / 59,896 emitted bytes, and the exact package is 102 files / 154,210 packed / 649,915 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732) and changes exactly replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; both gates now pass. The focused suite passes 4 files / 159 tests; the full suite and coverage pass 83 files / 865 tests at 85.47% statements, 79.76% branches, 91.91% functions and 88.97% lines; and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. All 14 frozen GoValidate formulations are ready at 3,965-3,998 tokens with all four queues, and 100 warm samples pass at 53.452208 ms p95. All other metrics and constraints remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag, or `main` action is authorized. +The active local candidate replaces the v1 rank/slice/traverse pipeline with one planner, workflow builder and authenticated hydrator. It measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`. The replacement trio is 1,424 source LOC / 60,928 emitted bytes, and the exact package is 102 files / 155,118 packed / 653,492 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732) and changes exactly replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues, and 100 warm samples pass at 49.48099999999977 ms p95. All other metrics and constraints remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag, or `main` action is authorized. ## Pending — installed no-fallback qualification #631 diff --git a/docs/security/mcp-threat-model.md b/docs/security/mcp-threat-model.md index 3aea60bb..a3522a07 100644 --- a/docs/security/mcp-threat-model.md +++ b/docs/security/mcp-threat-model.md @@ -18,7 +18,7 @@ Relevant threats include prompt injection in repository content, path traversal, - MCP advertises only the tools capability and exactly one tool, `retrieve`. It exposes no resources or prompts. - Requests are line-bounded and schema-validated. `question` is required and capped at 512 characters; `budget` must be a positive integer. -- Results are capped at 4,000 serialized tokens, 12 files, 25 snippets, and one directional closure pass. +- Results are capped at 4,000 serialized tokens, 12 files, 25 authenticated excerpts, and two bounded recovery passes. - Excerpts are authenticated and returned only when current source bytes match the canonical graph hash and exact range. - Graph and source paths must resolve beneath the accepted exact workspace. - Sensitive path classes are excluded during discovery. This is a path policy, not a content-level secret scanner. diff --git a/docs/tutorials/agent-quickstarts.md b/docs/tutorials/agent-quickstarts.md index 34de8f9d..d4680799 100644 --- a/docs/tutorials/agent-quickstarts.md +++ b/docs/tutorials/agent-quickstarts.md @@ -70,5 +70,5 @@ Fresh install, idempotent reinstall, and uninstall change zero repository bytes. - the running client lists the Madar MCP server. - the capability list contains tools only. - the tool list contains exactly `retrieve`. -- one forced test question produces authenticated evidence or an explicit boundary. -- the client does not present missing, unsupported, stale, unavailable, corrupt, disconnected, or truncated evidence as a complete answer. +- one forced test question produces a `ready` dossier or an exact non-ready state. +- the client never presents `incomplete`, `unsupported`, `stale`, `unavailable`, or `corrupt` as evidence or a complete answer. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md index d4144a53..bd5a57fd 100644 --- a/docs/tutorials/getting-started.md +++ b/docs/tutorials/getting-started.md @@ -23,9 +23,9 @@ cd examples/sample-workspace madar query "how does password reset request enqueue the reset email?" ``` -The result contains authenticated `matched_nodes`, directed `relationships`, explicit `boundaries`, and bounded `metrics`. It uses the same query application as MCP. +The result contains a top-level `state` and bounded `metrics`. `ready` adds a `dossier` with the normalized query, proven obligations, ordered flow, and authenticated evidence. It uses the same query application as MCP. -Madar returns at most 12 files, 25 snippets, and 4,000 serialized tokens. A smaller positive budget is optional: +Madar returns at most 12 files, 25 authenticated excerpts, and 4,000 serialized tokens, with at most two bounded recovery passes. A smaller positive budget is optional: ```bash madar query "how does password reset request enqueue the reset email?" --budget 2000 @@ -65,15 +65,14 @@ Other MCP clients are Registry or manual targets. Register command `madar`, argu ## Expected result behavior -- `outcome: "evidence"` means authenticated graph evidence survived selection. -- `outcome: "missing"` means the graph has no support for the question. -- `outcome: "unsupported"` means required source is outside the JavaScript/TypeScript index. -- `outcome: "stale"` means source bytes or ranges no longer match the graph. -- `outcome: "unavailable"` means required local source cannot be read safely. -- `outcome: "corrupt"` means required graph facts are malformed. -- `boundaries` may additionally report disconnected or truncated evidence. +- `state: "ready"` means every mandatory claim and required workflow handoff or terminal is proven in one non-truncated dossier. +- `state: "incomplete"` returns the exact missing obligation or limit. +- `state: "unsupported"` means the intent, subject, or required source is unsupported. +- `state: "stale"` means selected source bytes or ranges no longer match the graph. +- `state: "unavailable"` means required local source cannot be read safely. +- `state: "corrupt"` means required graph facts are malformed. -Do not treat a partial path as complete. Use the returned evidence first, report its boundary, and make only the focused source read needed to verify what is missing. +Do not treat a non-ready result as partial evidence. Report its exact missing requirement, reason, or failure and make only the focused source read needed to verify it. ## Refresh after changes @@ -98,7 +97,7 @@ madar generate . --watch - **The MCP server is absent:** rerun `madar install claude` or `madar install codex`, restart the client, and inspect its MCP list. - **The installer reports a conflict:** preserve the existing user-owned registration and resolve ownership explicitly; Madar will not overwrite it. - **The result is unsupported:** confirm the load-bearing code is JavaScript or TypeScript. -- **The result is truncated:** ask a narrower question; budgets above 4,000 do not increase the effective cap. +- **The result is incomplete:** inspect its exact `missing` entries; ask a narrower question when the token or selection bound is named. ## Optional next steps diff --git a/docs/tutorials/sample-workspace.md b/docs/tutorials/sample-workspace.md index 4356997f..3b003d19 100644 --- a/docs/tutorials/sample-workspace.md +++ b/docs/tutorials/sample-workspace.md @@ -8,7 +8,7 @@ cd examples/sample-workspace madar query "how does password reset request enqueue the reset email?" ``` -Expected evidence comes from: +The expected ready dossier is grounded in: - `src/routes/account-routes.ts` - `src/services/password-reset-service.ts` @@ -16,4 +16,4 @@ Expected evidence comes from: - `src/jobs/reset-email-job.ts` - `src/notifications/email-gateway.ts` -The exact selected symbols and relationships remain graph-derived. If you edit the sample, run `madar generate . --update` before querying again. +The exact selected entities and flow links remain graph-derived. If you edit the sample, run `madar generate . --update` before querying again. diff --git a/examples/mcp-tool-examples.md b/examples/mcp-tool-examples.md index 7347405d..6e916d41 100644 --- a/examples/mcp-tool-examples.md +++ b/examples/mcp-tool-examples.md @@ -10,7 +10,7 @@ Request: { "name": "retrieve", "arguments": { - "question": "How does payment processing work?" + "question": "Where is capturePayment defined?" } } ``` @@ -20,36 +20,43 @@ The server returns a text content item containing canonical JSON: ```json { "schema": "madar.retrieve", - "version": 1, - "outcome": "evidence", - "matched_nodes": [ - { - "node_id": "payments_capturepayment", - "label": "capturePayment()", - "node_kind": "function", - "source_file": "src/payments/capture-payment.ts", - "source_location": "src/payments/capture-payment.ts:18-46", - "line_number": 18, - "end_line_number": 46, - "source_domain": "production", - "provenance": [{ "extractor": "typescript" }], - "content_hash": "8f9c...", - "snippet": "export async function capturePayment(...) { ... }" + "version": 2, + "state": "ready", + "dossier": { + "query": { "intent": "locate", "subject": "capture payment", "terms": ["capture", "payment"] }, + "obligations": [ + { "id": "o1", "kind": "subject", "statement": "capture payment.", "proofs": ["s1"] } + ], + "flow": { "roots": [], "terminals": [], "links": [], "order": [] }, + "evidence": { + "digest_algorithm": "sha256-base64url", + "files": [{ "id": "f1", "path": "src/payments/capture-payment.ts", "digest": "..." }], + "excerpts": [{ "id": "x1", "file": "f1", "range": [18, 1, 46, 2], "text": "export async function capturePayment(...) { ... }" }], + "controls": [], + "entities": [{ "id": "s1", "kind": "symbol", "label": "capturePayment()", "file": "f1", "excerpt": "x1" }], + "proofs": [] } - ], - "relationships": [], - "boundaries": [], + }, "metrics": { + "budget_tokens": 4000, + "serialized_tokens": 350, "selected_files": 1, - "snippets": 1, - "closure_passes": 0, - "serialized_tokens": 318, - "truncated": false + "authenticated_excerpts": 1, + "required_obligations": 1, + "proven_obligations": 1, + "optional_bundles_omitted": 0, + "root_candidates": 1, + "initial_candidates": 1, + "explored_nodes": 1, + "causal_hops": 0, + "recovery_frontier_nodes": 0, + "alternate_seeds": 0, + "recovery_passes": 0 } } ``` -Use the exact returned excerpts as evidence. Do not treat the illustrative ids or hashes above as real repository facts. +Use the exact returned dossier as evidence. Do not treat the illustrative IDs, digest, or source text above as real repository facts. ## Bounded request @@ -65,72 +72,34 @@ Use the exact returned excerpts as evidence. Do not treat the illustrative ids o The requested budget is optional and must be a positive integer. The effective serialized result is always capped at 4,000 tokens. -## Partial evidence +## Non-ready result -A useful result can include a boundary: +If the load-bearing worker is outside the JavaScript/TypeScript index, Madar does not return partial answer evidence. The following shows the relevant response fields; it is abridged, and the canonical response also contains the complete bounded `metrics` object: ```json { "schema": "madar.retrieve", - "version": 1, - "outcome": "evidence", - "matched_nodes": [ - { - "node_id": "billing_enqueueSettlement", - "label": "enqueueSettlement()", - "node_kind": "function", - "source_file": "src/billing/enqueue-settlement.ts", - "source_location": "src/billing/enqueue-settlement.ts:12-30", - "line_number": 12, - "end_line_number": 30, - "source_domain": "production", - "provenance": [{ "extractor": "typescript" }], - "content_hash": "26a1...", - "snippet": "export async function enqueueSettlement(...) { ... }" - } - ], - "relationships": [], - "boundaries": [ - { - "kind": "unsupported", - "subject": "worker/settlement.go", - "detail": "The load-bearing worker is outside the JavaScript/TypeScript index." - } - ], - "metrics": { - "selected_files": 1, - "snippets": 1, - "closure_passes": 0, - "serialized_tokens": 421, - "truncated": false - } + "version": 2, + "state": "unsupported", + "reason": "unsupported_source", + "terms": ["settlement", "worker"] } ``` -Preserve the returned evidence, state the unsupported boundary, and verify only the missing load-bearing phase. +State the exact reason and verify only the missing load-bearing phase. ## Stale graph +This is likewise an abridged view of the relevant response fields; the canonical response also contains the complete bounded `metrics` object: + ```json { "schema": "madar.retrieve", - "version": 1, - "outcome": "stale", - "matched_nodes": [], - "relationships": [], - "boundaries": [ - { - "kind": "stale", - "subject": "src/payments/capture-payment.ts" - } - ], - "metrics": { - "selected_files": 0, - "snippets": 0, - "closure_passes": 0, - "serialized_tokens": 96, - "truncated": false - } + "version": 2, + "state": "stale", + "failures": [ + { "state": "stale", "subject": "src/payments/capture-payment.ts" } + ] } ``` @@ -139,7 +108,7 @@ Run `madar generate . --update`, then retry the same question. ## CLI equivalent ```bash -madar query "How does payment processing work?" +madar query "Where is capturePayment defined?" madar query "Trace invoice retry scheduling." --budget 2000 ``` diff --git a/examples/why-madar.md b/examples/why-madar.md index f4ceb950..01d3ea45 100644 --- a/examples/why-madar.md +++ b/examples/why-madar.md @@ -6,13 +6,13 @@ Large repositories make coding agents spend early turns rediscovering routes, se ```text madar generate . -madar install +madar install claude retrieve(question, budget?) ``` -For one repository question, Madar ranks graph anchors, follows one bounded directed closure, verifies source bytes against the canonical graph, and returns exact excerpts plus relationships. +For one repository question, Madar plans explicit locate, explain, or workflow obligations; selects a graph-coherent corridor; performs at most two bounded recovery passes; verifies exact source bytes and ranges; and atomically packs required claims with their proof. -The same call can return explicit missing, disconnected, unsupported, stale, unavailable, corrupt, or truncated boundaries. That is more useful than hiding an incomplete path behind a confidence label. +The same call returns either one complete, non-truncated `ready` dossier or the exact `incomplete`, `unsupported`, `stale`, `unavailable`, or `corrupt` state. That is more useful than hiding a gap behind a confidence label or presenting partial evidence as complete. ## What it does not do diff --git a/src/application/evidence-hydrator.ts b/src/application/evidence-hydrator.ts index 4bcab413..c20f1af9 100644 --- a/src/application/evidence-hydrator.ts +++ b/src/application/evidence-hydrator.ts @@ -120,8 +120,8 @@ function ready(i: ReadyQueryIndex, q: EvidenceHydrationTargets): HydratedEvidenc return ref } const fact = (v: IndexBodyFact, exact: boolean, keep = true): [string, string] => { - const owner = v.owner_symbol_id, had = ents.has(owner), own = ent(owner) - if ((!exact || !keep) && !had) ents.delete(owner) + const owner = v.owner_symbol_id, own = ents.get(owner)?.[0] + ?? (exact && keep ? ent(owner) : '') if (v.kind === 'call' && v.target_symbol_id) { node(v.target_symbol_id) if (ents.has(v.target_symbol_id)) used.add(ent(v.target_symbol_id)) diff --git a/src/application/retrieve-context.ts b/src/application/retrieve-context.ts index 2cf3c002..341d9f16 100644 --- a/src/application/retrieve-context.ts +++ b/src/application/retrieve-context.ts @@ -6,7 +6,7 @@ import { import { canonicalJsonString as json, compareCodeUnits as cmp } from '../domain/graph/canonical-json.js' import type { IndexBodyFact, IndexValue } from '../domain/index/model.js' import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' -import { planQuestion } from '../domain/query/plan.js' +import { lexicalTokens, planQuestion } from '../domain/query/plan.js' import { selectWorkflow, } from '../domain/query/workflow.js' @@ -46,8 +46,11 @@ function stat( flow?: WorkflowSelection, auth?: ReadyHydration, gaps = new Set(), + plan?: QueryPlan, ): RetrieveMetrics { - const must = flow?.obligations.filter(({ mandatory }) => mandatory) ?? [] + const selected = flow?.obligations ?? [] + const must = plan?.obligations.filter(({ mandatory }) => mandatory) + ?? selected.filter(({ mandatory }) => mandatory) const data = flow?.metrics const roots = data?.rootCandidateCount ?? 0 return { @@ -56,7 +59,10 @@ function stat( selected_files: auth?.files.size ?? 0, authenticated_excerpts: auth?.excerpts.size ?? 0, required_obligations: must.length, - proven_obligations: must.filter(({ proven, id }) => proven && !gaps.has(id)).length, + proven_obligations: must.filter((required) => !gaps.has(required.id) + && selected.some((candidate) => candidate.mandatory && candidate.proven + && candidate.id === required.id && candidate.kind === required.kind + && candidate.target === required.target)).length, optional_bundles_omitted: 0, root_candidates: roots, initial_candidates: data?.candidateCount ?? 0, @@ -81,10 +87,11 @@ const base = ( flow?: WorkflowSelection, auth?: ReadyHydration, gaps?: Set, + plan?: QueryPlan, ): { schema: typeof RETRIEVE_RESULT_SCHEMA; version: typeof RETRIEVE_RESULT_VERSION state: S; metrics: RetrieveMetrics } => ({ schema: RETRIEVE_RESULT_SCHEMA, version: RETRIEVE_RESULT_VERSION, - state, metrics: stat(req, flow, auth, gaps), + state, metrics: stat(req, flow, auth, gaps, plan), }) const ask = (plan: QueryPlan): QuerySummary => @@ -158,12 +165,55 @@ function miss( const gaps = new Set(missing.flatMap((entry) => entry.obligation_id ? [entry.obligation_id] : [])) return fit({ - ...base('incomplete', req, flow, auth, gaps), + ...base('incomplete', req, flow, auth, gaps, plan), query: ask(plan), missing, }, req.budget) } +function mandatoryObligationGaps( + plan: QueryPlan, + flow: WorkflowSelection, +): MissingRequirement[] { + const required = plan.obligations.filter(({ mandatory }) => mandatory) + const selected = flow.obligations.filter(({ mandatory, proven }) => mandatory && proven) + const unmatched = new Set(selected.map((_, index) => index)) + const missing: MissingRequirement[] = [] + for (const obligation of required) { + const match = selected.findIndex((candidate, index) => unmatched.has(index) + && candidate.id === obligation.id && candidate.kind === obligation.kind + && candidate.target === obligation.target) + if (match >= 0) unmatched.delete(match) + else missing.push({ + code: 'required_proof_missing', obligation_id: obligation.id, + target: obligation.target, + }) + } + for (const index of unmatched) { + const obligation = selected[index]! + missing.push({ + code: 'required_reference_missing', obligation_id: obligation.id, + target: obligation.target, + }) + } + return missing +} + +function unsupportedSubjectTerms( + index: ReadyQueryIndex, + plan: QueryPlan, + flow: WorkflowSelection, +): string[] | undefined { + if (!flow.missing.some(({ code }) => code === 'subject_unproven')) return undefined + const subject = lexicalTokens(plan.subject) + if (subject.length === 0) return undefined + const matched = index.unsupported_sources.some(({ path }) => { + const tokens = new Set(lexicalTokens(path)) + return subject.every((term) => tokens.has(term)) + }) + return matched ? [...new Set(subject)].sort(cmp) : undefined +} + const fail = ( req: NormalizedRetrieveRequest, state: 'stale' | 'unavailable' | 'corrupt', @@ -563,6 +613,13 @@ export function retrieveContext(index: QueryIndex, input: unknown): RetrieveCont } catch { return fail(req, 'corrupt', 'workflow selection') } + const unsupportedTerms = unsupportedSubjectTerms(index, plan, flow) + if (unsupportedTerms) { + return fit({ + ...base('unsupported', req, flow), + reason: 'unsupported_source', terms: unsupportedTerms, + }, req.budget) + } let auth: HydratedEvidenceResult try { auth = hydrateEvidence(index, select(plan, flow, index)) @@ -579,6 +636,10 @@ export function retrieveContext(index: QueryIndex, input: unknown): RetrieveCont ...(entry.target.length <= 96 ? { target: entry.target } : {}), })), flow, auth) } + const obligationGaps = mandatoryObligationGaps(plan, flow) + if (obligationGaps.length > 0) { + return miss(req, plan, obligationGaps, flow, auth) + } const over = auth.files.size > FILES ? ['required_file_limit', auth.files.size, FILES] as const : auth.excerpts.size > EXCERPTS @@ -592,7 +653,7 @@ export function retrieveContext(index: QueryIndex, input: unknown): RetrieveCont if ('code' in built) return miss( req, plan, [built], flow, auth) const ready = seal({ - ...base('ready', req, flow, auth), + ...base('ready', req, flow, auth, undefined, plan), dossier: built, }) if (ready.metrics.serialized_tokens > req.budget) return miss( diff --git a/src/domain/query/plan.ts b/src/domain/query/plan.ts index e4f93830..bb8c4528 100644 --- a/src/domain/query/plan.ts +++ b/src/domain/query/plan.ts @@ -3,6 +3,7 @@ import type { QueryIntent, QueryObligation, QuestionPlanResult, } from './types.js' +const OWNER = 'file|module|class|function|method|service|handler|worker|component|controller|repository' const sets = (...values: T): { [K in keyof T]: ReadonlySet } => values.map((value) => new Set(value.split(' '))) as { @@ -12,11 +13,11 @@ const [FLOW, LOCATE, EXPLAIN, COMMON] = sets( 'flow workflow pipeline lifecycle generate run execute create build produce process work', 'locate find define declare implement contain handle own write read save set update persist publish consume store use live', 'explain describe work behave operate handle process validate resolve compute calculate score select update apply evaluate mean control do use choose return reject allow call invoke', - 'a an the this that these those it its they them their we our you your i me my he she what which who where when why how does do did is are was were be been being can could would should will may might must get of for with without by in into on at as and or but if then than from through via to after before during while all every any some each please show trace explain describe end complete initial final full entire code repository file module class function method service handler definition declaration implementation behavior happen', + `a an the this that these those it its they them their we our you your i me my he she what which who where when why how does do did is are was were be been being can could would should will may might must get of for with without by in into on at as and or but if then than from through via to after before during while all every any some each please show trace explain describe end complete initial final full entire code ${OWNER.replaceAll('|', ' ')} definition declaration implementation behavior happen`, ) const ACTIONS = new Set([...FLOW, ...LOCATE, ...EXPLAIN, 'complete', 'get', 'happen', 'plan']) const BEHAVIOR = new Set( - 'apply allow calculate call choose compute consume control evaluate invoke persist publish read reject resolve return save score select store update validate write'.split(' '), + 'apply allow calculate choose compute consume control evaluate persist publish read reject resolve return save score select store update validate write'.split(' '), ) const IRREGULAR = new Map('built=build generation=generate got=get getting=get persistence=persist planned=plan planning=plan ran=run running=run setting=set written=write wrote=write'.split(' ').map((pair) => pair.split('=') as [string, string])) @@ -50,12 +51,10 @@ const [FW, LW, EW] = const AUX = 'is|are|was|were|does|do|did|can|could|would|should|will' const CLAUSE = 'when|after|before|on|during|if|from|through|via' const FN = 'flow|workflow|pipeline|lifecycle' -const FV = 'generate|run|execute|create|build|produce|process' -const OWNER = 'file|module|class|function|method|service|handler' - const isNoise = (token: string, mode: QueryIntent): boolean => COMMON.has(token) || (mode === 'workflow' ? FLOW.has(token) - : mode === 'locate' ? LOCATE.has(token) : EXPLAIN.has(token)) + : mode === 'locate' ? LOCATE.has(token) + : EXPLAIN.has(token) && !BEHAVIOR.has(token)) const content = ( value: string, mode: QueryIntent, plain = false, ): string[] => [...new Set(lexicalTokens(value).filter((token) => @@ -85,10 +84,12 @@ function coordinatedFlow(text: string): CoordinatedFlow | undefined { composed = /\b(compose|assemble|render) (.+?)(?= (?:and )?(?:write|save|store|persist)\b|$)/u .exec(text) if (!entry || !end) return undefined - const input = content(entry[1]!, 'workflow'), - output = content(composed?.[2] ?? end[1]!, 'workflow') - const first = input[0], rawLast = output.at(-1), last = composed - && /^(?:model|output|result)$/u.test(rawLast ?? '') ? 'report' : rawLast + const input = content(entry[1]!, 'workflow') + let output = content(composed?.[2] ?? end[1]!, 'workflow') + if (composed && /^(?:model|output|result)$/u.test(output.at(-1) ?? '')) + output = content(end[1]!, 'workflow') + const first = input[0], raw = output.at(-1), + last = /^(?:model|output|result)$/u.test(raw ?? '') ? first : raw if (!first || !last) return undefined const handoff = /\b(?:schedule|enqueue|queue|dispatch|publish|emit)\b/u.test(text) ? 'schedule' : undefined, @@ -167,7 +168,7 @@ function simpleSubject(text: string, mode: 'locate' | 'explain'): string { const locate = mode === 'locate' const rules = locate ? [ RegExp(`\\bwhere (?:(?:${AUX}) )?(.+?)(?= (?:${LW})\\b| (?:${CLAUSE})\\b|$)`), - RegExp(`\\b(?:which (?:${OWNER}) |what )(?:${LW}) (.+?)(?= (?:${CLAUSE})\\b|$)`), + RegExp(`\\b(?:(?:which|what) (?:${OWNER}) |what )(?:${LW}) (.+?)(?= (?:${CLAUSE})\\b|$)`), /\b(?:locate|find)(?: the)? (.+?)(?= (?:definition|declaration|implementation)\b|$)/, /\b(?:definition|declaration|implementation) (?:of|for) (.+)$/, ] : [ @@ -185,19 +186,21 @@ function simpleSubject(text: string, mode: 'locate' | 'explain'): string { export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanResult { const raw = request.question.normalize('NFKC'), text = lexicalTokens(request.question).join(' '), + tokens = text.split(' '), names = [...raw.matchAll(/(? match[1]!), ident = /\bwhere\s+(?:is|are|was|were)\s+[`'"]?([\p{L}_$][\p{L}\p{N}_$.-]*)[`'"]?\s+(?:defined|declared|implemented)\b/iu - .exec(raw)?.[1] + .exec(raw)?.[1], + ownerQuery = RegExp(`\\b(?:which|what) (?:${OWNER}) (?:${LW})\\b`).test(text) const mode: QueryIntent | undefined = - /\b(?:end to end|what happen when)\b|\bfrom\b.+\b(?:through|via)\b.+\bto\b|\btrace\b.+\bfrom\b.+\bto\b/.test(text) + ownerQuery ? 'locate' + : /\b(?:end to end|what happen when)\b|\bfrom\b.+\b(?:through|via)\b.+\bto\b|\btrace\b.+\bfrom\b.+\bto\b/.test(text) || /^follow /.test(text) || /^which .+\b(?:save|write)\b/.test(text) || /\bhow\b[\s\S]*\bgenerat(?:e|ed|es|ing)\b/iu.test(raw) || /\bhow\s+(?:(?:does|do|did|can|could|would|should|will)\s+)?(?!(?:does|do|did|can|could|would|should|will)\b)[\p{L}_$][\p{L}\p{N}_$]*\s+(?:generate|run|execute|create|build|produce|process)\b/iu.test(raw) ? 'workflow' : /\b(?:where|locate|find|definition|declaration|implementation)\b/.test(text) - || RegExp(`\\bwhich (?:${OWNER}) (?:${LW})\\b`).test(text) || RegExp(`\\bwhat (?:${LW})\\b`).test(text) ? 'locate' : /^trace\b|\b(?:flow|workflow|pipeline|lifecycle)\b/.test(text) ? 'workflow' : /\b(?:explain|describe|how|why|behavior)\b|\bwhat (?:does|do|is|are)\b/ @@ -207,7 +210,7 @@ export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanRe if (!mode) { return { status: 'unsupported', reason: 'unsupported_intent', - terms: [...new Set(text.split(' ').filter((token) => !COMMON.has(token)))].sort(), + terms: [...new Set(tokens.filter((token) => !COMMON.has(token)))].sort(), } } const coordinated = mode === 'workflow' ? coordinatedFlow(text) : undefined @@ -224,14 +227,14 @@ export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanRe span.stage = names.flatMap(lexicalTokens).join(' ') } const skip = new Set(ignored) - const terms = new Set((coordinated?.terms ?? lexicalTokens(text)).filter((token) => + const terms = new Set((coordinated?.terms ?? tokens).filter((token) => !isNoise(token, mode) && !skip.has(token))) lexicalTokens(topic).forEach((token) => terms.add(token)) const sorted = [...terms].sort() if (!topic || sorted.length === 0) { return { status: 'unsupported', reason: 'missing_subject', terms: sorted } } - const words = new Set(text.split(' ')) + const words = new Set(tokens) const access: LocateAccess | undefined = mode !== 'locate' ? undefined : ['read', 'find'].some((word) => words.has(word)) ? 'read' : ['write', 'save', 'set', 'update', 'persist', 'store'] @@ -239,9 +242,9 @@ export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanRe const kinds: readonly ObligationKind[] = mode === 'locate' ? ['subject'] : mode === 'explain' ? ['subject', 'behavior'] : ['subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal'] - const rest = sorted.filter((token) => !lexicalTokens(topic).includes(token)) - const verbs = text.split(' ').filter((token) => BEHAVIOR.has(token)) - const behavior = [...new Set(rest.length > 0 ? rest : verbs)].join(' ') + const rest = RegExp(`^(?:what (?:${FW})|how (?:is|are|was|were) .+ (?:${FW}))\\b`) + .test(text) ? [] + : sorted.filter((token) => !lexicalTokens(topic).includes(token)) return { status: 'supported', plan: { @@ -252,8 +255,8 @@ export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanRe : kind === 'stage' ? span.stage ?? topic : kind === 'handoff' ? span.handoff ?? topic : kind === 'terminal' ? span.terminal ?? topic - : kind === 'behavior' && mode === 'explain' && behavior - ? behavior : topic, + : kind === 'behavior' && mode === 'explain' && rest.length + ? rest.join(' ') : topic, mandatory: true, })), ...(access ? { access } : {}), diff --git a/src/domain/query/types.ts b/src/domain/query/types.ts index 6d194a52..6cddf55c 100644 --- a/src/domain/query/types.ts +++ b/src/domain/query/types.ts @@ -245,7 +245,7 @@ export type RetrieveContextResult = missing: L } | RB<'unsupported'> & { - reason: 'unsupported_intent' | 'missing_subject' + reason: 'unsupported_intent' | 'missing_subject' | 'unsupported_source' terms: L } | RB & { diff --git a/src/domain/query/workflow.ts b/src/domain/query/workflow.ts index 3b9cd8c5..1567e113 100644 --- a/src/domain/query/workflow.ts +++ b/src/domain/query/workflow.ts @@ -199,17 +199,20 @@ function buildView(i: ReadyQueryIndex): ExecutionView { pub[6] ? [pub[6], ...binding] : []]) } } - const hidden = new Set() + const d = new Set(), c = new Set() + for (const a of arcs) { + const k = `${a[0]}\0${a[1]}` + if (a[2] === 'channel') c.add(k) + else a[4].forEach((id) => d.add(`${k}\0${id}`)) + } + const h = new Set() arcs = arcs.filter((arc) => arc[2] !== 'channel' || !arc[4].some((id) => { const fact = i.operation_by_id.get(id) if (fact?.kind !== 'call' || !fact.target_symbol_id) return false - const redundant = arcs.some((direct) => direct[0] === arc[0] - && direct[1] === fact.target_symbol_id && direct[2] === 'direct' - && direct[4].includes(id)) - && arcs.some((inner) => inner[0] === fact.target_symbol_id - && inner[1] === arc[1] && inner[2] === 'channel') - if (redundant) arc[3].forEach((edge) => hidden.add(edge[0])) - return redundant + const r = d.has(`${arc[0]}\0${fact.target_symbol_id}\0${id}`) + && c.has(`${fact.target_symbol_id}\0${arc[1]}`) + if (r) arc[3].forEach((edge) => h.add(edge[0])) + return r })) arcs.sort((a, b) => cmp(a[0], b[0]) || cmp(a[1], b[1]) || cmp(a[3][0]![0], b[3][0]![0])) @@ -217,7 +220,7 @@ function buildView(i: ReadyQueryIndex): ExecutionView { for (const arc of arcs) { append(outgoing, arc[0], arc); append(incoming, arc[1], arc) } - const used = new Set([...arcs.flatMap(idsOf), ...hidden]), + const used = new Set([...arcs.flatMap(idsOf), ...h]), blocked = new Set(pubs.filter((edge) => byId.has(edge[1]) && !used.has(edge[0])).map((edge) => edge[1])), v: ExecutionView = [ @@ -387,45 +390,66 @@ function corridor( fwd[1], ends.length === 0 && (fwd[2] || chosen[2]) || pruned] } type PersistenceFact = Extract +type SwitchFact = Extract +function incomingArm( + i: ReadyQueryIndex, a: Arc, c: SwitchFact, +): string | undefined { + if (c.test?.kind !== 'template') return + const [root, ...path] = c.test.parts + if (root?.kind !== 'parameter' || root.position !== 0 + || !path.every((p): p is Extract => + p.kind === 'literal' && typeof p.value === 'string')) return + const pub = a[3][0]!, at = pub[7], call = i.operation_by_id.get(a[4][0]!), + ch = i.channels_by_id.get(pub[2])! + if (at === undefined || call?.kind !== 'call') return + let v: IndexValue | undefined = call.arguments[at] + if (ch.transport.startsWith('bull')) { + const key = path.shift()?.value + if (key === 'name' && ch.channel_kind === 'job' && !path[0]) + v = { kind: 'literal', value: ch.key } + else if (key !== 'data') return + } + for (const p of path) v = v?.kind === 'object' + ? v.entries.find((e) => e.key === p.value)?.value : undefined + if (v?.kind !== 'literal') return + return `case:${Buffer.from(JSON.stringify([ + typeof v.value, v.value, + ])).toString('base64url')}` +} function endFacts( - i: ReadyQueryIndex, arc: Arc, options: readonly PersistenceFact[], + i: ReadyQueryIndex, arc: Arc, facts: readonly PersistenceFact[], ): PersistenceFact[] { - if (arc[2] !== 'channel') return [...options] - const pub = arc[3][0]!, position = pub[7] - if (position === undefined) return [] - const call = i.operation_by_id.get(arc[4][0]!) - if (call?.kind !== 'call') return [] - const transport = i.channels_by_id.get(pub[2])!.transport, - matches = (i.operations_by_owner.get(arc[1]) ?? []).flatMap((cond) => { - if (cond.kind !== 'condition' || cond.condition_kind !== 'switch' - || cond.test?.kind !== 'template') return [] - const [parameter, ...rawPath] = cond.test.parts - if (parameter?.kind !== 'parameter' || parameter.position !== 0 - || rawPath.some((part) => part.kind !== 'literal' - || typeof part.value !== 'string')) return [] - const path = rawPath.map((part) => (part as Extract).value as string) - if (transport === 'bullmq' && path[0] === 'data') path.shift() - let value: IndexValue | undefined = call.arguments[position] - for (const key of path) value = value?.kind === 'object' - ? value.entries.find((entry) => entry.key === key)?.value : undefined - if (value?.kind !== 'literal') return [] - const arm = `case:${Buffer.from(JSON.stringify([ - typeof value.value, value.value, - ])).toString('base64url')}` - const eligible = options.filter((fact) => fact.control.some((frame) => + const cases = (i.operations_by_owner.get(arc[1]) ?? []).filter( + (fact): fact is SwitchFact => fact.kind === 'condition' + && fact.condition_kind === 'switch', + ) + if (!cases[0]) return facts.filter((fact) => !fact.control.length) + const hits = cases.flatMap((cond) => { + const arm = incomingArm(i, arc, cond) + if (!arm) return [] + const valid = facts.filter((fact) => fact.control.some((frame) => frame.kind === 'branch' && frame.controller_fact_id === cond.id && frame.arm === arm)) - return eligible.length > 0 ? [eligible] : [] + return valid[0] ? [valid] : [] }) - return matches.length === 1 ? matches[0]! : [] + return hits.length === 1 ? hits[0]! : [] } function controls( i: ReadyQueryIndex, arcs: readonly Arc[], ends: readonly string[], seeds: readonly string[], ): Control { const ops = i.operation_by_id + const ok = (a: Arc, b: Arc): boolean => b[4].every((id) => { + const op = ops.get(id) + return !op || op.owner_symbol_id !== b[0] + || op.control.every((f) => { + if (f.kind !== 'branch') return true + const ctl = ops.get(f.controller_fact_id) + if (ctl?.kind !== 'condition' || ctl.condition_kind !== 'switch') return true + const arm = incomingArm(i, a, ctl) + return arm === f.arm + }) + }) const factIds = [...new Set(arcs.flatMap((arc) => arc[4]))], core = new Set([...seeds, ...factIds]) const endOps = new Set() @@ -448,6 +472,17 @@ function controls( seqs = new Map[]>() const need = new Set(core) let proven = true + const ins = arcs.filter((arc) => arc[2] === 'channel') + for (const left of ins) { + const outs = arcs.filter((right) => right[0] === left[1]) + if (outs.length > 0 && !outs.some((right) => ok(left, right))) + proven = false + } + for (const right of arcs) { + const froms = ins.filter((left) => left[1] === right[0]) + if (froms.length > 0 && !froms.some((left) => ok(left, right))) + proven = false + } for (const id of need) { const fact = ops.get(id) if (!fact) { proven = false; continue } @@ -593,7 +628,7 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel ])], lastBound = bound('terminal'), stageNeed = bound('stage'), - behaviorNeed = bound('behavior'), + bNeed = bound('behavior'), asyncNeed = words(bound('handoff') ?? '').some((word) => /^(?:async|dispatch|emit|enqueue|event|job|publish|queue|schedule)$/u.test(word)), fail = goals.some((entry) => @@ -634,19 +669,18 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel .slice(0, CANDIDATES), focus = ranks[0]?.[0][0] const entryNeed = bound('entry'), + eligibleRoots = (ids: readonly string[]): readonly string[] => entryNeed + ? pickIds(v, ids, entryNeed, 'entry') : ids, entryPool = isFlow ? ranks.filter((entry) => !v[5].has(entry[0][0])) : [], entryIds = entryNeed - ? new Set(pickIds(v, entryPool.map((entry) => entry[0][0]), - entryNeed, 'entry')) : undefined + ? new Set(eligibleRoots(entryPool.map((entry) => entry[0][0]))) : undefined let entries = entryPool.filter((entry) => !entryIds || entryIds.has(entry[0][0])).slice(0, 3) let scan: ReturnType | undefined if (isFlow && ranks.length > 0 && (entries.length === 0 || entries.every((entry) => entry[0][7] !== 'production'))) { scan = scanRoots(v, ranks, goals) - const eligible = entryNeed - ? new Set(pickIds(v, scan[0], entryNeed, 'entry')) : undefined - const recovered = scan[0].filter((id) => !eligible || eligible.has(id)) + const recovered = eligibleRoots(scan[0]) .map((id) => cand(v[1].get(id)!)) entries = [...new Map([...recovered, ...entries].map((entry) => [entry[0][0], entry])).values()].slice(0, 3) @@ -724,11 +758,9 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel scan[1].forEach((id) => { rec.add(id); seen.add(id) }) bounded ||= scan[2] passes = 1 - const eligible = entryNeed - ? new Set(pickIds(v, scan[0], entryNeed, 'entry')) : undefined const alternates = flow[2].length === 0 && !locked - ? scan[0].filter((id) => id !== roots[0] - && (!eligible || eligible.has(id))).slice(0, 3 - tries) : [] + ? eligibleRoots(scan[0]).filter((id) => id !== roots[0]) + .slice(0, 3 - tries) : [] if (alternates.length > 0) passes = 2 for (const id of alternates) { tries += 1 @@ -820,12 +852,17 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel .filter((id) => chosenOps.has(id)).sort(cmp) : related(stageNodes, true) : ctl[0], stageEdges = stage ? [...new Set(stage[1].flatMap(idsOf))].sort(cmp) : edgeIds, - behaviorIds = behaviorNeed - ? pickIds(v, [...flow[0]], behaviorNeed, 'behavior') : behaviors, - allBehavior = related(behaviors), - behaviorOps = behaviorNeed ? related(behaviorIds, true) : allBehavior + aOps = related(behaviors), + bTerms = words(bNeed ?? ''), + bOps = bTerms[0] ? aOps.filter((id) => { + const fact = ops.get(id) + return fact?.kind === 'call' && bTerms.every((word) => + words(fact.callee).includes(word) + || v[1].get(fact.target_symbol_id ?? '')?.[3].has(word)) + }) : aOps, + bReady = !bTerms[0] || !!bOps[0] const inert = behaviors.filter((id) => !owners.has(id) - && !allBehavior.some((operation) => + && !aOps.some((operation) => ops.get(operation)?.owner_symbol_id === id)) const gaps = causal.filter((id) => v[4].has(id)) type ProofData = readonly [ @@ -839,11 +876,11 @@ export function selectWorkflow(i: ReadyQueryIndex, plan: QueryPlan): WorkflowSel stage: [stageNodes, stageOps, steps.length > 0 && (!stageNeed || stageNodes.length > 0)], handoff: [causal, isFlow ? arcOps : related(causal), - links.length > 0 && (!isFlow || gaps.length === 0) + links.length > 0 && ctl[2] && (!isFlow || gaps.length === 0) && (!asyncNeed || links.some((arc) => arc[2] === 'channel'))], - behavior: [behaviorIds, behaviorOps, + behavior: [bReady ? behaviors : [], bOps, behaviors.length > 0 && inert.length === 0 - && (!behaviorNeed || behaviorIds.length > 0)], + && bReady], ordering: [steps, arcOps, links.length > 0 && gaps.length === 0 && ctl[2] && links.every((arc) => arc[4].length > 0)], diff --git a/tests/unit/agent-governance-doc.test.ts b/tests/unit/agent-governance-doc.test.ts index 8b65fd06..672a4e97 100644 --- a/tests/unit/agent-governance-doc.test.ts +++ b/tests/unit/agent-governance-doc.test.ts @@ -4,13 +4,13 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' describe('agent governance documentation', () => { - it('requires one retrieve call, authenticated evidence, and explicit boundaries', () => { + it('requires one retrieve call, a ready dossier, and exact non-ready gaps', () => { const doc = readFileSync(resolve('docs/agent-governance.md'), 'utf8') expect(doc).toContain('call `retrieve` once') - expect(doc).toContain('authenticated nodes') - expect(doc).toContain('directed relationships') - expect(doc).toContain('state every returned evidence boundary') + expect(doc).toContain('`ready` dossier') + expect(doc).toContain('authenticated evidence') + expect(doc).toContain('exact non-ready `missing`, `reason`, or `failure`') expect(doc).toContain('guidance, not enforcement') expect(doc).not.toContain('context_pack') expect(doc).not.toContain('pack_confidence') diff --git a/tests/unit/canonical-index-execution-review-regressions.test.ts b/tests/unit/canonical-index-execution-review-regressions.test.ts index 0e7e0053..663393f1 100644 --- a/tests/unit/canonical-index-execution-review-regressions.test.ts +++ b/tests/unit/canonical-index-execution-review-regressions.test.ts @@ -2935,16 +2935,17 @@ export function outerNest(publisher: Publisher) { )).toMatchObject({ state: 'ready' }) }) - it('scopes unresolved import mutation authority to the exact module binding', () => { - const publisher = `import { Queue } from 'bullmq' + const unresolvedImportPublisher = `import { Queue } from 'bullmq' type Payload = { trigger: string } const queue = new Queue('sync') export async function publish(data: Payload) { return await queue.add('persist', data) } ` + + it('scopes unresolved import mutation authority to the exact module binding', () => { const unrelated = build({ - 'src/publisher.ts': publisher, + 'src/publisher.ts': unresolvedImportPublisher, 'src/unrelated.ts': `import { Missing } from 'unresolved-package' Missing.value = 1 `, @@ -2956,7 +2957,7 @@ Missing.value = 1 )).not.toEqual([]) const patched = build({ - 'src/publisher.ts': publisher, + 'src/publisher.ts': unresolvedImportPublisher, 'src/patch.ts': `import { Queue } from 'bullmq' Queue.prototype.add = async function() { return {} as never } `, @@ -2968,7 +2969,7 @@ Queue.prototype.add = async function() { return {} as never } )).toEqual([]) const namespacePatched = build({ - 'src/publisher.ts': publisher, + 'src/publisher.ts': unresolvedImportPublisher, 'src/patch.ts': `import * as Bull from 'bullmq' const { Queue: Patched } = Bull Patched.prototype.add = async function() { return {} as never } @@ -2979,9 +2980,10 @@ Patched.prototype.add = async function() { return {} as never } symbol(namespacePatched.nodes, 'publish')[0], 'publishes_to', )).toEqual([]) + }) - for (const [name, patch] of [ - ['shorthand', `import * as Bull from 'bullmq' + it.each([ + ['shorthand', `import * as Bull from 'bullmq' const { Queue } = Bull Queue.prototype.add = async function() { return {} as never } `], @@ -3021,9 +3023,11 @@ declare const key: string const { [key]: Patched } = Bull as Record Patched.prototype.add = async function() { return {} as never } `], - ] as const) { + ] as const)( + 'fails closed for unresolved import mutation through the %s binding', + (name, patch) => { const graph = build({ - 'src/publisher.ts': publisher, + 'src/publisher.ts': unresolvedImportPublisher, [`src/${name}.ts`]: patch, }) expect(outgoing( @@ -3031,10 +3035,12 @@ Patched.prototype.add = async function() { return {} as never } symbol(graph.nodes, 'publish')[0], 'publishes_to', ), name).toEqual([]) - } + }, + ) + it('does not attribute a different unresolved module member to Queue', () => { const otherMemberPatched = build({ - 'src/publisher.ts': publisher, + 'src/publisher.ts': unresolvedImportPublisher, 'src/patch.ts': `import * as Bull from 'bullmq' const { Worker: Patched } = Bull Patched.prototype.close = async function() {} diff --git a/tests/unit/core-reset-governance.test.ts b/tests/unit/core-reset-governance.test.ts index 04c3d928..5527ef4c 100644 --- a/tests/unit/core-reset-governance.test.ts +++ b/tests/unit/core-reset-governance.test.ts @@ -469,22 +469,22 @@ const OBLIGATION_RETRIEVAL_FILES = [ ] as const const OBLIGATION_RETRIEVAL_SOURCE = { production_typescript_files: 44, - production_typescript_loc: 15_770, - production_loc_added: 2_200, - production_loc_removed: 2_149, - production_loc_net: 51, + production_typescript_loc: 15_871, + production_loc_added: 2_302, + production_loc_removed: 2_150, + production_loc_net: 152, } as const const OBLIGATION_RETRIEVAL_PACKAGE = { npm_files: 102, - npm_packed_bytes: 154_210, - npm_unpacked_bytes: 649_915, - npm_shasum: 'a56d34339b674a117f986f987bd192232349c077', + npm_packed_bytes: 155_118, + npm_unpacked_bytes: 653_492, + npm_shasum: '6115dd200d5bfca6bfd322993f89c7f1f8bff20a', npm_integrity: - 'sha512-vvy6RxlvxLlP5e9Go/TqQvMn4M1K7uFxkEs5XteOT1uGiOVIamge00g94zyrZg8ytB7i//KqJ45Y93KI538KhQ==', - npm_artifact_sha256: '3d567b7763fab480cdd35ad39f965e9abe5cf872408b10cf586e9ac7143af27d', + 'sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw==', + npm_artifact_sha256: '1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414', } as const const OBLIGATION_RETRIEVAL_DIFF_SHA256 = - '3c3374453f05cb221248dad07debffa8179f882c11ea9901408dcc707caa7f3a' + '0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f' const OBLIGATION_RETRIEVAL_STOP_RECEIPT = 'https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732' const OBLIGATION_RETRIEVAL_AMENDMENT = @@ -1088,7 +1088,7 @@ describe('core reset governance', () => { expect(design).toContain(EVALUATION_TOOLING_RFC_MERGE_RECEIPT) expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE_TREE) - expect(design).toContain('1,384 source LOC / 59,896 emitted bytes') + expect(design).toContain('1,424 source LOC / 60,928 emitted bytes') expect(design).not.toContain('## Active amendment — generation and incremental index') expect(design).not.toContain('the phase remains active') expect(design).not.toContain('completion evidence remains open') @@ -1157,7 +1157,7 @@ describe('core reset governance', () => { expect(scorecard).toContain('Issues `#622`, `#625`, and `#632` are complete on `next`') expect(scorecard).toContain('supersedes its historical stop and reactivates it under exactly two amended ceilings') expect(scorecard).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) - expect(scorecard).toContain('1,384 source LOC / 59,896 emitted bytes') + expect(scorecard).toContain('1,424 source LOC / 60,928 emitted bytes') expect(scorecard).toContain( 'accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed', ) @@ -2139,8 +2139,8 @@ describe('core reset governance', () => { diff_sha256: OBLIGATION_RETRIEVAL_DIFF_SHA256, }, replacement_measurement: { - source_loc: 1_384, - emitted_bytes: 59_896, + source_loc: 1_424, + emitted_bytes: 60_928, }, package_measurement: { files: OBLIGATION_RETRIEVAL_PACKAGE.npm_files, @@ -2155,8 +2155,8 @@ describe('core reset governance', () => { replacement_source_loc_gate: 'passed', replacement_gate: 'passed', package_gate: 'passed', - focused_test_files_passed: 4, - focused_tests_passed: 159, + focused_test_files_passed: 7, + focused_tests_passed: 267, typecheck: 'passed', build: 'passed', build_eval: 'passed', @@ -2175,20 +2175,41 @@ describe('core reset governance', () => { ci_eval_regression_grounded_percent: 95, final_full_suite: 'passed', full_test_files_passed: 83, - full_tests_passed: 865, + full_tests_passed: 899, coverage: 'passed', - coverage_statements_percent: 85.47, - coverage_statements_covered: 7_696, - coverage_statements_total: 9_004, - coverage_branches_percent: 79.76, - coverage_branches_covered: 7_169, - coverage_branches_total: 8_988, - coverage_functions_percent: 91.91, - coverage_functions_covered: 1_341, - coverage_functions_total: 1_459, - coverage_lines_percent: 88.97, - coverage_lines_covered: 6_430, - coverage_lines_total: 7_227, + coverage_statements_percent: 85.6, + coverage_statements_covered: 7_771, + coverage_statements_total: 9_078, + coverage_branches_percent: 79.93, + coverage_branches_covered: 7_228, + coverage_branches_total: 9_042, + coverage_functions_percent: 92.02, + coverage_functions_covered: 1_362, + coverage_functions_total: 1_480, + coverage_lines_percent: 89.07, + coverage_lines_covered: 6_488, + coverage_lines_total: 7_284, + portable_ci_oracle: { + corpus_files: 18, + corpus_sha256: '712dff25a9cebcfdb0eb39e8ae381ec2dd20519ca609dc02390eab9ecaf567d6', + source_fingerprint: '11025a70b79251745ceab23a9fe36baa81fbdafeede6f57b95c42cb9da1e3077', + prompts: 14, + ready_results: 14, + mandatory_obligations_per_prompt: 7, + corridor_nodes: 9, + flow_links: 8, + channel_handoffs: 4, + terminal_persistence: 'MongoRepository.update', + flow_sha256: '6cca04d52e590ccccd48e6728ec0744fa7606334034ffbca0002e578a6dcca67', + evidence_sha256: '154efca16be4a163b24cb3812f85cf113c73696805d2de83734c252f8ce656f3', + serialized_tokens_min: 2_838, + serialized_tokens_max: 2_869, + negative_mutations_rejected: [ + 'wrong_corpus_attestation', + 'unknown_proof_reference', + 'missing_terminal_persistence', + ], + }, frozen_govalidate_acceptance: 'passed', frozen_govalidate: { prompts: 5, @@ -2210,7 +2231,7 @@ describe('core reset governance', () => { frozen_govalidate_all_variants: { prompts: 14, ready_results: 14, - serialized_tokens_min: 3_965, + serialized_tokens_min: 3_970, serialized_tokens_max: 3_998, all_four_required_queues: true, flow_sha256: 'c34295432be3a54ce7506c2019fd17f3e2ca631c78d578b696234cf981c8592b', @@ -2220,9 +2241,9 @@ describe('core reset governance', () => { warm_retrieval_reference: { warmups: 3, measured_queries: 100, - median_ms: 50.795208, - p95_ms: 53.452208, - max_ms: 56.98625, + median_ms: 48.61237500000061, + p95_ms: 49.48099999999977, + max_ms: 50.91725000000042, }, independent_review: 'pending', exact_head_ci: 'pending', @@ -2251,6 +2272,29 @@ describe('core reset governance', () => { }, }, }) + const limits = obligationRetrieval.delivery_limits + const packageBudget = obligationRetrieval.npm_package_budget + const candidate = obligationRetrieval.candidate + expect(candidate.replacement_measurement.emitted_bytes) + .toBeLessThanOrEqual(limits.replacement_emitted_bytes_max) + expect(candidate.replacement_measurement.source_loc) + .toBeLessThanOrEqual(limits.replacement_source_loc_max) + expect(candidate.source_measurement.production_typescript_loc) + .toBeLessThanOrEqual(limits.total_production_loc_max) + expect(candidate.source_measurement.net) + .toBeLessThanOrEqual(limits.net_production_loc_max) + expect(candidate.package_measurement.files) + .toBeLessThanOrEqual(packageBudget.files_max) + expect(candidate.package_measurement.packed_bytes) + .toBeLessThanOrEqual(packageBudget.packed_bytes_max) + expect(candidate.package_measurement.unpacked_bytes) + .toBeLessThanOrEqual(packageBudget.unpacked_bytes_max) + expect(limits.replacement_emitted_bytes_max) + .toBe(candidate.amendment.replacement_emitted_bytes_max) + expect(packageBudget.unpacked_bytes_max) + .toBe(candidate.amendment.npm_unpacked_bytes_max) + expect(obligationRetrieval.constraints.package_ceiling_change) + .toBe('forbidden_beyond_owner_amendment_5153369147') const noFallbackQualification = manifest.items.find( (item) => item.id === 'no-fallback-qualification-631', ) as any diff --git a/tests/unit/evidence-hydrator.test.ts b/tests/unit/evidence-hydrator.test.ts index 0ed90f0a..15f8e353 100644 --- a/tests/unit/evidence-hydrator.test.ts +++ b/tests/unit/evidence-hydrator.test.ts @@ -2,7 +2,6 @@ import { createHash } from 'node:crypto' import { mkdirSync, mkdtempSync, - readFileSync, rmSync, symlinkSync, unlinkSync, @@ -467,12 +466,6 @@ describe('selected evidence hydration', () => { expect(hydrateEvidence(value.index, value.targets)).toEqual({ state: 'unavailable', subject: path, }) - const implementation = readFileSync( - new URL('../../src/application/evidence-hydrator.ts', import.meta.url), - 'utf8', - ) - expect(implementation.indexOf('const rel = relative(root, file)')) - .toBeLessThan(implementation.indexOf('buf = readFileSync(file)')) }) it('rejects fatal UTF-8 while distinguishing it from stale bytes', () => { diff --git a/tests/unit/mcp-response-shape-doc.test.ts b/tests/unit/mcp-response-shape-doc.test.ts index ddc47e29..83b54dd9 100644 --- a/tests/unit/mcp-response-shape-doc.test.ts +++ b/tests/unit/mcp-response-shape-doc.test.ts @@ -4,33 +4,36 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' describe('MCP response documentation', () => { - it('documents the deterministic retrieve v1 envelope and its hard boundaries', () => { + it('documents the deterministic retrieve v2 dossier and its hard boundaries', () => { const doc = readFileSync(resolve('docs/mcp-response-shape.md'), 'utf8') expect(doc).toContain('# MCP response shape') expect(doc).toContain('"schema": "madar.retrieve"') - expect(doc).toContain('"version": 1') - expect(doc).toContain('matched_nodes') - expect(doc).toContain('relationships') - expect(doc).toContain('boundaries') + expect(doc).toContain('"version": 2') + expect(doc).toContain('"state": "ready"') + expect(doc).toContain('"dossier"') + expect(doc).toContain('"obligations"') + expect(doc).toContain('"flow"') + expect(doc).toContain('"roots": []') + expect(doc).toContain('"evidence"') + expect(doc).not.toContain('"node_kind": "function"') expect(doc).toContain('metrics') - expect(doc).toContain('content_hash') - expect(doc).toContain('SHA-256') - for (const boundary of [ - 'missing', - 'disconnected', + expect(doc).toContain('sha256-base64url') + for (const state of [ + 'ready', + 'incomplete', 'unsupported', 'stale', 'unavailable', 'corrupt', - 'truncated', ]) { - expect(doc).toContain(`\`${boundary}\``) + expect(doc).toContain(`\`${state}\``) } expect(doc).toContain('at most 12 source files') - expect(doc).toContain('at most 25 snippets') + expect(doc).toContain('25 authenticated excerpts') expect(doc).toContain('at most 4,000') - expect(doc).not.toContain('pack_confidence') - expect(doc).not.toContain('answerability') + expect(doc).toContain('at most two recovery passes') + expect(doc).not.toContain('"version": 1') + expect(doc).not.toContain('matched_nodes') }) }) diff --git a/tests/unit/query-plan.test.ts b/tests/unit/query-plan.test.ts index 499b1cd2..065e42e2 100644 --- a/tests/unit/query-plan.test.ts +++ b/tests/unit/query-plan.test.ts @@ -36,6 +36,24 @@ describe('planQuestion', () => { }) }) + it.each([ + 'Which worker writes retryCount?', + 'Which component writes retryCount?', + 'Which controller writes retryCount?', + 'Which repository writes retryCount?', + 'What function writes retryCount?', + ])('keeps ordinary owner-noun write questions focused: %s', (question) => { + expect(plan(question)).toEqual({ + intent: 'locate', + subject: 'retry count', + terms: ['count', 'retry'], + access: 'write', + obligations: [ + { id: 'o1', kind: 'subject', target: 'retry count', mandatory: true }, + ], + }) + }) + it('preserves action-shaped words inside captured identifier subjects', () => { expect(plan('Where is handleClick defined?').subject).toBe('handle click') expect(plan('Where is updateIndex defined?').subject).toBe('update index') @@ -70,10 +88,10 @@ describe('planQuestion', () => { expect(plan('How does generateInvoice validate input?')).toEqual({ intent: 'explain', subject: 'generate invoice', - terms: ['generate', 'input', 'invoice'], + terms: ['generate', 'input', 'invoice', 'validate'], obligations: [ { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, - { id: 'o2', kind: 'behavior', target: 'input', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input validate', mandatory: true }, ], }) }) @@ -109,8 +127,11 @@ describe('planQuestion', () => { 'What runs the monthly billing close?', 'monthly billing close', ['billing', 'close', 'monthly', 'run'], + 'monthly billing close', ], - ])('plans a bounded responsibility explanation: %s', (question, subject, terms) => { + ])('plans a bounded responsibility explanation: %s', ( + question, subject, terms, behavior = 'send', + ) => { const result = plan(question) expect(result.intent).toBe('explain') @@ -118,6 +139,7 @@ describe('planQuestion', () => { expect(result.terms).toEqual(terms) expect(result.obligations.map(({ kind }) => kind)) .toEqual(['subject', 'behavior']) + expect(result.obligations[1]?.target).toBe(behavior) }) it('plans every explicit workflow obligation', () => { @@ -220,9 +242,9 @@ describe('planQuestion', () => { ) expect(result.intent).toBe('workflow') - expect(result.subject).toBe('idea report') + expect(result.subject).toBe('idea') expect(result.terms).toEqual([ - 'assemble', 'idea', 'report', 'research', 'schedule', + 'assemble', 'idea', 'research', 'schedule', ]) expect(result.obligations.find(({ kind }) => kind === 'entry')?.target) .toBe('request idea') @@ -234,17 +256,43 @@ describe('planQuestion', () => { .toBe('persistence') }) + it('keeps a focused write locator out of workflow planning', () => { + expect(plan('Which method writes retryCount?')).toEqual({ + intent: 'locate', + subject: 'retry count', + terms: ['count', 'retry'], + access: 'write', + obligations: [ + { id: 'o1', kind: 'subject', target: 'retry count', mandatory: true }, + ], + }) + }) + + it('uses the stated terminal object instead of rewriting a generic result', () => { + const result = plan( + 'Which runtime components accept an invoice, compose the result, and write the ledger?', + ) + + expect(result.intent).toBe('workflow') + expect(result.subject).toBe('invoice ledger') + expect(result.terms).toEqual(['assemble', 'invoice', 'ledger']) + expect(result.obligations.find(({ kind }) => kind === 'terminal')?.target) + .toBe('persistence') + }) + it.each([ [ 'How does password policy login create a tenant session?', 'password policy login', ['create', 'login', 'password', 'policy', 'session', 'tenant'], + 'create session tenant', ], [ 'How is the monthly revenue report built?', 'monthly revenue report', ['build', 'monthly', 'report', 'revenue'], + 'monthly revenue report', ], ])('keeps bounded component behavior as an explanation: %s', ( - question, subject, terms, + question, subject, terms, behavior, ) => { const result = plan(question) expect(result.intent).toBe('explain') @@ -252,6 +300,7 @@ describe('planQuestion', () => { expect(result.terms).toEqual(terms) expect(result.obligations.map(({ kind }) => kind)) .toEqual(['subject', 'behavior']) + expect(result.obligations[1]?.target).toBe(behavior) }) it('keeps a get-passive workflow subject ahead of boundary clauses', () => { diff --git a/tests/unit/query-workflow.test.ts b/tests/unit/query-workflow.test.ts index 10664207..0b014d53 100644 --- a/tests/unit/query-workflow.test.ts +++ b/tests/unit/query-workflow.test.ts @@ -3,10 +3,11 @@ import { describe, expect, it } from 'vitest' import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' import type { IndexBodyFact, IndexChannelNode, IndexControlFrame, IndexRange, - IndexScalarValue, IndexValue, + IndexChannelTransport, IndexScalarValue, IndexValue, } from '../../src/domain/index/model.js' import type { ReadyQueryIndex } from '../../src/domain/query/index-status.js' -import type { QueryPlan } from '../../src/domain/query/types.js' +import { planQuestion } from '../../src/domain/query/plan.js' +import type { QueryObligation, QueryPlan } from '../../src/domain/query/types.js' import { selectWorkflow } from '../../src/domain/query/workflow.js' const evidence = { @@ -28,7 +29,7 @@ function triggerPayload(value: IndexScalarValue): IndexValue { } function plan(intent: QueryPlan['intent'], subject = 'idea report'): QueryPlan { - const obligations = intent === 'workflow' ? [ + const obligations: readonly QueryObligation[] = intent === 'workflow' ? [ { id: 'o1', kind: 'subject', target: subject, mandatory: true }, { id: 'o2', kind: 'entry', target: subject, mandatory: true }, { id: 'o3', kind: 'stage', target: subject, mandatory: true }, @@ -40,7 +41,7 @@ function plan(intent: QueryPlan['intent'], subject = 'idea report'): QueryPlan { { id: 'o1', kind: 'subject', target: subject, mandatory: true }, { id: 'o2', kind: 'behavior', target: subject, mandatory: true }, ] : [{ id: 'o1', kind: 'subject', target: subject, mandatory: true }] - return { intent, subject, terms: subject.split(' ').sort(), obligations } as QueryPlan + return { intent, subject, terms: subject.split(' ').sort(), obligations } } class Fixture { @@ -157,17 +158,18 @@ class Fixture { switchSelector( owner: string, id: string, - path: readonly string[] = ['data', 'trigger'], + path: readonly string[] | null = ['data', 'trigger'], + position = 0, ): this { this.add(owner, { ...this.base(owner, id), kind: 'condition', condition_kind: 'switch', - test: { + ...(path ? { test: { kind: 'template', parts: [ - { kind: 'parameter', position: 0 }, + { kind: 'parameter', position }, ...path.map((value): IndexValue => ({ kind: 'literal', value })), ], - }, + } } : {}), }) return this } @@ -222,8 +224,9 @@ class Fixture { publish( from: string, helper: string, channel: string, id: string, args: readonly IndexValue[] = [], dispatchPayloadArgument?: number, + control: readonly IndexControlFrame[] = [], ): this { - this.call(from, helper, id, [], args) + this.call(from, helper, id, control, args) const fact = this.facts.get(id) if (!fact) throw new Error(`Missing publish fact ${id}`) this.graph.addEdge(from, channel, { @@ -532,6 +535,92 @@ describe('deterministic workflow selection', () => { expect(result.obligations.find(({ kind }) => kind === 'behavior')?.proven).toBe(true) }) + it('does not let an input-only call prove an explicit validation behavior', () => { + const explained: QueryPlan = { + intent: 'explain', subject: 'generate invoice', + terms: ['generate', 'input', 'invoice', 'validate'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input validate', mandatory: true }, + ], + } + const fixture = new Fixture() + .symbol('entry', 'generateInvoiceValidateInput') + .symbol('logger', 'logInput') + .call('entry', 'logger') + + const result = selectWorkflow(fixture.index(), explained) + + expect(result.complete).toBe(false) + expect(result.missing).toContainEqual({ + code: 'behavior_unproven', target: 'input validate', obligationId: 'o2', + }) + }) + + it('proves an explicit behavior from the selected operation fact', () => { + const explained: QueryPlan = { + intent: 'explain', subject: 'generate invoice', + terms: ['generate', 'input', 'invoice', 'validate'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input validate', mandatory: true }, + ], + } + const fixture = new Fixture() + .symbol('entry', 'generateInvoice') + .symbol('validateInput', 'validateInput') + .call('entry', 'validateInput') + + const result = selectWorkflow(fixture.index(), explained) + + expect(result.complete).toBe(true) + expect(result.obligations.find(({ kind }) => kind === 'behavior')?.operationIds) + .toEqual(['entry-calls-validateInput']) + }) + + it('does not stitch an explicit behavior across unrelated operations', () => { + const explained: QueryPlan = { + intent: 'explain', subject: 'generate invoice', + terms: ['generate', 'input', 'invoice', 'validate'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input validate', mandatory: true }, + ], + } + const fixture = new Fixture() + .symbol('entry', 'generateInvoice') + .symbol('validateOutput', 'validateOutput') + .symbol('logInput', 'logInput') + .call('entry', 'validateOutput') + .call('entry', 'logInput') + + expect(selectWorkflow(fixture.index(), explained).complete).toBe(false) + }) + + it('proves a compound behavior within one call macro without cross-call stitching', () => { + const planned = planQuestion({ + question: 'How does password policy login create a tenant session?', + budget: 4_000, + }) + expect(planned.status).toBe('supported') + if (planned.status !== 'supported') throw new Error('expected a supported plan') + + const positive = new Fixture() + .symbol('login', 'passwordPolicyLogin') + .symbol('create', 'createSession') + .call('login', 'create') + .literal('create', 'tenant-field', 'tenantId') + expect(selectWorkflow(positive.index(), planned.plan).complete).toBe(true) + + const split = new Fixture() + .symbol('login', 'passwordPolicyLogin') + .symbol('audit', 'createAuditLog') + .symbol('log', 'logTenantSession') + .call('login', 'audit') + .call('login', 'log') + expect(selectWorkflow(split.index(), planned.plan).complete).toBe(false) + }) + it('selects only a complete exact job-to-queue channel macro', () => { const queue: IndexChannelNode = { id: 'queue', node_kind: 'channel', channel_kind: 'queue', @@ -598,6 +687,174 @@ describe('deterministic workflow selection', () => { .toContain('terminal_persistence_unproven') }) + it('accepts unconditional authenticated persistence in a terminal channel consumer', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .persistence('terminal') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .publish('entry', 'enqueue', 'queue', 'publish-report') + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(true) + expect(result.terminalSymbolIds).toEqual(['terminal']) + }) + + it.each([ + ['matching BullMQ', 'bullmq', 'assemble'], + ['mismatching BullMQ', 'bullmq', 'archive'], + ['matching Bull', 'bull', 'assemble'], + ['mismatching Bull', 'bull', 'archive'], + ] as const)('%s job.name terminal cases remain exact', ( + _name, transport: IndexChannelTransport, branch, + ) => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'job-name', ['name']) + .persistenceInCase('terminal', 'job-name', branch) + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport, key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport, key: 'assemble', parent_channel_id: 'queue', + }) + .publish( + 'entry', 'enqueue', 'job', 'publish-report', + [triggerPayload('assembly_complete')], 0, + ) + .route('job', 'queue', 'entry', 'publish-report') + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(branch === 'assemble') + expect(result.terminalSymbolIds).toEqual( + branch === 'assemble' ? ['terminal'] : [], + ) + }) + + it('does not let legacy Bull payload data shadow job.name', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'job-name', ['name']) + .persistenceInCase('terminal', 'job-name', 'archive') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bull', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bull', key: 'assemble', parent_channel_id: 'queue', + }) + .publish('entry', 'enqueue', 'job', 'publish-report', [{ + kind: 'object', entries: [{ + key: 'name', value: { kind: 'literal', value: 'archive' }, + }], + }], 0) + .route('job', 'queue', 'entry', 'publish-report') + .edge('queue', 'terminal', 'consumed_by') + + expect(selectWorkflow(fixture.index(), plan('workflow')).complete).toBe(false) + }) + + it('rejects a non-name BullMQ job selector backed only by payload data', () => { + const fixture = new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue', 'enqueueReportJob') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'job-id', ['id']) + .persistenceInCase('terminal', 'job-id', 'archive') + .channel({ + id: 'queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'reports', + }) + .channel({ + id: 'job', node_kind: 'channel', channel_kind: 'job', + transport: 'bullmq', key: 'assemble', parent_channel_id: 'queue', + }) + .publish('entry', 'enqueue', 'job', 'publish-report', [{ + kind: 'literal', value: 'archive', + }], 0) + .route('job', 'queue', 'entry', 'publish-report') + .edge('queue', 'terminal', 'consumed_by') + + const result = selectWorkflow(fixture.index(), plan('workflow')) + + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual([]) + }) + + it('rejects an intermediate publication guarded by an incompatible payload case', () => { + const fixture = ( + nextCase: string, + selector: readonly string[] | null = ['data', 'trigger'], + position = 0, + ) => new Fixture() + .symbol('entry', 'generateIdeaReport') + .symbol('enqueue-first', 'enqueueFirst') + .symbol('worker', 'processIdeaReport') + .switchSelector('worker', 'worker-trigger', selector, position) + .symbol('enqueue-second', 'enqueueSecond') + .symbol('terminal', 'persistIdeaReport') + .switchSelector('terminal', 'terminal-trigger') + .persistenceInCase('terminal', 'terminal-trigger', 'second_complete') + .channel({ + id: 'first-queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'first', + }) + .channel({ + id: 'second-queue', node_kind: 'channel', channel_kind: 'queue', + transport: 'bullmq', key: 'second', + }) + .publish( + 'entry', 'enqueue-first', 'first-queue', 'publish-first', + [triggerPayload('first_complete')], 0, + ) + .edge('first-queue', 'worker', 'consumed_by') + .publish( + 'worker', 'enqueue-second', 'second-queue', 'publish-second', + [triggerPayload('second_complete')], 0, [{ + kind: 'branch', controller_fact_id: 'worker-trigger', + arm: typedCase(nextCase), + }], + ) + .edge('second-queue', 'terminal', 'consumed_by') + + const result = selectWorkflow( + fixture('different_trigger').index(), plan('workflow'), + ) + expect(result.complete).toBe(false) + expect(result.terminalSymbolIds).toEqual(['terminal']) + expect(result.missing.map(({ code }) => code)) + .toEqual(expect.arrayContaining([ + 'adjacent_handoff_unproven', 'controller_dependency_unproven', + ])) + expect(selectWorkflow( + fixture('first_complete').index(), plan('workflow'), + ).complete).toBe(true) + const unresolved = selectWorkflow( + fixture('first_complete', null).index(), plan('workflow'), + ) + expect(unresolved.complete).toBe(false) + expect(unresolved.missing.map(({ code }) => code)) + .toContain('controller_dependency_unproven') + expect(selectWorkflow( + fixture('first_complete', ['data', 'trigger'], 1).index(), plan('workflow'), + ).complete).toBe(false) + }) + it.each([ ['value', 'section_complete' as IndexScalarValue, 'assembly_complete' as IndexScalarValue], ['type', 1 as IndexScalarValue, '1' as IndexScalarValue], diff --git a/tests/unit/retrieve-context-proof-eviction.test.ts b/tests/unit/retrieve-context-proof-eviction.test.ts index e1aef9b9..cadb5cd1 100644 --- a/tests/unit/retrieve-context-proof-eviction.test.ts +++ b/tests/unit/retrieve-context-proof-eviction.test.ts @@ -35,6 +35,7 @@ const obligation = { id: 'o1' as const, kind: 'subject' as const, target: 'report', mandatory: true, proven: true, symbolIds: ['symbol:report'], operationIds: [], edgeIds: [], } +const readyIndex = { state: 'ready', operation_by_id: new Map() } as never function selection(): WorkflowSelection { return { @@ -115,11 +116,18 @@ function wrapperSelection(withParallelPublisher: boolean): WorkflowSelection { }] : []), ], controlGroups: [], - obligations: [{ - id: 'o1', kind: 'handoff', target: 'report handoff', mandatory: true, - proven: true, symbolIds: ['entry', 'wrapper', 'terminal'], operationIds: [], - edgeIds: edges.map(({ id }) => id), - }], + obligations: [ + { + id: 'o1', kind: 'subject', target: 'report handoff', mandatory: true, + proven: true, symbolIds: ['entry', 'wrapper', 'terminal'], operationIds: [], + edgeIds: edges.map(({ id }) => id), + }, + { + id: 'o2', kind: 'behavior', target: 'report handoff', mandatory: true, + proven: true, symbolIds: ['entry', 'wrapper', 'terminal'], operationIds: [], + edgeIds: edges.map(({ id }) => id), + }, + ], missing: [], metrics: { ...metrics, causalRelationHops: edges.length }, } } @@ -161,7 +169,7 @@ describe('retrieve dossier eviction failures', () => { mocks.selection = selection() mocks.hydration = oversizedHydration(fileCount, excerptCount) - const result = retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext(readyIndex, { question: 'Where is report defined?', budget: 4_000, }) @@ -175,13 +183,13 @@ describe('retrieve dossier eviction failures', () => { it('reports the full ready dossier token count without returning a partial dossier', () => { mocks.selection = selection() mocks.hydration = hydratedReport() - const full = retrieveContext({ state: 'ready' } as never, { + const full = retrieveContext(readyIndex, { question: 'Where is report defined?', budget: 4_000, }) expect(full.state).toBe('ready') if (full.state !== 'ready') return - const constrained = retrieveContext({ state: 'ready' } as never, { + const constrained = retrieveContext(readyIndex, { question: 'Where is report defined?', budget: 256, }) @@ -215,7 +223,7 @@ describe('retrieve dossier eviction failures', () => { ]]), } - expect(retrieveContext({ state: 'ready' } as never, { + expect(retrieveContext(readyIndex, { question: 'Where is report defined?', budget: 4_000, })).toMatchObject({ state: 'incomplete', @@ -224,6 +232,51 @@ describe('retrieve dossier eviction failures', () => { }) }) + it.each([ + [ + 'drops', + [], + [{ code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }], + ], + [ + 'changes the target of', + [{ ...obligation, target: 'unrelated' }], + [ + { code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }, + { code: 'required_reference_missing', obligation_id: 'o1', target: 'unrelated' }, + ], + ], + [ + 'changes the identity of', + [{ ...obligation, id: 'o2' as const }], + [ + { code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }, + { code: 'required_reference_missing', obligation_id: 'o2', target: 'report' }, + ], + ], + [ + 'changes the kind of', + [{ ...obligation, kind: 'behavior' as const }], + [ + { code: 'required_proof_missing', obligation_id: 'o1', target: 'report' }, + { code: 'required_reference_missing', obligation_id: 'o1', target: 'report' }, + ], + ], + ] as const)( + 'fails closed when selection %s a mandatory planned obligation', + (_name, obligations, missing) => { + mocks.selection = { ...selection(), obligations } + mocks.hydration = hydratedReport() + + expect(retrieveContext(readyIndex, { + question: 'Where is report defined?', budget: 4_000, + })).toMatchObject({ + state: 'incomplete', missing, + metrics: { required_obligations: 1, proven_obligations: 0 }, + }) + }, + ) + it('keeps every bounded missing-obligation identity at the minimum budget', () => { mocks.selection = { ...selection(), complete: false, @@ -238,7 +291,7 @@ describe('retrieve dossier eviction failures', () => { entities: new Map(), proofs: new Map(), } - const result = retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext(readyIndex, { question: 'How does report work?', budget: 256, }) expect(result.state).toBe('incomplete') @@ -263,7 +316,7 @@ describe('retrieve dossier eviction failures', () => { entities: new Map(), proofs: new Map(), } - const result = retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext(readyIndex, { question: 'How does report work?', budget: 256, }) @@ -279,7 +332,7 @@ describe('retrieve dossier eviction failures', () => { mocks.selection = wrapperSelection(false) mocks.hydration = wrapperHydration(false) - const result = retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext(readyIndex, { question: 'How does the report handoff work?', budget: 4_000, }) @@ -299,7 +352,7 @@ describe('retrieve dossier eviction failures', () => { mocks.selection = wrapperSelection(true) mocks.hydration = wrapperHydration(true) - const result = retrieveContext({ state: 'ready' } as never, { + const result = retrieveContext(readyIndex, { question: 'How does the report handoff work?', budget: 4_000, }) diff --git a/tests/unit/retrieve-context.test.ts b/tests/unit/retrieve-context.test.ts index 18457716..ca76e256 100644 --- a/tests/unit/retrieve-context.test.ts +++ b/tests/unit/retrieve-context.test.ts @@ -7,7 +7,6 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' -import { performance } from 'node:perf_hooks' import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' import { afterEach, describe, expect, it } from 'vitest' @@ -155,7 +154,7 @@ describe('retrieveContext v2', () => { .toContain('"state":"ready"') }) - it('converges public report-flow paraphrases within the warm p95 gate', () => { + it('converges public report-flow paraphrases on one proven corridor', () => { const index = reportFlowFixture() const active = { question: 'How is an idea report generated? Explain the pipeline flow from request to final report.', @@ -264,14 +263,6 @@ describe('retrieveContext v2', () => { && awaitedEnqueueProof !== undefined && link.proofs.includes(awaitedEnqueueProof.id))).toBe(true) - for (let pass = 0; pass < 3; pass += 1) retrieveContext(index, active) - const samples = Array.from({ length: 20 }, () => { - const start = performance.now() - const result = retrieveContext(index, active) - expect(result.state).toBe('ready') - return performance.now() - start - }).sort((left, right) => left - right) - expect(samples[Math.ceil(samples.length * 0.95) - 1]).toBeLessThan(500) }) it('keeps focused locators declaration-only', () => { @@ -673,6 +664,25 @@ async function persistIdeaReport(repository: MongoRepository) { }) }) + it('returns unsupported when the requested subject exists only in a recognized unsupported source', () => { + const supported = workspace('export const unrelated = true').index + const result = retrieveContext({ + ...supported, + unsupported_sources: [{ path: 'src/checker.go', hash: '0'.repeat(64) }], + }, { + question: 'Where is checker defined?', + budget: 4_000, + }) + + expect(result).toMatchObject({ + schema: 'madar.retrieve', + version: 2, + state: 'unsupported', + reason: 'unsupported_source', + terms: ['checker'], + }) + }) + it('keeps every non-ready terminal state within the effective budget', () => { const unsupported = retrieveContext({ state: 'unavailable', subject: 'ignored' }, { question: 'Compare every architecture in this repository.', budget: 256, diff --git a/tests/unit/why-madar-doc.test.ts b/tests/unit/why-madar-doc.test.ts index cfa20014..a5e92042 100644 --- a/tests/unit/why-madar-doc.test.ts +++ b/tests/unit/why-madar-doc.test.ts @@ -17,10 +17,17 @@ describe('public product copy', () => { expect(readme).toContain('retrieve(question, budget?)') expect(readme).toContain('authenticated repository evidence') - expect(readme).toContain('at most 12 files, 25 snippets') + expect(readme).toContain('at most 12 files, 25 authenticated excerpts') expect(readme).toContain('not a runtime tracer, PR reviewer, vulnerability scanner') expect(why).toContain('## What it does') expect(why).toContain('## What it does not do') + expect(why).toContain('madar install claude') + expect(why).toContain('workflow obligations') + expect(why).toContain('`ready` dossier') + expect(why).toContain('`incomplete`') + expect(why).not.toContain('ranks graph anchors') + expect(why).not.toContain('one bounded directed closure') + expect(why).not.toContain('boundaries') expect(claims).toContain('## Demonstrated today') expect(claims).toContain('## Historical measurements') expect(claims).toContain('## Not yet measured') @@ -34,6 +41,19 @@ describe('public product copy', () => { expect(examples).toContain('exactly one MCP tool') expect(examples).toContain('"name": "retrieve"') expect(examples).toContain('"schema": "madar.retrieve"') + expect(examples).toContain('"version": 2') + expect(examples).toContain('"state": "ready"') + expect(examples).toContain('"dossier"') + expect(examples).toContain('"state": "unsupported"') + expect(examples).toContain('"state": "stale"') + expect(examples).toContain('"flow": { "roots": []') + expect(examples).toContain('it is abridged') + expect(examples).not.toContain('"node_kind": "function"') + expect(examples).not.toContain('"required_obligations": 0') + expect(examples).not.toContain('"version": 1') + expect(examples).not.toContain('matched_nodes') + expect(examples).not.toContain('"outcome"') + expect(examples).not.toContain('"boundaries"') expect(examples).toContain('madar query') for (const retired of [ 'context_pack', diff --git a/tools/eval/core-reset/benchmark.mjs b/tools/eval/core-reset/benchmark.mjs index 7e7716b8..a61ef734 100644 --- a/tools/eval/core-reset/benchmark.mjs +++ b/tools/eval/core-reset/benchmark.mjs @@ -1,17 +1,159 @@ import { createHash } from "node:crypto" -import { readFileSync } from "node:fs" +import { + lstatSync, + readdirSync, + readFileSync, +} from "node:fs" import { performance } from "node:perf_hooks" -import { dirname, resolve } from "node:path" +import { + dirname, + join, + relative, + resolve, +} from "node:path" import { fileURLToPath } from "node:url" import { loadGraphArtifact } from "../../../dist/src/adapters/filesystem/graph-artifact.js" +import { generateIndex } from "../../../dist/src/application/generate-index.js" import { retrieveContext } from "../../../dist/src/application/retrieve-context.js" import { inspectQueryIndex } from "../../../dist/src/domain/query/index-status.js" +import { planQuestion } from "../../../dist/src/domain/query/plan.js" +import { selectWorkflow } from "../../../dist/src/domain/query/workflow.js" const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..") +const frozenCorpusRoot = resolve( + repositoryRoot, + "tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace", +) const graphPath = process.argv[2] -if (!graphPath) { - throw new Error("usage: node tools/eval/core-reset/benchmark.mjs ") + ? resolve(process.argv[2]) + : generateIndex(frozenCorpusRoot).graphPath + +const EXPECTED_CORPUS_SHA256 = + "712dff25a9cebcfdb0eb39e8ae381ec2dd20519ca609dc02390eab9ecaf567d6" +const EXPECTED_GRAPH_SOURCE_FINGERPRINT = + "11025a70b79251745ceab23a9fe36baa81fbdafeede6f57b95c42cb9da1e3077" +const EXPECTED_QUESTIONS_SHA256 = + "2a1f5498f2aab8ededd9c506a7e2471e7d420a265bec6fa107f105fbffb04a22" +const EXPECTED_FLOW_SHA256 = + "6cca04d52e590ccccd48e6728ec0744fa7606334034ffbca0002e578a6dcca67" +const EXPECTED_EVIDENCE_SHA256 = + "154efca16be4a163b24cb3812f85cf113c73696805d2de83734c252f8ce656f3" + +const expectedChannelOrder = [ + "orchestration-queue", + "section-research-queue", + "assembly-queue", + "db-sync-queue", +] +const requiredQueues = [...expectedChannelOrder].sort() +const expectedCorridor = [ + [".generateFromProblem()", "src/modules/ideas/interface/http/idea-generation.controller.ts"], + [".process()", "src/modules/pipeline/workers/orchestrator.worker.ts"], + [".plan()", "src/modules/planning/planner.service.ts"], + [".process()", "src/modules/research/workers/section-research.worker.ts"], + [".researchSection()", "src/modules/research/research-agent.service.ts"], + [".process()", "src/modules/pipeline/assembly/assembly.worker.ts"], + [".assembleReport()", "src/modules/reports/assembly.service.ts"], + [".process()", "src/modules/pipeline/workers/db-sync.worker.ts"], + ["saveStructuredReport()", "src/modules/pipeline/workers/db-sync.worker.ts"], +] +const expectedObligationKinds = [ + "subject", + "entry", + "stage", + "handoff", + "behavior", + "ordering", + "terminal", +] + +function assert(condition, message) { + if (!condition) throw new Error(message) +} + +function hash(value) { + return createHash("sha256").update(JSON.stringify(value)).digest("hex") +} + +function uniqueMap(rows, label) { + const mapped = new Map() + for (const row of rows) { + assert(!mapped.has(row.id), `duplicate ${label} id ${row.id}`) + mapped.set(row.id, row) + } + return mapped +} + +function walkCorpus(path, rows = []) { + for (const entry of readdirSync(path).sort()) { + if (entry === "out") continue + const absolute = join(path, entry) + const stat = lstatSync(absolute) + assert(!stat.isSymbolicLink(), `frozen corpus symlink ${absolute}`) + if (stat.isDirectory()) walkCorpus(absolute, rows) + else { + assert(stat.isFile(), `frozen corpus non-file ${absolute}`) + rows.push({ + path: relative(frozenCorpusRoot, absolute).split("\\").join("/"), + hash: createHash("sha256").update(readFileSync(absolute)).digest("hex"), + }) + } + } + return rows +} + +function assertCorpusAttestation(artifact) { + const corpusRows = walkCorpus(frozenCorpusRoot) + assert(corpusRows.length === 18, `frozen corpus file count ${corpusRows.length}`) + assert(hash(corpusRows) === EXPECTED_CORPUS_SHA256, "frozen corpus attestation drift") + + const metadata = artifact?.metadata + const build = metadata?.index_build + const sources = build?.sources + const sourceRoot = build?.source_root + assert(metadata?.schema_version === 4, "graph schema version drift") + assert(build?.engine_id === "madar-typescript-index-v4-execution-1", "graph engine drift") + assert(/^[0-9a-f]{64}$/.test(build?.build_id ?? ""), "graph build attestation is missing") + assert( + sources?.fingerprint === EXPECTED_GRAPH_SOURCE_FINGERPRINT, + "graph source fingerprint drift", + ) + assert( + resolve(sourceRoot?.root_path ?? "") === frozenCorpusRoot, + "graph was not generated from the committed #630 corpus", + ) + assert( + sourceRoot?.kind === "primary_worktree" || sourceRoot?.kind === "linked_worktree", + "graph source is not a repository worktree", + ) + assert( + resolve(sourceRoot?.worktree_root ?? "") === repositoryRoot, + "graph source worktree drift", + ) + assert( + sourceRoot?.scope + === "tests/fixtures/pack-quality/runtime-generation-explain-report-flow/workspace", + "graph source scope drift", + ) + assert(build?.completeness?.summary?.state === "complete", "graph is incomplete") + assert(build?.completeness?.summary?.counts?.indexed === 17, "graph indexed count drift") + assert(build?.completeness?.summary?.counts?.failed === 0, "graph has failed sources") + assert(build?.corpus?.supported_files === 17, "graph supported-file count drift") + assert(build?.corpus?.unsupported_files === 0, "graph has unsupported sources") + assert(metadata?.discovery_safety?.summary?.total === 0, "graph has safety exclusions") + + const graphRows = [ + ...(sources?.controls ?? []), + ...(sources?.supported ?? []), + ].map(({ path, hash }) => ({ path, hash })).sort((left, right) => + left.path.localeCompare(right.path)) + assert( + JSON.stringify(graphRows) === JSON.stringify(corpusRows), + "graph source inventory does not match the committed #630 corpus", + ) + assert((sources?.unsupported ?? []).length === 0, "graph source inventory is partial") + return { corpusRows, sourceFingerprint: sources.fingerprint, buildId: build.build_id } } const queryFixture = JSON.parse(readFileSync(resolve( @@ -25,24 +167,219 @@ const questions = [ ...queryFixture.distant_paraphrases, ...queryFixture.field_incident_variants, ] -const requiredQueues = [ - "assembly-queue", - "db-sync-queue", - "orchestration-queue", - "section-research-queue", -] +assert(questions.length === 14, `expected 14 prompts, received ${questions.length}`) +assert(new Set(questions).size === 14, "the 14 frozen prompts must be unique") +assert(hash(questions) === EXPECTED_QUESTIONS_SHA256, "frozen prompt attestation drift") -function assert(condition, message) { - if (!condition) throw new Error(message) +function mandatoryPlan(question) { + const planned = planQuestion({ question, budget: 4_000 }) + assert(planned.status === "supported", `${question}: unsupported plan`) + assert(planned.plan.intent === "workflow", `${question}: non-workflow plan`) + const mandatory = planned.plan.obligations.filter(({ mandatory }) => mandatory) + assert( + JSON.stringify(mandatory.map(({ kind }) => kind)) + === JSON.stringify(expectedObligationKinds), + `${question}: mandatory obligation plan drift`, + ) + assert( + mandatory.length === planned.plan.obligations.length, + `${question}: unexpected optional obligation`, + ) + return { plan: planned.plan, mandatory } } -function hash(value) { - return createHash("sha256").update(JSON.stringify(value)).digest("hex") +function assertReferencesAndCorridor(result, plan, mandatory, question) { + const dossier = result.dossier + const files = uniqueMap(dossier.evidence.files, "file") + const excerpts = uniqueMap(dossier.evidence.excerpts, "excerpt") + const controls = uniqueMap(dossier.evidence.controls, "control") + const entities = uniqueMap(dossier.evidence.entities, "entity") + const proofs = uniqueMap(dossier.evidence.proofs, "proof") + const links = uniqueMap(dossier.flow.links, "link") + uniqueMap(dossier.flow.order, "order") + uniqueMap(dossier.obligations, "obligation") + + assert( + JSON.stringify(dossier.query) === JSON.stringify({ + intent: plan.intent, + subject: plan.subject, + terms: plan.terms, + }), + `${question}: dossier query does not match its plan`, + ) + assert(result.metrics.required_obligations === mandatory.length, `${question}: required count`) + assert(result.metrics.proven_obligations === mandatory.length, `${question}: proven count`) + assert( + JSON.stringify(dossier.obligations.map(({ id, kind }) => ({ id, kind }))) + === JSON.stringify(mandatory.map(({ id, kind }) => ({ id, kind }))), + `${question}: planned mandatory obligations are not a dossier bijection`, + ) + + for (const excerpt of excerpts.values()) { + assert(files.has(excerpt.file), `${question}: excerpt ${excerpt.id} has unknown file`) + assert(excerpt.text.length > 0, `${question}: excerpt ${excerpt.id} is empty`) + } + for (const control of controls.values()) { + assert(files.has(control.file), `${question}: control ${control.id} has unknown file`) + assert(control.ranges.length > 0, `${question}: control ${control.id} has no ranges`) + } + for (const entity of entities.values()) { + if (entity.kind === "symbol") { + assert(files.has(entity.file), `${question}: symbol ${entity.id} has unknown file`) + if (entity.excerpt) { + assert(excerpts.has(entity.excerpt), `${question}: symbol ${entity.id} has unknown excerpt`) + } + } else if (entity.kind === "operation") { + assert(entities.has(entity.owner), `${question}: operation ${entity.id} has unknown owner`) + assert(excerpts.has(entity.excerpt), `${question}: operation ${entity.id} has unknown excerpt`) + } else if (entity.parent) { + const parent = entities.get(entity.parent) + assert(parent?.kind === "channel", `${question}: channel ${entity.id} has unknown parent`) + } + } + for (const proof of proofs.values()) { + assert(entities.has(proof.from), `${question}: proof ${proof.id} has unknown from`) + assert(entities.has(proof.to), `${question}: proof ${proof.id} has unknown to`) + if ("excerpt" in proof) { + assert(excerpts.has(proof.excerpt), `${question}: proof ${proof.id} has unknown excerpt`) + } else { + assert(files.has(proof.file), `${question}: proof ${proof.id} has unknown file`) + } + } + for (const link of links.values()) { + assert(entities.has(link.from), `${question}: link ${link.id} has unknown from`) + assert(entities.has(link.to), `${question}: link ${link.id} has unknown to`) + assert(link.proofs.length > 0, `${question}: link ${link.id} is unproven`) + for (const proof of link.proofs) { + assert(proofs.has(proof), `${question}: link ${link.id} has unknown proof ${proof}`) + } + } + for (const group of dossier.flow.order) { + for (const member of group.members) { + assert( + entities.has(member) || proofs.has(member), + `${question}: order ${group.id} has unknown member ${member}`, + ) + } + if (group.controller) { + const [control, ordinal] = group.controller.split(":") + assert(controls.has(control), `${question}: order ${group.id} has unknown controller`) + assert(Number.isSafeInteger(Number(ordinal)), `${question}: invalid controller ordinal`) + } + for (const proof of group.proofs ?? []) { + assert(proofs.has(proof), `${question}: order ${group.id} has unknown proof ${proof}`) + } + } + + const claimReferences = new Set([...entities.keys(), ...proofs.keys(), ...links.keys()]) + for (const obligation of dossier.obligations) { + assert(obligation.proofs.length > 0, `${question}: obligation ${obligation.id} is unproven`) + for (const proof of obligation.proofs) { + assert( + claimReferences.has(proof), + `${question}: obligation ${obligation.id} has unknown proof ${proof}`, + ) + } + } + + assert(dossier.flow.roots.length === 1, `${question}: expected one request root`) + assert(dossier.flow.terminals.length === 1, `${question}: expected one terminal`) + assert(dossier.flow.links.length === 8, `${question}: corridor link count drift`) + let current = dossier.flow.roots[0] + const corridor = [current] + for (const link of dossier.flow.links) { + assert(link.from === current, `${question}: corridor is not ordered at ${link.id}`) + current = link.to + corridor.push(current) + } + assert(current === dossier.flow.terminals[0], `${question}: corridor misses terminal`) + assert(new Set(corridor).size === corridor.length, `${question}: corridor repeats an entity`) + + const observedCorridor = corridor.map((id) => { + const entity = entities.get(id) + assert(entity?.kind === "symbol", `${question}: corridor entity ${id} is not a symbol`) + return [entity.label, files.get(entity.file)?.path] + }) + assert( + JSON.stringify(observedCorridor) === JSON.stringify(expectedCorridor), + `${question}: request/planning/research/assembly/DB-sync corridor drift`, + ) + assert( + JSON.stringify(dossier.flow.links.map(({ kind }) => kind)) + === JSON.stringify(["channel", "direct", "channel", "direct", "channel", "direct", "channel", "direct"]), + `${question}: direct/channel ordering drift`, + ) + + const channelLinks = dossier.flow.links.filter(({ kind }) => kind === "channel") + assert(channelLinks.length === 4, `${question}: expected four channel handoffs`) + const observedQueues = channelLinks.map((link) => { + const chain = link.proofs.map((id) => proofs.get(id)) + const calls = chain.filter(({ relation }) => relation === "calls") + const publishes = chain.filter(({ relation }) => relation === "publishes_to") + const routes = chain.filter(({ relation }) => relation === "routes_through") + const consumes = chain.filter(({ relation }) => relation === "consumed_by") + assert(chain.length === 4, `${question}: ${link.id} proof-chain cardinality drift`) + assert(calls.length === 1, `${question}: ${link.id} producer-call proof drift`) + assert(publishes.length === 1, `${question}: ${link.id} publish proof drift`) + assert(routes.length === 1, `${question}: ${link.id} route proof drift`) + assert(consumes.length === 1, `${question}: ${link.id} consume proof drift`) + assert(calls[0].from === link.from, `${question}: ${link.id} producer mismatch`) + assert(calls[0].to === publishes[0].from, `${question}: ${link.id} publisher mismatch`) + assert(publishes[0].to === routes[0].from, `${question}: ${link.id} job mismatch`) + assert(routes[0].to === consumes[0].from, `${question}: ${link.id} queue mismatch`) + assert(consumes[0].to === link.to, `${question}: ${link.id} consumer mismatch`) + const job = entities.get(publishes[0].to) + const queue = entities.get(routes[0].to) + assert(job?.kind === "channel" && job.channel_kind === "job", `${question}: ${link.id} lacks job channel`) + assert(queue?.kind === "channel" && queue.channel_kind === "queue", `${question}: ${link.id} lacks queue channel`) + assert(job.parent === queue.id, `${question}: ${link.id} job/queue parent mismatch`) + assert(job.transport === "bullmq" && queue.transport === "bullmq", `${question}: ${link.id} transport drift`) + return queue.key + }) + assert(new Set(observedQueues).size === 4, `${question}: channel handoffs are not distinct`) + assert( + JSON.stringify(observedQueues) === JSON.stringify(expectedChannelOrder), + `${question}: channel handoff order drift`, + ) + + const terminal = dossier.flow.terminals[0] + const persistence = [...entities.values()].filter((entity) => + entity.kind === "operation" + && entity.operation_kind === "persistence" + && entity.owner === terminal) + assert(persistence.length === 1, `${question}: terminal persistence is not exact`) + assert(persistence[0].detail?.operation === "update", `${question}: terminal persistence operation drift`) + assert( + persistence[0].detail?.receiver_type === "MongoRepository", + `${question}: terminal persistence receiver drift`, + ) + const terminalObligation = dossier.obligations.find(({ kind }) => kind === "terminal") + assert( + terminalObligation?.proofs.includes(persistence[0].id), + `${question}: terminal obligation does not bind persistence evidence`, + ) +} + +function assertSelectionBijection(index, plan, mandatory, question) { + const selected = selectWorkflow(index, plan) + assert(selected.complete, `${question}: selected workflow is incomplete`) + const selectedMandatory = selected.obligations.filter(({ mandatory }) => mandatory) + assert( + JSON.stringify(selectedMandatory.map(({ id, kind, target }) => ({ id, kind, target }))) + === JSON.stringify(mandatory.map(({ id, kind, target }) => ({ id, kind, target }))), + `${question}: selected obligations do not bijectively match the mandatory plan`, + ) + assert( + selectedMandatory.every(({ proven }) => proven), + `${question}: selected mandatory obligation is unproven`, + ) } function retrieve(index, question) { + const { plan, mandatory } = mandatoryPlan(question) const result = retrieveContext(index, { question, budget: 4_000 }) assert(result.state === "ready", `${question}: ${JSON.stringify(result)}`) + assert(result.schema === "madar.retrieve" && result.version === 2, `${question}: result contract drift`) assert(result.metrics.serialized_tokens <= 4_000, `${question}: token ceiling`) assert(result.metrics.selected_files <= 12, `${question}: file ceiling`) assert(result.metrics.authenticated_excerpts <= 25, `${question}: excerpt ceiling`) @@ -52,38 +389,91 @@ function retrieve(index, question) { assert(result.metrics.causal_hops <= 24, `${question}: causal-hop ceiling`) assert(result.metrics.recovery_passes <= 2, `${question}: recovery-pass ceiling`) assert(result.metrics.recovery_frontier_nodes <= 64, `${question}: recovery ceiling`) + assert(result.metrics.alternate_seeds <= 3, `${question}: alternate-seed ceiling`) + assert(result.metrics.optional_bundles_omitted === 0, `${question}: ready dossier was truncated`) + assertSelectionBijection(index, plan, mandatory, question) + assertReferencesAndCorridor(result, plan, mandatory, question) const queues = [...new Set( - JSON.stringify(result.dossier).match(/[a-z]+(?:-[a-z]+)*-queue/g) ?? [], + result.dossier.evidence.entities + .filter((entity) => entity.kind === "channel" && entity.channel_kind === "queue") + .map(({ key }) => key), )].sort() assert( JSON.stringify(queues) === JSON.stringify(requiredQueues), `${question}: required queues ${JSON.stringify(queues)}`, ) - assert( - result.dossier.obligations.every((obligation) => obligation.proofs.length > 0), - `${question}: unproven obligation`, - ) + const flowSha256 = hash(result.dossier.flow) + const evidenceSha256 = hash(result.dossier.evidence) + assert(flowSha256 === EXPECTED_FLOW_SHA256, `${question}: flow attestation drift`) + assert(evidenceSha256 === EXPECTED_EVIDENCE_SHA256, `${question}: evidence attestation drift`) return result } -const index = inspectQueryIndex(loadGraphArtifact(resolve(graphPath))) -assert(index.state === "ready", `graph state: ${index.state}`) -const rows = questions.map((question) => { - const result = retrieve(index, question) - return { - question, - serialized_tokens: result.metrics.serialized_tokens, - flow_sha256: hash(result.dossier.flow), - evidence_sha256: hash(result.dossier.evidence), +function expectRejection(label, callback) { + try { + callback() + } catch { + return label } -}) + throw new Error(`negative mutation was accepted: ${label}`) +} + +const rawArtifact = JSON.parse(readFileSync(graphPath, "utf8")) +const corpusAttestation = assertCorpusAttestation(rawArtifact) +const index = inspectQueryIndex(loadGraphArtifact(graphPath)) +assert(index.state === "ready", `graph state: ${index.state}`) +const results = questions.map((question) => retrieve(index, question)) +const rows = results.map((result, index) => ({ + question: questions[index], + serialized_tokens: result.metrics.serialized_tokens, + flow_sha256: hash(result.dossier.flow), + evidence_sha256: hash(result.dossier.evidence), +})) assert(new Set(rows.map(({ flow_sha256 }) => flow_sha256)).size === 1, "flow drift") assert( new Set(rows.map(({ evidence_sha256 }) => evidence_sha256)).size === 1, "evidence drift", ) -const warmIndex = inspectQueryIndex(loadGraphArtifact(resolve(graphPath))) +const negativeMutations = [] +const wrongCorpus = structuredClone(rawArtifact) +wrongCorpus.metadata.index_build.sources.supported[0].hash = "0".repeat(64) +negativeMutations.push(expectRejection("wrong_corpus_attestation", () => + assertCorpusAttestation(wrongCorpus))) + +const { plan: firstPlan, mandatory: firstMandatory } = mandatoryPlan(questions[0]) +const brokenProofReference = structuredClone(results[0]) +brokenProofReference.dossier.flow.links[0].proofs[0] = "p999" +negativeMutations.push(expectRejection("unknown_proof_reference", () => + assertReferencesAndCorridor( + brokenProofReference, + firstPlan, + firstMandatory, + questions[0], + ))) + +const missingPersistence = structuredClone(results[0]) +const removedPersistenceIds = new Set(missingPersistence.dossier.evidence.entities + .filter(({ kind, operation_kind }) => + kind === "operation" && operation_kind === "persistence") + .map(({ id }) => id)) +missingPersistence.dossier.evidence.entities = missingPersistence.dossier.evidence.entities + .filter(({ id }) => !removedPersistenceIds.has(id)) +for (const obligation of missingPersistence.dossier.obligations) { + obligation.proofs = obligation.proofs.filter((id) => !removedPersistenceIds.has(id)) +} +for (const group of missingPersistence.dossier.flow.order) { + group.members = group.members.filter((id) => !removedPersistenceIds.has(id)) +} +negativeMutations.push(expectRejection("missing_terminal_persistence", () => + assertReferencesAndCorridor( + missingPersistence, + firstPlan, + firstMandatory, + questions[0], + ))) + +const warmIndex = inspectQueryIndex(loadGraphArtifact(graphPath)) for (let pass = 0; pass < 3; pass += 1) { retrieve(warmIndex, queryFixture.beta_3_broad) } @@ -97,13 +487,25 @@ const p95 = nearestRank(0.95) assert(p95 < 500, `warm retrieval p95 ${p95}ms is not below 500ms`) process.stdout.write(`${JSON.stringify({ - graph: resolve(graphPath), + graph: "generated:frozen-issue-630-corpus", + corpus: { + path: relative(repositoryRoot, frozenCorpusRoot).split("\\").join("/"), + files: corpusAttestation.corpusRows.length, + sha256: EXPECTED_CORPUS_SHA256, + source_fingerprint: corpusAttestation.sourceFingerprint, + graph_build_id: corpusAttestation.buildId, + }, prompts: rows.length, + unique_prompts: new Set(rows.map(({ question }) => question)).size, ready: rows.length, + mandatory_obligations_per_prompt: expectedObligationKinds.length, + corridor: expectedCorridor, + channel_handoffs: expectedChannelOrder, + terminal_persistence: "MongoRepository.update", flow_sha256: rows[0].flow_sha256, evidence_sha256: rows[0].evidence_sha256, serialized_tokens: rows.map(({ serialized_tokens }) => serialized_tokens), - required_queues: requiredQueues, + negative_mutations_rejected: negativeMutations, warm: { samples: samples.length, median_ms: nearestRank(0.5), diff --git a/tools/eval/core-reset/verify-isolation.mjs b/tools/eval/core-reset/verify-isolation.mjs index 123dba8d..edae4169 100644 --- a/tools/eval/core-reset/verify-isolation.mjs +++ b/tools/eval/core-reset/verify-isolation.mjs @@ -67,12 +67,25 @@ const manifest = parse(readFileSync( const activePhase = manifest.items?.find( (item) => item.id === manifest.current?.active_phase, ) +const obligationRetrieval = manifest.items?.find( + (item) => item.id === "obligation-driven-retrieval-630", +) const packageBudget = activePhase?.npm_package_budget ?? evaluationPackageBudget function assert(condition, message) { if (!condition) throw new Error(message) } +function physicalLines(path) { + const text = readFileSync(path, "utf8") + return text.length === 0 ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0) +} + +function replacementOutputs(source) { + const stem = source.replace(/^src\//u, "").replace(/\.ts$/u, "") + return [`dist/src/${stem}.js`, `dist/src/${stem}.d.ts`] +} + const inventory = sourceInventory() const toolingLayout = evaluationToolingLayout() validateContractSemantics(contract) @@ -101,6 +114,17 @@ const packageContentMarkers = loadBearingEvaluationMarkers( performanceMarkers, ) const packageMeasurement = inspectPackageContents(packageContentMarkers, packageBudget) +const replacementSources = obligationRetrieval?.sources ?? [] +const replacementMeasurement = { + source_loc: replacementSources.reduce( + (total, source) => total + physicalLines(resolve(repositoryRoot, source)), + 0, + ), + emitted_bytes: replacementSources.flatMap(replacementOutputs).reduce( + (total, output) => total + readFileSync(resolve(repositoryRoot, output)).byteLength, + 0, + ), +} const publishedRoots = new Set(packageJson.files ?? []) const packageScripts = packageJson.scripts ?? {} @@ -264,7 +288,22 @@ assert( ) assert( packageMeasurement.target_passed, - `npm package exceeds the active ceilings: ${packageMeasurement.file_count}/${packageBudget.files_max} files / ${packageMeasurement.packed_bytes}/${packageBudget.packed_bytes_max} packed bytes / ${packageMeasurement.unpacked_bytes}/${packageBudget.unpacked_bytes_max} unpacked bytes`, + `npm package exceeds the selected ceilings: ${packageMeasurement.file_count}/${packageBudget.files_max} files / ${packageMeasurement.packed_bytes}/${packageBudget.packed_bytes_max} packed bytes / ${packageMeasurement.unpacked_bytes}/${packageBudget.unpacked_bytes_max} unpacked bytes`, +) +assert( + replacementSources.length === 3 + && replacementMeasurement.source_loc + === obligationRetrieval?.candidate?.replacement_measurement?.source_loc + && replacementMeasurement.emitted_bytes + === obligationRetrieval?.candidate?.replacement_measurement?.emitted_bytes, + `#630 replacement receipt drifted: ${replacementMeasurement.source_loc} source LOC / ${replacementMeasurement.emitted_bytes} emitted bytes`, +) +assert( + replacementMeasurement.source_loc + <= obligationRetrieval.delivery_limits.replacement_source_loc_max + && replacementMeasurement.emitted_bytes + <= obligationRetrieval.delivery_limits.replacement_emitted_bytes_max, + `#630 replacement exceeds its ceilings: ${replacementMeasurement.source_loc}/${obligationRetrieval.delivery_limits.replacement_source_loc_max} source LOC / ${replacementMeasurement.emitted_bytes}/${obligationRetrieval.delivery_limits.replacement_emitted_bytes_max} emitted bytes`, ) assert( !existsSync(resolve(repositoryRoot, "dist", "tools")), @@ -285,6 +324,7 @@ process.stdout.write( `- Moved evaluation outputs absent from production dist: ${40 - toolingLayout.present_production_outputs.length}/40`, `- Moved evaluation outputs present in dist-eval: ${40 - toolingLayout.missing_evaluation_outputs.length}/40`, `- Package: ${packageMeasurement.file_count} files / ${packageMeasurement.packed_bytes} packed bytes / ${packageMeasurement.unpacked_bytes} unpacked bytes`, + `- #630 replacement: ${replacementMeasurement.source_loc} source LOC / ${replacementMeasurement.emitted_bytes} emitted bytes`, `- Exact moved modules or outputs in package: 0/${evaluationToolingMoves.length * 6} forbidden paths`, "- tools/** and dist-eval/** paths in package: 0", "- Evaluation assets in package: 0", From 5ec3fab427985e3385a94cff1f4ef08b292c3ad2 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sun, 2 Aug 2026 05:26:14 +0400 Subject: [PATCH 4/4] fix: close issue 630 review findings --- docs/concepts/pipelines.md | 2 +- docs/core-reset/removal-manifest.yml | 28 +++++++++++----------- docs/core-reset/scorecard.md | 2 +- docs/designs/2026-07-19-core-reset.md | 2 +- docs/integrations/agent-orchestration.md | 2 ++ docs/reference/cli-and-mcp.md | 4 ++-- docs/roadmap.md | 2 +- examples/mcp-tool-examples.md | 2 +- src/domain/query/plan.ts | 2 +- tests/unit/agent-governance-doc.test.ts | 3 +++ tests/unit/core-reset-governance.test.ts | 20 ++++++++-------- tests/unit/mcp-response-shape-doc.test.ts | 7 ++++++ tests/unit/query-plan.test.ts | 2 ++ tests/unit/why-madar-doc.test.ts | 3 ++- tools/eval/core-reset/benchmark.mjs | 14 ++++++++--- tools/eval/core-reset/verify-isolation.mjs | 6 +++++ 16 files changed, 65 insertions(+), 36 deletions(-) diff --git a/docs/concepts/pipelines.md b/docs/concepts/pipelines.md index e62c1f32..449afa92 100644 --- a/docs/concepts/pipelines.md +++ b/docs/concepts/pipelines.md @@ -30,7 +30,7 @@ question The retrieval pipeline has one deterministic planner, workflow builder, and evidence hydrator. It has no profile, LLM reranker, fallback search, second retrieval engine, session state, or task-specific product wrapper. -Its hard output limits are 12 files, 25 authenticated excerpts, three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, two recovery passes, and 4,000 serialized tokens. +Its hard output limits are 12 files, 25 authenticated excerpts, three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, two recovery passes sharing 64 total recovery-frontier nodes, three alternate seeds per missing obligation, and 4,000 serialized tokens. CLI `query`, direct application use, and MCP `retrieve` serialize byte-identical results for the same accepted graph and normalized request. MCP advertises only the tools capability, exactly one tool, and no resources or prompts. diff --git a/docs/core-reset/removal-manifest.yml b/docs/core-reset/removal-manifest.yml index 57af5e9b..a3c2b922 100644 --- a/docs/core-reset/removal-manifest.yml +++ b/docs/core-reset/removal-manifest.yml @@ -38,11 +38,11 @@ current: production_loc_removed: 2150 production_loc_net: 152 npm_files: 102 - npm_packed_bytes: 155118 - npm_unpacked_bytes: 653492 - npm_shasum: 6115dd200d5bfca6bfd322993f89c7f1f8bff20a - npm_integrity: sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw== - npm_artifact_sha256: 1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414 + npm_packed_bytes: 155124 + npm_unpacked_bytes: 653497 + npm_shasum: 77add32848cfd6f94be700dabe78efabf2bc3ed9 + npm_integrity: sha512-mj2bYY6JbNS8iIxWnq0bZSuNqdkQWlb3bLbob0wAodCxpT6iuuYFc75hhuZM7ijesm9rx94mo5iZ8VoKEsEr+g== + npm_artifact_sha256: 1db61f9760fc933de44faa34543d97775548ac8e96fbe40cbae03623de20164a measurement_state: source_and_package_exact snapshot_scope: obligation_driven_retrieval_630_candidate release_candidate: @@ -2459,17 +2459,17 @@ items: added: 2302 removed: 2150 net: 152 - diff_sha256: 0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f + diff_sha256: 76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693 replacement_measurement: source_loc: 1424 - emitted_bytes: 60928 + emitted_bytes: 60933 package_measurement: files: 102 - packed_bytes: 155118 - unpacked_bytes: 653492 - shasum: 6115dd200d5bfca6bfd322993f89c7f1f8bff20a - integrity: sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw== - artifact_sha256: 1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414 + packed_bytes: 155124 + unpacked_bytes: 653497 + shasum: 77add32848cfd6f94be700dabe78efabf2bc3ed9 + integrity: sha512-mj2bYY6JbNS8iIxWnq0bZSuNqdkQWlb3bLbob0wAodCxpT6iuuYFc75hhuZM7ijesm9rx94mo5iZ8VoKEsEr+g== + artifact_sha256: 1db61f9760fc933de44faa34543d97775548ac8e96fbe40cbae03623de20164a local_verification: source_gate: passed replacement_source_loc_gate: passed @@ -2500,7 +2500,7 @@ items: coverage_statements_percent: 85.6 coverage_statements_covered: 7771 coverage_statements_total: 9078 - coverage_branches_percent: 79.93 + coverage_branches_percent: 79.94 coverage_branches_covered: 7228 coverage_branches_total: 9042 coverage_functions_percent: 92.02 @@ -2599,7 +2599,7 @@ items: tag: forbidden registry_metadata_publication: forbidden main_target: forbidden - notes: 'Issue #630 began from exact protected next commit c88823ecbeb6da6284cf74ecbd304e9315ffd4fa and tree b715764668b4296e9e8ab4da715374f47af137db after #632 completed. The current local candidate replaces rank/slice/traverse with one obligation planner, workflow builder and authenticated evidence hydrator, then returns a deterministic v2 dossier only when every mandatory stage, adjacent async handoff, terminal action and proof is complete. Its source inventory and delta pass. All five field-incident prompts return ready dossiers covering 9 files, 12 excerpts, 12 links, 15 order groups, 21 entities, 20 proofs and all four required queues at 3977 / 3977 / 3979 / 3977 / 3977 tokens; all 14 frozen GoValidate formulations are ready at 3970-3998 tokens with all four queues and stable flow/evidence hashes. The separate repository-portable CI oracle regenerates an exact attested 18-file committed corpus, returns 14/14 ready dossiers with seven mandatory obligations, a 9-node/8-link corridor, all four handoffs and terminal persistence, and rejects wrong-corpus, unknown-proof and missing-persistence mutations. The real-GoValidate audit passes 14/14 and 100 warm measurements at median 48.61237500000061 ms, p95 49.48099999999977 ms and max 50.91725000000042 ms. The 7-file focused suite passes 267 tests, the full suite and coverage pass 83 files / 899 tests at 85.6% statements, 79.93% branches, 92.02% functions and 89.07% lines, and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The replacement trio is 1424 source LOC / 60928 emitted bytes under the owner-amended 61000-byte ceiling; the exact package is 102 files / 155118 packed / 653492 unpacked bytes under the owner-amended 655000-byte unpacked ceiling. The historical stop receipt is https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732; owner amendment https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 supersedes that stop and authorizes active continuation while changing only those two ceilings. All other metrics, constraints and prohibitions remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag or main action is authorized.' + notes: 'Issue #630 began from exact protected next commit c88823ecbeb6da6284cf74ecbd304e9315ffd4fa and tree b715764668b4296e9e8ab4da715374f47af137db after #632 completed. The current local candidate replaces rank/slice/traverse with one obligation planner, workflow builder and authenticated evidence hydrator, then returns a deterministic v2 dossier only when every mandatory stage, adjacent async handoff, terminal action and proof is complete. Its source inventory and delta pass. All five field-incident prompts return ready dossiers covering 9 files, 12 excerpts, 12 links, 15 order groups, 21 entities, 20 proofs and all four required queues at 3977 / 3977 / 3979 / 3977 / 3977 tokens; all 14 frozen GoValidate formulations are ready at 3970-3998 tokens with all four queues and stable flow/evidence hashes. The separate repository-portable CI oracle regenerates an exact attested 18-file committed corpus, returns 14/14 ready dossiers with seven mandatory obligations, a 9-node/8-link corridor, all four handoffs and terminal persistence, and rejects wrong-corpus, unknown-proof and missing-persistence mutations. The real-GoValidate audit passes 14/14 and 100 warm measurements at median 48.61237500000061 ms, p95 49.48099999999977 ms and max 50.91725000000042 ms. The 7-file focused suite passes 267 tests, the full suite and coverage pass 83 files / 899 tests at 85.6% statements, 79.94% branches, 92.02% functions and 89.07% lines, and typecheck, build, build-eval, governance, packed parity, release verification, registry validation, isolation, npm audit and CI evaluation regression pass. The replacement trio is 1424 source LOC / 60933 emitted bytes under the owner-amended 61000-byte ceiling; the exact package is 102 files / 155124 packed / 653497 unpacked bytes under the owner-amended 655000-byte unpacked ceiling. The historical stop receipt is https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732; owner amendment https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147 supersedes that stop and authorizes active continuation while changing only those two ceilings. All other metrics, constraints and prohibitions remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag or main action is authorized.' exit_gate: Every mandatory question obligation is proven and packed into one non-truncated ready dossier, or the result returns the exact non-ready state and missing obligations within the unchanged file, excerpt, token, recovery, package, and latency ceilings. - id: no-fallback-qualification-631 diff --git a/docs/core-reset/scorecard.md b/docs/core-reset/scorecard.md index f8340fac..e888e3dc 100644 --- a/docs/core-reset/scorecard.md +++ b/docs/core-reset/scorecard.md @@ -211,7 +211,7 @@ The following contract facts are historical. Issues #610 and #612, together with ### Obligation-driven retrieval #630 (active) and #631 (pending) - [#630](https://github.com/mohanagy/madar/issues/630) starts from protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit obligations, bounded recovery, strict answerability and the `madar.retrieve` v2 dossier. -- The active #630 candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`; its replacement planner, workflow and hydrator total 1,424 source LOC / 60,928 emitted bytes, and its exact package is 102 files / 155,118 packed / 653,492 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732), changing only the replacement emitted ceiling from 58,000 to 61,000 bytes and npm unpacked ceiling from 640,000 to 655,000 bytes; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues; 100 warm samples pass at 49.48099999999977 ms p95. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending. No publication, release, Registry metadata, tag, or `main` action is authorized. +- The active #630 candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693`; its replacement planner, workflow and hydrator total 1,424 source LOC / 60,933 emitted bytes, and its exact package is 102 files / 155,124 packed / 653,497 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732), changing only the replacement emitted ceiling from 58,000 to 61,000 bytes and npm unpacked ceiling from 640,000 to 655,000 bytes; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.94% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues; 100 warm samples pass at 49.48099999999977 ms p95. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending. No publication, release, Registry metadata, tag, or `main` action is authorized. - [#631](https://github.com/mohanagy/madar/issues/631) remains pending until active #630 completes. Its installed-package and no-fallback comparison has not started. - Neither issue authorizes provider traffic or spend, npm publication, GitHub Release, Registry metadata, tags, stable/`latest`, or `main`; any real campaign or beta publication requires separate owner authorization. diff --git a/docs/designs/2026-07-19-core-reset.md b/docs/designs/2026-07-19-core-reset.md index 4b83d754..00c2d85e 100644 --- a/docs/designs/2026-07-19-core-reset.md +++ b/docs/designs/2026-07-19-core-reset.md @@ -494,7 +494,7 @@ The `madar.retrieve` v2 result has six closed states: `ready`, `incomplete`, `un The unchanged public ceilings are 4,000 serialized tokens, 12 files and 25 excerpts. Planning is bounded to three roots, 32 initial candidates, 512 explored nodes and 24 causal hops; recovery is bounded to two passes, 64 total frontier nodes and three alternate seeds. The replacement planner, workflow builder and hydrator must stay at or below 1,500 source LOC and 61,000 emitted bytes, total production source at or below 15,954 LOC, the package at or below 102 files / 165,000 packed / 655,000 unpacked bytes, and warm loaded-graph p95 strictly below 500 ms. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical stop and changes exactly those two ceilings; every other metric, constraint and prohibition remains unchanged. -The active local candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`. Its replacement trio measures 1,424 source LOC / 60,928 emitted bytes, and the exact package measures 102 files / 155,118 packed / 653,492 unpacked bytes; both pass the amended ceilings. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. The repository-portable CI oracle and real-GoValidate audit remain separate evidence: the former regenerates the exact attested 18-file committed corpus, returns 14/14 ready dossiers with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations; the latter returns 14/14 ready dossiers at 3,970-3,998 tokens with all four required queues and passes 100 warm samples at 49.48099999999977 ms p95. The owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732). Independent review, exact-head CI, CodeRabbit, zero-thread and protected merge remain pending. No fallback engine, dependency change, provider traffic, npm publication, GitHub Release, Registry metadata, tag, or `main` is authorized. +The active local candidate measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693`. Its replacement trio measures 1,424 source LOC / 60,933 emitted bytes, and the exact package measures 102 files / 155,124 packed / 653,497 unpacked bytes; both pass the amended ceilings. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.94% branches, 92.02% functions, and 89.07% lines. The repository-portable CI oracle and real-GoValidate audit remain separate evidence: the former regenerates the exact attested 18-file committed corpus, returns 14/14 ready dossiers with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations; the latter returns 14/14 ready dossiers at 3,970-3,998 tokens with all four required queues and passes 100 warm samples at 49.48099999999977 ms p95. The owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732). Independent review, exact-head CI, CodeRabbit, zero-thread and protected merge remain pending. No fallback engine, dependency change, provider traffic, npm publication, GitHub Release, Registry metadata, tag, or `main` is authorized. ## Migration and compatibility diff --git a/docs/integrations/agent-orchestration.md b/docs/integrations/agent-orchestration.md index 30d213e9..637795e5 100644 --- a/docs/integrations/agent-orchestration.md +++ b/docs/integrations/agent-orchestration.md @@ -63,6 +63,8 @@ A good split is: Do not treat every continuation message as a new retrieval task. A clarification about already-returned evidence can remain in the current agent context. +If an implementation worker changes an indexed source file, run `madar generate .` and a fresh `retrieve` before reusing dossier claims about changed files. Continuation reuse remains valid only while the indexed sources behind those claims are unchanged. + ## Non-ready states When Madar reports `incomplete`, `unsupported`, `stale`, `unavailable`, or `corrupt`: diff --git a/docs/reference/cli-and-mcp.md b/docs/reference/cli-and-mcp.md index 5760560b..29133325 100644 --- a/docs/reference/cli-and-mcp.md +++ b/docs/reference/cli-and-mcp.md @@ -48,7 +48,7 @@ madar query "what calls enqueueInvoice?" --budget 2000 madar query "trace login" --graph out/graph.json ``` -`question` is required and limited to 512 characters. `budget` is an optional positive integer; the effective result is capped at 4,000 serialized tokens, 12 files, and 25 authenticated excerpts. Planning and graph recovery remain bounded to three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, and two recovery passes. +`question` is required and limited to 512 characters. `budget` is an optional positive integer; the effective result is capped at 4,000 serialized tokens, 12 files, and 25 authenticated excerpts. Planning and graph recovery remain bounded to three roots, 32 initial candidates, 512 explored nodes, 24 causal hops, two recovery passes sharing 64 total recovery-frontier nodes, and three alternate seeds per missing obligation. ## MCP @@ -56,7 +56,7 @@ madar query "trace login" --graph out/graph.json | Tool | Input | Result | | --- | --- | --- | -| `retrieve` | `{ "question": string, "budget"?: positive integer }` | Complete authenticated answer dossier, or an exact non-ready state and gaps | +| `retrieve` | `{ "question": string, "budget"?: positive integer }` | Complete authenticated answer dossier, or an exact non-ready state with its missing requirements, reason, or failures | Extra input properties are rejected. There are no MCP resources or prompts. diff --git a/docs/roadmap.md b/docs/roadmap.md index d71518d4..30f78de9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -175,7 +175,7 @@ PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory [#630](https://github.com/mohanagy/madar/issues/630) starts from exact protected `next` commit `c88823ecbeb6da6284cf74ecbd304e9315ffd4fa` and tree `b715764668b4296e9e8ab4da715374f47af137db`. It owns explicit question obligations, graph-coherent workflow construction, at most two bounded recovery passes, exact-range hydration, strict answerability, and the `madar.retrieve` v2 dossier. It retains the 4,000-token / 12-file / 25-excerpt ceilings and cannot publish. -The active local candidate replaces the v1 rank/slice/traverse pipeline with one planner, workflow builder and authenticated hydrator. It measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f`. The replacement trio is 1,424 source LOC / 60,928 emitted bytes, and the exact package is 102 files / 155,118 packed / 653,492 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732) and changes exactly replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.93% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues, and 100 warm samples pass at 49.48099999999977 ms p95. All other metrics and constraints remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag, or `main` action is authorized. +The active local candidate replaces the v1 rank/slice/traverse pipeline with one planner, workflow builder and authenticated hydrator. It measures 44 production files / 15,871 LOC at `+2,302/-2,150/net +152` with full-index diff SHA-256 `76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693`. The replacement trio is 1,424 source LOC / 60,933 emitted bytes, and the exact package is 102 files / 155,124 packed / 653,497 unpacked bytes. Owner [amendment](https://github.com/mohanagy/madar/issues/630#issuecomment-5153369147) supersedes the historical [stop receipt](https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732) and changes exactly replacement emitted bytes 58,000→61,000 and npm unpacked bytes 640,000→655,000; both gates now pass. The focused suite passes 7 files / 267 tests and the full suite passes 83 files / 899 tests. Coverage passes at 85.6% statements, 79.94% branches, 92.02% functions, and 89.07% lines. A distinct repository-portable CI oracle regenerates an attested 18-file corpus, returns all 14 dossiers ready with a nine-node/eight-link corridor, all four handoffs and terminal persistence, and rejects three negative mutations. The real GoValidate graph separately returns all 14 frozen formulations ready at 3,970-3,998 tokens with all four queues, and 100 warm samples pass at 49.48099999999977 ms p95. All other metrics and constraints remain unchanged. Independent review, exact-head CI, CodeRabbit, zero-thread and merge gates remain pending; no publication, release, Registry metadata, tag, or `main` action is authorized. ## Pending — installed no-fallback qualification #631 diff --git a/examples/mcp-tool-examples.md b/examples/mcp-tool-examples.md index 6e916d41..167d76cd 100644 --- a/examples/mcp-tool-examples.md +++ b/examples/mcp-tool-examples.md @@ -15,7 +15,7 @@ Request: } ``` -The server returns a text content item containing canonical JSON: +The server returns a text content item containing canonical JSON. The envelope below is pretty-printed for readability; JSON key order is not semantically significant: ```json { diff --git a/src/domain/query/plan.ts b/src/domain/query/plan.ts index bb8c4528..77df1636 100644 --- a/src/domain/query/plan.ts +++ b/src/domain/query/plan.ts @@ -187,7 +187,7 @@ export function planQuestion(request: NormalizedRetrieveRequest): QuestionPlanRe const raw = request.question.normalize('NFKC'), text = lexicalTokens(request.question).join(' '), tokens = text.split(' '), - names = [...raw.matchAll(/(? match[1]!), ident = /\bwhere\s+(?:is|are|was|were)\s+[`'"]?([\p{L}_$][\p{L}\p{N}_$.-]*)[`'"]?\s+(?:defined|declared|implemented)\b/iu .exec(raw)?.[1], diff --git a/tests/unit/agent-governance-doc.test.ts b/tests/unit/agent-governance-doc.test.ts index 672a4e97..0c3db97e 100644 --- a/tests/unit/agent-governance-doc.test.ts +++ b/tests/unit/agent-governance-doc.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' describe('agent governance documentation', () => { it('requires one retrieve call, a ready dossier, and exact non-ready gaps', () => { const doc = readFileSync(resolve('docs/agent-governance.md'), 'utf8') + const orchestration = readFileSync(resolve('docs/integrations/agent-orchestration.md'), 'utf8') expect(doc).toContain('call `retrieve` once') expect(doc).toContain('`ready` dossier') @@ -14,5 +15,7 @@ describe('agent governance documentation', () => { expect(doc).toContain('guidance, not enforcement') expect(doc).not.toContain('context_pack') expect(doc).not.toContain('pack_confidence') + expect(orchestration).toContain('run `madar generate .` and a fresh `retrieve`') + expect(orchestration).toContain('only while the indexed sources') }) }) diff --git a/tests/unit/core-reset-governance.test.ts b/tests/unit/core-reset-governance.test.ts index 5527ef4c..569d422e 100644 --- a/tests/unit/core-reset-governance.test.ts +++ b/tests/unit/core-reset-governance.test.ts @@ -476,15 +476,15 @@ const OBLIGATION_RETRIEVAL_SOURCE = { } as const const OBLIGATION_RETRIEVAL_PACKAGE = { npm_files: 102, - npm_packed_bytes: 155_118, - npm_unpacked_bytes: 653_492, - npm_shasum: '6115dd200d5bfca6bfd322993f89c7f1f8bff20a', + npm_packed_bytes: 155_124, + npm_unpacked_bytes: 653_497, + npm_shasum: '77add32848cfd6f94be700dabe78efabf2bc3ed9', npm_integrity: - 'sha512-anIx/G+SnAuTcl9yFw/h5KJY1g+Y/kU7YZ+OwcSDEIJ4+Npk6ybFB2sWouSU+3cW9kbgOmHUFeu+VtuZWrlscw==', - npm_artifact_sha256: '1fa88431a12a2ba00a415595002b99df599b9aa4670daeabeb9bfd8a2353c414', + 'sha512-mj2bYY6JbNS8iIxWnq0bZSuNqdkQWlb3bLbob0wAodCxpT6iuuYFc75hhuZM7ijesm9rx94mo5iZ8VoKEsEr+g==', + npm_artifact_sha256: '1db61f9760fc933de44faa34543d97775548ac8e96fbe40cbae03623de20164a', } as const const OBLIGATION_RETRIEVAL_DIFF_SHA256 = - '0f6a9c0151156c8a75d0de8693d54979926a4774c818762d2deddd7d2a3b487f' + '76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693' const OBLIGATION_RETRIEVAL_STOP_RECEIPT = 'https://github.com/mohanagy/madar/issues/630#issuecomment-5153255732' const OBLIGATION_RETRIEVAL_AMENDMENT = @@ -1088,7 +1088,7 @@ describe('core reset governance', () => { expect(design).toContain(EVALUATION_TOOLING_RFC_MERGE_RECEIPT) expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) expect(design).toContain(SEMANTIC_EXECUTION_INDEX_MERGE_TREE) - expect(design).toContain('1,424 source LOC / 60,928 emitted bytes') + expect(design).toContain('1,424 source LOC / 60,933 emitted bytes') expect(design).not.toContain('## Active amendment — generation and incremental index') expect(design).not.toContain('the phase remains active') expect(design).not.toContain('completion evidence remains open') @@ -1157,7 +1157,7 @@ describe('core reset governance', () => { expect(scorecard).toContain('Issues `#622`, `#625`, and `#632` are complete on `next`') expect(scorecard).toContain('supersedes its historical stop and reactivates it under exactly two amended ceilings') expect(scorecard).toContain(SEMANTIC_EXECUTION_INDEX_MERGE) - expect(scorecard).toContain('1,424 source LOC / 60,928 emitted bytes') + expect(scorecard).toContain('1,424 source LOC / 60,933 emitted bytes') expect(scorecard).toContain( 'accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed', ) @@ -2140,7 +2140,7 @@ describe('core reset governance', () => { }, replacement_measurement: { source_loc: 1_424, - emitted_bytes: 60_928, + emitted_bytes: 60_933, }, package_measurement: { files: OBLIGATION_RETRIEVAL_PACKAGE.npm_files, @@ -2180,7 +2180,7 @@ describe('core reset governance', () => { coverage_statements_percent: 85.6, coverage_statements_covered: 7_771, coverage_statements_total: 9_078, - coverage_branches_percent: 79.93, + coverage_branches_percent: 79.94, coverage_branches_covered: 7_228, coverage_branches_total: 9_042, coverage_functions_percent: 92.02, diff --git a/tests/unit/mcp-response-shape-doc.test.ts b/tests/unit/mcp-response-shape-doc.test.ts index 83b54dd9..6328d725 100644 --- a/tests/unit/mcp-response-shape-doc.test.ts +++ b/tests/unit/mcp-response-shape-doc.test.ts @@ -6,6 +6,8 @@ import { describe, expect, it } from 'vitest' describe('MCP response documentation', () => { it('documents the deterministic retrieve v2 dossier and its hard boundaries', () => { const doc = readFileSync(resolve('docs/mcp-response-shape.md'), 'utf8') + const pipeline = readFileSync(resolve('docs/concepts/pipelines.md'), 'utf8') + const reference = readFileSync(resolve('docs/reference/cli-and-mcp.md'), 'utf8') expect(doc).toContain('# MCP response shape') expect(doc).toContain('"schema": "madar.retrieve"') @@ -35,5 +37,10 @@ describe('MCP response documentation', () => { expect(doc).toContain('at most two recovery passes') expect(doc).not.toContain('"version": 1') expect(doc).not.toContain('matched_nodes') + for (const publicDoc of [pipeline, reference]) { + expect(publicDoc).toContain('64 total recovery-frontier nodes') + expect(publicDoc).toContain('three alternate seeds') + } + expect(reference).toContain('missing requirements, reason, or failures') }) }) diff --git a/tests/unit/query-plan.test.ts b/tests/unit/query-plan.test.ts index 065e42e2..43df3d61 100644 --- a/tests/unit/query-plan.test.ts +++ b/tests/unit/query-plan.test.ts @@ -59,6 +59,8 @@ describe('planQuestion', () => { expect(plan('Where is updateIndex defined?').subject).toBe('update index') expect(plan('Where is generateIdeaReport defined?').subject).toBe('generate idea report') expect(plan('Where is generateFromProblem defined?').subject).toBe('generate problem') + expect(plan('Where is api.client.fetch defined?').subject).toBe('api client fetch') + expect(plan('Where is api.client.fetch defined?').terms).toEqual(['api', 'client', 'fetch']) }) it('keeps an explicit workflow-named definition question as a locator', () => { diff --git a/tests/unit/why-madar-doc.test.ts b/tests/unit/why-madar-doc.test.ts index a5e92042..d2b994c3 100644 --- a/tests/unit/why-madar-doc.test.ts +++ b/tests/unit/why-madar-doc.test.ts @@ -27,7 +27,7 @@ describe('public product copy', () => { expect(why).toContain('`incomplete`') expect(why).not.toContain('ranks graph anchors') expect(why).not.toContain('one bounded directed closure') - expect(why).not.toContain('boundaries') + expect(why).not.toContain('truncated boundaries') expect(claims).toContain('## Demonstrated today') expect(claims).toContain('## Historical measurements') expect(claims).toContain('## Not yet measured') @@ -48,6 +48,7 @@ describe('public product copy', () => { expect(examples).toContain('"state": "stale"') expect(examples).toContain('"flow": { "roots": []') expect(examples).toContain('it is abridged') + expect(examples).toContain('JSON key order is not semantically significant') expect(examples).not.toContain('"node_kind": "function"') expect(examples).not.toContain('"required_obligations": 0') expect(examples).not.toContain('"version": 1') diff --git a/tools/eval/core-reset/benchmark.mjs b/tools/eval/core-reset/benchmark.mjs index a61ef734..d6c5b0f5 100644 --- a/tools/eval/core-reset/benchmark.mjs +++ b/tools/eval/core-reset/benchmark.mjs @@ -39,6 +39,7 @@ const EXPECTED_FLOW_SHA256 = "6cca04d52e590ccccd48e6728ec0744fa7606334034ffbca0002e578a6dcca67" const EXPECTED_EVIDENCE_SHA256 = "154efca16be4a163b24cb3812f85cf113c73696805d2de83734c252f8ce656f3" +const CONTROLLER_SELECTOR = /^([^:]+):(\d+(?:-\d+|\.\d+(?:\.\d+)*)?)$/u const expectedChannelOrder = [ "orchestration-queue", @@ -72,6 +73,13 @@ function assert(condition, message) { if (!condition) throw new Error(message) } +for (const selector of ["c1:0", "c1:0-3", "c1:0.2.5"]) { + assert(CONTROLLER_SELECTOR.test(selector), `valid controller selector rejected: ${selector}`) +} +for (const selector of ["c1:", "c1:0-", "c1:0..2", "c1:0:2"]) { + assert(!CONTROLLER_SELECTOR.test(selector), `invalid controller selector accepted: ${selector}`) +} + function hash(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex") } @@ -262,9 +270,9 @@ function assertReferencesAndCorridor(result, plan, mandatory, question) { ) } if (group.controller) { - const [control, ordinal] = group.controller.split(":") - assert(controls.has(control), `${question}: order ${group.id} has unknown controller`) - assert(Number.isSafeInteger(Number(ordinal)), `${question}: invalid controller ordinal`) + const match = CONTROLLER_SELECTOR.exec(group.controller) + assert(match, `${question}: invalid controller selector ${group.controller}`) + assert(controls.has(match[1]), `${question}: order ${group.id} has unknown controller`) } for (const proof of group.proofs ?? []) { assert(proofs.has(proof), `${question}: order ${group.id} has unknown proof ${proof}`) diff --git a/tools/eval/core-reset/verify-isolation.mjs b/tools/eval/core-reset/verify-isolation.mjs index edae4169..3a15272a 100644 --- a/tools/eval/core-reset/verify-isolation.mjs +++ b/tools/eval/core-reset/verify-isolation.mjs @@ -76,6 +76,12 @@ function assert(condition, message) { if (!condition) throw new Error(message) } +assert( + obligationRetrieval?.delivery_limits !== undefined + && obligationRetrieval?.candidate !== undefined, + "removal manifest is missing the obligation-driven-retrieval-630 candidate", +) + function physicalLines(path) { const text = readFileSync(path, "utf8") return text.length === 0 ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0)