Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 24 additions & 28 deletions docs/runbooks/ref-impact.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ whitespace belongs to the path and is preserved, including whitespace-only paths

## Ref-bound flow evidence

The CLI reads the existing CodeGraph index through a disposable private snapshot.
It never syncs or opens the original index. CodeGraph's own source enumeration and
readiness checks validate the snapshot. Indexed source hashes must match the
selected immutable Git blobs before declaration ranges are bound to that ref.
Only matching refs receive evidence; historical refs without matching facts stay
unknown. Hunk coordinates come from bounded comparisons of the same raw blobs.
The CLI reads the repository's existing CodeGraph index in-process through one
read-only handle (ADR-0058). It never syncs the index, opens it writable or copies
it, and it starts no CodeGraph process. The index must be ready first: compatible
with the pinned CodeGraph, and current with the worktree by CodeGraph's own change
test. Indexed source hashes must then match the selected immutable Git blobs before
declaration ranges are bound to that ref. Only matching refs receive evidence;
historical refs without matching facts stay unknown. Hunk coordinates come from
bounded comparisons of the same raw blobs.

Changed head ranges report the affected flow step. An authoritative zero-match
lookup in an indexed, supported, error-free file reports
Expand All @@ -34,29 +36,23 @@ ranges identify the step with `selector-evidence-unavailable`. Partial positive
findings remain visible while aggregate coverage stays unknown. Deleted base
lines require separately bound base evidence.

The snapshot uses CodeGraph 1.6.2's shipped source scanner, including its include,
includeIgnored and extension settings. The selected source/configuration files,
Git metadata and DB/WAL copy are limited to 512 MiB and 100,000 entries. Unsafe
symlinks in selected inputs, external Git object stores, unsupported path/config
overrides and changing inputs refuse evidence. Impact summarizes these snapshot
refusals as `ref-code-facts-unavailable`; it does not distinguish a missing index
from an unsupported snapshot layout. `flows check --no-sync` exposes the specific
snapshot failure reason. Ordinary ignored dependency trees
are not copied. Private temporary files are removed after reading.
The source population is CodeGraph 1.6.2's own file enumeration, including its
include, includeIgnored and extension settings. Impact summarizes every code-facts
refusal as `ref-code-facts-unavailable`: a missing index, an index that cannot be
opened, an index behind the worktree and an index that moved during the read.
`flows check --no-sync` reads the same way and exposes the specific reason:
`index-missing`, `index-open-failed`, `code-facts-read-not-ready` or
`code-facts-unavailable` (the CodeGraph provider could not load).

SDK queries run in the Node executable shipped with the same pinned CodeGraph
platform bundle. The caller receives complete query-result tables, without live
SQLite handles; the worker must exit successfully before strict snapshot removal.
The query boundary accepts at most 1 MiB of requests, 10,000 keys per table,
100,000 result items and 32 MiB of output, with a 30-second deadline. Missing or
malformed query coverage fails closed; an unrequested lookup is never an empty match.

Installed Git filter definitions alone do not prevent a snapshot. Git's effective
attributes are checked across all tracked paths, including embedded repositories;
selected clean/process filters are refused before provider status can run them.
Configured drivers named `unset` or `unspecified` are also refused when their names
make Git's attribute response ambiguous. The provider's name-status diff does not
request patches, so diff/textconv configuration alone does not execute a callback.
The read runs only the git commands ADR-0058 allows: `ls-files`, `rev-parse`,
`check-ignore` and `config --get core.excludesFile`, plus the `rev-parse`,
`ls-tree`, `cat-file` and `log` reads of the immutable refs. It never runs `git
status` or `git diff` and never writes the Git index, so configured clean/process
filters and index-writing hooks do not run, and a repository that uses them
still gets evidence. A configured `core.fsmonitor` hook still runs: `ls-files` and
`check-ignore` invoke it, as any git read of that repository does.
SQLite may leave the `codegraph.db-wal` and `codegraph.db-shm` sidecar files in
`.codegraph/`; nothing else is written.

Consumers must inspect reason codes, dependency coverage and truncation fields;
unknown or truncated output is not proof that no impact exists. Limits are
Expand Down
4 changes: 0 additions & 4 deletions packages/contracts/src/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ export const CODE_FACTS_ISSUE_MESSAGES = Object.freeze({
const policies: Record<string, Policy> = {
"history-shallow-repository": { code: "AC_PRECONDITION_FAILED", message: "Architecture history requires a complete Git repository.", action: "unshallow-repository", facts: { historyReadReason: "optional:historyReadReason" } },
"code-facts-read-not-ready": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "The existing CodeGraph index does not attest to the current source population.", facts: { issues: "codeFactsIssues" }, action: "codegraph-sync", retryable: false },
"code-facts-snapshot-changed": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "Source or index inputs changed while reading the private snapshot.", action: "retry-read", retryable: true, facts: {} },
"code-facts-snapshot-limit-exceeded": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "The private CodeGraph snapshot copy exceeds its 512 MiB or 100000 entry limit.", action: "reduce-snapshot-size", retryable: false, facts: { limit: "snapshotLimit", maxBytes: "uint", maxEntries: "uint", sourceBytes: "uint", sourceEntries: "uint", gitBytes: "uint", gitEntries: "uint", gitObjectEntries: "uint", indexBytes: "uint", indexEntries: "uint" } },
"code-facts-snapshot-layout-unsupported": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "The repository layout cannot be safely isolated for a zero-write CodeGraph read.", action: "inspect-repository-layout", retryable: false, facts: {} },
"daemon-root-missing": { code: "AC_RUNTIME_UNAVAILABLE", message: "The daemon start worktree no longer exists. Stop this daemon using its workspace state directory.", action: "stop-targeted-daemon", retryable: false, facts: {} },
"changeset-receipt-journal-unavailable": { code: "AC_PRECONDITION_FAILED", message: "No committed ChangeSet journal exists for this repository and journal id.", action: "select-committed-journal" },
"changeset-receipt-transition-unavailable": { code: "AC_PRECONDITION_FAILED", message: "This journal has no recorded model transition; a complete receipt cannot be exported.", action: "inspect-journal-evidence" },
Expand Down Expand Up @@ -394,7 +391,6 @@ const localEnums: Readonly<Record<string, readonly string[]>> = {
depsField: ["from", "into", "importNodeLimit", "edgeLimit"],
depsIssue: ["unknown-node", "invalid-limit", "missing-value"],
snapshotField: ["repositoryId", "workspaceId", "headSha", "worktreeDigest"],
snapshotLimit: ["bytes", "entries"],
jobsField: ["workerId"],
observedDomain: ["observed"],
inputAvailability: ["unavailable"],
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/test/failure-envelope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test("every registered failure reason has the strict four fields and bounded UTF
expectedBaseSha: "a".repeat(40), actualBaseSha: "b".repeat(40),
expectedWorktreeDigest: "sha256:"+"c".repeat(64), actualWorktreeDigest: "sha256:"+"d".repeat(64), expectedNamespace: "product.fixture", actualNamespace: "product.next", measuredBytes: 10, limitBytes: 9, kind: "timeout", confirmationToken: "public:github.com/acme/widgets:"+"a".repeat(40)+":audit_run.fixture", recordCommand: null, verifyCommand: null, observation: "before-resolve-append", field: reason === "projection-snapshot-mismatch" ? "worktreeDigest" : reason === "jobs-input-invalid" ? "workerId" : "into", issue: "unknown-node", inputDomain: "observed", availability: "unavailable", cause: "unclassified"
};
Object.assign(facts, {journalId:"journal.fixture",previousJournalId:"journal.previous",capabilityId:"capability.fixture",changeSetId:"changeset.fixture",manualRecoveryRequired:true,expectedModelDigest:"sha256:"+"a".repeat(64),actualModelDigest:"sha256:"+"b".repeat(64),expectedPlanDigest:"sha256:"+"a".repeat(64),actualPlanDigest:"sha256:"+"b".repeat(64),lookupKey:"sha256:"+"a".repeat(64),applyId:"sha256:"+"b".repeat(64),driftCount:1,rejectedCount:1,digestStable:false,pathCount:1,paths:["docs/file.md"],action:"remove-whole-file-outputs-and-reapply",commands:["archctx docs apply --approved"],omittedPathCount:0,omittedCommandCount:0,manifestPath:"docs/architecture/.projection-manifest.json",foundSchemaVersion:"archcontext.architecture-docs-projection-manifest/v1",supportedSchemaVersion:"archcontext.architecture-docs-projection-manifest/v3",upgradeCommand:"archctx docs apply --approved --profile <default|repo-harness/v1>",migrationGuide:"docs/runbooks/architecture-documentation-projections.md",subpath:"./sub",limit:"bytes",maxBytes:9,maxEntries:9,sourceBytes:10,sourceEntries:1,gitBytes:0,gitEntries:0,gitObjectEntries:1,indexBytes:0,indexEntries:0,historyReadReason:"history-file-mismatch"});
Object.assign(facts, {journalId:"journal.fixture",previousJournalId:"journal.previous",capabilityId:"capability.fixture",changeSetId:"changeset.fixture",manualRecoveryRequired:true,expectedModelDigest:"sha256:"+"a".repeat(64),actualModelDigest:"sha256:"+"b".repeat(64),expectedPlanDigest:"sha256:"+"a".repeat(64),actualPlanDigest:"sha256:"+"b".repeat(64),lookupKey:"sha256:"+"a".repeat(64),applyId:"sha256:"+"b".repeat(64),driftCount:1,rejectedCount:1,digestStable:false,pathCount:1,paths:["docs/file.md"],action:"remove-whole-file-outputs-and-reapply",commands:["archctx docs apply --approved"],omittedPathCount:0,omittedCommandCount:0,manifestPath:"docs/architecture/.projection-manifest.json",foundSchemaVersion:"archcontext.architecture-docs-projection-manifest/v1",supportedSchemaVersion:"archcontext.architecture-docs-projection-manifest/v3",upgradeCommand:"archctx docs apply --approved --profile <default|repo-harness/v1>",migrationGuide:"docs/runbooks/architecture-documentation-projections.md",subpath:"./sub",historyReadReason:"history-file-mismatch"});
if(reason==="runtime-ledger-mode-invalid") facts.field="rolloutMode";
if(reason==="projection-inputs-changed-during-run") facts.field="sourceFiles";
if(reason==="state-machine-model-invalid") Object.assign(facts,{issues:[".archcontext/model/state-machines/order.yaml: regions[0].initial: state-machine-initial-unknown"],omittedCount:0});
Expand Down
7 changes: 0 additions & 7 deletions packages/local-runtime/codegraph-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,6 @@ export {
type ProjectionCodeGraphStatusIssueCode,
type ProjectionCodeGraphStatusIssueV1
} from "./projection-handshake";
export {
CODEGRAPH_SNAPSHOT_MAX_BYTES,
CODEGRAPH_SNAPSHOT_MAX_ENTRIES,
codeGraphSnapshotQueries,
codeGraphSnapshotRuntime,
withExistingCodeGraphSnapshot
} from "./snapshot";
export {
CODEGRAPH_IMPORT_NODE_READ_LIMIT,
repositoryImportPairs,
Expand Down
50 changes: 50 additions & 0 deletions packages/local-runtime/codegraph-adapter/src/ready-index-read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { FailureError } from "@archcontext/contracts";
import { pinnedCodeGraphProvider, withCodeGraphReadSession, type CodeGraphIndexHandle } from "./read-session";
import { movingCodeFacts, openSessionCodeFacts, type RepositoryCodeFactsInspectionOptions, type RepositoryCodeFactsV1 } from "./repository-code-facts";

/**
* Runs `read` against the repository's own index through one read-only session (ADR-0058), and
* only while that index attests to the worktree: it is compatible and CodeGraph's change test finds
* no pending source change. Readiness and `read` use the same handle. Nothing syncs, copies or
* writes the index; `flows check --no-sync` and ref impact read through here.
*
* - no `.codegraph/codegraph.db`: `AC_CODE_FACTS_UNAVAILABLE` / `index-missing`
* - a linked `.codegraph`, or an index that cannot be opened and read: `index-open-failed`
* - an incompatible or stale index, a live build, or an index that moved during the read:
* `code-facts-read-not-ready` with the readiness issues; retryable for a live build or a move
* - any other failure, such as a provider that cannot load: `code-facts-unavailable`
*/
export function withReadyCodeGraphIndex<T>(root: string, read: (graph: CodeGraphIndexHandle) => T, options: RepositoryCodeFactsInspectionOptions = {}): T {
try {
const provider = options.provider ?? pinnedCodeGraphProvider();
return withCodeGraphReadSession(root, (opened) => {
switch (opened.state) {
case "index-missing":
throw new FailureError("AC_CODE_FACTS_UNAVAILABLE", "index-missing");
case "index-open-failed":
throw new FailureError("AC_CODE_FACTS_UNAVAILABLE", "index-open-failed");
case "build-in-progress":
throw notReady(movingCodeFacts(provider.version));
case "open": {
let readiness: { facts: RepositoryCodeFactsV1; retryable: boolean };
try { readiness = openSessionCodeFacts(opened.session, "freshness"); }
catch { throw new FailureError("AC_CODE_FACTS_UNAVAILABLE", "index-open-failed"); }
if (readiness.facts.state !== "ready") throw notReady(readiness);
const result = read(opened.session.graph);
// An unchanged revision after the read (design G4) proves readiness and the read saw one index state.
if (!opened.session.unchangedSinceOpen()) throw notReady(movingCodeFacts(provider.version));
return result;
}
}
}, { provider });
} catch (error) {
if (error instanceof FailureError) throw error;
// No provider or SDK text crosses this boundary.
throw new FailureError("AC_CODE_FACTS_UNAVAILABLE", "code-facts-unavailable");
}
}

function notReady({ facts, retryable }: { facts: RepositoryCodeFactsV1; retryable: boolean }): FailureError {
const issues = facts.state === "not-ready" ? facts.issues : [];
return new FailureError("AC_CODE_FACTS_UNAVAILABLE", "code-facts-read-not-ready", { issues }, retryable ? { retryable: true } : {});
}
29 changes: 15 additions & 14 deletions packages/local-runtime/codegraph-adapter/src/ref-impact-facts.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { isRepoRelativePosixPath, REF_IMPACT_LIMITS, type NativeContextModel, type NativeContextSnapshot } from "@archcontext/contracts";
import { nativeNodeSource, type NativeNode, type NativeModel } from "@archcontext/core/projection-engine";
import { nativeNodeSource, type NativeNode } from "@archcontext/core/projection-engine";
import { decodeUtf8, readGitBlobs, readGitBlobTree } from "../../git-adapter/src/blob-reader";
import type { RefImpactFacts } from "../../model-store-yaml/src/ref-impact";
import { codeGraphSnapshotQueries, withExistingCodeGraphSnapshot } from "./index";
type Sdk = typeof import("@colbymchenry/codegraph");
type SnapshotGraph = Pick<ReturnType<Sdk["CodeGraph"]["openSync"]>, "getFiles" | "getNodesByName">;
import { codeGraphBundle, type CodeGraphIndexHandle } from "./read-session";
import { withReadyCodeGraphIndex } from "./ready-index-read";
type RefImpactGraph = Pick<CodeGraphIndexHandle, "getFiles" | "getNodesByName">;
const nonDeclarations = new Set(["file", "import", "export", "parameter"]);
const requireFromAdapter = createRequire(import.meta.url);

/** Both refs are compared against one provider-authenticated private snapshot; no original DB open. */
/**
* Both refs are bound against the repository's own ready index, read in-process and read-only
* (ADR-0058). Every failure, including an index that is missing or behind the worktree, leaves the
* facts unknown.
*/
export function readRefImpactFacts(root: string, base: { model: NativeContextModel; snapshot: NativeContextSnapshot }, head: { model: NativeContextModel; snapshot: NativeContextSnapshot }): { baseFacts?: RefImpactFacts; headFacts?: RefImpactFacts } {
try {
return withExistingCodeGraphSnapshot(root, codeGraphSnapshotQueries([...base.model.nodes, ...head.model.nodes] as unknown as NativeModel["nodes"], "ref"), graph => {
return withReadyCodeGraphIndex(root, graph => {
const bind = (input: typeof head) => input.snapshot.source === "ref"
? bindSnapshotRefImpactFacts(root, input.model, input.snapshot, graph) : undefined;
? bindRefImpactFacts(root, input.model, input.snapshot, graph) : undefined;
return { baseFacts: bind(base), headFacts: bind(head) };
});
} catch {
Expand All @@ -24,20 +26,19 @@ export function readRefImpactFacts(root: string, base: { model: NativeContextMod
}

/**
* Called only inside withExistingCodeGraphSnapshot's authenticated ready callback.
* Every indexed file must match an immutable
* Called only inside withReadyCodeGraphIndex's read. Every indexed file must match an immutable
* blob, not just the current HEAD label. CodeGraph 1.6.2 stores SHA-256 of UTF-8 source text;
* strict UTF-8 and the 1 MiB extraction ceiling exclude replacement decoding and oversize stamps.
* A supported, error-free indexed file permits an authoritative exact-name no-match.
* That means the selector does not resolve in CodeGraph, not that no runtime code can exist.
* Missing files, unsupported languages and ambiguous declarations remain unknown.
*/
function bindSnapshotRefImpactFacts(
root: string, model: NativeContextModel, ref: { commitSha: string; treeOid: string }, graph: SnapshotGraph
function bindRefImpactFacts(
root: string, model: NativeContextModel, ref: { commitSha: string; treeOid: string }, graph: RefImpactGraph
): RefImpactFacts | undefined {
try {
const files = graph.getFiles();
const sdk = requireFromAdapter("@colbymchenry/codegraph") as Sdk;
const { sdk } = codeGraphBundle();
const supportedPaths = new Set(files.filter(file => sdk.isLanguageSupported(file.language)).map(file => file.path));
if (files.length > REF_IMPACT_LIMITS.codeFactFiles) return undefined;
const tree = new Map(readGitBlobTree(root, ref.treeOid, { sizes: true, trees: false }).map(entry => [entry.path, entry]));
Expand Down
Loading
Loading