Skip to content

Commit d779273

Browse files
committed
refactor: phase 4 cleanups (M5 layering cycle, M12 architecture, cat-file guard)
- M5: applyPrimaryTool (repo_config.js) no longer dynamically imports the sync compiler; the sync runner is injected by cli.js (the orchestration layer), keeping the config-leaf module acyclic. syncFn is now required at runtime. - M12: refresh ARCHITECTURE.md for the v0.20-v0.24 modules that were absent from the reference -- commit_gate (forge precommit), consensus (forge verify --deep), knowledge_router, deja, and docs_impact (forge docs impact). - LOW: ledger_store.js git cat-file -e ref resolver passes `--` before the ref. Full suite (1069 pass), typecheck, biome, and docs check all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7htTiesXKLvAUtv6ET2jz
1 parent 35770e7 commit d779273

5 files changed

Lines changed: 62 additions & 7 deletions

File tree

ARCHITECTURE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,43 @@ the series and a sustained alarm rides the gate's block reason. Proceeding under
299299
assumptions appends a record the advisory names and the next handoff surfaces — a guess
300300
can never silently become a fact.
301301

302+
**Commit-boundary gate (`src/commit_gate.js`, `forge precommit`).** The commit rung of
303+
the gate lattice (turn ⊂ commit ⊂ PR): the Stop hook gates the turn and CI's `docs check`
304+
gates the PR, so this runs the SAME registry-derived completeness classifier
305+
(`classifyPath` from `gate.js`) plus `hasSecret` over staged added lines at the commit
306+
boundary — code staged without its doc/state artifact, or a staged secret, is caught
307+
while the fix is still one `git add` away. Each rung is an independent catch layer, so
308+
the silent-miss probability falls multiplicatively.
309+
310+
**Deep verification (`src/consensus.js`, `forge verify --deep`).** Where plain `verify`
311+
asks one oracle (the tests) plus one heuristic, this runs a table of independent lenses
312+
and aggregates them with the same noisy-OR risk score `lessons.js` uses, behind a
313+
cross-family gate so correlated structural signals can't block alone. Deep `ok` is a
314+
conjunction: the core verifier must PASS **and** the lens consensus must not block; an
315+
unconfigured core can never yield `ok:true`. The score is a calibrated heuristic, not a
316+
proof.
317+
318+
**Knowledge routing (`src/knowledge_router.js`).** The third routing leg beside `route.js`
319+
(model tiers) and `intent.js` (intent classes), using the same exemplar k-NN math: a fact
320+
is routed to its storage home (decisions vs. the ledger vs. …) tuned by adding example
321+
rows, never regexes. It is TOTAL by construction — a fact resembling nothing falls back to
322+
the ledger (whose decay semantics make an unsure placement safe), never "nowhere".
323+
324+
**Anti-repetition memory (`src/deja.js`).** Closes the "why do I keep re-solving solved
325+
tasks" gap: a clean first-try success used to leave no durable trace (cortex only mints on
326+
correction). At Stop it mints one `summary` claim (a deterministic, secret-redacted gist),
327+
attaching a `test.run` confirm when the session's tests passed; `dejaLookup` then ranks
328+
prior summary/lesson/diagnosis claims for a new task with the same `retrieve()` (rel × rec
329+
× val) the ledger query uses. No new protocol — it reuses the shipped PCM machinery.
330+
331+
**The documentation-impact graph (`src/docs_impact.js`, `forge docs impact`).** Where
332+
`docs check` reconciles fixed registries and `docs sync` scans a diff for identifiers,
333+
this answers "I changed X — which documented surfaces mention X and are now potentially
334+
stale?" via a data-driven `EXTRACTORS` registry (commands, flags, env vars, MCP tools,
335+
exported symbols, brand tokens, version, package.json fields) reused from `docs_check.js`,
336+
an inverted entity → `file:line` index over every doc surface, and a diff-scoped impact
337+
query ranked by confidence. Advisory by default; `--strict` exits non-zero for CI.
338+
302339
**Deliberately not wired:** `checkpointCadence` (optimal-stopping check spacing) still
303340
has no runtime step-loop to consume it — wiring it would mean inventing one. It stays
304341
library math with tests until a real consumer exists.

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4848
- Removed the dead `plugin` entry from `package.json` `files`, the vestigial
4949
`.gitlab-ci.yml`, and the dangling `@AGENTS.md` import in `CLAUDE.md`; fixed the
5050
off-palette Codex plugin `brandColor`.
51+
- **Broke a layering cycle in `repo_config.js`.** `applyPrimaryTool` no longer reaches
52+
back into the sync compiler via a dynamic `import("./sync.js")`; the sync runner is now
53+
injected by the caller (`cli.js`), keeping the config leaf acyclic.
54+
- `ledger_store.js`'s `git cat-file -e` ref resolver now passes `--` before the ref
55+
(defense in depth against a ref that begins with `-`).
56+
- **Refreshed `ARCHITECTURE.md`** for the v0.20–v0.24 modules that were missing from the
57+
reference: `commit_gate` (`forge precommit`), `consensus` (`forge verify --deep`),
58+
`knowledge_router`, `deja`, and `docs_impact` (`forge docs impact`).
5159

5260
## [0.25.0] - 2026-07-20
5361

src/cli.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2439,7 +2439,12 @@ async function run(argv) {
24392439
process.exitCode = 1;
24402440
return;
24412441
}
2442-
const r = await applyPrimaryTool(root, name);
2442+
// Inject the sync runner from here (the orchestration layer) so repo_config —
2443+
// a config-leaf module — no longer reaches back into the sync compiler.
2444+
const { sync } = await import("./sync.js");
2445+
const r = await applyPrimaryTool(root, name, {
2446+
syncFn: (r2) => sync({ targetRoot: r2 }),
2447+
});
24432448
if (json) return console.log(JSON.stringify(r, null, 2));
24442449
heading(`${BRAND.brand} tools — primary set\n`);
24452450
console.log(` primary tool ${paint(r.primaryTool, "ok")}`);

src/ledger_store.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ const repoRootOf = (dir) => dirname(dirname(dir));
5555
// (and non-git repos) never spawn git.
5656
const gitResolver = (root) => (sha) => {
5757
try {
58-
execFileSync("git", ["cat-file", "-e", sha], {
58+
// `--` guards against a ref that begins with "-" being read as a flag (defense in
59+
// depth; execFileSync already avoids the shell, and refs here are validated).
60+
execFileSync("git", ["cat-file", "-e", "--", sha], {
5961
cwd: root,
6062
stdio: "ignore",
6163
});

src/repo_config.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,20 +311,23 @@ export function nonPrimaryTargets(report, primary) {
311311

312312
/**
313313
* Set `name` as the repo's primary tool and gitignore every other tool's emitted
314-
* artifacts. Runs a real sync to learn the emit targets (injectable via `syncFn` for
315-
* tests), so the gitignore block always reflects what Forge actually emits.
314+
* artifacts. The caller injects `syncFn` (the sync runner) so this config-leaf module
315+
* never imports the sync compiler; the gitignore block reflects what Forge actually emits.
316316
* @param {string} root
317317
* @param {string} name canonical primary-tool key (must be in KNOWN_TOOLS)
318-
* @param {{syncFn?:(root:string)=>Promise<{report:any[]}>|{report:any[]}}} [opts]
318+
* @param {{syncFn?:(root:string)=>Promise<{report:any[]}>|{report:any[]}}} [opts] syncFn is required at runtime
319319
* @returns {Promise<{primaryTool:string, configPath:string, targets:string[],
320320
* gitignore:string, gitignorePath:string}>}
321321
*/
322322
export async function applyPrimaryTool(root, name, { syncFn } = {}) {
323323
if (!KNOWN_TOOLS.includes(name))
324324
throw new Error(`unknown tool: ${name} (known: ${KNOWN_TOOLS.join(", ")})`);
325325
const cfgFile = setPrimaryTool(root, name);
326-
const runSync = syncFn || (async (r) => (await import("./sync.js")).sync({ targetRoot: r }));
327-
const { report } = await runSync(root);
326+
// syncFn is injected by the caller (cli.js / tests). Required so this config-leaf
327+
// module never imports the sync compiler — keeping the layering acyclic.
328+
if (typeof syncFn !== "function")
329+
throw new Error("applyPrimaryTool requires an injected syncFn(root) -> { report }");
330+
const { report } = await syncFn(root);
328331
const targets = nonPrimaryTargets(report, name);
329332
const gi = ensureGitignoreBlock(root, targets);
330333
return {

0 commit comments

Comments
 (0)