Skip to content

Commit a9dab40

Browse files
authored
feat: report truncated ADR briefings, trim the ADR corpus 15.6%, add GEN-005 briefing budget (#501)
## Summary A second trim sweep, this time over the **ADRs and agent memory** — and the discovery that trimming alone was treating a symptom. `review-context --verbose` extracts each ADR's `Decision` and `Do's and Don'ts` and **truncates both at 2,000 characters**. Everything past that never reaches the agent the ADR governs, and it was announced only by an inline marker buried in the prose. Governance that never arrived read as governance that passed — `archgate check` stays green because companion rules do not measure prose. So this PR does three things: reports the truncation, shrinks the corpus so it happens less, and adds a rule so it does not silently come back. ## 1. Truncation is now reported, not hidden | Signal | Audience | | ------------------------------------- | --------------------------------------------------------- | | `truncatedSections` per ADR briefing | machine — names which sections were cut | | `truncatedBriefings` at the top level | machine — affected ids without walking every domain | | stderr warning via `logWarn` | human — stdout stays parseable for `--json` (ARCH-003 §5) | ``` warn: ADR briefing prose truncated for ARCH-005, ARCH-022, ARCH-024, GEN-004 — Decision and/or Do's and Don'ts are incomplete. Read the full text with `archgate adr show <id>`. ``` The same gap existed for `checkSummary`: `review-context` caps violations at 20 per rule and set `truncated`, but **nothing ever surfaced it**. Now warned too. (`check` itself passes no cap, so it was never affected.) ## 2. ADR corpus trimmed 15.6% (353KB → 298KB) 27 ADRs compressed **without changing a single decision**. What came out is exactly the prose GEN-004 bans in code: incident retellings, `previously`/`used to` framing, release-version and violation-count literals that go stale every release, and the same point restated across Context, Decision, and Consequences. | ADR | Before → After | Notable | | ------------------------- | ------------------ | -------------------------------------------------------------------- | | ARCH-024 | 41.5KB → 24.6KB | Decision 14,511 → 3,574 | | ARCH-022 | 38.9KB → 29.4KB | Decision front-loaded so the four ordered guardrails survive the cut | | ARCH-005 | 25.9KB → 15.7KB | Do's/Don'ts 11,733 → 3,539 | | CI-002 / CI-001 / GEN-001 | −26% / −18% / −16% | all six briefed sections now fit | **ARCH-005 is the interesting case.** Both briefed sections share one budget, and its `Decision` had ~1,200 chars of unused headroom while `Do's and Don'ts` overflowed by 9,700. Moving three normative rules into that headroom took it from ~17% of the Do list visible to _the Decision plus all ten `DO` items_ visible. Its `Decision` grew on purpose. Invariants held: frontmatter byte-identical everywhere, the two briefed headings untouched (the engine matches them as literal strings), and numbered clauses keep their numbers — `ARCH-003 §7` and `ARCH-024 cl.7` are cited by number from code comments and agent memory. ## 3. GEN-005: ADR Briefing Budget A new ADR plus companion rule so this cannot silently regress. ``` warn GEN-005/briefing-budget "Decision" is 3574 chars; review-context truncates at 2000, hiding 1574 chars from agent briefings ``` Three deliberate design choices: - **`warning`, not `error`.** ARCH-024 and ARCH-022 enumerate security-critical identifiers — banned globals, the `.constructor` spellings, the guardrail sequence, the `-I` invariant — and have an irreducible floor above the cap. A blocking rule would be unfixable by design and would train authors to suppress it. - **The cap is read from `DEFAULT_MAX_SECTION_CHARS` in the engine**, not hardcoded, so the rule and `briefAdr` cannot drift apart. Falls back to the documented default in consumer projects. - **It mirrors `extractAdrSections` exactly** — same heading scan, same case-insensitive match — so it measures what the engine actually extracts. Verified: reported line numbers land precisely on the `## Decision` headings. Overflow is _legitimized rather than forbidden_: an ADR that genuinely cannot fit must **declare** it. ARCH-024, ARCH-022, ARCH-005 and GEN-004 now each state what cannot be cut, which brings the repo into compliance with the new ADR in the same change — the precedent GEN-004 set. ## 4. Memory Memory turned out to be the lean part: the eight topic files reviewed gave up only **~2%**, because they were already in the terse house style. The real waste was `MEMORY.md` — an _index_ whose entries had grown past 300 characters and duplicated CLAUDE.md. Also folded in four AST/scanner notes that existed only in a stale working tree, **after verifying each against current code**. One claimed the `ctx.ast()` cache covered TS/JS only; it now covers every language, which makes the trap _broader_ — corrected rather than copied. Fixed a cross-link pointing at a CLAUDE.md section that does not exist. ## Verification - `bun run validate` — 1,616 tests pass, 0 fail; lint, typecheck, format, knip, build all clean - `archgate check` — 47/47 rules pass; the only 6 warnings are `GEN-005/briefing-budget` firing as designed, all four affected ADRs declaring their overflow - `@reviewer` — APPROVED across Architecture, General/Process, and Legal; 0 violations - New tests cover `truncatedSections` (single, both, absent, prose-not-requested) and `truncatedBriefings` collection - Docs updated in all three locales per GEN-002, dropping payload-size figures that go stale on every ADR edit ## Reviewer notes - Where a Consequences subsection already fell below the `adr-author` floors (ARCH-002, ARCH-008, ARCH-012, ARCH-014, ARCH-018, LEGAL-002, GEN-003), the shortfall was **left as-is**. Inventing a risk or trade-off to reach a count would author new policy under cover of a formatting change. Flagged rather than filled. - ARCH-013 gained two Consequences entries that document `shim-readme-sync` and `shim-license-sync` — both rules already exist in the companion `.rules.ts`; this is accuracy, not new policy. --------- Signed-off-by: Rhuan Barreto <rhuan@barreto.work> Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c5d82c5 commit a9dab40

70 files changed

Lines changed: 1686 additions & 1594 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.archgate/adrs/ARCH-001-command-structure.md

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ files: ["src/commands/**/*.ts"]
88

99
## Context
1010

11-
The CLI needs a consistent pattern for defining and registering commands. As the command surface grows (init, check, adr, review-context, session-context, upgrade, clean), the registration mechanism must scale without introducing hidden coupling or making the dependency graph opaque.
11+
The CLI needs a consistent pattern for defining and registering commands. As the command surface grows, the registration mechanism must scale without introducing hidden coupling or making the dependency graph opaque.
1212

1313
**Alternatives considered:**
1414

15-
- **Auto-discovery via `executableDir()`** — Commander.js supports automatic command discovery by scanning a directory for executable files. This eliminates manual imports but hides the dependency graph: adding or removing a command has no type-checked reference, making dead command detection impossible. It also requires each command to be a standalone executable, which prevents in-process testing and forces separate process spawning for every subcommand invocation.
16-
- **Plugin-based registration**A plugin system where commands register themselves via a manifest or hook (similar to Oclif or Clipanion). This adds flexibility for third-party extensions but introduces significant complexity for an internal CLI with a known, finite set of commands. The indirection makes it harder to trace which code handles which command.
17-
- **Single-file command map**Define all commands in a single file as a map of name-to-handler. Simple but creates a monolithic file that grows with every command, making merge conflicts frequent and readability poor.
15+
- **Auto-discovery via `executableDir()`** — Commander.js can scan a directory for executables, but that hides the dependency graph (no type-checked reference, so dead commands are undetectable) and forces each command to be a standalone executable, preventing in-process testing.
16+
- **Plugin-based registration**Manifest- or hook-based self-registration (Oclif, Clipanion) buys third-party extensibility at the cost of indirection that obscures which code handles which command — overkill for a finite internal command set.
17+
- **Single-file command map**One file mapping names to handlers is simple but becomes a monolith that grows with every command, causing frequent merge conflicts and poor readability.
1818

19-
The explicit register pattern strikes the right balance: each command owns its registration logic, the entry point makes all commands visible at a glance, and in-process execution enables straightforward testing without process spawning.
19+
The explicit register pattern balances these: each command owns its registration, the entry point makes all commands visible at a glance, and in-process execution enables testing without process spawning.
2020

2121
## Decision
2222

@@ -34,20 +34,20 @@ Commands live in src/commands/ and export a register\*Command(program) function.
3434

3535
### Do
3636

37-
- Export a register\*Command function from each command module
38-
- Keep commands thin: parse args, call helpers/engine, format output
39-
- Use src/commands/<name>.ts for top-level commands
40-
- Use src/commands/<name>/index.ts for command groups with subcommands
41-
- Import the register function explicitly in src/cli.ts
42-
- Wrap all async logic in src/cli.ts in an async function main() and call it as main().catch((err) => { logError(String(err)); process.exit(2); }) — this is required for bun build --compile --bytecode compatibility
37+
- **DO** export a register\*Command function from each command module
38+
- **DO** keep commands thin: parse args, call helpers/engine, format output
39+
- **DO** use src/commands/<name>.ts for top-level commands
40+
- **DO** use src/commands/<name>/index.ts for command groups with subcommands
41+
- **DO** import the register function explicitly in src/cli.ts
42+
- **DO** wrap all async logic in src/cli.ts in an async function main() and call it as main().catch((err) => { logError(String(err)); process.exit(2); }) — this is required for bun build --compile --bytecode compatibility
4343

4444
### Don't
4545

46-
- Don't put business logic in command files — move it to src/engine/, src/helpers/, or src/formats/
47-
- Don't use executableDir() for command discovery
48-
- Don't call .parse() in command files — the entry point handles parsing
49-
- Don't create commands that spawn child processes for subcommand execution
50-
- Don't use top-level await in src/cli.ts — bun build --compile --bytecode (the binary compiler) rejects it even though bun run and tsc accept it. The symptom is a build-time parse error: "await" can only be used inside an "async" function
46+
- **DON'T** put business logic in command files — move it to src/engine/, src/helpers/, or src/formats/
47+
- **DON'T** use executableDir() for command discovery
48+
- **DON'T** call .parse() in command files — the entry point handles parsing
49+
- **DON'T** create commands that spawn child processes for subcommand execution
50+
- **DON'T** use top-level await in src/cli.ts — bun build --compile --bytecode (the binary compiler) rejects it even though bun run and tsc accept it. The symptom is a build-time parse error: "await" can only be used inside an "async" function
5151

5252
## Implementation Pattern
5353

@@ -59,22 +59,17 @@ import type { Command } from "@commander-js/extra-typings";
5959
import { loadRuleAdrs } from "../engine/loader";
6060
import { runChecks } from "../engine/runner";
6161
import { reportConsole, reportJSON, getExitCode } from "../engine/reporter";
62-
import { logError } from "../helpers/log";
6362

6463
export function registerCheckCommand(program: Command) {
6564
program
6665
.command("check")
6766
.description("Run automated ADR compliance checks")
6867
.option("--json", "Output results as JSON")
69-
.option("--staged", "Only check staged files")
7068
.action(async (opts) => {
7169
const adrs = await loadRuleAdrs();
72-
const results = await runChecks(adrs, { staged: opts.staged });
73-
if (opts.json) {
74-
reportJSON(results);
75-
} else {
76-
reportConsole(results);
77-
}
70+
const results = await runChecks(adrs);
71+
if (opts.json) reportJSON(results);
72+
else reportConsole(results);
7873
process.exit(getExitCode(results));
7974
});
8075
}
@@ -84,11 +79,9 @@ export function registerCheckCommand(program: Command) {
8479
// src/cli.ts — explicit imports make all commands visible
8580
import { registerCheckCommand } from "./commands/check";
8681
import { registerInitCommand } from "./commands/init";
87-
import { registerAdrCommand } from "./commands/adr";
8882

8983
registerInitCommand(program);
9084
registerCheckCommand(program);
91-
registerAdrCommand(program);
9285
```
9386

9487
### Bad Example
@@ -97,12 +90,11 @@ registerAdrCommand(program);
9790
// BAD: business logic inside command file
9891
export function registerCheckCommand(program: Command) {
9992
program.command("check").action(async () => {
100-
// Business logic should NOT be here
10193
const files = await glob("src/**/*.ts");
10294
for (const file of files) {
10395
const content = await Bun.file(file).text();
10496
const violations = content.match(/console\.error/g);
105-
// ... complex processing ...
97+
// ... complex processing that belongs in src/engine/ ...
10698
}
10799
});
108100
}
@@ -117,9 +109,6 @@ bun build --compile --bytecode — the command used to produce standalone binari
117109
import { logError } from "./helpers/log";
118110

119111
// Synchronous bootstrap checks can remain at top level
120-
if (!semver.satisfies(Bun.version, ">=1.2.21"))
121-
throw new Error("You need to update Bun to version 1.2.21 or higher");
122-
123112
createPathIfNotExists(paths.cacheFolder);
124113

125114
async function main() {
@@ -129,10 +118,7 @@ async function main() {
129118
registerInitCommand(program);
130119
// ... register other commands ...
131120

132-
const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
133121
await program.parseAsync(process.argv);
134-
const notice = await updateCheckPromise;
135-
if (notice) console.log(notice);
136122
}
137123

138124
main().catch((err) => {
@@ -143,11 +129,7 @@ main().catch((err) => {
143129

144130
```typescript
145131
// src/cli.ts — BAD: top-level await breaks bun build --compile --bytecode
146-
createPathIfNotExists(paths.cacheFolder);
147-
148132
await installGit(); // ERROR: "await" can only be used inside an "async" function
149-
150-
const program = new Command().name("archgate").version(packageJson.version);
151133
await program.parseAsync(process.argv); // also breaks
152134
```
153135

@@ -158,8 +140,6 @@ await program.parseAsync(process.argv); // also breaks
158140
import type { Command } from "@commander-js/extra-typings";
159141
import { registerAdrCreateCommand } from "./create";
160142
import { registerAdrListCommand } from "./list";
161-
import { registerAdrShowCommand } from "./show";
162-
import { registerAdrUpdateCommand } from "./update";
163143

164144
export function registerAdrCommand(program: Command) {
165145
const adr = program
@@ -168,8 +148,6 @@ export function registerAdrCommand(program: Command) {
168148

169149
registerAdrCreateCommand(adr);
170150
registerAdrListCommand(adr);
171-
registerAdrShowCommand(adr);
172-
registerAdrUpdateCommand(adr);
173151
}
174152
```
175153

@@ -180,19 +158,23 @@ export function registerAdrCommand(program: Command) {
180158
- **In-process execution enables testing** — Commands can be tested by calling register\*Command() directly, without spawning subprocesses or mocking executables
181159
- **Explicit imports make dependencies clear** — Opening src/cli.ts shows every command the CLI supports. No hidden commands loaded at runtime.
182160
- **Subcommand nesting is straightforward** — Command groups use the same pattern as top-level commands, with an index.ts that composes children
183-
- **Type-safe registration**Commander.js @commander-js/extra-typings provides full type inference for options and arguments within each register function
184-
- **Binary-compatible entry point** — The main() wrapper pattern ensures src/cli.ts compiles cleanly with bun build --compile --bytecode for standalone binary distribution
161+
- **Type-safe registration**@commander-js/extra-typings provides full type inference for options and arguments within each register function
162+
- **Binary-compatible entry point** — The main() wrapper ensures src/cli.ts compiles cleanly with bun build --compile --bytecode for standalone binary distribution
185163

186164
### Negative
187165

188-
- **Manual import bookkeeping** — Each new command requires adding an import and registration call in src/cli.ts. This is a minor overhead for a CLI with fewer than 15 commands.
189-
- **No hot-reload of commands** — Adding a new command requires restarting the CLI process. Acceptable for a development tool.
166+
- **Manual import bookkeeping** — Each new command requires an import and registration call in src/cli.ts. Minor overhead at this command count.
167+
- **No hot-reload of commands** — Adding a command requires restarting the CLI process. Acceptable for a development tool.
168+
- **Entry-point indirection** — Because top-level await is forbidden, src/cli.ts carries a main()/.catch() scaffold instead of straight-line bootstrap code.
190169

191170
### Risks
192171

193-
- **Stale imports when commands are removed** — If a command file is deleted but its import in src/cli.ts is not removed, TypeScript will catch the error at compile time. The bun run typecheck step in the validation pipeline prevents this from reaching production.
194-
- **Command group index.ts confused with barrels** — The index.ts files in command group directories (e.g., src/commands/adr/index.ts) contain real composition logic, not re-exports. ARCH-004 No Barrel Files explicitly permits index.ts files with logic.
195-
- **Top-level await regression** — A developer unfamiliar with the --bytecode constraint may introduce top-level await back into src/cli.ts. Mitigation: The bun run build:check step in the validate pipeline catches this immediately — bun run validate will fail locally before the code reaches CI.
172+
- **Stale imports when commands are removed** — Deleting a command file without removing its import in src/cli.ts leaves a dangling reference.
173+
- **Mitigation:** TypeScript catches it at compile time; the bun run typecheck step in the validation pipeline blocks it before production.
174+
- **Command group index.ts confused with barrels** — The index.ts files in command group directories (e.g., src/commands/adr/index.ts) contain real composition logic, not re-exports.
175+
- **Mitigation:** ARCH-004 No Barrel Files explicitly permits index.ts files with logic.
176+
- **Top-level await regression** — A developer unfamiliar with the --bytecode constraint may reintroduce top-level await in src/cli.ts.
177+
- **Mitigation:** The bun run build:check step in the validate pipeline catches it immediately — bun run validate fails locally before the code reaches CI.
196178

197179
## Compliance and Enforcement
198180

0 commit comments

Comments
 (0)