Skip to content

refactor(codegraph-adapter): split index.ts by responsibility (#574 S1b) - #596

Merged
Ancienttwo merged 1 commit into
mainfrom
refactor/574-s1b-split-adapter
Nov 13, 2026
Merged

Ancienttwo merged 1 commit into
mainfrom
refactor/574-s1b-split-adapter

Conversation

@Ancienttwo

Copy link
Copy Markdown
Owner

Refs #574 · Tier Medium (one GPT review) · slice S1b (joint decision "parallelizing the remaining issues", item 3)

Move-only split of packages/local-runtime/codegraph-adapter/src/index.ts (2226 lines) by responsibility. Base: main 449b61b (S1, #589).

Goal

Give S2+S3, S4 and S5 their own modules so they can run in parallel, one writer per file. Behavior does not change.

Change: what moved where

Every top-level declaration is copied verbatim, in its original relative order. The only edits are moving declarations, adding imports and exports between the new modules, adding export to 17 moved declarations that another module now imports, and dropping imports that became unused. No logic edit, rename, signature change or reorder inside a body.

Module Contents
cli-provider.ts (397) telemetry env (CODEGRAPH_TELEMETRY_*, disableCodeGraphTelemetryByDefault, MutableEnv), DEFAULT_CODEGRAPH_BINARY, requireFromAdapter, CLI output/timeout limits, CodeGraphProvider, CodeGraphCliProvider, CodeGraphCliInvocation, packagedCodeGraphCliInvocation, codeGraphCliInvocation, resolveExecutable, isExecutablePath, resolvePackagedCodeGraphShim, packagedCodeGraphRuntimeDigest, isNodeRuntimeScript, readFilePrefix, runCodeGraphCli, the output parsers (CodeGraphCliNode, syntheticSymbolId, normalizeCliNode, scopedTaskQuery, parseNodeTrail, trailEntries, uniqueSymbols, uniqueEdges, parseExploreSymbols), digestFile
repository-code-facts.ts (224) RepositoryCodeFactsPendingChangesV1, RepositoryCodeFactsV1, REPOSITORY_CODE_FACTS_SCHEMA … assertRepositoryCodeFactsReady, codeGraphIndexAvailable
projection-handshake.ts (465) the projection section comment, CodeGraphProjectionStatusV1, CodeGraphProjectionHandshakeV1, PreparedProjectionCodeFacts, PreparedArchitectureDocumentationProjectionSnapshot, prepareArchitectureDocumentationProjectionSnapshot … runProjectionCodeGraph (code facts, status issues, status digest)
snapshot.ts (298) codeGraphSnapshotRuntime, codeGraphSourceScanner, CODEGRAPH_SNAPSHOT_*, codeGraphSnapshotQueries, withExistingCodeGraphSnapshot, assertSnapshotGitReadSafety, pinSnapshotHooks, mirrorCodeGraphInputs
import-population.ts (281) CODEGRAPH_IMPORT_NODE_READ_LIMIT, CODEGRAPH_IMPORT_NODE_OUTPUT_BYTES, SOURCE_EXTENSIONS, importQuery, importEdgesFromQueryNodes, resolvedImportPairs, resolveImportTarget, importTargetCandidates, fileSymbolId, normalizeChangedPaths, uniqueStrings, ProjectionImportPopulationV1, RepositoryImportPair(s)V1, repositoryImportPairs, importPairsOf, statusImportNodeCount, codeGraphImportPopulation, capabilityImportGraphs
selector-index.ts (372) codeGraphSdk, CapabilityCodeGraphProjectionInputs, CodeGraphSelectorIndex, CODEGRAPH_NO_DOWNLOAD_ENV, the symbol-reader types, NON_DECLARATION_KINDS, openCodeGraphSymbolReader, indexedDeclaration, memoizedSourceDeclarations, loadCapabilityCodeGraphProjectionInputs, RUNTIME_BUILTIN_CALL_NAMES, exactSelectorEvidence, indexedSymbolsAt, edgeSites, isFalseBuiltinCall, normalizedIndexPath
index.ts (309) CodeGraphAdapter, LandscapeContextInput, MultiRepoCodeGraphAdapter, assertNoCodeGraphInternalPathAccess, architectureDeltaTask, and explicit re-exports of the same 64 public names

Boundary adjustments (one shared symbol each, placed with its main user):

  • digestFile → cli-provider.ts. packagedCodeGraphRuntimeDigest calls it 4 times and prepareProjectionCodeFacts once.
  • normalizeChangedPaths and uniqueStrings → import-population.ts (importQuery uses both), not index.ts. That way no module imports the index.ts barrel, which re-exports all of them. CodeGraphAdapter and architectureDeltaTask import them from there.
  • CodeGraphProvider → cli-provider.ts, next to its implementation, for the same reason.
  • Small helpers used only by one module go with it: codeGraphSdk → selector-index, codeGraphSourceScanner → snapshot, CODEGRAPH_NO_DOWNLOAD_ENV → selector-index (only loadCapabilityCodeGraphProjectionInputs reads it), and SOURCE_EXTENSIONS and fileSymbolId → import-population.

read-session.ts, snapshot-query.ts, child-env.ts and ref-impact-facts.ts are not touched. No test file is changed. Every test imports ../src/index or the package entry. Test budget: 0 test lines.

Evidence

Move-only proof

The script below is run from the repository root as node move-only-proof.mjs origin/main. It is not committed, because the write scope is src/ only. It checks two things:

  1. Diff check. Every line removed from index.ts appears verbatim in a changed src/ file. A moved declaration may gain a leading export . No other line is added. Import statements, export { } from statements and blank lines are ignored. It also asserts that index.ts is the only modified file and every other changed file is new.
  2. Block check. Each of the 118 top-level blocks of the old index.ts (one declaration with its leading comment) appears as one contiguous run in exactly one file. So no body was edited or reordered.

A tamper test changed timeout: 5_000 to 6_000 in one moved function, and the script reports FAIL on both checks.

base origin/main (449b61b3)
changed files: A cli-provider.ts, A import-population.ts, M index.ts, A projection-handshake.ts, A repository-code-facts.ts, A selector-index.ts, A snapshot.ts
diff: 1981 lines removed from index.ts, 2101 lines added under src/
  ignored: 40 removed import/export lines, 158 added import/export lines, 232 blank lines
  moved verbatim: 1826 of 1826 removed code lines
  'export ' added to 17 moved declarations:
    cli-provider.ts:16 export const DEFAULT_CODEGRAPH_BINARY = "codegraph";
    cli-provider.ts:20 export const CODEGRAPH_OUTPUT_MAX_BYTES = 32 * 1024 * 1024;
    cli-provider.ts:21 export const CODEGRAPH_QUERY_TIMEOUT_MS = 30_000;
    cli-provider.ts:190 export function isExecutablePath(path: string): boolean {
    cli-provider.ts:221 export function packagedCodeGraphRuntimeDigest(): string {
    cli-provider.ts:280 export interface CodeGraphCliNode {
    cli-provider.ts:358 export function digestFile(path: string): string {
    cli-provider.ts:366 export function runCodeGraphCli(binary: string, workspaceRoot: string, a
    import-population.ts:10 export function importQuery(changedPaths: string[]): string | undefined 
    import-population.ts:16 export function importEdgesFromQueryNodes(gitVisibleFiles: ReadonlySet<s
    import-population.ts:44 export function resolveImportTarget(gitVisibleFiles: ReadonlySet<string>
    import-population.ts:63 export function normalizeChangedPaths(paths: string[]): string[] {
    import-population.ts:70 export function uniqueStrings(items: string[]): string[] {
    import-population.ts:199 export function statusImportNodeCount(status: CodeGraphProjectionStatusV
    import-population.ts:242 export function capabilityImportGraphs(
    projection-handshake.ts:321 export function readProjectionCodeGraphStatus(
    selector-index.ts:370 export function normalizedIndexPath(path: string): string {
  removed lines not found in any new file: 0
  added lines that are not moved lines: 0
blocks: 118 top-level blocks of the old index.ts, each found contiguous in exactly one file: true
  cli-provider.ts: 30
  import-population.ts: 19
  index.ts: 5
  projection-handshake.ts: 21
  repository-code-facts.ts: 14
  selector-index.ts: 20
  snapshot.ts: 9
MOVE-ONLY: PASS
move-only-proof.mjs
#!/usr/bin/env node
// Move-only proof for the #574 S1b split of codegraph-adapter/src/index.ts.
// Usage: node move-only-proof.mjs <base-ref>   (run from the repository root)
// 1. Diff check: every line removed from index.ts is added verbatim in src/ (a moved declaration
//    may gain a leading `export `), and no other line is added. Import statements, `export { } from`
//    re-export statements and blank lines are ignored.
// 2. Block check: every top-level block of the old index.ts (a declaration with its leading comment)
//    appears as one contiguous run in exactly one file under src/, so no body was edited or reordered.
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";

const base = process.argv[2] ?? "origin/main";
const dir = "packages/local-runtime/codegraph-adapter/src";
const git = (...args) => execFileSync("git", args, { encoding: "utf8", maxBuffer: 64 << 20 });
const oldIndex = git("show", `${base}:${dir}/index.ts`).split("\n");
const changed = git("diff", "--name-status", "--no-renames", base, "--", dir).trim().split("\n").map((line) => line.split("\t"));
// Only index.ts may change; every other changed file must be new. read-session.ts, snapshot-query.ts,
// child-env.ts and ref-impact-facts.ts stay untouched.
for (const [status, path] of changed) {
  if (!(status === "M" && path === `${dir}/index.ts`) && status !== "A") throw new Error(`unexpected change: ${status} ${path}`);
}
const files = Object.fromEntries(changed.map(([, path]) => [path, readFileSync(path, "utf8").split("\n")]));

/** Line numbers (1-based) that belong to an import statement or an `export { ... } from` statement. */
function importExportLines(lines) {
  const marked = new Set();
  for (let i = 0; i < lines.length; i++) {
    if (!/^(import\b|export \{|export type \{|export \* from)/.test(lines[i])) continue;
    let j = i;
    while (!/;\s*$/.test(lines[j])) j++;
    if (lines[i].startsWith("export") && !/\bfrom\s+"[^"]+";\s*$/.test(lines[j])) continue;
    for (let k = i; k <= j; k++) marked.add(k + 1);
    i = j;
  }
  return marked;
}

// --- 1. Diff check -------------------------------------------------------------------------
const diff = git("diff", "--no-color", "--no-renames", "-U0", base, "--", dir);
const removed = [];
const added = [];
let file, oldLine, newLine;
for (const line of diff.split("\n")) {
  if (line.startsWith("+++ ")) { file = line.slice(4).replace(/^b\//, ""); continue; }
  if (line.startsWith("--- ")) continue;
  const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
  if (hunk) { oldLine = Number(hunk[1]); newLine = Number(hunk[2]); continue; }
  if (line.startsWith("-")) removed.push({ file, n: oldLine++, text: line.slice(1) });
  else if (line.startsWith("+")) added.push({ file, n: newLine++, text: line.slice(1) });
}
const oldMarks = importExportLines(oldIndex);
const newMarks = Object.fromEntries(Object.entries(files).map(([f, lines]) => [f, importExportLines(lines)]));
const notes = { removedImportExport: 0, addedImportExport: 0, blank: 0, exportAdded: [] };
const pool = new Map();
for (const r of removed) {
  if (r.file !== "/dev/null" && !r.file.endsWith("/index.ts")) throw new Error(`unexpected removal in ${r.file}`);
  if (!r.text.trim()) { notes.blank++; continue; }
  if (oldMarks.has(r.n)) { notes.removedImportExport++; continue; }
  pool.set(r.text, (pool.get(r.text) ?? 0) + 1);
}
const removedCount = [...pool.values()].reduce((a, b) => a + b, 0);
const take = (text) => {
  const count = pool.get(text) ?? 0;
  if (count === 0) return false;
  count === 1 ? pool.delete(text) : pool.set(text, count - 1);
  return true;
};
const unexplained = [];
for (const a of added) {
  if (!a.text.trim()) { notes.blank++; continue; }
  if (newMarks[a.file]?.has(a.n)) { notes.addedImportExport++; continue; }
  if (take(a.text)) continue;
  if (a.text.startsWith("export ") && take(a.text.slice("export ".length))) { notes.exportAdded.push(`${a.file.split("/").pop()}:${a.n} ${a.text.slice(0, 72)}`); continue; }
  unexplained.push(`${a.file}:${a.n}: ${a.text}`);
}
const missing = [...pool.entries()].map(([text, count]) => `${count}x ${text}`);

// --- 2. Block check ------------------------------------------------------------------------
const body = oldIndex.map((text, i) => ({ text, n: i + 1 })).filter(({ n }) => !oldMarks.has(n));
const blocks = [];
for (let i = 0; i < body.length; i++) {
  const { text } = body[i];
  // A block starts at column 0 after a blank line, or at a declaration or comment opener that does
  // not continue a leading comment, so adjacent one-line declarations are separate blocks.
  const previous = i === 0 ? "" : body[i - 1].text;
  const startsBlock = text.trim() && /^[^\s})\]]/.test(text)
    && (!previous.trim() || (/^(export |const |let |function |async function |class |type |interface |\/\*\*|\/\/)/.test(text) && !/^(\/\*\*|\s\*|\/\/)/.test(previous)));
  if (startsBlock) blocks.push([]);
  if (blocks.length && (text.trim() || blocks.at(-1).length)) blocks.at(-1).push(body[i].text);
}
for (const block of blocks) while (block.length && !block.at(-1).trim()) block.pop();
const sameLine = (actual, expected) => actual === expected || actual === `export ${expected}`;
const blockFailures = [];
const placement = {};
for (const block of blocks) {
  const hits = [];
  for (const [f, lines] of Object.entries(files)) {
    for (let i = 0; i + block.length <= lines.length; i++) {
      if (block.every((expected, k) => sameLine(lines[i + k], expected))) hits.push(f);
    }
  }
  if (hits.length !== 1) blockFailures.push(`${hits.length} placements: ${block[0]}`);
  else placement[hits[0].split("/").pop()] = (placement[hits[0].split("/").pop()] ?? 0) + 1;
}

console.log(`base ${base} (${git("rev-parse", "--short", base).trim()})`);
console.log(`changed files: ${changed.map(([status, path]) => `${status} ${path.split("/").pop()}`).join(", ")}`);
console.log(`diff: ${removed.length} lines removed from index.ts, ${added.length} lines added under src/`);
console.log(`  ignored: ${notes.removedImportExport} removed import/export lines, ${notes.addedImportExport} added import/export lines, ${notes.blank} blank lines`);
console.log(`  moved verbatim: ${removedCount - missing.length} of ${removedCount} removed code lines`);
console.log(`  'export ' added to ${notes.exportAdded.length} moved declarations:`);
for (const note of notes.exportAdded) console.log(`    ${note}`);
console.log(`  removed lines not found in any new file: ${missing.length}`);
for (const m of missing) console.log(`    ${m}`);
console.log(`  added lines that are not moved lines: ${unexplained.length}`);
for (const u of unexplained) console.log(`    ${u}`);
console.log(`blocks: ${blocks.length} top-level blocks of the old index.ts, each found contiguous in exactly one file: ${blockFailures.length === 0}`);
for (const [f, count] of Object.entries(placement).sort()) console.log(`  ${f}: ${count}`);
for (const failure of blockFailures) console.log(`  FAIL ${failure}`);
const ok = missing.length === 0 && unexplained.length === 0 && blockFailures.length === 0;
console.log(ok ? "MOVE-ONLY: PASS" : "MOVE-ONLY: FAIL");
process.exit(ok ? 0 : 1);

git diff --color-moved=zebra --stat origin/main:

 .../codegraph-adapter/src/cli-provider.ts          |  397 ++++
 .../codegraph-adapter/src/import-population.ts     |  281 +++
 .../local-runtime/codegraph-adapter/src/index.ts   | 2045 +-------------------
 .../codegraph-adapter/src/projection-handshake.ts  |  465 +++++
 .../codegraph-adapter/src/repository-code-facts.ts |  224 +++
 .../codegraph-adapter/src/selector-index.ts        |  372 ++++
 .../codegraph-adapter/src/snapshot.ts              |  298 +++
 7 files changed, 2101 insertions(+), 1981 deletions(-)

With --color-moved=zebra, 1960 added and 1947 removed lines are coloured as moved. The lines that are not are imports and re-exports, blank lines, the 17 export prefixes, and the 3-line uniqueStrings body, which is below git's moved-block minimum.

Public export list is unchanged

The TS checker's getExportsOfModule(index.ts) gives the same 64 names before (449b61b) and after: diff exports-before.txt exports-after.txt is empty. The runtime Object.keys(await import(index.ts)) also matches: 36 value exports.

the 64 exports (name, kind)
CODEGRAPH_IMPORT_NODE_READ_LIMIT	value
CODEGRAPH_PROVIDER_INTERNAL_EXPORTS	value
CODEGRAPH_SNAPSHOT_MAX_BYTES	value
CODEGRAPH_SNAPSHOT_MAX_ENTRIES	value
CODEGRAPH_TELEMETRY_DISABLED_VALUE	value
CODEGRAPH_TELEMETRY_ENV	value
CapabilityCodeGraphProjectionInputs	type
CodeGraphAdapter	value
CodeGraphCallee	type
CodeGraphCliInvocation	type
CodeGraphCliProvider	value
CodeGraphIndexCompatibilityV1	type
CodeGraphIndexFreshnessV1	type
CodeGraphIndexRevisionV1	type
CodeGraphIndexedDeclaration	type
CodeGraphProjectionHandshakeV1	type
CodeGraphProjectionStatusV1	type
CodeGraphProvider	type
CodeGraphProviderUnavailableError	value
CodeGraphReadProvider	type
CodeGraphReadSession	type
CodeGraphReadSessionOpenV1	type
CodeGraphSelectorIndex	type
CodeGraphSymbolDeclaration	type
CodeGraphSymbolReader	type
CodeGraphSymbolReaderResult	type
LandscapeContextInput	type
MultiRepoCodeGraphAdapter	value
PreparedArchitectureDocumentationProjectionSnapshot	type
PreparedProjectionCodeFacts	type
ProjectionCodeFactsUnavailableError	value
ProjectionCodeGraphStatusIssueCode	type
ProjectionCodeGraphStatusIssueV1	type
ProjectionImportPopulationV1	type
REQUIRED_CODEGRAPH_PACKAGE	value
REQUIRED_CODEGRAPH_VERSION	value
RUNTIME_BUILTIN_CALL_NAMES	value
RepositoryCodeFactsInspectionOptions	type
RepositoryCodeFactsPendingChangesV1	type
RepositoryCodeFactsV1	type
RepositoryImportPairV1	type
RepositoryImportPairsV1	type
assertNoCodeGraphInternalPathAccess	value
assertProjectionCodeFactsAvailable	value
assertRepositoryCodeFactsReady	value
codeGraphCliInvocation	value
codeGraphIndexAvailable	value
codeGraphSnapshotQueries	value
codeGraphSnapshotRuntime	value
disableCodeGraphTelemetryByDefault	value
inspectRepositoryCodeFacts	value
loadCapabilityCodeGraphProjectionInputs	value
memoizedSourceDeclarations	value
openCodeGraphReadSession	value
openCodeGraphSymbolReader	value
packagedCodeGraphCliInvocation	value
pinnedCodeGraphProvider	value
prepareArchitectureDocumentationProjectionSnapshot	value
prepareProjectionCodeFacts	value
projectionCodeFactsRequired	value
projectionCodeGraphStatusIssues	value
repositoryImportPairs	value
withCodeGraphReadSession	value
withExistingCodeGraphSnapshot	value

Checks (Bun 1.4.4, macOS arm64)

  • bun run typecheck: pass. tsc --noEmit --noUnusedLocals reports nothing in codegraph-adapter/src, so no import is left unused.
  • bun test packages/local-runtime/codegraph-adapter: 113 pass, 0 fail (8 files).
  • bun test packages/local-runtime/runtime-daemon: 855 pass, 1 skip, 0 fail (47 files).
  • bun test packages/surfaces/cli: 312 pass, 0 fail (29 files).
  • bun test on core/application/test/control-loop.test.ts, core/context-compiler/test/context-compiler.test.ts, tests/projection-prior-committed-applies.test.ts and tests/ownership-change-acceptance-recovery.test.ts (the other direct importers): 25 pass, 0 fail.
  • bun run test:codegraph-selector-node: verified. It bundles src/index.ts for Node and checks the selectors, the snapshot and the read session.
  • node scripts/package-boundary-audit.mjs and node scripts/production-mock-reachability-audit.mjs: pass.

Risk

  • Import cycles between the new modules. cli-provider ↔ import-population, repository-code-facts ↔ projection-handshake, and import-population → projection-handshake → selector-index → import-population are cycles. They follow from the agreed boundaries; in the single file these were all forward references. They are safe because no module reads an imported binding at evaluation time. Every top-level initializer is a literal, a node:/@archcontext/contracts value or createRequire(import.meta.url). Cross-module references sit only in function bodies, class members and default parameters. The Bun runtime (all suites above) and the Bun-built Node bundle (test:codegraph-selector-node) both load them.
  • requireFromAdapter = createRequire(import.meta.url) now lives in cli-provider.ts. That file is in the same directory, so package resolution is unchanged.
  • 17 formerly private helpers are now exported from their module, so the new modules can use them. index.ts does not re-export them, so the package's public surface is unchanged.

Rollback

Revert this single commit. No data, schema, contract or test changes.

Move-only split of codegraph-adapter/src/index.ts into cli-provider,
repository-code-facts, projection-handshake, snapshot, import-population
and selector-index. index.ts keeps CodeGraphAdapter and
MultiRepoCodeGraphAdapter and re-exports the same public names.
No behavior change.

Refs #574
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-13T19:22:54.202936Z
We couldn’t complete this review. Comment @codex review to try again.
d5ac750 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 1
Subject: d5ac750
Tier: medium
Agent: codex (gpt-6.1-terra), read-only

@Ancienttwo Ancienttwo left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repo-harness acceptance review
Round: 1
Subject: d5ac750
Tier: medium
Verdict: PASS
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Independent move-only check against 449b61b. A normalized diff comparison found 1,826 removed and 1,826 added code lines with no unmatched line, and the zebra diff identifies the moved blocks. The same 64 public export names and type/value classifications appear before and after, and the package export is unchanged. The new modules have import cycles, but no cyclic binding is read during module initialization. Only the seven codegraph-adapter/src files changed; git diff --check passes.

NITs

None.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance record
Claim: #596 (comment)

Round Role Subject Verdict
1 codex (gpt-6.1-terra), read-only d5ac75094f612b5748d0e805d62fc282482e5659 PASS: none

Model relation: different-family

Findings:

  • none

@Ancienttwo

Copy link
Copy Markdown
Owner Author

Joint decision (orchestrator + advisor), 2026-11-14: merge #596 (#574 S1b) at d5ac750 onto main 449b61b. All 35 checks pass. GPT PASS: an independent normalized check found 1826 removed and 1826 added code lines with none unmatched, and the 64 export names are identical (596#issuecomment-6862759587). The chatgpt-codex-connector summary failed on the Codex usage limit and left no inline finding.

Seat: orchestrator t7

@Ancienttwo
Ancienttwo merged commit 6efd32a into main Nov 13, 2026
35 checks passed
@Ancienttwo
Ancienttwo deleted the refactor/574-s1b-split-adapter branch November 13, 2026 19:56
Ancienttwo added a commit that referenced this pull request Nov 14, 2026
## Summary

Refs #574

**Tier: High** (GPT plus Opus review). S5 of ADR-0058: SDK-backed provider reads. Base: `main` 6efd32a (S1 #589, S1b #596).

## Goal
`CodeGraphCliProvider` uses the CodeGraph CLI only to build the index (`sync` under `codeGraphChildEnv`). `buildContext`, `findSymbols` and `getImpactRadius` read through one S1 read session per read (`openCodeGraphReadSession`, read-only, closed after settling the read). No read starts a CodeGraph process, and no read opens the index writable.

## Changes (`codegraph-adapter/src/cli-provider.ts` only)
- **Split.** A private `CodeGraphCliIndexBuilder` keeps `sync`. The provider composes it with SDK reads. `runCodeGraphCli` delegates to one shared `runCodeGraphInvocation` and keeps its signature (S2 still uses it).
- **Deleted:** every CLI read (`explore`, `query`, `node`, `impact`), `parseExploreSymbols`, `parseNodeTrail`, `trailEntries`, `normalizeCliNode`, `syntheticSymbolId` and the `findSymbols` fallback inside `buildContext`. There is no CLI path, no snapshot path and no mode branch.
- **`buildContext` (C6).** The file set comes from `findRelevantContext(query)`: entry-point files first, then node order, capped at `maxSymbols`. The query nodes come from `searchNodes`. Symbols are the non-import query nodes, followed by the index's file nodes for the relevant files (`getNodesInFiles`). Import edges are unchanged: the same scoped `import <paths>` search with `kinds: ["import"]`, resolved by `importEdgesFromQueryNodes`. The digest and evidence bind the relevant-file list where they used to bind the `explore` markdown. The evidence summary is now `Observed by CodeGraph for task: …`.
- **`findSymbols`.** `searchNodes(query, { limit, kinds })` in a total order: hand-written files before generated ones (`generatedFilePredicate`, as the CLI ranks them), then score, then file path, start line and id. Equal scores therefore never reorder.
- **Impact.** `symbolId` resolves to the node with that id. Otherwise it resolves to every node whose exact qualified name or exact name matches. An unknown symbol gives an empty result, and an ambiguous name gives the union over its definitions. `affectedPaths` lists the unique, sorted files of `getImpactRadius(id, depth)` for each definition. `callers` and `callees` are the direct `calls`/`instantiates` edges of the definitions, with CodeGraph node ids on both ends.
- **Failures.** Each read opens one session, and nothing is retried. Provider-load exceptions map to AC_CODE_FACTS_UNAVAILABLE / code-facts-unavailable, matching readiness. A close failure preserves an existing mapped read error; after a successful read it becomes index-open-failed. A throw while the index moved is retryable.
  - `index-missing` → `AC_CODE_FACTS_UNAVAILABLE`/`index-missing`
  - open failure → `AC_CODE_FACTS_UNAVAILABLE`/`index-open-failed`
  - live build lock → `AC_CODE_FACTS_NOT_READY`/`code-facts-not-ready` (`index-incomplete`, retryable)
  - Any throw inside an SDK call, including a fault-injected write that the read-only handle refuses, is a read failure: `index-open-failed`, with no provider text in it. It is never retried, and never with a writable open.
  - An index that moved during the read (`unchangedSinceOpen()` is false, design G4) is refused like a build in progress.

`index.ts`, `read-session.ts`, `real-index.ts`, `read-provider-fixture.ts` and `failure.ts` are untouched. The class keeps the name `CodeGraphCliProvider` and its constructor `(root, binary, readProvider?)`. A rename would touch `index.ts` (S4) and `runtime-daemon`, so it is left for S6 if wanted.

## C6 before/after (same fixture as the new real-index tests)
`buildContext("change the cart total pricing", { maxSymbols: 8, changedPaths: ["src/checkout/cart.ts"] })` on the original five-file fixture (the final real-index fixture also includes the type-reference pair) (`leaf.ts` ← `tax.ts` ← `cart.ts`, plus the two `render` definitions):

| | before (main, CLI `explore` + `query`) | after (SDK) |
|---|---|---|
| symbols | `function cartTotal`, `file cart.ts`, `file leaf.ts`, `file tax.ts` | same four, same ids |
| edges | `cart.ts → tax.ts`, `tax.ts → leaf.ts` | same |
| evidence summary | `Observed by codegraph explore for task: …` | `Observed by CodeGraph for task: …` |
| digest | `sha256:3c427fa5…f11582` | `sha256:594ad299…6eaef` |

The symbol set matches on this fixture, but the digest changes once because it now binds the structured file set instead of the markdown. When a query has few search hits, the relevant context adds files: `buildContext("basePrice", { maxSymbols: 4 })` gives `basePrice`, `leaf.ts`, `tax.ts`, `cart.ts`, which the new test pins.

Impact, same fixture, `getImpactRadius("render", 1)`:
- before: one caller with a synthetic id (`codegraph.6c225064b482 → "render"`). The CLI trail picked one definition. `affectedPaths` was `[page.ts, page.ts, widget.ts, widget.ts]`.
- after: two callers, `showPage → render()` and `showWidget → Widget::render`, with node ids. `affectedPaths` is `[src/views/page.ts, src/views/widget.ts]`.

## Release note (0.15.0)
- `buildTaskContext` reads CodeGraph in-process: files come from `findRelevantContext` and query nodes from `searchNodes`, with no `codegraph explore`, so the code-context digest, the explorer's observed views and practice matches change once. Impact callers and callees are typed call edges with CodeGraph node ids, and an ambiguous name gives the union over its definitions.

## Tests (one case per branch)
`codegraph-adapter.test.ts`, a new real-index describe (indexed once by `indexCodeGraphFixture`):
- `buildContext` returns the query node, then the relevant files in relevance order, with the exact `[kind, name, path, range]` list and the import edge. A second call returns a deep-equal result.
- `findSymbols` with `kinds: ["method"]` gives only `Widget.render`, and with `["function"]` only `render()`.
- impact at depth 1 gives `leaf.ts`, `tax.ts`, and at depth 2 adds `cart.ts`. The caller is the `taxedPrice → basePrice` call edge. A node id resolves like its name, and callees are pinned too.
- an unknown symbol gives an empty impact.
- an ambiguous `render` gives the union of both callers and both files. The qualified `Widget::render` narrows it to the method.
- **read-only refusal:**
  - The wrapped real handle makes a real write through the read-only database inside `findRelevantContext`, and SQLite refuses it (`attempt to write a readonly database`, asserted).
  - The result is a `FailureError` `AC_CODE_FACTS_UNAVAILABLE`/`index-open-failed` with no provider text.
  - A spy on the SDK's `CodeGraph.open`/`openSync` records exactly `[["openSync", { readOnly: true }]]`, so there is no retry and no writable open. The handle closed once.

Session refusals, on the fake provider: no index, open fails, build lock and revision moves. Each case asserts its code, reason, retryable flag and the handles opened and closed.

### Changed assertions, each justified
- `extracts import edges … scoped by changed paths`: the reads moved from the fake CLI to an injected provider. The argv checks (`-k import`, last query has the changed path and `import`, not the task, not the escapes) became the same checks on the last `searchNodes` call (`kinds: ["import"]`, same query text checks). The edge and delta assertions are unchanged.
- Removed `extracts callers and callees from the CodeGraph node trail` and `resolves callers and callees to empty arrays when the CLI node trail has no recognizable calls`. They pinned the deleted text-trail parser (duplicate collapse, `+28 more`, synthetic ids). Their behaviors are now covered on a real index by the impact, unknown and ambiguous cases above.
- `refactor-baseline.test.ts`: the `normalized-import-edges` freeze now reads from an injected provider answering the same import dump, with scores in dump order so the search keeps the CLI's order. The frozen fixture and its digest are unchanged. The fake CLI loses the non-import `query` and `explore` branches, which nothing calls now.

### Test budget (`git diff --numstat origin/main`)
```
187 added product lines; 248 added test lines; ratio 1.33:1; 0 added test files.
Measured against 6efd32a...HEAD; product roots: packages/**/src + scripts; test roots: packages/**/test.
```

## Previous verification on f14b6a8 (Bun 1.4.4, macOS arm64, Node v24.22.1)
- `bun run typecheck`: clean
- `bun test packages/local-runtime/codegraph-adapter`: 118 pass, 0 fail
- `bun test --timeout 60000 packages/surfaces/cli/test/code-facts-not-ready.test.ts packages/local-runtime/runtime-daemon`: 871 pass, 0 fail
- `bun test packages/core/context-compiler packages/core/application`: 31 pass
- `bun run test:codegraph-selector-node`: verified
- `node scripts/package-boundary-audit.mjs` and `node scripts/production-mock-reachability-audit.mjs`: pass

## Current handoff and verification

**Draft, not accepted for merge.** The owner requested that the uncommitted round-2 work be saved to this PR for another machine. Current content is preserved unchanged from the S5 writer; commit `d59c8f83cead0f1dcf311d281b2b2ba1745e426b`. See [handoff](#574 (comment)) and [approved design amendment](#574 (comment)).

On this content, Codex reran `bun run typecheck && bun test --timeout 60000 packages/local-runtime/codegraph-adapter` using Bun 1.4.4: **120 pass, 0 fail, exit 0**. `git diff --check` passed. The worker also reported 902 pass/0 fail across 903 affected tests and a verified Node integration before stopping for provider credit exhaustion; those are prior pane evidence, not new-head CI receipts.

The old f14b6a8 CI passed, but the GPT round-1 acceptance was FAIL and the Opus round-1 record was PASS with P3 findings. Neither old review nor old CI establishes acceptance of d59c8f8. Updated-head round-2 review and required CI remain pending. Review the changed assertions explicitly for tests bent to fit implementation; no new PASS is claimed here.

Changed assertions in the current addition:
- Extend the existing session-refusal table with read-throws-while-index-moves and read-plus-close failure; retain code/reason/retryability and handle-count assertions.
- Add a provider-load mapping test that requires the readiness failure envelope and no provider text.
- Add a real-index type-reference case: affectedPaths includes the reference, callers/callees remain empty.
- Replace ambiguous-name length/containment assertions with an exact ordered callers array; add the unfiltered findSymbols tie-break assertion. These strengthen order coverage.
- Correct the fault-injection comment: pinned CodeGraph 1.6.2 findRelevantContext makes no heal write; the test deliberately injects the refused write.
- The four S2 import-graph baseline cases remain untouched by this addition. Impact port removal stays in S6.

## Risk
- C6 changes the context output once (above). It is not on a timed path.
- Callers and callees no longer count type references as calls, because the old text trail mixed them in. `affectedPaths` still covers them through the impact radius.
- `buildContext` awaits `findRelevantContext` while the session is open, and the handle is closed after the awaited read settles.

## Rollback
Revert this PR. There is no data, schema or ledger change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant