Skip to content

feat(codegraph-adapter): one in-process read-only read session (#574 S1) - #589

Merged
Ancienttwo merged 10 commits into
mainfrom
feat/574-s1-read-session
Nov 13, 2026
Merged

Ancienttwo merged 10 commits into
mainfrom
feat/574-s1-read-session

Conversation

@Ancienttwo

@Ancienttwo Ancienttwo commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Refs #574

S1 of ADR-0058: one in-process, read-only CodeGraph read session, with the repository readiness check rebuilt on it. Base: main 60f2968 (0.14.1, #593 and Bun 1.4.4 from #595).

Goal

inspectRepositoryCodeFacts (doctor, complete debt, the private snapshot) and assertRepositoryCodeFactsReady (the checkpoint, debt, projection and CLI-provider gate) read the index only through CodeGraph.openSync(root, { readOnly: true }). They start no CodeGraph process and run no git status or git diff.

Changes

Read session (codegraph-adapter/src/read-session.ts, new)

  • openCodeGraphReadSession / withCodeGraphReadSession open one handle per operation and close it in finally.
  • The session offers version(), compatibility(), freshness() (memoized per session), revision(), unchangedSinceOpen(), graph and close().
  • Open states, checked in this order:
  • Coherent read (design G4): unchangedSinceOpen() re-checks the writer lock and compares the revision taken at open with the revision after the reads. When anything moved, readiness answers not-ready (index-incomplete, every index field null), and the gate refuses with code-facts-not-ready and retryable: true. It never passes.
  • freshness() is CodeGraph's own change test: the provider's enumeration, plus its change hash (readBoundedSourceSync, oversizeStamp, hashContent).
    • An indexed file that left the enumeration or the disk counts as removed.
    • A file that exists but cannot be read counts as modified, because it cannot be attested.
  • pinnedCodeGraphProvider() takes its version from the manifest of the loaded platform bundle, so REQUIRED_CODEGRAPH_VERSION stays the only pin.
  • CODEGRAPH_PROVIDER_INTERNAL_EXPORTS is the Decision 7 table of provider-internal modules and exports.

Readiness (codegraph-adapter/src/index.ts)

  • One reader. inspectRepositoryCodeFacts reads both tiers, compatibility and freshness. assertRepositoryCodeFactsReady reads compatibility only and reads no source file.
  • withExistingCodeGraphSnapshot calls the same inspectRepositoryCodeFacts(snapshotRoot). The CLI probe and CodeGraphSnapshotOptions are deleted. S4 removes the snapshot itself.
  • RepositoryCodeFactsV1 keeps its shape. C2: unavailable.reasonCode gains "index-open-failed".
  • binary removed from the read options. RepositoryCodeFactsInspectionOptions is now { provider? }. CodeGraphCliProvider takes an optional third argument, readProvider; its binary now only builds the index (sync). prepareArchitectureDocumentationProjectionSnapshot accepts readProvider for its gate. runtime-daemon/src/flow-selector-check.ts no longer passes binary to the snapshot.

A .codegraph without codegraph.db is index-missing (round-2 item 1). The design table maps an absent database to AC_CODE_FACTS_UNAVAILABLE / index-missing, and the surviving snapshot path (index.ts:1225 on main) already refused it that way. Round 1 had returned not-initialized (AC_CODE_FACTS_NOT_READY); that state is gone. Doctor reports index-missing with indexPresent: false, and the checkpoint gate refuses with index-missing. A 0-byte or schema-less codegraph.db now gives index-open-failed instead of not-initialized.

Contracts

  • C2: the failure.ts policy table gains index-open-failed (AC_CODE_FACTS_UNAVAILABLE, inspect-diagnostics, retryable: false).

Agreed scope widening

  • runtime-daemon/src/architecture-debt-code-facts.ts: the two inspect calls drop binary and take the injected provider.
  • 1a: surfaces/cli/src/main.ts passes process.execArgv to the daemon it starts, only when process.versions.bun is set. Under Node nothing new is passed.
  • 2a was removed under Bun 1.4.4. The read session, selector index and symbol reader close exactly as on main, with no GC (see "Bun 1.4.4").
  • 1b: the bun.lock hand-edit is dropped. Main's lock was regenerated by Bun 1.4.4 (chore: pin Bun 1.4.4 (#594) #595) and already points the workspace archctx bin at ./cli/bin/archctx. This PR changes no byte of bun.lock. bun install --frozen-lockfile under 1.4.4 is accepted, and readlink node_modules/.bin/archctx → ../@archcontext/surfaces/cli/bin/archctx.
  • Dev runtime on darwin: packages/surfaces/cli/bin/bun-darwin-sqlite.ts runs only under Bun on darwin. It calls bun:sqlite Database.setCustomSQLite with a library from a closed list (/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib, /usr/local/opt/sqlite/lib/libsqlite3.dylib), and throws with a brew install sqlite hint when neither exists.
    • The bunfig.toml test preload imports it. Dev bin/archctx imports it and preloads it into the CLI process it starts.
    • The probe's CI stderr line is removed.
    • Still needed under Bun 1.4.4 (see below).
    • It never reaches the npm tarball: the package boundary audit passes, and a release-style bundle has no setCustomSQLite, no bun-darwin-sqlite and no libsqlite3.dylib.

Refactors

  • SDK loading, the platform-bundle lookup and the writer-lock check move from index.ts into read-session.ts. REQUIRED_CODEGRAPH_PACKAGE and REQUIRED_CODEGRAPH_VERSION are re-exported unchanged.
  • codeGraphSnapshotRuntime and the snapshot's source scanner use the shared bundle lookup.
  • repositoryCodeFactsFromStatus is the one evaluator of an index state.
  • The gate's unreachable pending-source-changes branch is removed, because the compatibility tier never measures pending changes.

Paths not yet moved, each with the slice that moves it. These are sequencing, not fallbacks.

  • The projection handshake (prepareProjectionCodeFacts) and the import population still use the CLI status and query. S2+S3 move them.
  • The snapshot copy and its Node query child stay until S4.

Round 4

  • R3-1: the database check is a stat, with the same rule as the .codegraph lstat: only ENOENT and ENOTDIR mean index-missing, and any other error is index-open-failed.
    • Test: an unreadable .codegraph (chmod 000, so codegraph.db cannot be stat'ed) and an unsearchable project directory (chmod 000, so .codegraph cannot be lstat'ed) are both index-open-failed, and neither opens the index.
    • The test is skipped on win32 and when running as root.
  • Opus F1: the open-state list above now states the lstat rule exactly.
  • Opus F3: the snapshot comment now says readiness opens the copied index in-process through the read session, while the queries still run in the provider's Node child until S4. The snapshot's rmSync is unchanged.

Bun 1.4.4 (rebased onto 60f2968)

Both checks were run locally with Bun 1.4.4 on macOS.

  • macOS sidecar: still needed. After codegraph sync removed the sidecar files, Bun 1.4.4 with the system SQLite (3.51.0) still fails the read-only open with "unable to open database file". The darwin module stays unchanged.
  • Windows EBUSY guard: removed.
    • Measured with lsof: after close(), Bun 1.4.4 holds 0 of the 5 database files it had open. Bun 1.4.3 held 6 of 6 until a GC after the handle was released.
    • In all 22 protocol fixtures, the test process held 0 files under the fixture root after daemon.stop() without any GC, against 8 under 1.4.3, and had no child process.
    • So collectReleasedCodeGraphHandles and its three call sites are deleted, and so is the test-side Bun.gc(true).
    • The protocol fixture keeps only rmSync's maxRetries: 5, retryDelay: 100 on win32, for a transient hold outside the process.
    • This is a macOS measurement. The Windows shards confirm it on this head; if EBUSY returns there, the drop commit (9db4488) is the one to revert.

Round 3

  • R2-1: the snapshot keeps C2. When the session inspector answers unavailable / index-open-failed, withExistingCodeGraphSnapshot throws index-open-failed; every other unavailable answer stays code-facts-unavailable. Test: a non-SQLite codegraph.db through the snapshot gives AC_CODE_FACTS_UNAVAILABLE / index-open-failed.
  • R2-2: lstat(.codegraph) failing with ENOENT or ENOTDIR is index-missing. Any other failure (permission, I/O) is index-open-failed, because it does not prove the index is absent.
  • Opus R1: the fake gains readFailsAfterOpen, which throws only from the build info that compatibility() reads after the open. That test asserts index-open-failed with open = close = 1. The former readFails test is renamed to say it covers a read during the open.
  • Opus R2: a moving index refuses with retryable: true only when the loaded provider has the pinned version. With a version mismatch it refuses retryable: false with install-compatible-codegraph, asserted in the G4 test.

Windows EBUSY (round-2 item 5, superseded by Bun 1.4.4)

Under Bun 1.4.3, an instrumented local run (not committed) of all 23 protocol tests showed:

  • 8 files held under the fixture root before Bun.gc(true), and 0 after it;
  • no child process, with the daemon running in-process.

The remaining Windows hold was therefore outside the test process; the rmSync retries cover it. Under 1.4.4 no GC is needed (above), and the retries stay.

Changed or removed assertions, and their coverage

codegraph-adapter/test/projection-codegraph-status.test.ts (inspect section, rewritten against an injected provider). The projectionCodeGraphStatusIssues describe is kept unchanged, because S2+S3 still evaluate CLI status -j with it.

  • "unavailable CodeGraph" (nonexistent binary) → "a provider that cannot load". The fake is a Proxy whose version getter throws CodeGraphProviderUnavailableError. That is the error pinnedCodeGraphProvider() throws when the SDK, the platform bundle, its manifest or a Decision 7 export is missing. The real loader cannot be made to fail in a running process. The Decision 7 bump guard (read-session.test.ts) asserts that every listed module path and export exists in the pinned bundle.
  • "missing index from --version alone, never status or sync" → "missing index from the provider version alone and opens nothing" (open count 0).
  • "ready index calls only --version and status" → "ready index from one read-only session and closes it" (open = close = 1). In addition, read-session.test.ts reads a real index with no holder process, and its git spy proves no git status or git diff runs.
  • Per-issue test.each:
    • not-initialized, index-incomplete, extraction, project-path, worktree-mismatch and pending → incompatible-index test.each (bundle version, build version, unknown build version, partial, pending refs, stale extraction, other project root), plus the pending-changes test.
    • not-initialized → index-missing (item 1).
    • worktree-mismatch is dropped for the session. openSync(root) never walks up to another worktree's index (ADR-0058 table), so the session sets worktreeMismatch: null. The evaluator's worktree-mismatch branch stays covered by the kept projectionCodeGraphStatusIssues tests.
  • "binary version diagnostics do not copy raw output" → "issue messages are the closed vocabulary, never provider output". The open-failure test also asserts the raw provider error never reaches the facts.
  • "stale extraction and pending as issues" → the stale-extraction case of the test.each plus the pending-changes test.
  • "binary of another version" → the bundle-version case.
  • "failing or invalid status is unavailable" and "status JSON not an object" → no status JSON is parsed any more. Replaced by "an open that fails" and "a read that fails after the open", both index-open-failed.
  • "always passes CODEGRAPH_NO_DOWNLOAD to --version and status" → removed, because readiness starts no process at all: no environment to pass. code-facts-not-ready.test.ts asserts an empty process log for every readiness path. The remaining CLI read, the import query until S2, still passes CODEGRAPH_NO_DOWNLOAD=1, and architecture-debt-code-facts.test.ts still asserts it (query=1).
  • "packaged shim with no platform bundle does not download" → removed, because readiness no longer runs the npm shim. The SDK loader (npm-sdk.js) never downloads ("we never trigger a network download from inside require()"); it only reuses an installed or cached bundle. A missing bundle surfaces as CodeGraphProviderUnavailableError → codegraph-unavailable, which the Proxy test covers. The shim still builds the index (init, sync), and that is unchanged.

codegraph-adapter/test/build-lock.test.ts. Live, stale and malformed lock, ported from the CLI call log to provider open counts: a live lock never opens the index, a stale or malformed one opens and closes it once.

codegraph-adapter/test/codegraph-adapter.test.ts. The two shim tests inject readProvider; their sync and query assertions are unchanged.

surfaces/cli/test/code-facts-not-ready.test.ts

  • Process-log expectations change from --version\nstatus\n to empty: no readiness process.
  • The checkpoint and debt gate's issues are compatibility-only.
  • "present uninitialized index" → ".codegraph without a database is index-missing" (item 1).
  • The projection-snapshot gate injects readProvider.
  • The real incomplete index (initSync) still yields version-mismatch plus index-incomplete.

surfaces/cli/test/doctor-code-facts.test.ts. An empty .codegraph is now index-missing with indexPresent: false (item 1). Doctor ok is unchanged.

runtime-daemon/test/architecture-debt-code-facts.test.ts. Readiness comes from the injected provider, which opens and closes twice around the reads. The process log keeps only query=1 (S2), and a missing or stale index starts nothing.

runtime-daemon/test/flow-selector-check.test.ts, surfaces/cli/test/ref-impact.test.ts. Their subprocess runs the dev launcher bin/archctx instead of src/main.ts. The snapshot's readiness now reads in-process (item 3), so on darwin the CLI process needs the same SQLite library as the test process. The assertions are unchanged.

surfaces/cli/test/projection-protocol-lifecycle.test.ts. Cleanup uses rmSync retries on win32; no assertion changed.

New tests

  • read-session.test.ts:
    • each open state, the dangling link, and the coherent-read case (an injected revision change between reads gives not-ready and a retryable refusal);
    • compatibility/revision, freshness counts and memoization, an unreadable source, and close-on-return/throw with refusal after close;
    • a real index read in-process with no holder: ready; edit, add and delete counted; an oversize file fresh; .git/index mtime and the database sha256 unchanged; the git spy;
    • the Decision 7 bump guard.
  • scripts/codegraph-selector-node-integration.mjs (verify:pre, all three OSes) runs the read session under Node with no holder: compatibility and freshness, pending counts, the compatibility-only gate, the git spy, and .git/index and the database unchanged. A live-writer case did not fit this script cheaply and is not added; the injected revision test covers G4.

Test budget (git diff --numstat origin/main, head 8649579)

  • Product (src, bin, main.ts, contracts): +556 / −144.
  • Config: bunfig.toml +1 / −1. bun.lock unchanged.
  • Tests: +656 / −302, including the 109-line fixture.
  • Script: +74 / −2.

Verification (Bun 1.4.4)

  • bun install --frozen-lockfile: accepted.
  • bun run typecheck: exit 0.
  • Head 9db4488: bun test --timeout 120000 over codegraph-adapter, code-facts-not-ready, doctor-code-facts, architecture-debt-code-facts, flow-selector-check, ref-impact, complete-architecture-debt, projection-protocol-lifecycle, docs-anchors, local-product-e2e and the contracts tests: 699 pass, 0 fail.
  • Head 8649579: bun run typecheck exit 0. codegraph-adapter, code-facts-not-ready, doctor-code-facts, architecture-debt-code-facts, flow-selector-check, ref-impact and complete-architecture-debt: 162 pass, 0 fail.
  • bun run test:codegraph-selector-node: verified.
  • node scripts/packaged-cli-smoke.mjs: OK.
  • node scripts/package-boundary-audit.mjs: passed.

Risk

  • On macOS, the Bun dev and test runtime needs a Homebrew SQLite (the error says how to install it). The product runtime, Node, is unchanged.
  • The pinned provider-internal modules break loudly on a provider bump, through the bump guard.
  • A .codegraph without a database moves from not-ready to index-missing, the same refusal as no index at all.

Rollback

Revert this PR. There is no data migration; the WAL sidecar files are managed by SQLite.

@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-13T15:04:32.829961Z
We couldn’t complete this review. Comment @codex review to try again.
4cc2537 Draft marked ready
ℹ️ 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: 35795ac
Tier: high
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: 35795ac
Tier: high
Verdict: FAIL
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Cross-family review (Codex CLI, read-only) of S1 at 35795ac against 0983ae9. The read session uses the in-process read-only handle, the lockfile diff changes only the agreed bin entry, and the package boundary audit passes. Two P1 findings and one P2.

NITs

None.

Findings outside the diff

  • P2 F3 at PR #589 body:53: Changed and removed assertions are not justified item by item; the missing-platform-bundle/no-download assertion was removed and its replacement tests a different behavior. -> Document each changed or removed assertion with its retained or replacement coverage, including the missing-provider behavior.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
1 codex (gpt-6.1-terra), read-only 35795ac7546e3f5ae250a092b22bff1d97c7421a FAIL: 2 P1, 1 P2

Model relation: different-family

Findings:

  • P1 F1 at packages/local-runtime/codegraph-adapter/src/read-session.ts:207: A .codegraph directory without codegraph.db returns not-initialized, so the gate reports AC_CODE_FACTS_NOT_READY; the S1 design requires AC_CODE_FACTS_UNAVAILABLE / index-missing whenever the database is absent, and the new test asserts the conflicting behavior. -> Map an absent database to index-missing and update the behavior tests.
  • P1 F2 at packages/local-runtime/codegraph-adapter/src/read-session.ts:208: The writer lock is checked only before opening; a sync can start afterwards, and compatibility and freshness are read in separate calls with no transaction or post-read validation. -> Establish a coherent read boundary against a sync that starts or finishes during the session, test it, and fail closed when consistency cannot be established.
  • P2 F3 at PR feat(codegraph-adapter): one in-process read-only read session (#574 S1) #589 body:53: Changed and removed assertions are not justified item by item; the missing-platform-bundle/no-download assertion was removed and its replacement tests a different behavior. -> Document each changed or removed assertion with its retained or replacement coverage, including the missing-provider behavior.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 1
Subject: 35795ac
Tier: high
Agent: pi (claude-opus-5-5), 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: 35795ac
Tier: high
Verdict: FAIL
Reviewer: pi (claude-opus-5-5)
Model relation: same-family

Fresh same-family review (Pi, Opus 5.5) of S1 at 35795ac. The reviewer returned PASS with one P2. A P2 blocks under the fix-loop rule, so the orchestrator records the round as FAIL. One read path, failure codes, handle lifetime, 1a/1b/darwin module and the release bundle all verified, including a fresh frozen install and a bundle build.

NITs

  • F2: index.ts:1686 the gate reads the compatibility tier, which never adds pending-source-changes, so the issues.every(pending-source-changes) branch is dead; remove it.
  • F3: read-session.ts:204-207 a missing codegraph.db maps to not-initialized against the design table's index-missing, undeclared in the PR body; a dangling .codegraph symlink returns index-missing because existsSync runs before lstat.
  • F4: read-session.ts:208 the build lock is checked once before the open; a sync that starts afterwards can tear compatibility() and getFiles(). Re-check after the reads and cover a live writer in the Node integration script.
  • F5: index.ts:1925 loadCapabilityCodeGraphProjectionInputs runs Bun.gc(true) on win32 even when no selector index was opened; guard it.
  • F6: cli/bin/bun-darwin-sqlite.ts:22,41 the CI stderr diagnostic line from the probe is still present; the exists seam and 1a have no test.
  • F7: projection-codegraph-status.test.ts removed cases (packaged shim with no platform bundle, CODEGRAPH_NO_DOWNLOAD, worktree-mismatch) are not listed in the PR body.
  • F8: PR body test budget counts are slightly off: product +575/-136 and tests +556/-289 by numstat.
  • F9: The SDK's validateDirectory rewrites .codegraph/.gitignore on a readOnly openSync when it is missing or an old default, outside ADR-0058 Decision 4; provider behavior to record on #574 or amend in S6.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
1 pi (claude-opus-5-5), read-only 35795ac7546e3f5ae250a092b22bff1d97c7421a FAIL: 1 P2, 8 P3

Model relation: same-family

Findings:

  • P2 F1 at packages/local-runtime/codegraph-adapter/src/index.ts:1199: S1 split the shared inspectRepositoryCodeFacts into a CLI-only copy, inspectSnapshotCodeFactsByCli (--version + status -j), so the same readiness facts can still be read two ways; its CodeGraphSnapshotOptions {binary, timeouts} parameter is never passed by any caller. -> Have withExistingCodeGraphSnapshot call the session-based inspectRepositoryCodeFacts(snapshotRoot) and delete the CLI probe and CodeGraphSnapshotOptions.
  • P3 F2: index.ts:1686 the gate reads the compatibility tier, which never adds pending-source-changes, so the issues.every(pending-source-changes) branch is dead; remove it.
  • P3 F3: read-session.ts:204-207 a missing codegraph.db maps to not-initialized against the design table's index-missing, undeclared in the PR body; a dangling .codegraph symlink returns index-missing because existsSync runs before lstat.
  • P3 F4: read-session.ts:208 the build lock is checked once before the open; a sync that starts afterwards can tear compatibility() and getFiles(). Re-check after the reads and cover a live writer in the Node integration script.
  • P3 F5: index.ts:1925 loadCapabilityCodeGraphProjectionInputs runs Bun.gc(true) on win32 even when no selector index was opened; guard it.
  • P3 F6: cli/bin/bun-darwin-sqlite.ts:22,41 the CI stderr diagnostic line from the probe is still present; the exists seam and 1a have no test.
  • P3 F7: projection-codegraph-status.test.ts removed cases (packaged shim with no platform bundle, CODEGRAPH_NO_DOWNLOAD, worktree-mismatch) are not listed in the PR body.
  • P3 F8: PR body test budget counts are slightly off: product +575/-136 and tests +556/-289 by numstat.
  • P3 F9: The SDK's validateDirectory rewrites .codegraph/.gitignore on a readOnly openSync when it is missing or an old default, outside ADR-0058 Decision 4; provider behavior to record on codegraph-adapter: read the index only through the in-process read-only handle #574 or amend in S6.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 2
Subject: f2a38df
Tier: high
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: 2
Subject: f2a38df
Tier: high
Verdict: FAIL
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Round-2 delta review. All three round-1 findings are resolved: a missing database maps to index-missing, the G4 lock and revision checks are in, and the PR body justifies the changed assertions. The launcher change keeps its assertions. Two failure-classification defects remain on the new paths.

NITs

None.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
2 codex (gpt-6.1-terra), read-only f2a38dfb27f5e7894a63aaa6ac31f1e86a615f41 FAIL: 2 P2

Model relation: different-family

Findings:

  • P2 R2-1 at packages/local-runtime/codegraph-adapter/src/index.ts:1260: The snapshot uses the session inspector but converts every unavailable result to code-facts-unavailable, so a real index-open-failed loses its C2 reason on the flows check --no-sync path. -> Preserve index-open-failed when propagating the inspector result, and cover the mapping.
  • P2 R2-2 at packages/local-runtime/codegraph-adapter/src/read-session.ts:208: Every lstatSync error maps to index-missing, including permission and I/O errors, which do not establish that the index is absent. -> Map absence errors (ENOENT) to index-missing and other access failures to index-open-failed.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 2
Subject: f2a38df
Tier: high
Agent: pi (claude-opus-5-5), 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: 2
Subject: f2a38df
Tier: high
Verdict: PASS
Reviewer: pi (claude-opus-5-5)
Model relation: same-family

Round-2 review by a fresh same-family context (Pi, Opus 5.5). Round-1 F1 to F3 are resolved. Re-verified: frozen install, tsc clean, 165 pass across the adapter, not-ready, doctor, debt and protocol suites, 17 pass for flow-selector-check plus ref-impact, and test:codegraph-selector-node under Node 24. P3 notes only.

NITs

  • R1: projection-codegraph-status.test.ts:181: with readFails the fake's getIndexRevision throws at open, so the 'read fails after the open' test exercises the open path; no test reaches the inspect-level catch after a good open.
  • R2: index.ts:1662: when moving is true and the bundle version is not the pin, the refusal carries install-compatible-codegraph with retryable true; retrying cannot fix a version mismatch.
  • R3: read-session.ts:212-219: a 0-byte or schema-less codegraph.db now gives index-open-failed where it gave not-initialized; the PR body does not mention it.
  • R4: bun-darwin-sqlite.ts:22 and main.ts:3454: selectDarwinSqliteLibrary and the Bun-only execArgv forwarding have no unit test.
  • R5: No live-writer or live-WAL case; the SDK open can rewrite .codegraph/.gitignore outside ADR-0058 Decision 4. Record both for S6.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
2 pi (claude-opus-5-5), read-only f2a38dfb27f5e7894a63aaa6ac31f1e86a615f41 PASS: 5 P3

Model relation: same-family

Findings:

  • P3 R1: projection-codegraph-status.test.ts:181: with readFails the fake's getIndexRevision throws at open, so the 'read fails after the open' test exercises the open path; no test reaches the inspect-level catch after a good open.
  • P3 R2: index.ts:1662: when moving is true and the bundle version is not the pin, the refusal carries install-compatible-codegraph with retryable true; retrying cannot fix a version mismatch.
  • P3 R3: read-session.ts:212-219: a 0-byte or schema-less codegraph.db now gives index-open-failed where it gave not-initialized; the PR body does not mention it.
  • P3 R4: bun-darwin-sqlite.ts:22 and main.ts:3454: selectDarwinSqliteLibrary and the Bun-only execArgv forwarding have no unit test.
  • P3 R5: No live-writer or live-WAL case; the SDK open can rewrite .codegraph/.gitignore outside ADR-0058 Decision 4. Record both for S6.

Draft for the CI probe: readiness reads the index through
openCodeGraphReadSession; Bun on darwin loads a read-only-capable SQLite
through packages/surfaces/cli/bin/bun-darwin-sqlite.ts (test preload and
dev bin/archctx).

Refs #574
…ntime fixes (#574 S1)

- read-session tests: one per branch, a real index without a holder, the
  git spy, and the provider bump guard (ADR-0058 Decision 7).
- freshness counts an indexed file gone from disk as removed; a deleted
  tracked file is not a new source.
- close() runs Bun.gc(true) under Bun on Windows only (cached statements
  hold the database file until GC).
- main.ts forwards process.execArgv to the daemon only under Bun.
- codegraph-selector-node-integration exercises the session under Node.

Refs #574
…574 S1)

close() drops the session's only reference to the handle before
Bun.gc(true); a GC that runs while the handle is reachable cannot
finalize its cached statements, so the database file stayed open.

Refs #574
… on Windows (#574 S1)

The selector index and the symbol reader close the same way as the read
session: drop the only reference, then collect under Bun on Windows.

Refs #574
…te (#574 S1)

The bun.lock hand-edit this commit carried is dropped: main's lock,
regenerated by Bun 1.4.4 (#595), already points the workspace archctx
bin at cli/bin/archctx.

Refs #574
…e readiness reader (#574 S1)

- A .codegraph without codegraph.db is index-missing; lstat runs first,
  so a dangling .codegraph link is index-open-failed.
- Design G4: after the reads, the session re-checks the writer lock and
  the revision taken at open; a moved index is not-ready and retryable.
- withExistingCodeGraphSnapshot reads readiness through the session; the
  CLI probe and CodeGraphSnapshotOptions are gone.
- Dead pending branch in the gate removed; selector GC guarded; probe
  stderr line removed.
- Protocol fixture cleanup retries a transient Windows hold after GC.

Refs #574
…rors, retryable only on a matching version (#574 S1)

Refs #574
… 1.4.4 (#574 S1)

Bun 1.4.4 releases the database files on close(): measured with lsof,
0 files stay open after close() (Bun 1.4.3 kept 5-8 until GC), and the
protocol fixtures hold no file under root after stop() without any GC.
The read session, selector index and symbol reader close as on main;
the protocol fixture keeps only rmSync retries for a transient Windows
hold outside the process.

Refs #574
@Ancienttwo
Ancienttwo force-pushed the feat/574-s1-read-session branch from f2a38df to 9db4488 Compare November 13, 2026 18:05
@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 3
Subject: 9db4488
Tier: high
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: 3
Subject: 9db4488
Tier: high
Verdict: FAIL
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Round-3 delta review against the clean merge of f2a38df with 60f2968. R2-1 is resolved (the snapshot keeps index-open-failed). R2-2 is resolved for lstat(.codegraph), but the following database existence check still collapses access errors into index-missing. The changed handle paths close explicitly, including when the initial revision read fails; Windows CI on this head decides the GC removal.

NITs

None.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
3 codex (gpt-6.1-terra), read-only 9db4488bb936bc17d090dc7d868fba9729081750 FAIL: 1 P2

Model relation: different-family

Findings:

  • P2 R3-1 at packages/local-runtime/codegraph-adapter/src/read-session.ts:206: existsSync(codegraph.db) returns false for access failures as well as absence; if .codegraph can be lstated but not traversed, an existing database is reported as index-missing rather than index-open-failed. -> Use a database check that distinguishes ENOENT/ENOTDIR from permission and I/O failures, and cover the classification.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 3
Subject: 9db4488
Tier: high
Agent: pi (claude-opus-5-5), 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: 3
Subject: 9db4488
Tier: high
Verdict: PASS
Reviewer: pi (claude-opus-5-5)
Model relation: same-family

Round-3 review (Pi, Opus 5.5) with Bun 1.4.4 on a git archive copy of 9db4488: tsc clean; 184 pass across the adapter, not-ready, doctor, debt, protocol, flow-selector-check and ref-impact suites; test:codegraph-selector-node verified under Node 24. Round-2 notes resolved, no handle references collectReleasedCodeGraphHandles, the darwin module is still needed and fails closed without it, and the PR changes no byte of bun.lock. P3 notes only.

NITs

  • F1: The PR body still says lstat(.codegraph) failure gives index-missing; the code maps only ENOENT/ENOTDIR there, and no test covers the non-ENOENT branch.
  • F2: Carried over: selectDarwinSqliteLibrary and 1a have no unit test, and there is no live-writer case.
  • F3: index.ts:1263-1264 the comment saying Node owns all SDK/SQLite handles is stale now that snapshot readiness opens in-process under Bun, and the snapshot's rmSync(temporaryRoot) has no retries; Windows CI on this head decides it.
  • F4: Dropping 2a and the 1b lock edit departs from the recorded #574 decisions; the reasons are only in the PR body.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
3 pi (claude-opus-5-5), read-only 9db4488bb936bc17d090dc7d868fba9729081750 PASS: 4 P3

Model relation: same-family

Findings:

  • P3 F1: The PR body still says lstat(.codegraph) failure gives index-missing; the code maps only ENOENT/ENOTDIR there, and no test covers the non-ENOENT branch.
  • P3 F2: Carried over: selectDarwinSqliteLibrary and 1a have no unit test, and there is no live-writer case.
  • P3 F3: index.ts:1263-1264 the comment saying Node owns all SDK/SQLite handles is stale now that snapshot readiness opens in-process under Bun, and the snapshot's rmSync(temporaryRoot) has no retries; Windows CI on this head decides it.
  • P3 F4: Dropping 2a and the 1b lock edit departs from the recorded codegraph-adapter: read the index only through the in-process read-only handle #574 decisions; the reasons are only in the PR body.

…pen-failed (#574 S1)

The codegraph.db check uses stat with the .codegraph rule: ENOENT or
ENOTDIR is index-missing, any other error is index-open-failed. Also
corrects the snapshot comment: readiness opens the copy in-process.

Refs #574
@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 4
Subject: 8649579
Tier: high
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: 4
Subject: 8649579
Tier: high
Verdict: PASS
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Round-4 delta review (9db4488..8649579). R3-1 is resolved: the database check distinguishes absence (ENOENT/ENOTDIR) from access and I/O failures, which map to index-open-failed. The new permission test would fail on 9db4488 for a non-root POSIX process and skips on win32 and under root. No new defect.

NITs

None.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
4 codex (gpt-6.1-terra), read-only 86495797f4e86d4002fb9242b99ce429ceae72f7 PASS: none

Model relation: different-family

Findings:

  • none

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness acceptance claim
Role: acceptor
Round: 4
Subject: 8649579
Tier: high
Agent: pi (claude-opus-5-5), 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: 4
Subject: 8649579
Tier: high
Verdict: PASS
Reviewer: pi (claude-opus-5-5)
Model relation: same-family

Round-4 review (Pi, Opus 5.5) with Bun 1.4.4 on a git archive copy of 8649579: tsc clean; 137 pass across codegraph-adapter, code-facts-not-ready, doctor-code-facts and flow-selector-check. Round-3 notes resolved. P3 notes only.

NITs

  • G1: index.ts:1232 the private snapshot still pre-checks existsSync(.codegraph/codegraph.db), so an unreadable .codegraph refuses there as index-missing; it fails closed and S4 deletes this path.
  • G2: Carried over: darwin module and 1a lack unit tests, no live-writer case, and the #574 record should note the 2a/1b supersession and the S6 follow-ups.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
4 pi (claude-opus-5-5), read-only 86495797f4e86d4002fb9242b99ce429ceae72f7 PASS: 2 P3

Model relation: same-family

Findings:

  • P3 G1: index.ts:1232 the private snapshot still pre-checks existsSync(.codegraph/codegraph.db), so an unreadable .codegraph refuses there as index-missing; it fails closed and S4 deletes this path.
  • P3 G2: Carried over: darwin module and 1a lack unit tests, no live-writer case, and the codegraph-adapter: read the index only through the in-process read-only handle #574 record should note the 2a/1b supersession and the S6 follow-ups.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

The chatgpt-codex-connector summary is for the earlier commit 4cc2537. Its follow-up reports the Codex usage limit, and it left no inline finding. The required High-tier reviews are the GPT and Opus receipts; both are PASS on 8649579 in round 4.

Seat: orchestrator t7

@Ancienttwo

Copy link
Copy Markdown
Owner Author

Joint decision (orchestrator + advisor), 2026-11-14: merge #589 (#574 S1) at 8649579 onto main 60f2968.

  • Required CI is green, including the Windows shards. That run shows the GC removal holds under Bun 1.4.4.
  • Round 4: GPT PASS with no findings, and Opus PASS with P3 notes only.
  • Blocking findings went 4, 2, 1, 0 across the rounds.
  • The receipt threads and the bot comment are answered.
  • P3 G1 goes away in S4. The S6 scope names the follow-ups: unit tests for the darwin module and 1a, a live-writer test for the read session, and the provider's .gitignore write on open.

Seat: orchestrator t7

@Ancienttwo
Ancienttwo merged commit 449b61b into main Nov 13, 2026
35 checks passed
@Ancienttwo
Ancienttwo deleted the feat/574-s1-read-session branch November 13, 2026 18:53
Ancienttwo added a commit that referenced this pull request Nov 13, 2026
…1b) (#596)

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 (449b61b)
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
```

<details><summary>move-only-proof.mjs</summary>

```js
#!/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);
```
</details>

`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.

<details><summary>the 64 exports (name, kind)</summary>

```
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
```
</details>

### 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.
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