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/.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..449afa92 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 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. @@ -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 b6411b53..a3c2b922 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: 15871 + production_loc_added: 2302 + production_loc_removed: 2150 + production_loc_net: 152 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: 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: 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,230 @@ 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: 15871 + added: 2302 + removed: 2150 + net: 152 + diff_sha256: 76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693 + replacement_measurement: + source_loc: 1424 + emitted_bytes: 60933 + package_measurement: + files: 102 + 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 + replacement_gate: passed + package_gate: passed + focused_test_files_passed: 7 + focused_tests_passed: 267 + 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: 899 + coverage: passed + coverage_statements_percent: 85.6 + coverage_statements_covered: 7771 + coverage_statements_total: 9078 + coverage_branches_percent: 79.94 + 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 + 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: 3970 + 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: 48.61237500000061 + p95_ms: 49.48099999999977 + max_ms: 50.91725000000042 + 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_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 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 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 @@ -2409,7 +2623,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..e888e3dc 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 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 #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,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. ## Graph gates diff --git a/docs/designs/2026-07-19-core-reset.md b/docs/designs/2026-07-19-core-reset.md index 51b9c425..00c2d85e 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,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/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..637795e5 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,27 @@ 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 +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. -When Madar reports a missing, unsupported, stale, unavailable, corrupt, disconnected, or truncated boundary: +## Non-ready states -- preserve the evidence it did return -- state the boundary plainly -- verify only the missing load-bearing target +When Madar reports `incomplete`, `unsupported`, `stale`, `unavailable`, or `corrupt`: + +- 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..29133325 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, 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 }` | Authenticated nodes, directed relationships, explicit boundaries, and metrics | +| `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. @@ -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 d81636f5..30f78de9 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,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 -[#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/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..167d76cd 100644 --- a/examples/mcp-tool-examples.md +++ b/examples/mcp-tool-examples.md @@ -10,46 +10,53 @@ Request: { "name": "retrieve", "arguments": { - "question": "How does payment processing work?" + "question": "Where is capturePayment defined?" } } ``` -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 { "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/adapters/mcp/protocol.ts b/src/adapters/mcp/protocol.ts index 3ef7a55c..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 the smallest deterministic evidence path 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: 'The codebase question to answer from authenticated graph evidence.', + 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. Madar returns one deterministic authenticated evidence path or a terminal evidence boundary.', + '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 new file mode 100644 index 00000000..c20f1af9 --- /dev/null +++ b/src/application/evidence-hydrator.ts @@ -0,0 +1,229 @@ +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 { IndexBodyFact, IndexRange } from '../domain/index/model.js' +import type { QueryIndex, ReadyQueryIndex } from '../domain/query/index-status.js' +import type { + EvidenceHydrationTargets, HydratedControl, HydratedEntity, HydratedEvidenceResult, + HydratedExcerpt, HydratedFile, HydratedProof, SelectedEvidenceEdge, +} from '../domain/query/types.js' + +type Failure = Extract +type Source = [string, string, string, number[], number[], string] +type Call = Extract +class Halt { constructor(readonly value: Failure) {} } +function halt(state: Failure['state'], key: string): never { + throw new Halt({ state, subject: key }) +} +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(text.length) + return [starts, ends] +} + +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 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) + } + 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 { + 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) + } + buf = readFileSync(file) + } catch (err) { + if (err instanceof Halt) throw err + halt('unavailable', path) + } + 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 + } + 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) + } + return ref + } + const fact = (v: IndexBodyFact, exact: boolean, keep = true): [string, string] => { + 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)) + } + 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) 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) 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) + } + 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 [id, entry] of ents) { + if (entry[1] === 'symbol' && !used.has(entry[0])) bad(id) + } + return { + state: 'ready', files: fs, controls: ctrls, + excerpts: cuts, entities: ents, proofs: refs, + } +} + +export function hydrateEvidence( + index: QueryIndex, input: EvidenceHydrationTargets, +): HydratedEvidenceResult { + try { + 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' } + } +} diff --git a/src/application/retrieve-context.ts b/src/application/retrieve-context.ts index 51021e1a..341d9f16 100644 --- a/src/application/retrieve-context.ts +++ b/src/application/retrieve-context.ts @@ -1,383 +1,671 @@ -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 as tokens } 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 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 { lexicalTokens, planQuestion } from '../domain/query/plan.js' +import { + selectWorkflow, +} from '../domain/query/workflow.js' +import { + MAX_RETRIEVE_EXCERPTS as EXCERPTS, + MAX_RETRIEVE_FILES as FILES, + RETRIEVE_RESULT_SCHEMA, + RETRIEVE_RESULT_VERSION, + normalizeRetrieveRequest, + valueHas, + type AnswerDossier, + type DossierEntity, + type DossierLink, + type DossierOrderGroup, + type DossierProof, + type EvidenceHydrationTargets, + 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 +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[] => [{ code, required, limit }] + +function stat( + req: NormalizedRetrieveRequest, + flow?: WorkflowSelection, + auth?: ReadyHydration, + gaps = new Set(), + plan?: QueryPlan, +): RetrieveMetrics { + 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 { + budget_tokens: req.budget, + serialized_tokens: 0, + selected_files: auth?.files.size ?? 0, + authenticated_excerpts: auth?.excerpts.size ?? 0, + required_obligations: must.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, + 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), + } } - | { 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 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 validLine(value: unknown): value is number { - return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 +const base = ( + state: S, + req: NormalizedRetrieveRequest, + 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, plan), +}) + +const ask = (plan: QueryPlan): QuerySummary => + ({ intent: plan.intent, subject: plan.subject, terms: plan.terms }) + +type TerminalResult = Exclude +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 + } + return seal({ + schema: out.schema, version: out.version, state: 'corrupt', metrics: out.metrics, + failures: [{ state: 'corrupt', subject: 'terminal result budget' }], + }) } -function stringFact(attrs: GraphAttributes, key: string): string | null { - const value = attrs[key] - return typeof value === 'string' && value.length > 0 ? value : null +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' && 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) + 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') 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)), + } } -function insideRoot(root: string, source: string): boolean { - const path = relative(root, source) - return path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path) +function miss( + req: NormalizedRetrieveRequest, + plan: QueryPlan, + missing: readonly MissingRequirement[], + flow?: WorkflowSelection, + auth?: ReadyHydration, +): RetrieveContextResult { + const gaps = new Set(missing.flatMap((entry) => + entry.obligation_id ? [entry.obligation_id] : [])) + return fit({ + ...base('incomplete', req, flow, auth, gaps, plan), + query: ask(plan), + missing, + }, req.budget) } -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 +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, + }) } - - const expected = index.file_hashes.get(file) - if (!expected) return remember({ state: 'stale', subject: file }) - - 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(), + for (const index of unmatched) { + const obligation = selected[index]! + missing.push({ + code: 'required_reference_missing', obligation_id: obligation.id, + target: obligation.target, }) - } catch { - return remember({ state: 'unavailable', subject: file }) } + return missing } -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) - } - ends.push(text.length) - return { starts, ends } +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 } -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 +const fail = ( + req: NormalizedRetrieveRequest, + state: 'stale' | 'unavailable' | 'corrupt', + subject: string, + flow?: WorkflowSelection, +): RetrieveContextResult => fit({ + ...base(state, req, flow), + failures: [{ state, subject: subject.slice(0, 96) }], +}, req.budget) + +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 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') 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 === 'redacted') return { + redacted: value.sha256, bytes: value.byte_length, + } + return { unknown: value.reason } } -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) -} +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 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 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 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 info( + fact: IndexBodyFact, + ref: (id: string) => string | undefined, +): Readonly> { + 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 ? ref(fact.target_symbol_id) : undefined + const args = fact.arguments.some((entry) => valueHas( + entry, (candidate) => candidate.kind === 'literal', + )) ? 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) }), + } + 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 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) row.value = value(item) + } + return row } -function checkFactProofs( +function pack( + plan: QueryPlan, + flow: WorkflowSelection, + auth: ReadyHydration, 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 - } - return true -} - -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) - } - return actual === expected -} - -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) +): AnswerDossier | MissingRequirement { + const { + 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]), + }) + } + 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[] = [] + paths.forEach((path, index) => { + if (consumed.has(index)) return + const nextIndex = folded.get(index), next = nextIndex === undefined + ? undefined : paths[nextIndex] + links.push({ + 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 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 = auth.proofs.get(id) + const excerpt = proof?.[1] === 'declaration' || proof?.[1] === 'operation' + ? proof[3] : undefined + if (item[1] === 'symbol') { + return [{ + id: alias, kind: 'symbol', label: item[2], + ...(/^(?:function|method|class)$/u.test(item[3]) + ? {} : { node_kind: item[3] }), + file: item[4], + ...(excerpt ? { excerpt } : {}), + }] + } + 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] } : {}), + }] } - proofCache.set(index, byOwner) + 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) : [] } - return byOwner.get(ownerId) ?? [] -} - -function checkChannelProofs( - 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 } + 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 } } - const source = readSource(index, file, sources) - if (source.state !== 'ready') return source - if (!proofMatches(source, range, expected)) { - return { state: 'corrupt', subject: edgeId } + 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 { 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 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) + } + } + byFile.set(chain[3], ranges) } - - const source = readSource(index, file, sources) - if (source.state !== 'ready') return source - if (!checkFactProofs(index, nodeId, source)) { - return { state: 'corrupt', subject: nodeId } - } - 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 } : {}), + 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, + } + }) + 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) } : {}), + }) } - if (nodeKind === 'file') { - return { state: 'ready', node: { ...common, evidence_kind: 'structural_file', node_kind: 'file' } } + 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 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 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 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, + } + } + claimsOut.push({ + id: claim.id, kind: claim.kind, + statement: claim.kind === 'subject' ? `${plan.subject}.` : `${claim.kind} proven.`, + proofs: refs, + }) } - 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, + 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 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, +export function retrieveContext(index: QueryIndex, input: unknown): RetrieveContextResult { + const req = normalizeRetrieveRequest(input) + const planned = planQuestion(req) + if (planned.status === 'unsupported') { + return fit({ + ...base('unsupported', req), + reason: planned.reason, + terms: planned.terms.slice(0, 8).map((term) => term.slice(0, 32)), + }, req.budget) } -} - -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 + const plan = planned.plan + if (index.state !== 'ready') return fail(req, index.state, index.subject) + let flow: WorkflowSelection + try { + flow = selectWorkflow(index, plan) + } catch { + return fail(req, 'corrupt', 'workflow selection') } - 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, - }) -} - -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 unsupportedTerms = unsupportedSubjectTerms(index, plan, flow) + if (unsupportedTerms) { + return fit({ + ...base('unsupported', req, flow), + reason: 'unsupported_source', terms: unsupportedTerms, + }, req.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) + let auth: HydratedEvidenceResult + try { + auth = hydrateEvidence(index, select(plan, flow, index)) + } catch { + return fail(req, 'corrupt', 'evidence hydration', flow) } - - 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)) - } + if (auth.state !== 'ready') { + return fail(req, auth.state, auth.subject, flow) + } + 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 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 + ? ['required_excerpt_limit', auth.excerpts.size, EXCERPTS] as const + : undefined + if (over) return miss( + req, plan, cap(over[0], over[1], over[2]), flow, auth, + ) + try { + 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, undefined, plan), + dossier: built, + }) + 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 fail(req, 'corrupt', 'dossier packing', flow) } - - 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)) - } - 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 { - return canonicalJsonString(result) + return json(result) } diff --git a/src/domain/query/plan.ts b/src/domain/query/plan.ts new file mode 100644 index 00000000..77df1636 --- /dev/null +++ b/src/domain/query/plan.ts @@ -0,0 +1,265 @@ +import type { + LocateAccess, NormalizedRetrieveRequest, ObligationKind, + 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 { + [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 ${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 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])) + +function canonical(value: string): string { + 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 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 = 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` + 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 isNoise = (token: string, mode: QueryIntent): boolean => + COMMON.has(token) || (mode === 'workflow' ? FLOW.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) => + !(plain ? COMMON.has(token) : isNoise(token, mode))))] + +function pick( + text: string, mode: QueryIntent, rules: readonly RegExp[], plain = false, +): string { + 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 [topic: string, ignored: readonly string[]] + +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') + 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, + 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) (.+)$/, + ]) || pick(text, 'workflow', [ + /\bwalk (?:me )?through (.+?)(?= from\b| via\b| to\b|$)/, + ]) || pick(text, 'workflow', [ + /\btrace (?:the )?(.+)(?= from .+ (?:to|through|via)\b)/, + /\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 (direct) return [direct, []] + const active = RegExp( + `\\bhow (?:(?:${AUX}) )?(.+?) (${FW}) (.+?)(?= (?:${CLAUSE})\\b| end to end\\b|$)`, + ).exec(text) + if (active) { + const object = content(active[3]!, 'workflow').join(' ') + if (object) return [object, content(active[1]!, 'workflow', true)] + } + 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 [topic || content(text, 'workflow').join(' '), []] +} + +type FlowBounds = { + entry?: string | undefined + stage?: string | undefined + handoff?: string | undefined + terminal?: string | undefined +} +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(text) + 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(text: string, mode: 'locate' | 'explain'): string { + const locate = mode === 'locate' + const rules = locate ? [ + RegExp(`\\bwhere (?:(?:${AUX}) )?(.+?)(?= (?:${LW})\\b| (?:${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) (.+)$/, + ] : [ + 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(text, mode, rules, + locate || /\b(?:call|invoke)\b/u.test(text)) + || content(text, mode).join(' ') +} + +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], + ownerQuery = RegExp(`\\b(?:which|what) (?:${OWNER}) (?:${LW})\\b`).test(text) + const mode: QueryIntent | undefined = + 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(`\\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(text) + || RegExp(`\\b(?:what (?:${FW}|${EW})|which (?:${OWNER}))\\b`) + .test(text) ? 'explain' : undefined + if (!mode) { + return { + status: 'unsupported', reason: 'unsupported_intent', + terms: [...new Set(tokens.filter((token) => !COMMON.has(token)))].sort(), + } + } + 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 ?? 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(tokens) + 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[] = mode === 'locate' ? ['subject'] + : mode === 'explain' ? ['subject', 'behavior'] + : ['subject', 'entry', 'stage', 'handoff', 'behavior', 'ordering', 'terminal'] + 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: { + intent: mode, subject: topic, terms: sorted, + obligations: kinds.map((kind, index): QueryObligation => ({ + id: `o${index + 1}`, kind, + 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' && rest.length + ? rest.join(' ') : topic, + 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..6cddf55c 100644 --- a/src/domain/query/types.ts +++ b/src/domain/query/types.ts @@ -1,69 +1,257 @@ -import type { GraphAttributes } from '../graph/directed-multigraph.js' -import type { IndexRange } from '../index/model.js' -export const RETRIEVE_RESULT_SCHEMA = 'madar.retrieve' as const -export const RETRIEVE_RESULT_VERSION = 1 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 +import type { IndexBodyFact, IndexRange, IndexValue } from '../index/model.js' + +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, +): 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 L = readonly T[] +type M = ReadonlyMap +type FS = 'stale' | 'unavailable' | 'corrupt' +type EF = { state: FS; subject: string } +export type RetrieveObligationKind = + | 'subject' + | 'entry' + | 'stage' + | 'handoff' + | 'behavior' + | 'ordering' + | 'terminal' +export type RetrieveState = + | 'ready' + | 'incomplete' + | 'unsupported' + | FS + +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 & { + recovery_passes: 0 | 1 | 2 +} + +export interface QuerySummary { + intent: RetrieveIntent + subject: string + terms: L +} +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: L + obligations: L; 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: L +} + +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 SF = Record +type Tag = { kind: K } +type DR = SF<'id' | K> +type PR = DR & { + proofs: L +} +type OR = { operationIds: L } +type WR = OR & { symbolIds: L } -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 = SF<'id' | 'fromId' | 'toId'> & { + relation: WorkflowRelation +} +export type WorkflowHandoff = OR & SF<'fromId' | 'toId'> & { + kind: 'direct' | 'channel'; edgeIds: L +} +export type WorkflowControlGroup = WR & { + kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' + controllerOperationId?: string; arm?: string +} +export type WorkflowObligationProof = WR & { + id: `o${number}`; kind: RetrieveObligationKind; target: string + mandatory: boolean; proven: boolean; edgeIds: L +} +export type WorkflowMissingReason = { + code: WorkflowMissingCode; obligationId?: string; target: string +} +export type WorkflowSelection = WR & { + complete: boolean + rootSymbolIds: L + terminalSymbolIds: L + edges: L + links: L + controlGroups: L + obligations: L + missing: L 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 = PR<'statement'> & { + kind: RetrieveObligationKind +} + +export type DossierFile = DR<'path' | 'digest'> + +export type DossierExcerpt = DR<'file' | 'text'> & { + range: readonly [number, number, number, number] +} + +export type DossierControl = DR<'file'> & { + ranges: L +} + +export type DossierEntity = DR & (Tag<'symbol'> & SF<'label' | 'file'> & { + node_kind?: string + excerpt?: string +} | Tag<'channel'> & SF<'transport' | 'key'> & { + channel_kind: 'queue' | 'job' | 'event' + parent?: string + scope?: string +} | Tag<'operation'> & { excerpt: string } & SF<'operation_kind' | 'owner'> & { + detail: Readonly> +}) + +export type DossierProof = DR<'from' | 'to' | 'relation'> & ( + SF<'excerpt'> | { file: string; range: readonly [number, number, number, number] } +) + +export type DossierLink = PR<'from' | 'to'> & { + kind: 'direct' | 'channel' +} + +export type DossierOrderGroup = DR & { + kind: 'branch' | 'loop' | 'parallel' | 'cycle' | 'sequence' + controller?: string + arm?: string + detail?: Readonly> + depths?: L + members: L + proofs?: L +} + +export interface AnswerDossier { + query: QuerySummary + obligations: L + flow: { + roots: L + terminals: L + links: L + order: L + } + evidence: { + digest_algorithm: 'sha256-base64url' + files: L + excerpts: L + controls: L + entities: L + proofs: L + } +} + +export type SelectedEvidenceEdge = DR<'fromId' | 'toId'> & { + relation?: string +} + +export interface EvidenceHydrationTargets { + symbolIds: L + declarationSymbolIds: L + operationIds: L + validationOperationIds?: L + edges: L +} + +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 [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 [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: M + controls: M + entities: M + excerpts: M + proofs: M +} | EF + +interface RB { + schema: typeof RETRIEVE_RESULT_SCHEMA + version: typeof RETRIEVE_RESULT_VERSION + state: S + metrics: RetrieveMetrics +} + +export type RetrieveContextResult = + | RB<'ready'> & { dossier: AnswerDossier } + | RB<'incomplete'> & { + query: QuerySummary + missing: L + } + | RB<'unsupported'> & { + reason: 'unsupported_intent' | 'missing_subject' | 'unsupported_source' + terms: L + } + | RB & { + failures: L + } + 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 +262,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..1567e113 --- /dev/null +++ b/src/domain/query/workflow.ts @@ -0,0 +1,930 @@ +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, + 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', + 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, + evidence: string, owner: string, operation: string, + dispatchPayload: number | undefined, +] +type Arc = readonly [ + from: string, to: string, kind: 'direct' | 'channel', + edges: readonly IndexedEdge[], ops: readonly string[], +] +type ExecutionView = readonly [ + nodes: readonly SymbolNode[], byId: ReadonlyMap, + outgoing: ReadonlyMap, + incoming: ReadonlyMap, blocked: ReadonlySet, + nonEntries: ReadonlySet, ops: ReadonlyMap, +] +type Candidate = readonly [ + symbol: SymbolNode, lexical: number, rank: number, + affinity: number, exact: number, +] +type Reach = readonly [ + dist: Map, actual: Set, bounded: boolean, + prev: Map, +] +type Selection = readonly [ + nodes: Set, arcs: Arc[], ends: string[], + actual: Set, bounded: boolean, +] +type Control = readonly [ + ops: 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 === '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 bad(fact: IndexBodyFact | undefined): boolean { + return !!fact && (fact.control.some((frame) => + frame.kind === 'exception' && frame.arm === 'catch') + || fact.kind === 'call' + && (adverse(fact.callee) + || fact.arguments.some((argument) => + valueHas(argument, (value) => value.kind === 'literal' + && typeof value.value === 'string' + && adverse(value.value))))) +} +const hasTerminal = (v: ExecutionView, id: string, adverse: boolean): boolean => + v[1].get(id)?.[5].some((fact) => terminal(fact) + && (adverse || !bad(fact) && !bad(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 isRequest(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 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) ?? [], + 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(' ')) + 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), isRequest(attrs)]) + } + const byId = new Map(nodes.map((symbol) => [symbol[0], symbol])), + exact = new Map(), + routes = new Map(), + subs = new Map(), pubs: IndexedEdge[] = [], + nonEntries = new Set() + 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'), + 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' + && (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, + `${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') { + 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(subs, from, edge) + else pubs.push(edge) + } + 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 + 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 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[5] === pub[1] && edge[4] === pub[4]) + : [], + route = routed.length === 1 ? routed[0] : undefined + if (channel.channel_kind === 'job' && !route) continue + 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 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 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])) + const outgoing = new Map(), incoming = new Map() + for (const arc of arcs) { + append(outgoing, arc[0], arc); append(incoming, arc[1], arc) + } + 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 = [ + nodes, byId, outgoing, incoming, blocked, nonEntries, i.operation_by_id, + ] + cache.set(i, v) + return v +} +function score(symbol: SymbolNode, goals: readonly string[]): number { + let result = 0 + for (const target of goals) { + const terms = words(target), compact = terms.join('') + 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 + } + } + return result +} +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) + return lexical - penalty(symbol[7]) - Math.min(24, Math.max(0, degree - 8) * 2) + - (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 (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) => bad(v[6].get(operation)))) + }) +} +function reach( + v: ExecutionView, seeds: readonly string[], back: boolean, + cap: number, allow?: (arc: Arc) => boolean, + allowed?: { has(id: string): boolean }, stops?: ReadonlySet, +): Reach { + const dist = new Map(seeds.map((seed) => [seed, 0])) + const actual = new Set(seeds), queue = [...seeds] + const prev = new Map(), extra = new Set() + let bounded = false + while (queue.length > 0) { + queue.sort((left, right) => dist.get(left)! - dist.get(right)! + || cmp(left, right)) + 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 (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 + 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 [ + dist, actual, bounded || [...extra].some((id) => !dist.has(id)), + prev, + ] +} +function orderBy( + 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, cap: number, goals: readonly string[], + fail: boolean, endNeed?: string, channelEnd = false, +): Selection { + 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] + } + 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 = fwd[0] + let ends = pool.filter((id) => { + const below = reach( + v, [id], false, cap, allow, allowed, + )[0] + return !pool.some((other) => other !== id && below.has(other)) + }) + 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) => + 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] + } + 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) => + (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 [nodes, arcs, ends.filter((id) => nodes.has(id)), + 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, facts: readonly PersistenceFact[], +): PersistenceFact[] { + 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 valid[0] ? [valid] : [] + }) + 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() + 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) => 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, ops: Set, nodes: Set] + const groupsBy = new Map(), + 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 } + if (factIds.includes(id) && fact.kind === 'call' + && !fact.control.some((frame) => frame.kind === 'parallel')) { + append(seqs, `${fact.owner_symbol_id}\0${JSON.stringify(fact.control)}`, fact) + } + 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 = groupsBy.get(key) + ?? [frame.kind, frame.controller_fact_id, arm, new Set(), new Set()] as 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 = [...groupsBy.values()] + .map(([ + kind, controllerOperationId, arm, ops, nodes, + ]) => ({ + kind, controllerOperationId, ...(arm ? { arm } : {}), + operationIds: [...ops].sort(cmp), symbolIds: [...nodes].sort(cmp), + })).concat(ordered) + return [[...need].sort(cmp), groups, proven, [...endOps].sort(cmp)] +} +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 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 pickIds( + v: ExecutionView, ids: readonly string[], target: string, + role: 'subject' | 'exact' | 'names' | 'entry' | 'stage' | 'behavior' | 'terminal', +): string[] { + 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 = lexical.filter((token) => token !== 'request') + return rest.length === 0 ? entries : entries.filter((id) => + rest.every((token) => v[1].get(id)![3].has(token))) + } + 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 : [] + } + 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 channelFit( + i: ReadyQueryIndex, id: string, target: string, +): boolean { + const channel = i.channels_by_id.get(id) + if (!channel) return false + 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 stageMatch( + i: ReadyQueryIndex, v: ExecutionView, selection: Selection, target: string, + 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) => + channelFit(i, edge[1], target) || channelFit(i, edge[2], target))) + return [[...new Set(arcs.flatMap((arc) => [arc[0], arc[1]]))].sort(cmp), arcs] +} +function scanRoots( + v: ExecutionView, + ranks: readonly Candidate[], + goals: readonly string[], +): readonly [ids: string[], actual: Set, bounded: boolean] { + const traversal = reach( + v, ranks.map((entry) => entry[0][0]), true, RECOVERY, + ) + const rank = (id: string): number => { + const symbol = v[1].get(id)! + return rootRank(v, symbol, score(symbol, goals)) + } + 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), ops = i.operation_by_id, + { intent, subject: target, terms, obligations, access } = plan, + isFlow = intent === 'workflow', + 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), + ])], + lastBound = bound('terminal'), + stageNeed = bound('stage'), + 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) => + 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 + : 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')) + : Number(symbol[5].some((fact) => fact.kind === 'persistence' + && READ.test(fact.operation))), + exact = isFlow ? Number(!v[5].has(symbol[0])) + : Number(pickIds(v, [symbol[0]], target, 'names').length > 0) + return [symbol, lexical, rootRank(v, symbol, lexical), affinity, exact] + } + 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]))) + .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[2] - a[2] + || b[3] - a[3] || b[1] - a[1] || cmp(a[0][0], b[0][0])) + .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(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 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) + } + 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') : [], + wanted = callTarget ? direct.find((arc) => + pickIds(v, [arc[1]], callTarget, 'names').length > 0) : undefined, + callArcs = direct.filter((arc) => + 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, 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, goals, fail, lastBound, asyncNeed) + trial[3].forEach((cand) => seen.add(cand)) + tries += 1 + 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])))) { + roots = [id] + flow = trial + 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 rec frontier. Alternates are admitted + // only when they are structural entries; disconnected middle-stage matches + // can never manufacture an entry-to-persistence corridor. + 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 = 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 alternates = flow[2].length === 0 && !locked + ? 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 + const room = RECOVERY - rec.size + 1 + if (room <= 0) { bounded = true; break } + const trial = corridor(v, id, room, goals, fail, lastBound, asyncNeed) + bounded ||= trial[4] + 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 = ranks.slice(1, 4).find((entry) => + entry[0][5].some(behavior)) + if (alternate) { + passes = 1 + tries += 1 + const id = alternate[0][0] + rec.add(id); seen.add(id) + flow = [new Set([id]), [], [], new Set([id]), false] + } + } + const [nodes, links, ends] = flow + const rootIds = isFlow + ? roots.filter((id) => nodes.has(id)).sort(cmp) + : callArcs.length > 0 && focus ? [focus] : [] + const causal = [...new Set([ + ...rootIds, ...ends, ...links.flatMap((arc) => [arc[0], arc[1]]), + ])].sort(cmp) + 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 = pickIds(v, symbolIds, target, + intent === 'locate' && !access ? 'names' : 'subject'), + behaviors = isFlow ? causal : subjects, + 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) && (fail || !bad(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, links, ends, seeds) + const steps = isFlow ? causal : symbolIds, + edgeIds = edges.map((edge) => edge.id), + chosenOps = new Set(ctl[0]), + 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(links.flatMap((arc) => arc[4]))] + .filter((id) => chosenOps.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, + 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) + && !aOps.some((operation) => + ops.get(operation)?.owner_symbol_id === id)) + const gaps = causal.filter((id) => v[4].has(id)) + type ProofData = readonly [ + nodes: readonly string[], ops: readonly string[], proven: boolean, + ] + const data: Record = { + subject: [subjects, intent === 'locate' && access ? locateOps : related(subjects), + subjects.length > 0 && (!access || locateOps.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 && ctl[2] && (!isFlow || gaps.length === 0) + && (!asyncNeed || links.some((arc) => arc[2] === 'channel'))], + behavior: [bReady ? behaviors : [], bOps, + behaviors.length > 0 && inert.length === 0 + && bReady], + ordering: [steps, arcOps, + 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 => { + 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' && gaps.length > 0 + ? gaps.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: terminalIds, + edges, + links: links.map((arc) => ({ + fromId: arc[0], toId: arc[1], kind: arc[2], + edgeIds: idsOf(arc), + operationIds: arc[4].filter((id) => + chosenOps.has(id) && ops.get(id)?.owner_symbol_id === arc[0]), + })), + controlGroups: [...ctl[1], ...cycles(new Set(causal), links)], + obligations: proofs, + missing, + metrics: { + candidateCount: ranks.length, rootCandidateCount: tries, + actualNodeCount: seen.size, + causalRelationHops: edges.length, recoveryPasses: passes, + recoveryFrontierCount: rec.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/agent-governance-doc.test.ts b/tests/unit/agent-governance-doc.test.ts index 8b65fd06..0c3db97e 100644 --- a/tests/unit/agent-governance-doc.test.ts +++ b/tests/unit/agent-governance-doc.test.ts @@ -4,15 +4,18 @@ 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') + const orchestration = readFileSync(resolve('docs/integrations/agent-orchestration.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') + expect(orchestration).toContain('run `madar generate .` and a fresh `retrieve`') + expect(orchestration).toContain('only while the indexed sources') }) }) diff --git a/tests/unit/benchmark-quality.test.ts b/tests/unit/benchmark-quality.test.ts index 93842fe2..3fcdeac7 100644 --- a/tests/unit/benchmark-quality.test.ts +++ b/tests/unit/benchmark-quality.test.ts @@ -6,10 +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[] = [] @@ -20,8 +25,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 +42,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 +64,35 @@ afterEach(() => { }) describe('Core Reset retrieval quality evaluator', () => { - it('grades only authenticated nodes returned by the one query', () => { + 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('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 handle request publish event?', - expected_labels: ['handleRequest()', 'publishEvent()'], + question: 'How does request flow end to end?', + expected_labels: ['handleRequest()', 'persistRequest()'], }], 3_000, { graphPath }, @@ -69,11 +109,48 @@ 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( loadGraphArtifact(graphPath), - [{ question: 'publish event', expected_labels: ['publish'] }], + [{ question: 'Where is persistRequest defined?', expected_labels: ['persist'] }], 3_000, { graphPath }, ) @@ -81,12 +158,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/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/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 169833b4..569d422e 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_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: 155_124, + npm_unpacked_bytes: 653_497, + npm_shasum: '77add32848cfd6f94be700dabe78efabf2bc3ed9', + npm_integrity: + 'sha512-mj2bYY6JbNS8iIxWnq0bZSuNqdkQWlb3bLbob0wAodCxpT6iuuYFc75hhuZM7ijesm9rx94mo5iZ8VoKEsEr+g==', + npm_artifact_sha256: '1db61f9760fc933de44faa34543d97775548ac8e96fbe40cbae03623de20164a', +} as const +const OBLIGATION_RETRIEVAL_DIFF_SHA256 = + '76340caade75454a96e546117c55128e1a69d15720dc60d1a800f5ceb4971693' +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,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') @@ -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,424 source LOC / 60,933 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,47 +2068,233 @@ 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_424, + emitted_bytes: 60_933, + }, + 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: 7, + focused_tests_passed: 267, + 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: 899, + coverage: 'passed', + coverage_statements_percent: 85.6, + coverage_statements_covered: 7_771, + coverage_statements_total: 9_078, + coverage_branches_percent: 79.94, + 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, + 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_970, + 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: 48.61237500000061, + p95_ms: 49.48099999999977, + max_ms: 50.91725000000042, + }, + 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 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 @@ -2475,18 +2728,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 +3541,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 +3592,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 +3911,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 +4106,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 +4360,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 +4991,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 +5506,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 +5984,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 +6575,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 +6585,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 new file mode 100644 index 00000000..15f8e353 --- /dev/null +++ b/tests/unit/evidence-hydrator.test.ts @@ -0,0 +1,481 @@ +import { createHash } from 'node:crypto' +import { + mkdirSync, + mkdtempSync, + 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 +} + +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' || 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()', + '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_range' && proof[4] === 'consumed_by')).toHaveLength(7) + 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_range')) + .toHaveLength(7) + 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_range')) + .toHaveLength(7) + }) + + 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 identity', () => { + const mismatch = fixture() + expect(hydrateEvidence(mismatch.index, { + ...mismatch.targets, + edges: [{ id: mismatch.directEdgeId, fromId: targetId, toId: ownerId }], + })).toEqual({ state: 'corrupt', subject: mismatch.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, + }) + }) + + 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/mcp-response-shape-doc.test.ts b/tests/unit/mcp-response-shape-doc.test.ts index ddc47e29..6328d725 100644 --- a/tests/unit/mcp-response-shape-doc.test.ts +++ b/tests/unit/mcp-response-shape-doc.test.ts @@ -4,33 +4,43 @@ 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') + 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"') - 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') + 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-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..43df3d61 --- /dev/null +++ b/tests/unit/query-plan.test.ts @@ -0,0 +1,428 @@ +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.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') + 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', () => { + 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', 'validate'], + obligations: [ + { id: 'o1', kind: 'subject', target: 'generate invoice', mandatory: true }, + { id: 'o2', kind: 'behavior', target: 'input validate', 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 }, + ], + }) + 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([ + [ + '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'], + 'monthly billing close', + ], + ])('plans a bounded responsibility explanation: %s', ( + question, subject, terms, behavior = 'send', + ) => { + 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']) + expect(result.obligations[1]?.target).toBe(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'], + ])('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 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') + expect(result.terms).toEqual([ + 'assemble', 'idea', '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('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, behavior, + ) => { + 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']) + expect(result.obligations[1]?.target).toBe(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?', + ) + + 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..0b014d53 --- /dev/null +++ b/tests/unit/query-workflow.test.ts @@ -0,0 +1,2244 @@ +import { describe, expect, it } from 'vitest' + +import { KnowledgeGraph } from '../../src/domain/graph/directed-multigraph.js' +import type { + IndexBodyFact, IndexChannelNode, IndexControlFrame, IndexRange, + IndexChannelTransport, IndexScalarValue, IndexValue, +} from '../../src/domain/index/model.js' +import type { ReadyQueryIndex } from '../../src/domain/query/index-status.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 = { + 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: 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 }, + { 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 } +} + +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[] | null = ['data', 'trigger'], + position = 0, + ): this { + this.add(owner, { + ...this.base(owner, id), kind: 'condition', condition_kind: 'switch', + ...(path ? { test: { + kind: 'template', + parts: [ + { kind: 'parameter', position }, + ...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, + control: readonly IndexControlFrame[] = [], + ): this { + 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, { + 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('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', + 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('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], + ])('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 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') + .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..cadb5cd1 --- /dev/null +++ b/tests/unit/retrieve-context-proof-eviction.test.ts @@ -0,0 +1,369 @@ +import { countTokens } from 'gpt-tokenizer/encoding/cl100k_base' +import { describe, expect, it, vi } from 'vitest' + +import type { + HydratedEntity, HydratedEvidenceResult, HydratedExcerpt, HydratedFile, + HydratedProof, 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, + serializeRetrieveContextResult, +} 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: [], +} +const readyIndex = { state: 'ready', operation_by_id: new Map() } as never + +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]]), + 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, + ]]), + } +} + +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: '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 }, + } +} + +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.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 = oversizedHydration(fileCount, excerptCount) + + const result = retrieveContext(readyIndex, { + question: 'Where is report defined?', budget: 4_000, + }) + + expect(result).toMatchObject({ + state: 'incomplete', + missing: [{ code, required, limit }], + }) + expect(result).not.toHaveProperty('dossier') + }) + + it('reports the full ready dossier token count without returning a partial dossier', () => { + mocks.selection = selection() + mocks.hydration = hydratedReport() + 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(readyIndex, { + 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([['report.ts', ['f0', 'a'.repeat(64)] as const]]), + controls: new Map(), excerpts: new Map(), proofs: new Map(), + entities: new Map([[ + 'symbol:report', ['e0', 'symbol', 'report()', 'function', 'f0'] as const, + ]]), + } + + expect(retrieveContext(readyIndex, { + 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.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, + 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(), controls: new Map(), excerpts: new Map(), + entities: new Map(), proofs: new Map(), + } + + const result = retrieveContext(readyIndex, { + 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(), controls: new Map(), excerpts: new Map(), + entities: new Map(), proofs: new Map(), + } + + const result = retrieveContext(readyIndex, { + 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('folds only a sole wrapper channel and preserves its complete proof chain', () => { + mocks.selection = wrapperSelection(false) + mocks.hydration = wrapperHydration(false) + + const result = retrieveContext(readyIndex, { + 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([{ + 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('does not fold a wrapper channel beside a direct parallel publisher', () => { + mocks.selection = wrapperSelection(true) + mocks.hydration = wrapperHydration(true) + + const result = retrieveContext(readyIndex, { + 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 0af2e25f..ca76e256 100644 --- a/tests/unit/retrieve-context.test.ts +++ b/tests/unit/retrieve-context.test.ts @@ -1,17 +1,14 @@ -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 { 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 +17,700 @@ 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 incident = new Set(first.dossier.evidence.entities.flatMap((entity) => + entity.kind !== 'operation' ? [] : [entity.owner]).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 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.', + 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 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 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 passive = { + question: 'How does the idea report get generated from the initial request through to the completed report?', + budget: 4_000, } - - 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 })) + const named = { + question: 'How does GoValidate generate an idea report end to end?', + budget: 4_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 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 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)) + 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'], + ) + } } - - 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'], + 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 ?? []), ], - ] - 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) + ])) + 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) } - 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) + const behavior = first.dossier.obligations.find(({ kind }) => kind === 'behavior')! + 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) + && entity.owner === stage) + expect(outgoing || operation).toBe(true) } - }) - - 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 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), + ]) } - 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()') - }) + expect(first.dossier.flow.links.some((link) => + link.kind === 'channel' + && awaitedEnqueueProof !== undefined + && link.proofs.includes(awaitedEnqueueProof.id))).toBe(true) - 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(result.outcome).toBe('evidence') - expect(result.matched_nodes.map((node) => node.label)).toContain('Order()') - }) - - 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'), + }) + const result = retrieveContext(index, { + question: 'How does submit order call save order?', budget: 4_000, }) - 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.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).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'", - '}', + expect(symbols.find(({ label }) => label === 'saveOrder()')) + .not.toHaveProperty('excerpt') + 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), + })) + 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', () => { + 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')) - } - write(root, 'tsconfig.json', '{"compilerOptions":{"strict":true}}\n') - - const result = retrieveContext(readyIndex(root), { - question: 'Trace `startLocal00` through `middleLocal07` to `finishLocal14`.', + ].join('\n'), }) - - 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.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.', + const result = retrieveContext(index, { + question: 'How does submit order call save order?', budget: 4_000, }) - 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(result.state).toBe('ready') + if (result.state !== 'ready') return + 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')") + }) + + 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.state).toBe('ready') + if (result.state !== 'ready') return + const sequence = result.dossier.flow.order.find(({ kind }) => kind === 'sequence') + expect(sequence).toBeDefined() + 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.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' }, - ]) - }) + 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) - 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`) + 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(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(':')}`) + } } - 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.', - }) - - 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.metrics.truncated).toBe(true) + expect(result.dossier.evidence.controls).toHaveLength(1) + expect(controllerRanges).toHaveLength(2) + expect(new Set(controllerRanges).size).toBe(controllerRanges.length) }) - 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`.', - }) - - 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`.', + 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.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 + 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('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('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', }) - 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')], - ]), - } - const result = retrieveContext(escapedIndex, { - question: 'Explain `escapeLocal00`.', + 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: escapedSource }, + expect(result.state).toBe('ready') + if (result.state !== 'ready') return + expect(result.dossier.evidence.entities).toEqual([ + expect.objectContaining({ kind: 'symbol', label: 'handleClick()' }), ]) }) - 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, + 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 ignoredIndex: ReadyQueryIndex = { - ...fixture.index, - graph: fixture.graph, - file_hashes: new Map([ - ...fixture.index.file_hashes, - [ignoredSource, 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') } - 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 ', + const tooSmall = retrieveContext(workspace(reportSource()).index, { + question: 'How is an idea report generated end to end?', + budget: 1, }) + expect(tooSmall.state).toBe('incomplete') + if (tooSmall.state === 'incomplete') { + expect(tooSmall.missing).toContainEqual(expect.objectContaining({ + code: 'required_token_budget', + limit: 256, + })) + } }) - 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: [], - }) + it('fails closed on stale selected source bytes', () => { + const fixture = workspace(reportSource()) + writeFileSync(fixture.path, `${fixture.source}// changed\n`, 'utf8') - const result = retrieveContext({ ...indexed.index, graph }, { - question: 'Explain `malformedProvenance`.', + const result = retrieveContext(fixture.index, { + question: 'Where is generateIdeaReport defined?', + budget: 4_000, }) - 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' }], + expect(result).toMatchObject({ + state: 'stale', + failures: [{ state: 'stale', subject: 'src/report.ts' }], }) }) - 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', - ] - - 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`, - })), + 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 }, ) - 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, - }) - - 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({ + schema: 'madar.retrieve', + version: 2, + state: 'unsupported', + reason: 'unsupported_intent', }) - - 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, + 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.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('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_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, + }) + 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/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/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/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/tests/unit/why-madar-doc.test.ts b/tests/unit/why-madar-doc.test.ts index cfa20014..d2b994c3 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('truncated boundaries') expect(claims).toContain('## Demonstrated today') expect(claims).toContain('## Historical measurements') expect(claims).toContain('## Not yet measured') @@ -34,6 +41,20 @@ 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).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') + 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 new file mode 100644 index 00000000..d6c5b0f5 --- /dev/null +++ b/tools/eval/core-reset/benchmark.mjs @@ -0,0 +1,523 @@ +import { createHash } from "node:crypto" +import { + lstatSync, + readdirSync, + readFileSync, +} from "node:fs" +import { performance } from "node:perf_hooks" +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] + ? 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 CONTROLLER_SELECTOR = /^([^:]+):(\d+(?:-\d+|\.\d+(?:\.\d+)*)?)$/u + +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) +} + +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") +} + +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( + 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, +] +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 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 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 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}`) + } + } + + 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`) + 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`) + 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( + 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)}`, + ) + 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 +} + +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 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) +} +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: "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), + negative_mutations_rejected: negativeMutations, + 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..3a15272a 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,11 +60,38 @@ 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 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) } +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) +} + +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) @@ -90,7 +119,18 @@ const packageContentMarkers = loadBearingEvaluationMarkers( contract, performanceMarkers, ) -const packageMeasurement = inspectPackageContents(packageContentMarkers) +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 ?? {} @@ -254,7 +294,22 @@ 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 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")), @@ -275,6 +330,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", diff --git a/tools/eval/lib/infrastructure/benchmark/quality.ts b/tools/eval/lib/infrastructure/benchmark/quality.ts index 57058712..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' @@ -94,8 +98,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 +110,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', @@ -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, @@ -206,7 +236,30 @@ 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 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 + && excerptIds.has(entity.excerpt)) { + groundedSymbols.add(entity.id) + } + if (entity.kind === 'operation' && excerptIds.has(entity.excerpt)) { + groundedSymbols.add(entity.owner) + } + } + } const matchedLabels = expectedLabels.filter((expected) => returnedLabels.some((returned) => isExactMatch(returned, normalizeExpectedLabel(expected))), @@ -229,13 +282,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 +295,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))), } }