mandrel 2.16.0 → 2.18.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/docs/agentrc-reference.json +10 -0
- package/.agents/docs/configuration.md +9 -0
- package/.agents/docs/quality-gates.md +137 -0
- package/.agents/schemas/agentrc.schema.json +48 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +4 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/acceptance-eval.js +52 -12
- package/.agents/scripts/audit-to-stories.js +92 -25
- package/.agents/scripts/boot-sweep.js +67 -8
- package/.agents/scripts/check-baseline-drift.js +138 -0
- package/.agents/scripts/coverage-capture.js +74 -25
- package/.agents/scripts/deliver-recover.js +45 -18
- package/.agents/scripts/drain-pending-cleanup.js +67 -23
- package/.agents/scripts/generate-lens-checklists.js +81 -30
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +88 -17
- package/.agents/scripts/lib/baselines/drift-detector.js +351 -0
- package/.agents/scripts/lib/baselines/envelope.js +7 -0
- package/.agents/scripts/lib/baselines/kernel.js +31 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +76 -0
- package/.agents/scripts/lib/baselines/reader.js +12 -1
- package/.agents/scripts/lib/baselines/refresh-service.js +7 -1
- package/.agents/scripts/lib/baselines/writer.js +10 -0
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -8
- package/.agents/scripts/lib/cli-utils.js +48 -13
- package/.agents/scripts/lib/close-validation/projections/advisories.js +184 -0
- package/.agents/scripts/lib/close-validation/projections/crap.js +303 -0
- package/.agents/scripts/lib/close-validation/runner.js +68 -0
- package/.agents/scripts/lib/config/gates/crap.schema.js +7 -0
- package/.agents/scripts/lib/config/quality.js +40 -0
- package/.agents/scripts/lib/config/temp-paths.js +27 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +69 -0
- package/.agents/scripts/lib/coverage-utils.js +92 -9
- package/.agents/scripts/lib/crap-engine.js +113 -23
- package/.agents/scripts/lib/crap-utils.js +159 -93
- package/.agents/scripts/lib/dynamic-workflow/audit-orchestrator.js +97 -10
- package/.agents/scripts/lib/dynamic-workflow/degraded-coverage.js +81 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +15 -8
- package/.agents/scripts/lib/observability/terse-result.js +7 -3
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +35 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +13 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +16 -1
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +19 -41
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +122 -0
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +9 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +15 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +31 -1
- package/.agents/scripts/lib/orchestration/story-deliver-terminal-schema.js +166 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +21 -50
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +26 -12
- package/.agents/scripts/lib/single-story-sweep.js +11 -0
- package/.agents/scripts/lib/stdio-flush.js +71 -0
- package/.agents/scripts/lib/temp-retention.js +559 -0
- package/.agents/scripts/lib/transpile.js +133 -6
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +47 -101
- package/.agents/scripts/lib/workers/crap-worker.js +49 -76
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +81 -8
- package/.agents/scripts/nav-registry-diff.js +30 -8
- package/.agents/scripts/plan-run-epilogue.js +27 -11
- package/.agents/scripts/resolve-doc-tiers.js +18 -8
- package/.agents/scripts/single-story-close.js +9 -92
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/sync-branch-from-base.js +6 -1
- package/.agents/scripts/update-crap-baseline.js +13 -0
- package/README.md +14 -6
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/version-helpers.js +7 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +15 -8
- package/package.json +5 -1
|
@@ -128,6 +128,16 @@
|
|
|
128
128
|
"docsFreshness": {
|
|
129
129
|
"paths": ["README.md"]
|
|
130
130
|
},
|
|
131
|
+
"tempRetention": {
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"staleDays": 7,
|
|
134
|
+
"classes": {
|
|
135
|
+
"orchestrationLogs": true,
|
|
136
|
+
"validationEvidence": true,
|
|
137
|
+
"auditResults": true,
|
|
138
|
+
"planDirs": true
|
|
139
|
+
}
|
|
140
|
+
},
|
|
131
141
|
"deliverRunner": {
|
|
132
142
|
"concurrencyCap": 3
|
|
133
143
|
},
|
|
@@ -125,6 +125,14 @@ top-level keys are validation errors.
|
|
|
125
125
|
| `lease.ttlMs` | No | `integer` | — | — |
|
|
126
126
|
| `docsFreshness` | No | `object` | — | Nested configuration block. |
|
|
127
127
|
| `docsFreshness.paths` | No | `array` | — | — |
|
|
128
|
+
| `tempRetention` | No | `object` | — | Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path. |
|
|
129
|
+
| `tempRetention.enabled` | No | `boolean` | — | Master switch. Default true — reclaiming a landed Story's gate transcripts and validation evidence is the behaviour, and this knob turns it off. When false every purge path is a reported no-op. |
|
|
130
|
+
| `tempRetention.staleDays` | No | `integer` | — | Age floor (days, default 7) for the families no Story id can be recovered from — roster-level audit reports and abandoned plan-<slug>/ dirs. Story-keyed artifacts do not wait for it: they are purged as soon as their merge is confirmed. |
|
|
131
|
+
| `tempRetention.classes` | No | `object` | — | Per-class opt-out. Each defaults to true; set one false to keep that family while the rest are purged. |
|
|
132
|
+
| `tempRetention.classes.orchestrationLogs` | No | `boolean` | — | <tempRoot>/orchestration/*.log — close gate transcripts and terse-result detail dumps. |
|
|
133
|
+
| `tempRetention.classes.validationEvidence` | No | `boolean` | — | Per-Story validation-evidence.json, lifecycle.ndjson, and manifest.md under the standalone and per-run story trees. |
|
|
134
|
+
| `tempRetention.classes.auditResults` | No | `boolean` | — | <tempRoot>/audits/ — audit lens reports. |
|
|
135
|
+
| `tempRetention.classes.planDirs` | No | `boolean` | — | <tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded. |
|
|
128
136
|
| `deliverRunner` | No | `object` | — | Nested configuration block. |
|
|
129
137
|
| `deliverRunner.concurrencyCap` | No | `integer` | — | Maximum ready Stories dispatched by /deliver at once. Default 3. Moderate by design — keeps host-quota consumption predictable while allowing a small ready-set fan-out. Set 1 for strictly sequential delivery; raise further on hosts with adequate parallel-agent quota. See deliver.md for the sequencing model and throughput tradeoff. |
|
|
130
138
|
| `worktreeIsolation` | No | `object` | — | Nested configuration block. |
|
|
@@ -174,6 +182,7 @@ top-level keys are validation errors.
|
|
|
174
182
|
| `quality.gates.crap.targetDirs` | No | `string[]` or `{ append?, prepend? }` | — | Directories whose JS sources the CRAP gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal. |
|
|
175
183
|
| `quality.gates.crap.newMethodCeiling` | No | `integer` | — | — |
|
|
176
184
|
| `quality.gates.crap.requireCoverage` | No | `boolean` | — | — |
|
|
185
|
+
| `quality.gates.crap.minMethodResolutionRate` | No | `number` | — | Fail-closed floor on the per-method coverage JOIN (Story #4775): the fraction of methods that must resolve a coverage entry, counted only over files that HAVE one, before `update-crap-baseline.js` will persist. A broken join is silent by construction — unresolved methods are simply absent — so the updater refuses rather than writing a thin baseline and logging it as success. Not enforced below 25 joinable methods, where a diff-scoped run's rate is noise. Default 0.75; a healthy repo resolves ~98%. |
|
|
177
186
|
| `quality.gates.crap.friction` | No | `object` | — | Nested configuration block. |
|
|
178
187
|
| `quality.gates.crap.friction.markerKey` | No | `string` | — | — |
|
|
179
188
|
| `quality.gates.crap.refreshTag` | No | `string` | — | — |
|
|
@@ -361,6 +361,143 @@ There is no CI guardrail rejecting unlabeled baseline edits; the convention is
|
|
|
361
361
|
preserved so the operator can grep refresh commits in a PR diff, but
|
|
362
362
|
self-policing is the operator's job during `/deliver`'s watch loop.
|
|
363
363
|
|
|
364
|
+
### The per-method coverage join (Story #4775)
|
|
365
|
+
|
|
366
|
+
CRAP is the only gate that joins two independently-produced artifacts: the
|
|
367
|
+
per-method complexity escomplex derives from the source, and the per-function
|
|
368
|
+
coverage istanbul derives from the test run. Everything below exists because
|
|
369
|
+
that join is silent when it fails — an unresolved method is simply absent from
|
|
370
|
+
the baseline, so a broken join looks exactly like a small repo.
|
|
371
|
+
|
|
372
|
+
**One coordinate system.** For a TS/TSX source, escomplex parses the
|
|
373
|
+
*transpiled* output and reports each method's `lineStart` in transpiled
|
|
374
|
+
coordinates, while `coverage-final.json` is keyed against the *original*
|
|
375
|
+
source. The scorer therefore asks `transpileIfNeeded` for a source map
|
|
376
|
+
(`{ withLineMap: true }`, backed by Node's built-in `SourceMap` — no extra
|
|
377
|
+
runtime dependency) and remaps each method start into original coordinates
|
|
378
|
+
before the lookup. JavaScript is a passthrough: its coordinates already are
|
|
379
|
+
original coordinates, so no map is computed and nothing changes. The
|
|
380
|
+
maintainability path never requests a map, and the emitted code is
|
|
381
|
+
byte-identical either way, so MI scores are unaffected.
|
|
382
|
+
|
|
383
|
+
**Tolerant matching.** Remapping alone is insufficient: escomplex's method
|
|
384
|
+
start and istanbul's `decl.start.line` disagree by a line when a decorator, a
|
|
385
|
+
leading `export`, or a wrapped parameter list sits between them. The lookup is
|
|
386
|
+
exact-line first (so every already-resolving row keeps its exact prior value),
|
|
387
|
+
then innermost containment, then nearest declaration within ±1.
|
|
388
|
+
|
|
389
|
+
**`requireCoverage: false` means score it.** A method with no coverage entry
|
|
390
|
+
scores as 0% covered — `crap = c² + c`, the formula's own treatment of
|
|
391
|
+
untested code — and lands in the baseline. It used to be dropped individually
|
|
392
|
+
regardless of the flag, which made the flag a no-op for baseline population.
|
|
393
|
+
`requireCoverage: true` still skips and counts it.
|
|
394
|
+
|
|
395
|
+
**The updater fails closed on a thin result.** `update-crap-baseline.js`
|
|
396
|
+
reports `resolved/joinable` over files that *have* coverage and refuses to
|
|
397
|
+
persist below `delivery.quality.gates.crap.minMethodResolutionRate` (default
|
|
398
|
+
`0.75`), naming the worst unresolved files. The floor is not enforced below 25
|
|
399
|
+
joinable methods, where a diff-scoped run's rate is noise. A healthy repo
|
|
400
|
+
resolves ~98%; the 4–6% signature of a coordinate-system mismatch is far below
|
|
401
|
+
the floor.
|
|
402
|
+
|
|
403
|
+
**Re-derive your floors after adopting this — but do not re-pin `max`.** A
|
|
404
|
+
`crap.floors` `max` ceiling pinned before the fix was computed over the
|
|
405
|
+
minority of methods the join could see, so it is not a real ceiling — it is an
|
|
406
|
+
artefact. The honest scan sees far more (in this repository, 2215 → 4058
|
|
407
|
+
visible methods), and the newly-visible methods include the worst ones.
|
|
408
|
+
|
|
409
|
+
The tempting response — raise `*.max` until the gate is green again — produces a
|
|
410
|
+
floor fitted to the tree's current high-water mark, which **can never fire**:
|
|
411
|
+
nothing breaches it until something becomes worse than the worst method already
|
|
412
|
+
present. Prefer a *count* budget over a max ceiling:
|
|
413
|
+
|
|
414
|
+
```jsonc
|
|
415
|
+
"crap": {
|
|
416
|
+
// Number of methods allowed to score above 20. Ratchet this down; it
|
|
417
|
+
// breaches the moment the count grows, which a `max` ceiling cannot do.
|
|
418
|
+
"floors": { "*": { "methodsAbove20": 40 } }
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`max` remains available and is the right instrument when you genuinely have a
|
|
423
|
+
hard per-method ceiling to hold. It is the wrong instrument for absorbing
|
|
424
|
+
pre-existing debt.
|
|
425
|
+
|
|
426
|
+
Note that neither choice is what protects new code. `floors` is an absolute
|
|
427
|
+
tree-wide comparison against the rollup; the forward pressure lives in
|
|
428
|
+
`newMethodCeiling` (a *new* method scoring above it fails, default 30) and in
|
|
429
|
+
`compareCrap`'s ratchet (an *existing* method fails when it regresses against
|
|
430
|
+
its own baseline row). Both are unaffected by how much old debt the gate can
|
|
431
|
+
now see, and neither consults `floors`.
|
|
432
|
+
|
|
433
|
+
**Old baselines are invalidated explicitly.** Rows scored by the previous join
|
|
434
|
+
are not comparable to rows scored by this one, and neither `kernelVersion` nor
|
|
435
|
+
`escomplexVersion` moves (both track the same upstream package). The envelope
|
|
436
|
+
therefore carries a `scoringSemantics` stamp; `check-baselines` fails closed on
|
|
437
|
+
a mismatch with the exact re-baseline command rather than comparing across the
|
|
438
|
+
boundary. Bump the stamp whenever the coverage join, the line coordinate
|
|
439
|
+
system, or the unresolved-method policy changes.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Keeping a baseline fresh (Story #4776)
|
|
444
|
+
|
|
445
|
+
Populating a baseline correctly is only half the loop. The other half is
|
|
446
|
+
keeping it correct as the tree grows, and that half has two distinct holes —
|
|
447
|
+
one at close time, one over the long run. Both are **advisory**:
|
|
448
|
+
`check-baselines` already fails closed on a real regression, and duplicating
|
|
449
|
+
that would double-gate the same defect.
|
|
450
|
+
|
|
451
|
+
### Pre-merge projections — the refresh nudge at close time
|
|
452
|
+
|
|
453
|
+
Close-validation projects, after its gates pass, which committed baseline rows
|
|
454
|
+
the post-merge tree would breach, and names the exact remedy while the operator
|
|
455
|
+
still has the branch in hand:
|
|
456
|
+
|
|
457
|
+
- `lib/close-validation/projections/maintainability.js` — per-file MI.
|
|
458
|
+
- `lib/close-validation/projections/crap.js` — per-method CRAP, against each
|
|
459
|
+
method's baseline row or, for methods with no row, `newMethodCeiling`.
|
|
460
|
+
|
|
461
|
+
Both are wired through `projections/advisories.js`, which
|
|
462
|
+
`close-validation/runner.js` calls once. Each self-skips — logging the reason,
|
|
463
|
+
never erroring — when its gate is disabled, when no baseline exists, when the
|
|
464
|
+
diff has no scorable files, or when the CRAP scorer finds no coverage
|
|
465
|
+
artifact. A projected breach never changes the close verdict.
|
|
466
|
+
|
|
467
|
+
> The maintainability projection shipped in v1 fully written and fully
|
|
468
|
+
> unit-tested, and the v2 Epic-tier collapse removed its only caller. It sat
|
|
469
|
+
> importable-but-unimported for the whole of v2, so its advisory never fired
|
|
470
|
+
> once. `tests/lib/close-validation/runner-projections.test.js` now walks the
|
|
471
|
+
> import graph and fails if **any** module under `projections/` is reachable
|
|
472
|
+
> from nothing in production — the orphaning itself is the regression.
|
|
473
|
+
|
|
474
|
+
### `check-baseline-drift.js` — the scheduled full-scope re-score
|
|
475
|
+
|
|
476
|
+
Every per-PR enforcement site (close-validation, pre-push, CI) is
|
|
477
|
+
**diff-scoped**: it compares the files a branch touched against their baseline
|
|
478
|
+
rows. A file nobody touches after its row is written is therefore never
|
|
479
|
+
re-scored, so drift introduced *indirectly* — a dependency getting more
|
|
480
|
+
complex, coverage moving underneath a method — stays invisible indefinitely.
|
|
481
|
+
Full-scope scoring on every push is far too expensive to be the answer.
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
node .agents/scripts/check-baseline-drift.js # both kinds
|
|
485
|
+
node .agents/scripts/check-baseline-drift.js --gate crap # one kind
|
|
486
|
+
node .agents/scripts/check-baseline-drift.js --tolerance 1 --json
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
It re-scores full-scope through the *same* scorer that writes the baseline
|
|
490
|
+
(`refresh-service.resolveDefaultScorer`) — scoring by a second implementation
|
|
491
|
+
would report the two implementations' disagreement as drift — and prints a
|
|
492
|
+
per-row before/after table for everything that moved beyond the gate's
|
|
493
|
+
tolerance, **in either direction**. A row that silently improved is equally
|
|
494
|
+
strong evidence the baseline no longer describes the tree.
|
|
495
|
+
|
|
496
|
+
Exit codes: `0` no drift (or every kind skipped), `1` drift detected, `2` the
|
|
497
|
+
check could not run. It is designed to be wired as a scheduled CI job;
|
|
498
|
+
scheduling it is deliberately consumer-side work, and nothing in this
|
|
499
|
+
repository runs it automatically.
|
|
500
|
+
|
|
364
501
|
---
|
|
365
502
|
|
|
366
503
|
## Bundle-size ratchet — one-shot refresh/acknowledge (Story #151)
|
|
@@ -441,6 +441,45 @@
|
|
|
441
441
|
},
|
|
442
442
|
"additionalProperties": false
|
|
443
443
|
},
|
|
444
|
+
"tempRetention": {
|
|
445
|
+
"type": "object",
|
|
446
|
+
"description": "Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path.",
|
|
447
|
+
"properties": {
|
|
448
|
+
"enabled": {
|
|
449
|
+
"type": "boolean",
|
|
450
|
+
"description": "Master switch. Default true — reclaiming a landed Story's gate transcripts and validation evidence is the behaviour, and this knob turns it off. When false every purge path is a reported no-op."
|
|
451
|
+
},
|
|
452
|
+
"staleDays": {
|
|
453
|
+
"type": "integer",
|
|
454
|
+
"minimum": 1,
|
|
455
|
+
"description": "Age floor (days, default 7) for the families no Story id can be recovered from — roster-level audit reports and abandoned plan-<slug>/ dirs. Story-keyed artifacts do not wait for it: they are purged as soon as their merge is confirmed."
|
|
456
|
+
},
|
|
457
|
+
"classes": {
|
|
458
|
+
"type": "object",
|
|
459
|
+
"description": "Per-class opt-out. Each defaults to true; set one false to keep that family while the rest are purged.",
|
|
460
|
+
"properties": {
|
|
461
|
+
"orchestrationLogs": {
|
|
462
|
+
"type": "boolean",
|
|
463
|
+
"description": "<tempRoot>/orchestration/*.log — close gate transcripts and terse-result detail dumps."
|
|
464
|
+
},
|
|
465
|
+
"validationEvidence": {
|
|
466
|
+
"type": "boolean",
|
|
467
|
+
"description": "Per-Story validation-evidence.json, lifecycle.ndjson, and manifest.md under the standalone and per-run story trees."
|
|
468
|
+
},
|
|
469
|
+
"auditResults": {
|
|
470
|
+
"type": "boolean",
|
|
471
|
+
"description": "<tempRoot>/audits/ — audit lens reports."
|
|
472
|
+
},
|
|
473
|
+
"planDirs": {
|
|
474
|
+
"type": "boolean",
|
|
475
|
+
"description": "<tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded."
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
"additionalProperties": false
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
"additionalProperties": false
|
|
482
|
+
},
|
|
444
483
|
"docsFreshness": {
|
|
445
484
|
"type": "object",
|
|
446
485
|
"properties": {
|
|
@@ -774,6 +813,12 @@
|
|
|
774
813
|
"requireCoverage": {
|
|
775
814
|
"type": "boolean"
|
|
776
815
|
},
|
|
816
|
+
"minMethodResolutionRate": {
|
|
817
|
+
"type": "number",
|
|
818
|
+
"minimum": 0,
|
|
819
|
+
"maximum": 1,
|
|
820
|
+
"description": "Fail-closed floor on the per-method coverage JOIN (Story #4775): the fraction of methods that must resolve a coverage entry, counted only over files that HAVE one, before `update-crap-baseline.js` will persist. A broken join is silent by construction — unresolved methods are simply absent — so the updater refuses rather than writing a thin baseline and logging it as success. Not enforced below 25 joinable methods, where a diff-scoped run's rate is noise. Default 0.75; a healthy repo resolves ~98%."
|
|
821
|
+
},
|
|
777
822
|
"friction": {
|
|
778
823
|
"type": "object",
|
|
779
824
|
"properties": {
|
|
@@ -1318,6 +1363,9 @@
|
|
|
1318
1363
|
"docsFreshness": {
|
|
1319
1364
|
"$ref": "#/$defs/docsFreshness"
|
|
1320
1365
|
},
|
|
1366
|
+
"tempRetention": {
|
|
1367
|
+
"$ref": "#/$defs/tempRetention"
|
|
1368
|
+
},
|
|
1321
1369
|
"deliverRunner": {
|
|
1322
1370
|
"$ref": "#/$defs/deliverRunner"
|
|
1323
1371
|
},
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"format": "date-time",
|
|
22
22
|
"description": "ISO-8601 timestamp of the baseline run. Used for stale-baseline detection and audit trails."
|
|
23
23
|
},
|
|
24
|
+
"scoringSemantics": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Optional per-kind stamp identifying the SCORING SEMANTICS that produced these rows — the dimension kernelVersion cannot express, because a kind's own scoring can change while the upstream package it stamps does not. A baseline whose stamp differs from the running scorer's is incomparable and the gate fails closed with re-baseline guidance rather than comparing across the boundary."
|
|
27
|
+
},
|
|
24
28
|
"rollup": {
|
|
25
29
|
"type": "object",
|
|
26
30
|
"description": "Aggregate metrics keyed by component. The '*' key is reserved for the whole-repo rollup and is REQUIRED; additional keys MAY be present, one per declared component.",
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
"allOf": [{ "$ref": "baseline-envelope.schema.json" }],
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
|
+
"scoringSemantics": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Identifier for the per-method coverage-join semantics that produced these rows (Story #4775). Deliberately NOT constrained to the current value here: an old-semantics baseline must reach the gate's compat axis, which fails it closed with an explicit re-baseline command, rather than being rejected with a raw schema error."
|
|
12
|
+
},
|
|
9
13
|
"rollup": {
|
|
10
14
|
"type": "object",
|
|
11
15
|
"required": ["*"],
|
|
@@ -86,13 +86,18 @@
|
|
|
86
86
|
"followUps",
|
|
87
87
|
"statusResync",
|
|
88
88
|
"refCleanup",
|
|
89
|
-
"baseFastForward"
|
|
89
|
+
"baseFastForward",
|
|
90
|
+
"tempPurge"
|
|
90
91
|
],
|
|
91
92
|
"properties": {
|
|
92
93
|
"followUps": { "type": "boolean" },
|
|
93
94
|
"statusResync": { "type": "boolean" },
|
|
94
95
|
"refCleanup": { "type": "boolean" },
|
|
95
96
|
"baseFastForward": { "type": "boolean" },
|
|
97
|
+
"tempPurge": {
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"description": "Story #4794 — the merged Story's spent temp artifacts (gate transcripts, validation evidence) were purged under delivery.tempRetention. A disabled policy reports true: the operator turned the purge off, so doing nothing IS the correct outcome. Only a real failure — an unreadable temp root, an undeletable artifact — reports false, and like every tail step that degrades the report, never the land."
|
|
100
|
+
},
|
|
96
101
|
"details": {
|
|
97
102
|
"type": "object",
|
|
98
103
|
"description": "Per-step diagnostic detail — the reason a false step reported false.",
|
|
@@ -82,10 +82,14 @@ const VERDICT_SCHEMA_PATH = path.resolve(
|
|
|
82
82
|
'acceptance-eval-verdict.schema.json',
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
-
let cachedValidator = null;
|
|
86
|
-
|
|
87
85
|
/**
|
|
88
|
-
* Compile
|
|
86
|
+
* Compile the Ajv2020 validator for the verdict schema.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately **not** memoised in a module-level variable: a module-level
|
|
89
|
+
* cache is reset only on re-import, so it silently defeats the `io` seam below
|
|
90
|
+
* (the second caller's injected reader is never consulted) and makes parallel
|
|
91
|
+
* test isolation impossible — `.agents/rules/test-seams.md` rule 3. The gate
|
|
92
|
+
* compiles the schema once per CLI run, so there is nothing to memoise.
|
|
89
93
|
*
|
|
90
94
|
* @param {string} [schemaPath]
|
|
91
95
|
* @param {{ readFileSync: typeof readFileSync }} [io]
|
|
@@ -95,12 +99,10 @@ function getVerdictValidator(
|
|
|
95
99
|
schemaPath = VERDICT_SCHEMA_PATH,
|
|
96
100
|
io = { readFileSync },
|
|
97
101
|
) {
|
|
98
|
-
if (cachedValidator) return cachedValidator;
|
|
99
102
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
100
103
|
addFormats(ajv);
|
|
101
104
|
const schema = JSON.parse(io.readFileSync(schemaPath, 'utf8'));
|
|
102
|
-
|
|
103
|
-
return cachedValidator;
|
|
105
|
+
return ajv.compile(schema);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -235,7 +237,37 @@ export async function runAcceptanceEval(
|
|
|
235
237
|
return { envelope, exitCode };
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The gate's CLI core: argv → verdict read → schema validation → decision →
|
|
242
|
+
* envelope. Extracted from the `main` shell so the whole error table (missing
|
|
243
|
+
* flags, unreadable verdict, non-JSON verdict, storyId mismatch, block) is
|
|
244
|
+
* reachable without spawning the CLI or writing a real verdict file.
|
|
245
|
+
*
|
|
246
|
+
* Every seam on the optional final `deps` parameter defaults to the real
|
|
247
|
+
* implementation (`.agents/rules/test-seams.md` rules 1-2, 4), so `main` and
|
|
248
|
+
* every production invocation are unchanged.
|
|
249
|
+
*
|
|
250
|
+
* @param {string[]} [argv]
|
|
251
|
+
* @param {{
|
|
252
|
+
* readFileSyncImpl?: typeof readFileSync,
|
|
253
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
254
|
+
* validateVerdictImpl?: typeof validateVerdict,
|
|
255
|
+
* runAcceptanceEvalImpl?: typeof runAcceptanceEval,
|
|
256
|
+
* logger?: { info: Function },
|
|
257
|
+
* }} [deps]
|
|
258
|
+
* @returns {Promise<object>} the emitted envelope.
|
|
259
|
+
*/
|
|
260
|
+
export async function runAcceptanceEvalCli(
|
|
261
|
+
argv = process.argv.slice(2),
|
|
262
|
+
deps = {},
|
|
263
|
+
) {
|
|
264
|
+
const {
|
|
265
|
+
readFileSyncImpl = readFileSync,
|
|
266
|
+
resolveConfigImpl = resolveConfig,
|
|
267
|
+
validateVerdictImpl = validateVerdict,
|
|
268
|
+
runAcceptanceEvalImpl = runAcceptanceEval,
|
|
269
|
+
logger = Logger,
|
|
270
|
+
} = deps;
|
|
239
271
|
const { storyId, verdictPath, emitSignal } = parseCliArgs(argv);
|
|
240
272
|
|
|
241
273
|
if (!storyId) {
|
|
@@ -249,7 +281,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
249
281
|
|
|
250
282
|
let raw;
|
|
251
283
|
try {
|
|
252
|
-
raw =
|
|
284
|
+
raw = readFileSyncImpl(path.resolve(verdictPath), 'utf8');
|
|
253
285
|
} catch (err) {
|
|
254
286
|
throw new Error(
|
|
255
287
|
`acceptance-eval: cannot read verdict file at ${verdictPath}: ${
|
|
@@ -269,7 +301,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
269
301
|
);
|
|
270
302
|
}
|
|
271
303
|
|
|
272
|
-
const verdict =
|
|
304
|
+
const verdict = validateVerdictImpl(parsed);
|
|
273
305
|
|
|
274
306
|
// A verdict whose embedded storyId disagrees with the CLI flag is a
|
|
275
307
|
// wiring error worth failing on, not a silent mismatch.
|
|
@@ -279,15 +311,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
279
311
|
);
|
|
280
312
|
}
|
|
281
313
|
|
|
282
|
-
const config =
|
|
283
|
-
const { envelope, exitCode } = await
|
|
314
|
+
const config = resolveConfigImpl();
|
|
315
|
+
const { envelope, exitCode } = await runAcceptanceEvalImpl({
|
|
284
316
|
storyId,
|
|
285
317
|
verdict,
|
|
286
318
|
config,
|
|
287
319
|
emitSignal,
|
|
288
320
|
});
|
|
289
321
|
|
|
290
|
-
|
|
322
|
+
logger.info(JSON.stringify(envelope));
|
|
291
323
|
|
|
292
324
|
if (exitCode !== 0) {
|
|
293
325
|
const names = envelope.unmetCriteria
|
|
@@ -302,6 +334,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
302
334
|
return envelope;
|
|
303
335
|
}
|
|
304
336
|
|
|
337
|
+
/**
|
|
338
|
+
* @param {string[]} [argv]
|
|
339
|
+
* @returns {Promise<object>}
|
|
340
|
+
*/
|
|
341
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
342
|
+
return runAcceptanceEvalCli(argv);
|
|
343
|
+
}
|
|
344
|
+
|
|
305
345
|
runAsCli(import.meta.url, main, {
|
|
306
346
|
source: 'acceptance-eval',
|
|
307
347
|
usage: {
|
|
@@ -224,8 +224,38 @@ function dedupDegradedWarning(entries) {
|
|
|
224
224
|
);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Scan → group → dedup → (optionally) reconcile the cross-run ledger, and
|
|
229
|
+
* return the plan envelope.
|
|
230
|
+
*
|
|
231
|
+
* Every seam on the optional final `deps` parameter defaults to the real
|
|
232
|
+
* implementation (`.agents/rules/test-seams.md` rules 1-2, 4), so `main`,
|
|
233
|
+
* `runAuto`, and every production caller are unchanged.
|
|
234
|
+
*
|
|
235
|
+
* @param {{ glob?: string, severity?: string, useProvider?: boolean, ledger?: object }} params
|
|
236
|
+
* @param {{
|
|
237
|
+
* collectReportPathsImpl?: typeof collectReportPaths,
|
|
238
|
+
* readReportsImpl?: typeof readReports,
|
|
239
|
+
* loadProviderImpl?: typeof loadProvider,
|
|
240
|
+
* classifyGroupsImpl?: typeof classifyGroupsAgainstGitHub,
|
|
241
|
+
* reconcileScanLedgerImpl?: typeof reconcileScanLedger,
|
|
242
|
+
* logger?: { warn: Function },
|
|
243
|
+
* }} [deps]
|
|
244
|
+
* @returns {Promise<object>} the plan envelope.
|
|
245
|
+
*/
|
|
246
|
+
async function buildPlan(
|
|
247
|
+
{ glob: pattern, severity, useProvider, ledger },
|
|
248
|
+
deps = {},
|
|
249
|
+
) {
|
|
250
|
+
const {
|
|
251
|
+
collectReportPathsImpl = collectReportPaths,
|
|
252
|
+
readReportsImpl = readReports,
|
|
253
|
+
loadProviderImpl = loadProvider,
|
|
254
|
+
classifyGroupsImpl = classifyGroupsAgainstGitHub,
|
|
255
|
+
reconcileScanLedgerImpl = reconcileScanLedger,
|
|
256
|
+
logger = Logger,
|
|
257
|
+
} = deps;
|
|
258
|
+
const reportPaths = await collectReportPathsImpl(pattern ?? DEFAULT_GLOB);
|
|
229
259
|
if (reportPaths.length === 0) {
|
|
230
260
|
return {
|
|
231
261
|
generatedAt: new Date().toISOString(),
|
|
@@ -245,8 +275,8 @@ async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
|
245
275
|
};
|
|
246
276
|
}
|
|
247
277
|
|
|
248
|
-
const reports =
|
|
249
|
-
const allFindings = parseAuditReports(reports);
|
|
278
|
+
const reports = readReportsImpl(reportPaths);
|
|
279
|
+
const allFindings = parseAuditReports(reports, { repoRoot: process.cwd() });
|
|
250
280
|
const filtered = allFindings.filter((f) => meetsSeverity(f, severity));
|
|
251
281
|
const stamped = withFingerprints(filtered);
|
|
252
282
|
const { groups, edges } = groupFindings(stamped);
|
|
@@ -261,9 +291,9 @@ async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
|
261
291
|
let dedupApplied = false;
|
|
262
292
|
|
|
263
293
|
if (useProvider) {
|
|
264
|
-
const provider = await
|
|
294
|
+
const provider = await loadProviderImpl();
|
|
265
295
|
if (provider) {
|
|
266
|
-
const result = await
|
|
296
|
+
const result = await classifyGroupsImpl({
|
|
267
297
|
groups,
|
|
268
298
|
provider,
|
|
269
299
|
searchCandidates: provider.searchCandidates,
|
|
@@ -275,19 +305,19 @@ async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
|
275
305
|
// the --scan JSON on stdout stays clean) naming the groups that degraded
|
|
276
306
|
// to create because their lookup could not complete (Story #4678).
|
|
277
307
|
if (summary.dedupDegraded?.count > 0) {
|
|
278
|
-
|
|
308
|
+
logger.warn(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
279
309
|
}
|
|
280
310
|
} else {
|
|
281
311
|
// The provider could not resolve a searchIssues port — the dedup gate
|
|
282
312
|
// is silently a no-op without this. Surface it loudly (stderr, so the
|
|
283
313
|
// --scan JSON on stdout stays clean) so the operator does not read a
|
|
284
314
|
// create-only plan as "no duplicates found".
|
|
285
|
-
|
|
315
|
+
logger.warn(dedupSkippedWarning('no-provider-port'));
|
|
286
316
|
}
|
|
287
317
|
} else {
|
|
288
318
|
// Operator explicitly opted out via --no-provider. Still warn so a
|
|
289
319
|
// duplicate-opening re-run is never a surprise.
|
|
290
|
-
|
|
320
|
+
logger.warn(dedupSkippedWarning('disabled'));
|
|
291
321
|
}
|
|
292
322
|
|
|
293
323
|
// Cross-run ledger (Story #4626): fold this scan onto the committed memory,
|
|
@@ -296,7 +326,7 @@ async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
|
296
326
|
// --scan path leaves it untouched so it never mutates a committed file.
|
|
297
327
|
let ledgerSummary;
|
|
298
328
|
if (ledger) {
|
|
299
|
-
const suppressed =
|
|
329
|
+
const suppressed = reconcileScanLedgerImpl({
|
|
300
330
|
ledgerPath: ledger.path ?? DEFAULT_LEDGER_PATH,
|
|
301
331
|
findings: stamped,
|
|
302
332
|
classifications,
|
|
@@ -546,9 +576,42 @@ export const __testing = {
|
|
|
546
576
|
issueStatesFromClassifications,
|
|
547
577
|
};
|
|
548
578
|
|
|
549
|
-
|
|
579
|
+
/**
|
|
580
|
+
* The CLI core: dispatch one of the four sub-commands and persist its output.
|
|
581
|
+
* Extracted from the `main` shell so the whole sub-command table (including
|
|
582
|
+
* the no-sub-command usage throw) is reachable without spawning the CLI.
|
|
583
|
+
*
|
|
584
|
+
* Every seam on the optional final `deps` parameter defaults to the real
|
|
585
|
+
* implementation (`.agents/rules/test-seams.md` rules 1-2, 4), so `main` and
|
|
586
|
+
* every production invocation are unchanged.
|
|
587
|
+
*
|
|
588
|
+
* @param {string[]} [argv]
|
|
589
|
+
* @param {{
|
|
590
|
+
* buildPlanImpl?: typeof buildPlan,
|
|
591
|
+
* runAutoImpl?: typeof runAuto,
|
|
592
|
+
* loadPlanImpl?: typeof loadPlan,
|
|
593
|
+
* buildAndGateStoriesImpl?: typeof buildAndGateStories,
|
|
594
|
+
* buildPlanSeedMarkdownImpl?: typeof buildPlanSeedMarkdown,
|
|
595
|
+
* persistImpl?: typeof persist,
|
|
596
|
+
* stdout?: { write: (s: string) => void },
|
|
597
|
+
* }} [deps]
|
|
598
|
+
* @returns {Promise<void>}
|
|
599
|
+
*/
|
|
600
|
+
export async function runAuditToStories(
|
|
601
|
+
argv = process.argv.slice(2),
|
|
602
|
+
deps = {},
|
|
603
|
+
) {
|
|
604
|
+
const {
|
|
605
|
+
buildPlanImpl = buildPlan,
|
|
606
|
+
runAutoImpl = runAuto,
|
|
607
|
+
loadPlanImpl = loadPlan,
|
|
608
|
+
buildAndGateStoriesImpl = buildAndGateStories,
|
|
609
|
+
buildPlanSeedMarkdownImpl = buildPlanSeedMarkdown,
|
|
610
|
+
persistImpl = persist,
|
|
611
|
+
stdout = process.stdout,
|
|
612
|
+
} = deps;
|
|
550
613
|
const { values } = parseArgs({
|
|
551
|
-
args:
|
|
614
|
+
args: argv,
|
|
552
615
|
options: {
|
|
553
616
|
scan: { type: 'boolean' },
|
|
554
617
|
auto: { type: 'boolean' },
|
|
@@ -567,47 +630,47 @@ async function main() {
|
|
|
567
630
|
});
|
|
568
631
|
|
|
569
632
|
if (values.auto) {
|
|
570
|
-
const { summary } = await
|
|
633
|
+
const { summary } = await runAutoImpl({
|
|
571
634
|
glob: values.glob,
|
|
572
635
|
severity: values.severity,
|
|
573
636
|
dryRun: values['dry-run'],
|
|
574
637
|
useProvider: !values['no-provider'],
|
|
575
638
|
ledgerPath: values.ledger,
|
|
576
639
|
});
|
|
577
|
-
|
|
578
|
-
if (!values.out)
|
|
640
|
+
persistImpl(JSON.stringify(summary, null, 2), values.out);
|
|
641
|
+
if (!values.out) stdout.write('\n');
|
|
579
642
|
return;
|
|
580
643
|
}
|
|
581
644
|
|
|
582
645
|
if (values.scan) {
|
|
583
|
-
const plan = await
|
|
646
|
+
const plan = await buildPlanImpl({
|
|
584
647
|
glob: values.glob,
|
|
585
648
|
severity: values.severity,
|
|
586
649
|
useProvider: !values['no-provider'],
|
|
587
650
|
});
|
|
588
651
|
const out = JSON.stringify(plan, null, 2);
|
|
589
|
-
|
|
590
|
-
if (!values.out)
|
|
652
|
+
persistImpl(out, values.out);
|
|
653
|
+
if (!values.out) stdout.write('\n');
|
|
591
654
|
return;
|
|
592
655
|
}
|
|
593
656
|
|
|
594
657
|
if (values['emit-plan-seed']) {
|
|
595
|
-
const plan =
|
|
596
|
-
const md =
|
|
658
|
+
const plan = loadPlanImpl(values.plan);
|
|
659
|
+
const md = buildPlanSeedMarkdownImpl({
|
|
597
660
|
groups: plan.groups ?? [],
|
|
598
661
|
findings: plan.findings ?? [],
|
|
599
662
|
sourceReports: plan.sourceReports ?? [],
|
|
600
663
|
});
|
|
601
|
-
|
|
664
|
+
persistImpl(md, values.out);
|
|
602
665
|
return;
|
|
603
666
|
}
|
|
604
667
|
|
|
605
668
|
if (values['emit-stories']) {
|
|
606
|
-
const plan =
|
|
669
|
+
const plan = loadPlanImpl(values.plan);
|
|
607
670
|
const eligible = (plan.classifications ?? [])
|
|
608
671
|
.filter((c) => c.action === 'create')
|
|
609
672
|
.map((c) => c.group);
|
|
610
|
-
const built =
|
|
673
|
+
const built = buildAndGateStoriesImpl(eligible, plan.edges ?? []);
|
|
611
674
|
const out = values.json
|
|
612
675
|
? JSON.stringify(built, null, 2)
|
|
613
676
|
: built
|
|
@@ -616,8 +679,8 @@ async function main() {
|
|
|
616
679
|
`--- story ${i + 1} ---\nTitle: ${s.title}\nLabels: ${s.labels.join(', ')}\n\n${s.body}\n`,
|
|
617
680
|
)
|
|
618
681
|
.join('\n');
|
|
619
|
-
|
|
620
|
-
if (!values.out)
|
|
682
|
+
persistImpl(out, values.out);
|
|
683
|
+
if (!values.out) stdout.write('\n');
|
|
621
684
|
return;
|
|
622
685
|
}
|
|
623
686
|
|
|
@@ -626,6 +689,10 @@ async function main() {
|
|
|
626
689
|
);
|
|
627
690
|
}
|
|
628
691
|
|
|
692
|
+
async function main() {
|
|
693
|
+
await runAuditToStories();
|
|
694
|
+
}
|
|
695
|
+
|
|
629
696
|
runAsCli(import.meta.url, main, {
|
|
630
697
|
source: 'audit-to-stories',
|
|
631
698
|
usage: {
|