mandrel 1.60.0 → 1.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +74 -32
- package/.agents/docs/SDLC.md +8 -9
- package/.agents/docs/configuration.md +61 -4
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +2 -3
- package/.agents/runtime-deps.json +2 -2
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
- package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
- package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/label-taxonomy.js +2 -2
- package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
- package/.agents/scripts/lib/onboard/init-tail.js +218 -0
- package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/workflows/agents-update.md +14 -29
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/plan.md +45 -3
- package/README.md +18 -30
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +24 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +66 -7
- package/lib/cli/registry.js +41 -145
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +145 -192
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +6 -6
- package/.agents/workflows/onboard.md +0 -208
- package/lib/cli/__tests__/migrate.test.js +0 -268
- package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
- package/lib/cli/__tests__/sync.test.js +0 -372
- package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
- package/lib/cli/__tests__/update-major.test.js +0 -217
- package/lib/cli/__tests__/update-reexec.test.js +0 -513
- package/lib/cli/__tests__/update.test.js +0 -696
- package/lib/cli/__tests__/version-check.test.js +0 -398
- package/lib/migrations/__tests__/index.test.js +0 -216
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
# Quality Gates
|
|
2
|
+
|
|
3
|
+
This is the consumer-facing reference for the quality gates the framework
|
|
4
|
+
runs against your repo: the lint baseline ratchet, the maintainability
|
|
5
|
+
ratchet, the CRAP per-method gate, the **absolute quality floors**
|
|
6
|
+
(90/85/90 coverage, MI ≥ 70, CRAP ≤ 20), the anti-thrashing protocol,
|
|
7
|
+
and the concurrent close-safety retry that protects the Epic branch when
|
|
8
|
+
multiple Stories close in quick succession.
|
|
9
|
+
|
|
10
|
+
The floor + ratchet duo is intentional: the ratchet protects against
|
|
11
|
+
regressions on touched files; the floor enforces an absolute threshold
|
|
12
|
+
on every in-scope file regardless of diff scope. See
|
|
13
|
+
[§ Absolute quality floors (Epic #1184)](#absolute-quality-floors-epic-1184)
|
|
14
|
+
below for the policy and [`docs/decisions.md`](../../docs/decisions.md) (ADR
|
|
15
|
+
20260512-coupling-stance) for the framework-wide stance that motivates
|
|
16
|
+
the lift the floor gate represents.
|
|
17
|
+
|
|
18
|
+
The configuration knobs that drive these gates live in
|
|
19
|
+
[`.agents/docs/configuration.md`](../docs/configuration.md) under
|
|
20
|
+
`delivery.quality.*` and the framework-internal `DEFAULT_STORY_MERGE_RETRY` constant. This
|
|
21
|
+
file is the runbook side — what the gate does, when it fires, and how to
|
|
22
|
+
bootstrap or refresh it.
|
|
23
|
+
|
|
24
|
+
The **baseline envelope, per-kind shapes, component model, writer/reader
|
|
25
|
+
contract, and floor-override path** are documented in the
|
|
26
|
+
[Baseline reference](#baseline-reference) section at the end of this
|
|
27
|
+
document. Each per-gate section below cross-links to that section; consult
|
|
28
|
+
it once and reuse the context as you read through any individual gate.
|
|
29
|
+
|
|
30
|
+
> **Story-level gates.** Quality gates run against the Story branch
|
|
31
|
+
> after the single Story-implementation phase completes. Friction
|
|
32
|
+
> comments flip the Story to `agent::blocked` and post on the Story
|
|
33
|
+
> ticket.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Concurrent close safety
|
|
38
|
+
|
|
39
|
+
`/deliver`'s wave loop may close multiple Stories into the same
|
|
40
|
+
`epic/<epicId>` branch in quick succession. The push step inside `story-close.js` retries
|
|
41
|
+
on a non-fast-forward rejection — fetch, replay the story merge on top of
|
|
42
|
+
the new remote tip, push again — bounded by
|
|
43
|
+
`DEFAULT_STORY_MERGE_RETRY.maxAttempts` (3) and
|
|
44
|
+
`DEFAULT_STORY_MERGE_RETRY.backoffMs` (`[250, 500, 1000]`) from
|
|
45
|
+
`.agents/scripts/lib/config/runners.js`.
|
|
46
|
+
A real
|
|
47
|
+
content conflict (both stories touched the same lines) aborts the loop
|
|
48
|
+
with a clear error and leaves the local tree clean for manual resolution.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Test runner concurrency
|
|
53
|
+
|
|
54
|
+
`npm test` (via [`.agents/scripts/run-tests.js`](../scripts/run-tests.js))
|
|
55
|
+
derives `--test-concurrency` from `os.availableParallelism()` at startup,
|
|
56
|
+
clamped into the `[TEST_CONCURRENCY_MIN, TEST_CONCURRENCY_MAX]` range of
|
|
57
|
+
`[1, 16]` (`resolveTestConcurrency`). The clamp keeps the value sane at
|
|
58
|
+
both extremes: on the GitHub Actions 2-vCPU runner the derived value
|
|
59
|
+
matches the host instead of leaving wall-clock on the table, and on
|
|
60
|
+
very-wide dev hosts the cap of 16 bounds the filesystem-race surface
|
|
61
|
+
from shared FS fixtures (`memfs` mounts, `temp/` snapshot dirs, the
|
|
62
|
+
`coverage/` artifact directory shared with the CRAP gate).
|
|
63
|
+
|
|
64
|
+
The coverage run is the exception: `npm run test:coverage`
|
|
65
|
+
([`.agents/scripts/run-coverage.js`](../scripts/run-coverage.js))
|
|
66
|
+
still pins `--test-concurrency=8` so coverage timings stay comparable
|
|
67
|
+
across hosts. That fixed 8 sits in the same neighborhood as the cap=8
|
|
68
|
+
orchestration helpers (`SUBTICKET_HYDRATION_CONCURRENCY`, and
|
|
69
|
+
historically the since-deleted wave-gate helper, removed in PR #3936)
|
|
70
|
+
that settled on 8 as the project house-style ceiling. Any change to the
|
|
71
|
+
clamp bounds or the coverage pin must be paired with a benchmark run on
|
|
72
|
+
both a Windows dev host and a GitHub Actions runner to confirm it
|
|
73
|
+
doesn't reintroduce concurrency flakes.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Coverage baseline gate
|
|
78
|
+
|
|
79
|
+
> Baseline envelope, axes, and component model: see the
|
|
80
|
+
> [Baseline reference](#baseline-reference) section below.
|
|
81
|
+
|
|
82
|
+
`npm run test:coverage` drives
|
|
83
|
+
[`.agents/scripts/run-coverage.js`](../scripts/run-coverage.js),
|
|
84
|
+
which runs the unit-test suite with `NODE_V8_COVERAGE` set, post-processes
|
|
85
|
+
the V8 dumps with `c8 report`, then delegates to
|
|
86
|
+
[`.agents/scripts/check-baselines.js`](../scripts/check-baselines.js)
|
|
87
|
+
for the gate decision. There is no global `lines/branches/functions`
|
|
88
|
+
threshold — the gate compares **per-file** coverage in
|
|
89
|
+
`coverage/coverage-final.json` against the floors recorded in
|
|
90
|
+
[`baselines/coverage.json`](../../baselines/coverage.json) and fails on:
|
|
91
|
+
|
|
92
|
+
- a regression on any axis (lines, branches, or functions) for any file
|
|
93
|
+
whose coverage dropped more than `0.01` percentage points below its
|
|
94
|
+
recorded floor;
|
|
95
|
+
- an in-scope file with no baseline entry (a brand-new untested CLI
|
|
96
|
+
shell would otherwise sail through with 0 % coverage and no recorded
|
|
97
|
+
floor to drop below).
|
|
98
|
+
|
|
99
|
+
Scope (include/exclude) and reporters are declared in
|
|
100
|
+
[`.c8rc.cjs`](../../.c8rc.cjs); the gate reads the same file so `c8 report`
|
|
101
|
+
and the per-file checker agree on what's in scope. Bootstrap or
|
|
102
|
+
ratchet the baseline when an intentional scope change shifts coverage:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm run test:coverage # produces coverage/coverage-final.json (gate
|
|
106
|
+
# warns + passes when no baseline exists yet)
|
|
107
|
+
npm run coverage:update # writes baselines/coverage.json from the run
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`npm run coverage:check` runs the gate standalone against an existing
|
|
111
|
+
`coverage-final.json` artifact (useful from CI hooks or close-validation
|
|
112
|
+
runners that orchestrate coverage capture separately).
|
|
113
|
+
|
|
114
|
+
The same files-out-of-scope list as before, declared in `.c8rc.cjs`:
|
|
115
|
+
|
|
116
|
+
- `.agents/scripts/agents-bootstrap-github.js` — one-shot bootstrap CLI
|
|
117
|
+
whose meaningful logic (label taxonomy + project field defs) lives
|
|
118
|
+
in `lib/label-taxonomy.js` and is unit-tested there. The CLI shell
|
|
119
|
+
itself is integration-only against a live GitHub repo.
|
|
120
|
+
- `.agents/scripts/hydrate-context.js` — thin wrapper around the
|
|
121
|
+
unit-tested hydration engine; end-to-end coverage requires a real
|
|
122
|
+
provider tree and Story prompt context, which lives in integration
|
|
123
|
+
tests.
|
|
124
|
+
- `epic-plan-decompose.js`, `epic-plan-spec.js`,
|
|
125
|
+
`epic-plan-healthcheck.js` — `/epic-plan` slash-command CLI shells
|
|
126
|
+
with no unit-test seam; the meaningful orchestration logic lives in
|
|
127
|
+
`lib/orchestration/plan-runner/*` and is unit-tested there.
|
|
128
|
+
- A larger Story #1702 carve-out of top-level CLI gates, orchestration
|
|
129
|
+
CLIs, git-manipulation CLIs, and `lib/*` glue (e.g. `lint-baseline.js`,
|
|
130
|
+
`story-close.js`, `dispatcher.js`, `run-tests.js`,
|
|
131
|
+
`lib/config-schema.js`) — see the `.c8rc.cjs` header comment for the
|
|
132
|
+
per-category rationale and the authoritative entry list.
|
|
133
|
+
|
|
134
|
+
Each excluded file also carries `/* node:coverage ignore file */` at
|
|
135
|
+
the top of its source as a second line of defence; the full
|
|
136
|
+
justification for each exclusion lives in the header comment of
|
|
137
|
+
[`.c8rc.cjs`](../../.c8rc.cjs) and MUST be updated when the list changes.
|
|
138
|
+
|
|
139
|
+
The current shape of this pipeline (NODE_V8_COVERAGE +
|
|
140
|
+
`c8 report` instead of wrapping the run in `c8 <cmd>`) was chosen
|
|
141
|
+
after a one-off A/B benchmark showed it was ~19 % faster end-to-end
|
|
142
|
+
on a Windows dev host while producing the same `coverage-final.json`
|
|
143
|
+
artifact.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Absolute quality floors (Epic #1184)
|
|
148
|
+
|
|
149
|
+
The per-file ratchet only protects against **regressions** — if a file
|
|
150
|
+
has been sitting at 60 % coverage or MI = 58 since the v5 baseline, the
|
|
151
|
+
ratchet is perfectly happy to keep it there forever. Epic #1184 layers
|
|
152
|
+
an absolute-threshold gate on top of the ratchet that fails the build
|
|
153
|
+
when any in-scope file is below floor, regardless of whether the diff
|
|
154
|
+
touched it:
|
|
155
|
+
|
|
156
|
+
| Metric | Floor | Scope |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| Coverage — lines | ≥ 90 % | per file |
|
|
159
|
+
| Coverage — branches | ≥ 85 % | per file |
|
|
160
|
+
| Coverage — functions | ≥ 90 % | per file |
|
|
161
|
+
| Maintainability Index | ≥ 70 | per file |
|
|
162
|
+
| CRAP | ≤ 20 | per method |
|
|
163
|
+
|
|
164
|
+
The floors are declared in [`.agentrc.json`](../../.agentrc.json) under
|
|
165
|
+
`delivery.quality.gates.<gate>.floors.*` (defaults baked into the helper
|
|
166
|
+
match the table above) and resolved at runtime by the shared
|
|
167
|
+
helper [`lib/orchestration/check-baselines/phases/floors.js`](../scripts/lib/orchestration/check-baselines/phases/floors.js).
|
|
168
|
+
All three gates run through `check-baselines.js` (coverage,
|
|
169
|
+
maintainability, crap), which invokes the floors phase **after** the
|
|
170
|
+
ratchet decision so a file that's below floor but matched the (stale)
|
|
171
|
+
baseline still trips the gate.
|
|
172
|
+
|
|
173
|
+
### When the floor gate fires
|
|
174
|
+
|
|
175
|
+
- **Pre-push** (`.husky/pre-push`): diff-scoped, fast path only —
|
|
176
|
+
`quality-preview.js --changed-since origin/main` (MI + CRAP preview),
|
|
177
|
+
then `coverage-capture.js` and `npm run crap:check` (unified
|
|
178
|
+
dispatcher, diff-scoped via `delivery.quality.gateScoping`). Full-repo
|
|
179
|
+
lint, docs generation checks, and the complete test suite are **not**
|
|
180
|
+
run on push; use `npm run verify` locally before a PR. CI enforces the
|
|
181
|
+
authoritative full gate set on every PR.
|
|
182
|
+
- **CI** (`.github/workflows/ci.yml`): the `validate` job runs
|
|
183
|
+
**Lint and Format** (`npm run lint`), a **Maintainability Check**
|
|
184
|
+
(`npm run maintainability:check` → `check-baselines.js --gate
|
|
185
|
+
maintainability`, diff-scoped on PRs via
|
|
186
|
+
`delivery.quality.gateScoping`, full scope on push-to-main via
|
|
187
|
+
`BASELINE_SCOPE=full`), and **Run Tests with Coverage**
|
|
188
|
+
(`npm run test:coverage`), uploading the `test-results` and
|
|
189
|
+
`coverage-final` artifacts. A separate required **baselines** job runs
|
|
190
|
+
the unified `node .agents/scripts/check-baselines.js --format text`,
|
|
191
|
+
which enforces floors across every configured gate.
|
|
192
|
+
|
|
193
|
+
### Opt-out
|
|
194
|
+
|
|
195
|
+
There is no floor opt-out flag on the check path. The `*:update`
|
|
196
|
+
baseline-snap scripts snapshot whatever the current numbers are without
|
|
197
|
+
floor enforcement **by construction** — they are writers, not gates —
|
|
198
|
+
so no disable switch exists or is needed (the floors phase at
|
|
199
|
+
[`lib/orchestration/check-baselines/phases/floors.js`](../scripts/lib/orchestration/check-baselines/phases/floors.js)
|
|
200
|
+
has no off switch).
|
|
201
|
+
|
|
202
|
+
### No silent excludes (`.c8rc.cjs` policy)
|
|
203
|
+
|
|
204
|
+
The floor gate is only as strict as its scope, so the `exclude` list in
|
|
205
|
+
[`.c8rc.cjs`](../../.c8rc.cjs) carries three hard requirements that are
|
|
206
|
+
enforced by review (and partially by the audit suite):
|
|
207
|
+
|
|
208
|
+
1. **One-line rationale per entry.** Every file in `exclude[]` MUST have
|
|
209
|
+
a bulleted justification in the `.c8rc.cjs` header comment naming
|
|
210
|
+
*why* it is excluded — typically "thin CLI shell, meaningful logic
|
|
211
|
+
lives in `lib/<X>` and is unit-tested there." A bare path with no
|
|
212
|
+
rationale is a review-block.
|
|
213
|
+
2. **`/* node:coverage ignore file */` pragma at source.** Every
|
|
214
|
+
excluded file MUST carry the Node coverage pragma at the top of its
|
|
215
|
+
own source. This is the second line of defence: when `c8 report` and
|
|
216
|
+
the baseline checker disagree about scope (different cwd, different
|
|
217
|
+
glob expansion, partial install), the pragma keeps the file out of
|
|
218
|
+
the gate's numerator from the inside.
|
|
219
|
+
3. **Excluded file's callees clear the floor.** A CLI shell is only a
|
|
220
|
+
legitimate exclude if the `lib/` module it wraps actually clears the
|
|
221
|
+
floor (coverage 90/85/90, MI ≥ 70, CRAP ≤ 20). Excluding a shell
|
|
222
|
+
that delegates to under-tested helpers re-introduces the very
|
|
223
|
+
risk the floor gate exists to surface; the audit suite spot-checks
|
|
224
|
+
the callee map at exclude-list churn time.
|
|
225
|
+
|
|
226
|
+
Story #1602 audit pass (2026-05-13) removed two stale exclude entries
|
|
227
|
+
(`epic-runner.js`, `ticket-decomposer.js`) whose source files had already
|
|
228
|
+
been deleted in earlier refactors. Every remaining entry was re-verified
|
|
229
|
+
against requirements 1 and 2 above.
|
|
230
|
+
|
|
231
|
+
### Discontinuity with v5 baselines
|
|
232
|
+
|
|
233
|
+
The floor gate landed alongside a fresh baseline reset
|
|
234
|
+
(Tasks #1623, #1625, #1626, #1629). Any direct numeric comparison
|
|
235
|
+
against pre-floor-gate baseline snapshots is meaningless because the
|
|
236
|
+
pre-rebrand scope included files the current tree excludes (CLI shells,
|
|
237
|
+
generated artifacts) and because the absolute-floor gate is new —
|
|
238
|
+
historical files that were "green" on the ratchet may now show as below
|
|
239
|
+
floor and require either real test additions or an intentional
|
|
240
|
+
`.c8rc.cjs` exclude. The Story #1602 close-out lists every file that
|
|
241
|
+
flipped category in the reset.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Anti-thrashing protocol
|
|
246
|
+
|
|
247
|
+
Agents MUST halt, summarize blockers, and re-plan if they hit consecutive
|
|
248
|
+
tool errors or perform consecutive analysis steps without modifying a
|
|
249
|
+
file. When any threshold under
|
|
250
|
+
the qualitative anti-thrashing cues in
|
|
251
|
+
[`.agents/instructions.md`](../instructions.md) are tripped, the
|
|
252
|
+
friction logger flips the Story to `agent::blocked` and
|
|
253
|
+
posts a structured `friction` comment on the Task so the operator has
|
|
254
|
+
the trace.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Per-Story acceptance self-eval gate
|
|
259
|
+
|
|
260
|
+
After a Story's implementation commits land and **before** the Story
|
|
261
|
+
proceeds to close, delivery runs a bounded acceptance self-eval loop
|
|
262
|
+
(Step 1a of
|
|
263
|
+
[`helpers/epic-deliver-story`](../workflows/helpers/epic-deliver-story.md)
|
|
264
|
+
and `helpers/single-story-deliver`; the shared per-round mechanic lives
|
|
265
|
+
in
|
|
266
|
+
[`helpers/acceptance-self-eval`](../workflows/helpers/acceptance-self-eval.md),
|
|
267
|
+
with the gate CLI at
|
|
268
|
+
[`.agents/scripts/acceptance-eval.js`](../scripts/acceptance-eval.js)).
|
|
269
|
+
Each round, a fresh-context **critic pass** — independent of the
|
|
270
|
+
implementing agent — scores the working diff against every inline
|
|
271
|
+
`acceptance[]` item, using `verify[]` output as evidence, and yields one
|
|
272
|
+
of three decisions:
|
|
273
|
+
|
|
274
|
+
- **proceed** — all criteria met; the Story continues to close.
|
|
275
|
+
- **redraft** — unmet criteria are redrafted and re-implemented, then
|
|
276
|
+
re-evaluated in the next round.
|
|
277
|
+
- **block** — criteria remain unmet after the round cap; the Story
|
|
278
|
+
escalates to the blocked path (`agent::blocked`) for operator review.
|
|
279
|
+
|
|
280
|
+
The loop is always on (hard cutover, no enable flag) and bounded by
|
|
281
|
+
`delivery.acceptanceEval.maxRounds` (default 2, clamped to a minimum of
|
|
282
|
+
1 so the cap cannot be disabled). This gate is complementary to the
|
|
283
|
+
close-validation chain above: that chain proves the code is *healthy*;
|
|
284
|
+
this loop proves it satisfies *this Story's* acceptance criteria. See
|
|
285
|
+
[`.agents/docs/configuration.md`](../docs/configuration.md) for
|
|
286
|
+
the `delivery.acceptanceEval` field reference.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Lint baseline ratchet
|
|
291
|
+
|
|
292
|
+
> Baseline envelope, axes, and component model: see the
|
|
293
|
+
> [Baseline reference](#baseline-reference) section below.
|
|
294
|
+
|
|
295
|
+
The lint baseline engine enforces zero-deterioration during Epic
|
|
296
|
+
workflows. Integrations fail if new lint warnings are introduced, and the
|
|
297
|
+
baseline automatically tightens when the codebase improves.
|
|
298
|
+
|
|
299
|
+
The canonical baseline file lives at `baselines/lint.json` (override via
|
|
300
|
+
`delivery.quality.gates.lint.baselinePath`). Refresh with:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
node .agents/scripts/lint-baseline.js capture
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Refresh commits should use a `baseline-refresh:` subject + non-empty body so
|
|
307
|
+
the operator can spot baseline edits in review — same convention as the CRAP
|
|
308
|
+
and maintainability ratchets. The CI guardrail that mechanically enforced
|
|
309
|
+
this was removed in a pre-npm-era release; the operator is now the gate.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Maintainability ratchet
|
|
314
|
+
|
|
315
|
+
> Baseline envelope, axes, and component model: see the
|
|
316
|
+
> [Baseline reference](#baseline-reference) section below.
|
|
317
|
+
|
|
318
|
+
A per-file maintainability scoring engine computes composite scores based
|
|
319
|
+
on cyclomatic complexity, file length, and dependency counts. The
|
|
320
|
+
`baselines/maintainability.json` baseline prevents score degradation
|
|
321
|
+
between Epics.
|
|
322
|
+
|
|
323
|
+
Refresh with `npm run maintainability:update`.
|
|
324
|
+
|
|
325
|
+
`delivery.quality.gates.maintainability.targetDirs` controls the scanned
|
|
326
|
+
directories — defaults to `["src"]`, accepts `{ "append": [...] }` /
|
|
327
|
+
`{ "prepend": [...] }` for additive overrides.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## CRAP gate — Consumer onboarding
|
|
332
|
+
|
|
333
|
+
> Baseline envelope, axes, and component model: see the
|
|
334
|
+
> [Baseline reference](#baseline-reference) section below.
|
|
335
|
+
|
|
336
|
+
A sibling per-method gate alongside the maintainability ratchet. CRAP
|
|
337
|
+
scores each JavaScript method via `c² · (1 − cov)³ + c`, combining
|
|
338
|
+
`typhonjs-escomplex` cyclomatic complexity with per-method coverage from
|
|
339
|
+
the `coverage/coverage-final.json` artifact your test runner already
|
|
340
|
+
produces. No new runtime dependencies. Runs at three sites:
|
|
341
|
+
`close-validation` (story close), `ci.yml` (push + PR), and
|
|
342
|
+
`.husky/pre-push`.
|
|
343
|
+
|
|
344
|
+
If you're a consumer repo that installed the framework via the
|
|
345
|
+
`mandrel` npm package (`mandrel sync`), this is what you need to know.
|
|
346
|
+
|
|
347
|
+
### First-run behavior — bootstrap before the first push
|
|
348
|
+
|
|
349
|
+
As of Story #791 the gate is hard-enforcing across all three firing sites
|
|
350
|
+
(close-validation, pre-push, CI). With `crap.enabled: true` and no
|
|
351
|
+
`baselines/crap.json` on disk, the CRAP gate (`npm run crap:check`)
|
|
352
|
+
prints:
|
|
353
|
+
|
|
354
|
+
```text
|
|
355
|
+
[CRAP] ❌ no baseline found — run the matching baseline-update command and commit with a 'baseline-refresh:' subject to bootstrap
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
…and exits `1`. Bootstrap explicitly: run `npm run test:coverage` to
|
|
359
|
+
produce `coverage/coverage-final.json`, then `npm run crap:update` to
|
|
360
|
+
generate `baselines/crap.json`, and commit the file with a
|
|
361
|
+
`baseline-refresh:` tagged subject + non-empty body so the
|
|
362
|
+
refresh-guardrail accepts it on the next PR.
|
|
363
|
+
|
|
364
|
+
The transitional informational mode (exit 0 on first sync) was retired in
|
|
365
|
+
Story #791 because it allowed broken pipelines to ride green for an
|
|
366
|
+
indeterminate window. If your test runner doesn't produce per-method
|
|
367
|
+
coverage, see "Disabling the gate" below.
|
|
368
|
+
|
|
369
|
+
### Disabling the gate (single-flag opt-out)
|
|
370
|
+
|
|
371
|
+
If your repo doesn't run coverage, set `enabled: false` in your
|
|
372
|
+
`.agentrc.json`:
|
|
373
|
+
|
|
374
|
+
```jsonc
|
|
375
|
+
{
|
|
376
|
+
"delivery": {
|
|
377
|
+
"quality": {
|
|
378
|
+
"gates": {
|
|
379
|
+
"crap": { "enabled": false }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
All three gate sites self-skip with `[CRAP] gate skipped (disabled)` — no
|
|
387
|
+
source edits required. The maintainability ratchet keeps running.
|
|
388
|
+
|
|
389
|
+
### Extending `targetDirs` without re-listing framework defaults
|
|
390
|
+
|
|
391
|
+
The config resolver supports deep-merge for list-valued keys. To add your
|
|
392
|
+
own source dirs to the framework default (`["src"]`):
|
|
393
|
+
|
|
394
|
+
```jsonc
|
|
395
|
+
{
|
|
396
|
+
"delivery": {
|
|
397
|
+
"quality": {
|
|
398
|
+
"gates": {
|
|
399
|
+
"crap": {
|
|
400
|
+
"targetDirs": { "append": ["packages/foo/src", "packages/bar/src"] }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
`{ "append": [...] }` and `{ "prepend": [...] }` are the deep-merge forms.
|
|
409
|
+
Passing a plain array replaces the default entirely — useful when you
|
|
410
|
+
want exactly your dirs and not the framework's. Unknown keys under
|
|
411
|
+
`delivery.quality.gates.crap` warn but don't fail resolution, so you can
|
|
412
|
+
extend forward-compatibly.
|
|
413
|
+
|
|
414
|
+
### Interpreting the JSON report
|
|
415
|
+
|
|
416
|
+
`npm run crap:check` runs the unified dispatcher
|
|
417
|
+
(`check-baselines.js --gate crap`), which emits its structured report on
|
|
418
|
+
**stdout** — `--format json` is the default (pass `--format text` for the
|
|
419
|
+
human-readable summary). There is no file-writing flag; to capture a file
|
|
420
|
+
artifact, redirect:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npm run crap:check > temp/crap-report.json
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
CI does **not** upload a `crap-report` artifact — `ci.yml` uploads only
|
|
427
|
+
`test-results` (the test/coverage run log) and `coverage-final`
|
|
428
|
+
(`coverage/coverage-final.json`).
|
|
429
|
+
|
|
430
|
+
The JSON envelope is the unified check-baselines report (see
|
|
431
|
+
[`lib/orchestration/check-baselines/phases/report.js`](../scripts/lib/orchestration/check-baselines/phases/report.js)):
|
|
432
|
+
top-level totals (`totalBreaches`, `totalRegressions`,
|
|
433
|
+
`kernelDriftCount`, `schemaErrors`) plus a `gates[]` array where each
|
|
434
|
+
gate entry carries its `kind`, breach/regression counts,
|
|
435
|
+
kernel-version match info, and per-`components[]` floor `violations[]`
|
|
436
|
+
(`axis`, `value`, `floor`, `direction`).
|
|
437
|
+
|
|
438
|
+
### Refreshing the baseline (when the drift is justified)
|
|
439
|
+
|
|
440
|
+
`npm run crap:update` regenerates `baselines/crap.json`. The refresh
|
|
441
|
+
should land in a commit whose:
|
|
442
|
+
|
|
443
|
+
1. Subject starts with the configured `refreshTag` (default
|
|
444
|
+
`baseline-refresh:`).
|
|
445
|
+
2. Body is non-empty and explains why the refresh is justified.
|
|
446
|
+
|
|
447
|
+
The CI guardrail that mechanically rejected unlabeled baseline edits was
|
|
448
|
+
removed in a pre-npm-era release alongside the bot-approver pipeline. The convention is
|
|
449
|
+
preserved so the operator can grep refresh commits in PR diff, but
|
|
450
|
+
self-policing is the operator's job during `/deliver`'s Phase 7
|
|
451
|
+
watch loop — an unjustified baseline ratchet is no longer caught by CI.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## HITL blocker escalation
|
|
456
|
+
|
|
457
|
+
`risk::high` is informational/planning metadata only. Runtime execution
|
|
458
|
+
does not pause automatically on `risk::high`.
|
|
459
|
+
|
|
460
|
+
The sole runtime HITL pause point is `agent::blocked`: when an agent
|
|
461
|
+
encounters an unresolvable blocker (including unsafe destructive actions
|
|
462
|
+
lacking explicit authorization), it flips the ticket/Epic to
|
|
463
|
+
`agent::blocked`, posts friction context, and waits for operator resume
|
|
464
|
+
(`agent::executing`).
|
|
465
|
+
|
|
466
|
+
`planning.riskHeuristics` remains the rubric for identifying
|
|
467
|
+
high-impact operations that should trigger blocker escalation.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Post-floor-gate baseline reset (Story #1701)
|
|
472
|
+
|
|
473
|
+
**Date:** 2026-05-14
|
|
474
|
+
**Commit:** `0657272` (Story #1701, Epic #1653)
|
|
475
|
+
**Files refreshed:** `baselines/coverage.json`,
|
|
476
|
+
`baselines/maintainability.json`, `baselines/crap.json`.
|
|
477
|
+
|
|
478
|
+
A one-time baseline reset captured fresh coverage, maintainability, and
|
|
479
|
+
CRAP snapshots on the post-remediation `main` HEAD. The ratchet
|
|
480
|
+
continues from this new floor, not from any pre-floor-gate history.
|
|
481
|
+
|
|
482
|
+
**Policy:** these baselines are **non-comparable** to any prior
|
|
483
|
+
baseline. Do not diff per-file numbers against pre-reset entries to
|
|
484
|
+
reason about regressions — the post-remediation tree contains refactors,
|
|
485
|
+
extractions, and coverage gains that shift the absolute numbers in ways
|
|
486
|
+
the per-file ratchet cannot reconcile across the discontinuity. Use the
|
|
487
|
+
post-reset capture as the new floor; ratchet from there.
|
|
488
|
+
|
|
489
|
+
**Why:** Epic #1184 closed the floor-gate rollout. The absolute-floor
|
|
490
|
+
gate (coverage 90/85/90, MI ≥ 70, CRAP ≤ 20) is wired into
|
|
491
|
+
`.husky/pre-push` and the CI coverage workflow (see
|
|
492
|
+
[`§ Absolute quality floors`](#absolute-quality-floors-epic-1184)).
|
|
493
|
+
With the floor enforced on every in-scope file, every per-file baseline
|
|
494
|
+
entry must clear the absolute floor — this snapshot is the first
|
|
495
|
+
capture that holds that invariant repository-wide.
|
|
496
|
+
|
|
497
|
+
**Operator action:** none. The baseline is committed and
|
|
498
|
+
`maintainability:check` / `coverage:check` / `crap:check` pass against
|
|
499
|
+
it out of the box. The next regression you see will be diffed against
|
|
500
|
+
this baseline, not against pre-reset history.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Baseline reference
|
|
505
|
+
|
|
506
|
+
This is the authoritative reference for the canonical baseline shape used
|
|
507
|
+
by every quality gate in the framework — `lint`, `coverage`, `crap`,
|
|
508
|
+
`maintainability`, `mutation`, `lighthouse`, and `bundle-size`. It covers
|
|
509
|
+
the envelope, the per-kind shapes, the component model, how paths are
|
|
510
|
+
canonicalised, the writer/reader contract, how consumers override floors,
|
|
511
|
+
and how kernel-version drift surfaces as friction. The runbook sections
|
|
512
|
+
above describe the runtime behaviour of each gate (when it fires, what it
|
|
513
|
+
asserts, how to refresh); this section is the data-shape contract those
|
|
514
|
+
gates read and write.
|
|
515
|
+
|
|
516
|
+
Cross-references:
|
|
517
|
+
|
|
518
|
+
- [`.agents/docs/configuration.md`](../docs/configuration.md) — the `.agentrc.json`
|
|
519
|
+
configuration surface that backs the gates.
|
|
520
|
+
- [`.agents/README.md`](../README.md) — consumer onboarding.
|
|
521
|
+
|
|
522
|
+
> The `mutation` gate ships **dormant** (built-but-unwired, intentionally
|
|
523
|
+
> opt-in). The cost/fit analysis behind deferring its activation lives in
|
|
524
|
+
> the header comment of
|
|
525
|
+
> [`.agents/scripts/update-mutation-baseline.js`](../scripts/update-mutation-baseline.js).
|
|
526
|
+
|
|
527
|
+
### Envelope
|
|
528
|
+
|
|
529
|
+
Every baseline file under `baselines/<kind>.json` shares the same
|
|
530
|
+
top-level envelope:
|
|
531
|
+
|
|
532
|
+
```json
|
|
533
|
+
{
|
|
534
|
+
"$schema": ".agents/schemas/baselines/<kind>.schema.json",
|
|
535
|
+
"kernelVersion": "1.1.0",
|
|
536
|
+
"generatedAt": "2026-05-15T19:30:00.000Z",
|
|
537
|
+
"rollup": {
|
|
538
|
+
"*": { "<axis>": <number>, "...": <number> }
|
|
539
|
+
},
|
|
540
|
+
"rows": [
|
|
541
|
+
{ "path": "<repo-relative-path>", "<axis>": <number>, "...": <number> }
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
| Field | Purpose |
|
|
547
|
+
| --------------- | ----------------------------------------------------------------- |
|
|
548
|
+
| `$schema` | Per-kind JSON Schema path. Drives validation in the shared AJV. |
|
|
549
|
+
| `kernelVersion` | Version stamp of the writer that produced the file. See below. |
|
|
550
|
+
| `generatedAt` | ISO 8601 timestamp; advisory — not load-bearing for gate logic. |
|
|
551
|
+
| `rollup` | Per-component aggregate keyed by component name. `*` is required. |
|
|
552
|
+
| `rows` | Sorted, canonicalised per-file (or per-route/per-bundle) entries. |
|
|
553
|
+
|
|
554
|
+
The schemas live under [`.agents/schemas/baselines/`](../schemas/baselines/).
|
|
555
|
+
The shared AJV instance is built by `buildBaselineSchemaAjv()` in
|
|
556
|
+
[`.agents/scripts/lib/baseline-schema-registry.js`](../scripts/lib/baseline-schema-registry.js).
|
|
557
|
+
|
|
558
|
+
### Per-kind shapes
|
|
559
|
+
|
|
560
|
+
Each kind contributes a `rows[]` schema and a `rollup` axis set. The
|
|
561
|
+
authoritative declarations live in the per-kind modules at
|
|
562
|
+
[`.agents/scripts/lib/baselines/kinds/`](../scripts/lib/baselines/kinds/):
|
|
563
|
+
|
|
564
|
+
| Kind | Key field | Row axes | Rollup axes |
|
|
565
|
+
| ----------------- | --------- | -------------------------------------------------------------- | ---------------------------------------- |
|
|
566
|
+
| `lint` | `path` | `errorCount`, `warningCount` | `errorCount`, `warningCount` |
|
|
567
|
+
| `coverage` | `path` | `lines`, `branches`, `functions`, `statements` | `lines`, `branches`, `functions` |
|
|
568
|
+
| `crap` | `path` | `method`, `startLine`, `crap` | `max`, `p95`, `methodsAboveCeiling` |
|
|
569
|
+
| `maintainability` | `path` | `maintainability` | `min`, `p50`, `p95` |
|
|
570
|
+
| `mutation` | `path` | `score`, `killed`, `survived`, `noCoverage`, `timeout`, `total`| `score`, `survived`, `noCoverage` |
|
|
571
|
+
| `lighthouse` | `route` | `route`, `performance`, `accessibility`, `bestPractices`, `seo`| per-category scores |
|
|
572
|
+
| `bundle-size` | `bundle` | `bundle`, `bytes`, `gzippedBytes` | `bytes`, `gzippedBytes` |
|
|
573
|
+
|
|
574
|
+
The `keyField` is the per-row identifier the writer canonicalises and the
|
|
575
|
+
component grouper matches against (see below). Lighthouse keys rows on
|
|
576
|
+
`route`; bundle-size keys on `bundle`; every other kind keys on `path`.
|
|
577
|
+
|
|
578
|
+
### Component model
|
|
579
|
+
|
|
580
|
+
A component is a named bucket of rows that share a floor and a tolerance.
|
|
581
|
+
Components let an operator slice a baseline so per-component floors can
|
|
582
|
+
be evaluated independently (e.g. `api`, `worker`, `infra` each with its
|
|
583
|
+
own coverage floor).
|
|
584
|
+
|
|
585
|
+
Shape:
|
|
586
|
+
|
|
587
|
+
```json
|
|
588
|
+
"components": {
|
|
589
|
+
"<name>": ["<glob>", "<glob>", "..."]
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Rules:
|
|
594
|
+
|
|
595
|
+
- The component literally named `*` is the **whole-repo bucket** and
|
|
596
|
+
captures every row regardless of declared globs. Every baseline emits
|
|
597
|
+
`rollup['*']` for backwards compatibility with pre-component gates.
|
|
598
|
+
- Glob matching uses
|
|
599
|
+
[`minimatch`](https://github.com/isaacs/minimatch) with `dot: true`.
|
|
600
|
+
- **Overlap is allowed by design** — a row matched by two components is
|
|
601
|
+
reported under both.
|
|
602
|
+
- When a gate omits `components`, the default is `{ "*": ["**"] }`. The
|
|
603
|
+
resolver lives in
|
|
604
|
+
[`.agents/scripts/lib/baselines/components.js`](../scripts/lib/baselines/components.js)
|
|
605
|
+
(`resolveComponents` + `groupRows`).
|
|
606
|
+
|
|
607
|
+
### Path canonicalisation
|
|
608
|
+
|
|
609
|
+
Every path-like field in a baseline (`rows[].path`, `rows[].route`,
|
|
610
|
+
`rows[].bundle`) is canonicalised to a forward-slashed, repo-relative
|
|
611
|
+
form before it is written:
|
|
612
|
+
|
|
613
|
+
- Windows backslashes are normalised to forward slashes.
|
|
614
|
+
- Leading `./` is stripped.
|
|
615
|
+
- A `.worktrees/<workspace>/` prefix — which would leak into a hand-edit
|
|
616
|
+
made inside a story worktree — is stripped.
|
|
617
|
+
- Absolute paths are rejected (the writer throws rather than silently
|
|
618
|
+
rewrite identity).
|
|
619
|
+
|
|
620
|
+
The canonicaliser lives at
|
|
621
|
+
[`.agents/scripts/lib/baselines/path-canon.js`](../scripts/lib/baselines/path-canon.js).
|
|
622
|
+
The reader applies a defensive second pass (`canonicaliseRowPath`) when
|
|
623
|
+
loading so downstream consumers never have to special-case the worktree
|
|
624
|
+
prefix.
|
|
625
|
+
|
|
626
|
+
### Writer/reader contract
|
|
627
|
+
|
|
628
|
+
The single funnel for **writing** a baseline is
|
|
629
|
+
[`.agents/scripts/lib/baselines/writer.js`](../scripts/lib/baselines/writer.js)
|
|
630
|
+
— `write({ kind, rows, components, kernelVersion?, generatedAt? })`:
|
|
631
|
+
|
|
632
|
+
1. Resolve the per-kind module from the kernel registry.
|
|
633
|
+
2. Project every row through `projectRow` (which canonicalises the key
|
|
634
|
+
field and asserts the result with `assertCanonical`).
|
|
635
|
+
3. Sort the rows deterministically for stable on-disk diffs.
|
|
636
|
+
4. Compute the per-component rollup, always including `*`.
|
|
637
|
+
5. Stamp `$schema`, `kernelVersion`, and `generatedAt` via
|
|
638
|
+
`buildEnvelope`.
|
|
639
|
+
6. Validate the envelope against the per-kind schema via the shared AJV.
|
|
640
|
+
7. Return the envelope. `writeFile(absPath, envelope)` is the separate
|
|
641
|
+
serialise + atomic-rename seam.
|
|
642
|
+
|
|
643
|
+
The single funnel for **reading** a baseline is
|
|
644
|
+
[`.agents/scripts/lib/baselines/reader.js`](../scripts/lib/baselines/reader.js)
|
|
645
|
+
— `reader.load(kind, { cwd?, configPath? })`:
|
|
646
|
+
|
|
647
|
+
1. Resolve the on-disk path from `delivery.quality.gates.<kind>.baselinePath`,
|
|
648
|
+
falling back to the canonical default (`baselines/<kind>.json`).
|
|
649
|
+
2. Read the file as UTF-8 JSON.
|
|
650
|
+
3. Validate against the per-kind schema.
|
|
651
|
+
4. Apply the defensive path canonicalisation pass to `rows[]`.
|
|
652
|
+
5. Return `{ rollup, rows, kernelVersion, generatedAt }`.
|
|
653
|
+
|
|
654
|
+
Every gate reads through this module — the unified
|
|
655
|
+
[`check-baselines.js`](../scripts/check-baselines.js) dispatcher
|
|
656
|
+
(whose per-kind gate logic lives in
|
|
657
|
+
[`.agents/scripts/lib/baselines/kinds/`](../scripts/lib/baselines/kinds/)
|
|
658
|
+
— `lint.js`, `coverage.js`, `crap.js`, `maintainability.js`,
|
|
659
|
+
`mutation.js`, etc.), the audit-suite delta emitter, and the
|
|
660
|
+
per-component drift signals. No gate opens
|
|
661
|
+
`JSON.parse(readFileSync(...))` of a baseline directly.
|
|
662
|
+
|
|
663
|
+
`loadFile(absolutePath, { kind? })` is the same contract for ad-hoc
|
|
664
|
+
fixture paths; the kind is inferred from `$schema` when not supplied.
|
|
665
|
+
|
|
666
|
+
### Floor overrides
|
|
667
|
+
|
|
668
|
+
Consumers override floors per gate in `.agentrc.json` under
|
|
669
|
+
`delivery.quality.gates.<kind>`:
|
|
670
|
+
|
|
671
|
+
```json
|
|
672
|
+
{
|
|
673
|
+
"delivery": {
|
|
674
|
+
"quality": {
|
|
675
|
+
"gates": {
|
|
676
|
+
"coverage": {
|
|
677
|
+
"floors": {
|
|
678
|
+
"*": { "lines": 90, "branches": 85, "functions": 90 },
|
|
679
|
+
"api": { "lines": 95, "branches": 90, "functions": 95 }
|
|
680
|
+
},
|
|
681
|
+
"components": {
|
|
682
|
+
"api": ["src/api/**", "src/server/**"]
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Behaviour:
|
|
692
|
+
|
|
693
|
+
- `floors['*']` is the whole-repo floor. Every gate falls back to `*`
|
|
694
|
+
when a component-scoped floor is not declared.
|
|
695
|
+
- A per-component floor overrides `*` for that component only. Other
|
|
696
|
+
components still inherit `*`.
|
|
697
|
+
- The `components` map is optional. When omitted, the default
|
|
698
|
+
`{ "*": ["**"] }` applies and only `*` rows are ever evaluated.
|
|
699
|
+
- The unified `check-baselines.js` reports breaches per component, with
|
|
700
|
+
`*` always present in the output. The per-component progress signals
|
|
701
|
+
(`crap-drift.js#detectComponentRegressions`,
|
|
702
|
+
`maintainability-drift.js#detectComponentRegressions`) name the
|
|
703
|
+
breached component in their bullet so a `*` rollup is not falsely
|
|
704
|
+
implicated when only a component-scoped floor was crossed.
|
|
705
|
+
|
|
706
|
+
#### Floor axes must match rollup axes
|
|
707
|
+
|
|
708
|
+
A configured floor axis is only enforced when the rollup actually exposes
|
|
709
|
+
that axis — `check-baselines.js#compareToFloor` skips axes whose value is
|
|
710
|
+
missing from the rollup. As of Story #2193, the unified dispatcher
|
|
711
|
+
**fails closed** when a configured floor axis is absent from the rollup:
|
|
712
|
+
the gate exits non-zero with an actionable error naming the missing axis
|
|
713
|
+
and listing the available rollup keys (so a typo like
|
|
714
|
+
`{ maintainability: 70 }` against the maintainability rollup — which
|
|
715
|
+
exposes `min` / `p50` / `p95` — surfaces immediately instead of silently
|
|
716
|
+
passing).
|
|
717
|
+
|
|
718
|
+
Match the floor axis names to the rollup axes documented in the [Per-kind
|
|
719
|
+
shapes](#per-kind-shapes) table above. For maintainability specifically:
|
|
720
|
+
|
|
721
|
+
```json
|
|
722
|
+
{
|
|
723
|
+
"delivery": {
|
|
724
|
+
"quality": {
|
|
725
|
+
"gates": {
|
|
726
|
+
"maintainability": {
|
|
727
|
+
"floors": {
|
|
728
|
+
"*": { "min": 70 }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
The maintainability rollup exposes `min` (lowest per-file `mi`), `p50`
|
|
738
|
+
(median), and `p95` (95th percentile); a floor on `min` is the framework
|
|
739
|
+
default and enforces a hard lower bound on individual files. Floors keyed
|
|
740
|
+
on the legacy `maintainability` axis (which never appears in the rollup)
|
|
741
|
+
are rejected with an explanatory error.
|
|
742
|
+
|
|
743
|
+
For the full configuration surface (every gate-level key with defaults
|
|
744
|
+
and types) see [`.agents/docs/configuration.md`](../docs/configuration.md) and the
|
|
745
|
+
`delivery.quality.*` section.
|
|
746
|
+
|
|
747
|
+
#### Shipped surface vs follow-up
|
|
748
|
+
|
|
749
|
+
The unified [`check-baselines.js`](../scripts/check-baselines.js)
|
|
750
|
+
ships **floor + tolerance + schema + kernel-mismatch** logic and is the
|
|
751
|
+
**only** baseline gate. Epic #1943 (Story #1981) absorbed the per-kind
|
|
752
|
+
regression / scope / git-base-ref logic and deleted the per-kind
|
|
753
|
+
`check-<kind>.js` CLIs (no `check-coverage.js`, `check-crap.js`, or
|
|
754
|
+
`check-maintainability.js` exists in `.agents/scripts/`; see the
|
|
755
|
+
`baselines` job comment in `.github/workflows/ci.yml` and the
|
|
756
|
+
Story #2210 note in
|
|
757
|
+
`.agents/scripts/lib/close-validation/gates.js`). Consumers wire only
|
|
758
|
+
the unified `baselines` status check into branch protection (see
|
|
759
|
+
`.agentrc.json` → `github.branchProtection.requiredChecks`).
|
|
760
|
+
|
|
761
|
+
### Kernel-version friction
|
|
762
|
+
|
|
763
|
+
Every per-kind module exports a `kernelVersion()` function that returns
|
|
764
|
+
the writer's version of the analysis it produces. The writer stamps the
|
|
765
|
+
version on the envelope; the reader returns it; the unified gate
|
|
766
|
+
compares it against the running kernel.
|
|
767
|
+
|
|
768
|
+
When `baseline.kernelVersion !== runningKernelVersion`, the gate emits a
|
|
769
|
+
`baseline-kernel-mismatch` friction signal (suppressed with
|
|
770
|
+
`--no-friction`) but does **not** change its exit code — kernel drift is
|
|
771
|
+
advisory. The friction record points the reviewer at the regenerate
|
|
772
|
+
workflow for the kind in question.
|
|
773
|
+
|
|
774
|
+
Refresh paths:
|
|
775
|
+
|
|
776
|
+
- `npm run test:coverage` then `npm run coverage:update` — rewrites
|
|
777
|
+
`baselines/coverage.json`.
|
|
778
|
+
- `node .agents/scripts/update-crap-baseline.js` — rewrites
|
|
779
|
+
`baselines/crap.json`.
|
|
780
|
+
- `node .agents/scripts/update-maintainability-baseline.js` — rewrites
|
|
781
|
+
`baselines/maintainability.json`.
|
|
782
|
+
- `node .agents/scripts/lint-baseline.js capture` — rewrites
|
|
783
|
+
`baselines/lint.json`.
|
|
784
|
+
|
|
785
|
+
After a kernel bump, regenerate every baseline whose `kernelVersion`
|
|
786
|
+
drifted, then commit the refreshed files. The writer guarantees
|
|
787
|
+
deterministic ordering and canonical paths, so the diff is the kernel
|
|
788
|
+
delta and nothing else.
|
|
789
|
+
|
|
790
|
+
### Baseline source of truth
|
|
791
|
+
|
|
792
|
+
- [`.agents/docs/configuration.md`](../docs/configuration.md) — full `.agentrc.json`
|
|
793
|
+
surface.
|
|
794
|
+
- [`.agents/scripts/lib/baselines/`](../scripts/lib/baselines/) —
|
|
795
|
+
source of truth for the writer, reader, kernel registry, components
|
|
796
|
+
resolver, envelope schemas, and per-kind modules.
|