Skip to content

fix(model): one model validator for YAML and ledger read modes (#576) - #593

Merged
Ancienttwo merged 3 commits into
mainfrom
fix/576-one-model-validator
Nov 13, 2026
Merged

Ancienttwo merged 3 commits into
mainfrom
fix/576-one-model-validator

Conversation

@Ancienttwo

@Ancienttwo Ancienttwo commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Closes #576.

Goal

Ledger read mode (ARCHCONTEXT_LEDGER_READ_MODE=ledger) validates a model the same way YAML validation does. This PR implements the joint decision in 576#issuecomment-6851382166. There is one model validator over a set of model files, and both read modes call it. The ledger validator is removed, not patched up to parity, and there is no compatibility path.

Change

  • Shared validator: packages/local-runtime/model-store-yaml/src/index.ts adds validateModelFiles(files: ModelFile[]): ModelValidationResult. It holds the body of the former YamlModelStore.validateModel, moved out of the class with the same checks in the same order: required files, per-file node JSON schema, the schemaVersion checks, the flow version, runtime topology, state machines (including the state machines: a YAML file nested under state-machines/ is silently skipped by validate and the loader #571 state-machine-path-nested error), ADR appliesTo, dependency constraints, review policy, warnings and modelDigest.
    • YamlModelStore.validateModel(workspace) is now validateModelFiles(listModelFiles(workspace.root)).
    • Required files are checked by their presence in the file set. Before, the YAML store checked with readFileSync. listModelFiles includes .archcontext/manifest.yaml and .archcontext/product.yaml exactly when they exist as files, so YAML results do not change. The ledger validator already used the set check.
  • Ledger read path: packages/local-runtime/runtime-daemon/src/index.ts, ArchitectureLedgerReadModelStore.validateModel, now returns { ...validateModelFiles(readback.files), architectureLedger: {…} }. readback.files is the ledger projection of nodes, relations and constraints, merged with the Git files the ledger does not own. This is the same file set as before.
  • Removed: validateLedgerModelFiles, the ledger-only subset validator. Also removed: modelDigestForFiles, which repeated the validator's modelDigest calculation, and the four architecture-domain imports that only the removed validator used.
  • One file order (round 2, R1 F1): validateModelFiles orders its own input with orderModelFiles (exported from model-store-yaml): by path, in UTF-16 code unit order. collectArchContextFiles uses the same comparator, compareModelPaths, so listModelFiles produces the same order as before (default .sort()). Error order and modelDigest therefore never depend on the caller. The ledger path used to sort its merged file set with localeCompare. That localeCompare sort is gone; the ledger path now calls orderModelFiles. It still sorts and is not just deleted, because loadModel returns the same list in ledger read mode (completeTask reads constraints and the review policy from it), and an unsorted list would leave loadModel ordered differently in the two modes.
  • No ledger-specific check: validateLedgerModelFiles had no check that applied only to ledger files. Every check it ran is in the shared validator, so nothing is added on top of it.

Where the shared validator lives, and why

It lives in model-store-yaml, next to ModelFile:

  • Both callers already import from model-store-yaml. runtime-daemon imports listModelFiles, YamlModelStore and type ModelFile from it.
  • model-store-yaml already depends on every input the validator needs: the node schema JSON, runtimeTopologyModelIssues from contracts, architecture-domain and state-machine-engine.

A core home does not work. @archcontext/core/architecture-domain cannot host it, because state-machine-engine imports architecture-domain (parseJsonOrStableYaml) and the move would create an import cycle. No other core package owns ModelFile.

Refactor stated (owner's rule)

  • The ledger read path now derives the schemaVersion of projected files with the shared extractSchemaVersion (newly exported from model-store-yaml). Its duplicated schemaVersionFromModelBody is deleted. The validator reads file.schemaVersion for the missing-version, flow-version and nested-path checks, so both modes must derive it the same way. On the canonical YAML that the projection emits, the two functions return the same value.
  • validateModel moved out of the class into validateModelFiles. It was re-indented as part of the move. Its logic is unchanged.

Tests

  • runtime-daemon/test/local-runtime.test.ts, new: "ledger read mode validates with the YAML validator: a rejected model fails the same way and a valid model passes (ledger read mode: validateModel has no parity with YAML validation (schemas, topology, state machines) #576)". It uses test.each, so each row fails on its own, with one row per branch:

    • a valid model (runtime topology plus a state machine);
    • a node schema violation ($.mount.target: required);
    • a topology error (target: topology-relation-target-kind-invalid);
    • a nested state machine (state-machine-path-nested);
    • two invalid files whose paths sort differently under the two comparators (sub/B.yaml, sub/a.yaml). The expected order is B before a in both modes. On the round-1 head this row fails: ledger mode returned a before B.

    For each row, the test first validates with YamlModelStore and asserts the expected errors. It then builds the ledger state from those files with planYamlToArchitectureLedgerImport and deletes nodes/ and relations/ from Git, so the ledger is their only source. Next it validates through a ledger-authoritative daemon and asserts readAuthority: "ledger", the entity count, and { valid, errors } equal to the YAML result. It also asserts that each mode's modelDigest is the digest of its own file set in path order. The test oracle builds that order with a default .sort() on the paths, not with orderModelFiles. The two modes have different digests by design, because their per-file digests come from different bodies; the existing test "ledger-authoritative runtime read surfaces …" asserts that they differ. Against the origin/main source, the schema-violation row fails: its ledger result is valid: true, errors: [].

  • model-store-yaml/test/validate-model.test.ts, new: validateModelFiles gets the same files in reverse localeCompare order and returns an identical result: the same errors, in the same order (nodes/B.yaml first), and the same modelDigest. On the round-1 head this test fails.

  • model-store-yaml/test/validate-model.test.ts, new: required-files branch. A model without product.yaml fails with missing .archcontext/product.yaml and a missing-file diagnostic. This branch had no YAML-level test before.

  • Changed existing test: runtime-daemon/test/ledger-layer-validation.test.ts.

    • It now imports validateModelFiles, because validateLedgerModelFiles is deleted.
    • Its fixture node was { schemaVersion, id, layer }, which fails the node schema. The old ledger validator did not check the node schema; that gap is this issue's defect. The fixture now carries the required kind, name, status and summary.
    • The test title says "model validation of ledger files".
    • The assertions are unchanged, apart from a non-null ! on the now-optional layerOrderErrors.

Verification

Head 5c3670ae1054f2ecadf3b8c0334d6ab5768bbdb7 (round 2), macOS (darwin arm64), bun 1.4.3:

Command Result Time
bun run typecheck exit 0 <1s
bun test packages/local-runtime/model-store-yaml packages/core/changeset-engine packages/core/context-compiler 252 pass, 0 fail (changeset-engine and context-compiler consume validateModel) 29s
bun test packages/local-runtime/runtime-daemon 855 pass, 1 skip, 0 fail (856 tests) 385s
bun test tests/ 4 pass, 0 fail 55s

Test budget (git diff --numstat origin/main...HEAD): test +115/−8, product +99/−139, no new test file. The product count includes the moved, re-indented validator body.

  • New: a 5-row test.each parity test (about 0.9s in total) and 2 single tests.
  • Changed: 1 existing test.

Architecture slice unchanged, see #576. No .archcontext/model/** or capability entrypoint changes.

Risk

  • In ledger read mode, a ledger state that was accepted before now fails validation if its projected nodes or relations break the node schema or a topology rule, or if a Git-side state-machine file is invalid. This is the intended change. ledger is not the default read authority in 0.14.x, because YAML is the only authority.
  • Ledger validation results can now include nodeSchemaErrors. The YAML results already did. changeSetBaseModelBlockingErrors consumes this field the same way in both modes.

Rollback

Revert this PR. The change touches no stored data and no schema.

Out of scope (possible follow-ups)

  • runtime-daemon/src/index.ts has unused imports that existed before this PR (errorEnvelope, auditConsentRequiredEnvelope). They were left as they are.

YamlModelStore.validateModel and the ledger read path both call
validateModelFiles in model-store-yaml. validateLedgerModelFiles, the
ledger-only subset validator, is removed. Ledger read mode now runs node
schema, runtime topology and state-machine checks, including the nested
state-machine path error from #571.

The ledger read path derives projected file schemaVersions with the
shared extractSchemaVersion; schemaVersionFromModelBody is removed.
@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-13T12:49:40.205345Z
We couldn’t complete this review. Comment @codex review to try again.
22ce466 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: 22ce466
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: 22ce466
Tier: high
Verdict: FAIL
Reviewer: codex (gpt-6.1-terra)
Model relation: different-family

Cross-family review (Codex CLI, read-only). Both read paths call the single validateModelFiles validator; the old ledger subset validator and duplicate schema-version parser are removed; the moved YAML checks keep their sequence. The two callers order files differently, so ledger and YAML can report the same failures in a different order. The PR omits the added-line test budget counts.

NITs

  • F2: PR body omits the added-line counts: 85 test, 86 product, no new test file; within the caps.

Findings outside the diff

  • P2 F1 at packages/local-runtime/runtime-daemon/src/index.ts:244: Ledger files sort with localeCompare, YAML files with default sort (model-store-yaml/src/index.ts:447); B.yaml precedes a.yaml in YAML order and follows it in ledger order. The shared validator appends errors in input order, so multiple invalid files can produce a different first error and differently ordered error arrays. -> Use the same path ordering in both paths and cover multiple invalid files in the parity test.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
1 codex (gpt-6.1-terra), read-only 22ce4661b1c79d6012a22625a24c32eb90b4b81f FAIL: 1 P2, 1 P3

Model relation: different-family

Findings:

  • P2 F1 at packages/local-runtime/runtime-daemon/src/index.ts:244: Ledger files sort with localeCompare, YAML files with default sort (model-store-yaml/src/index.ts:447); B.yaml precedes a.yaml in YAML order and follows it in ledger order. The shared validator appends errors in input order, so multiple invalid files can produce a different first error and differently ordered error arrays. -> Use the same path ordering in both paths and cover multiple invalid files in the parity test.
  • P3 F2: PR body omits the added-line counts: 85 test, 86 product, no new test file; within the caps.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Fresh-context Opus review (Pi seat, magpie). One validator, validateModelFiles in model-store-yaml, called by YamlModelStore.validateModel and ArchitectureLedgerReadModelStore.validateModel; validateLedgerModelFiles, modelDigestForFiles and schemaVersionFromModelBody are deleted with no references left; no compatibility path. git diff -w shows the old validateModel body moved unchanged except an equivalent required-files membership check. Ran the PR's daemon test against origin/main: schema, topology and nested state-machine rows all returned valid in ledger mode, so the tests are real. typecheck pass; validate-model + ledger-layer-validation 44 pass; local-runtime -t ledger 9 pass.

NITs

  • F1: runtime-daemon index.ts:244 sorts with localeCompare while listModelFiles uses default sort; confirmed orders differ ([x/B,x/a,x/a1,x/a_b] vs [x/a_b,x/a,x/a1,x/B]), so error order and modelDigest can differ between modes. Pre-existing; use one comparator and add a two-error row whose paths sort differently.
  • F2: local-runtime.test.ts:2333 table-driven test stops at the first failing row; optional test.each so each row fails on its own.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

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

Model relation: same-family

Findings:

  • P3 F1: runtime-daemon index.ts:244 sorts with localeCompare while listModelFiles uses default sort; confirmed orders differ ([x/B,x/a,x/a1,x/a_b] vs [x/a_b,x/a,x/a1,x/B]), so error order and modelDigest can differ between modes. Pre-existing; use one comparator and add a two-error row whose paths sort differently.
  • P3 F2: local-runtime.test.ts:2333 table-driven test stops at the first failing row; optional test.each so each row fails on its own.

validateModelFiles orders its input by path in code unit order, the order
listModelFiles produces, so error order and modelDigest never depend on
the caller. The ledger read path ordered its file set with localeCompare
and now uses the same exported orderModelFiles.

Siblings covered: every ledger-mode caller of the merged file set
(validateModel and loadModel), and the YAML listModelFiles order, which
now uses the same comparator. Parity test rows now run with test.each.
@Ancienttwo

Copy link
Copy Markdown
Owner Author

Round 1 fixes are in 5c3670a.

  • GPT P2 F1 and Opus F1 (file order): validateModelFiles now orders its own input with orderModelFiles, by path in code unit order. listModelFiles uses the same comparator. The ledger path's localeCompare sort is replaced by orderModelFiles. It still sorts, because loadModel returns that list in ledger mode. Siblings: ledger validateModel and loadModel, and the YAML listModelFiles order. New tests: a parity row with sub/B.yaml and sub/a.yaml that asserts the same error order in both modes and that each mode's modelDigest is computed in path order, plus a validator unit test showing that reversed localeCompare input gives an identical result. Both fail on the round-1 head.
  • Opus F2: the parity table now runs with test.each, so each row fails on its own.
  • GPT P3 F2: the line counts are in the PR body.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Cross-family review (Codex CLI, read-only) of the round-2 delta. The validator owns the file order and the parity table gains a two-file row; the budget matches (115 test / 99 product, no new file); prior assertions are intact. The daemon parity test checks each mode's modelDigest against its own file set rather than asserting equality between modes.

NITs

  • F2: runtime-daemon index.ts:245 loadLedgerModel still calls orderModelFiles; validateModelFiles sorts again. Keep ordering for loadModel where needed and let the validator own validation order.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
2 codex (gpt-6.1-terra), read-only 5c3670ae1054f2ecadf3b8c0334d6ab5768bbdb7 FAIL: 1 P2, 1 P3

Model relation: different-family

Findings:

  • P2 F1 at packages/local-runtime/runtime-daemon/test/local-runtime.test.ts:2392: The parity test compares error arrays but checks each modelDigest against a separate file set; equal-input digest parity between modes is not asserted. -> Add a parity case where YAML and ledger read the same paths and bodies and assert equal modelDigest.
  • P3 F2: runtime-daemon index.ts:245 loadLedgerModel still calls orderModelFiles; validateModelFiles sorts again. Keep ordering for loadModel where needed and let the validator own validation order.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Fresh-context Opus review (Pi seat, magpie) of the round-2 delta. validateModelFiles starts with orderModelFiles (compareModelPaths, identical to default string sort), so YAML error order and modelDigest are unchanged for existing repositories. loadLedgerModel keeps orderModelFiles because completeTask reads loadModel's list in ledger mode, justified in the PR body; localeCompare is gone. Cross-mode digest equality is not a property of the design (ledger hashes canonical projected YAML, YAML hashes disk bodies); instead each mode's digest is checked against its own set in default order, and a unit test proves validateModelFiles is input-order independent (same errors, order and modelDigest for reverse-localeCompare input). test.each adopted; budget matches numstat. typecheck pass; 45 + 13 pass; parity row 5 and the order unit test fail on 22ce466, so the tests are real.

NITs

  • R2-F1: local-runtime.test.ts:2394 digest oracle builds ledgerSet from all listModelFiles(root) plus projected files, while the real ledger path removes ledger-managed Git paths first; correct for this fixture, optionally filter to mirror isArchitectureLedgerManagedModelPath.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

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

Model relation: same-family

Findings:

  • P3 R2-F1: local-runtime.test.ts:2394 digest oracle builds ledgerSet from all listModelFiles(root) plus projected files, while the real ledger path removes ledger-managed Git paths first; correct for this fixture, optionally filter to mirror isArchitectureLedgerManagedModelPath.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Follow-up on the same head after the head owner's decline of round-2 F1, with no code change. The shared validator test feeds the same files in different orders and compares the full result, errors and modelDigest included; the daemon test checks each mode's digest against its own file set in default path order. Together they verify the ordering requirement without asserting equal digests for different file bodies, so F1 is resolved.

NITs

  • F2: runtime-daemon index.ts:245 loadLedgerModel still calls orderModelFiles while validateModelFiles sorts again; loadModel keeps its ordered output by design.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

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

Round Role Subject Verdict
3 codex (gpt-6.1-terra), read-only 5c3670ae1054f2ecadf3b8c0334d6ab5768bbdb7 PASS: 1 P3

Model relation: different-family

Findings:

  • P3 F2: runtime-daemon index.ts:245 loadLedgerModel still calls orderModelFiles while validateModelFiles sorts again; loadModel keeps its ordered output by design.

@Ancienttwo

Copy link
Copy Markdown
Owner Author

repo-harness integration status
Re: orchestrator t7 merge-only sync request (main frozen until #591; #591 merged)
Head: 82310e8, parents 5c3670a + main e41f70f
Delta: none beyond main (12 release files from 0.14.1; no overlap with the 5 files in this PR); git merge --no-edit, no conflict, no other change
Checks: none run locally on the merge (merge-only sync as requested); CI pending
Review: delta check for the new head per acceptor.md §2.2 (merge-only, no PR-file changes)

@Ancienttwo

Copy link
Copy Markdown
Owner Author

Merge-only sync: 82310e8 has parents 5c3670a (the reviewed head) and e41f70f (main after #591). git diff $(git merge-tree --write-tree 5c3670ae e41f70fe) 82310e8a is empty, so the head adds nothing beyond the clean merge. The round-2 Opus PASS and round-3 GPT PASS on 5c3670a carry over to this head. It merges when the required CI is green.

Seat: orchestrator t7

@Ancienttwo

Copy link
Copy Markdown
Owner Author

The chatgpt-codex-connector review summary is for the earlier commit 22ce466. Its follow-up reports the Codex usage limit, so it raised no finding on the current head. The required reviews come from the GPT and Opus receipts above.

Seat: orchestrator t7

@Ancienttwo
Ancienttwo merged commit 0983ae9 into main Nov 13, 2026
15 checks passed
@Ancienttwo
Ancienttwo deleted the fix/576-one-model-validator branch November 13, 2026 13:30
Ancienttwo added a commit that referenced this pull request Nov 13, 2026
…S1) (#589)

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:
  - `lstat(.codegraph)` fails with `ENOENT` or `ENOTDIR`: `index-missing`. Any other `lstat` error: `index-open-failed`.
  - `.codegraph` is a link, dangling or not: `index-open-failed`.
  - `stat(codegraph.db)` fails with `ENOENT` or `ENOTDIR`: `index-missing`. Any other `stat` error: `index-open-failed`. This is the same rule as for `.codegraph`.
  - A live writer lock (#426): `build-in-progress`, and the index is never opened.
  - The read-only open or the first revision read throws: `index-open-failed`.
  - Otherwise `open`.
- **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 (#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.
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.

ledger read mode: validateModel has no parity with YAML validation (schemas, topology, state machines)

1 participant