mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -41,16 +41,31 @@
|
|
|
41
41
|
* tier along with the per-AC-cluster `--epic <id> --cluster <id>` mode that
|
|
42
42
|
* scored an Epic `## Acceptance Table` against a `main..epic/<id>` diff.)
|
|
43
43
|
*
|
|
44
|
+
* One gate call per round (Story #4951). A round may fan out into N parallel
|
|
45
|
+
* maker-blind cluster critics, but their per-cluster verdicts are merged by
|
|
46
|
+
* the caller into ONE verdict — `criteria[]` in acceptance-array order — and
|
|
47
|
+
* scored here exactly once. Invoking the gate per cluster instead would burn
|
|
48
|
+
* one Story-level round per cluster (distinct fingerprints defeat the replay
|
|
49
|
+
* guard) and race the `signals.ndjson` round ledger. `--expected-criteria`
|
|
50
|
+
* makes that contract enforceable: a partial (single-cluster) verdict is
|
|
51
|
+
* rejected before scoring, so the mistake costs no round.
|
|
52
|
+
*
|
|
44
53
|
* CLI:
|
|
45
|
-
* --story <id>
|
|
46
|
-
* --verdict <path>
|
|
47
|
-
* --
|
|
54
|
+
* --story <id> Story ID (required).
|
|
55
|
+
* --verdict <path> Path to the round's verdict JSON (required).
|
|
56
|
+
* --expected-criteria <n> Reject a verdict not covering exactly n criteria.
|
|
57
|
+
* --no-signal Suppress the signal emit (tests).
|
|
48
58
|
*
|
|
49
59
|
* Stdout: a single JSON envelope
|
|
50
60
|
* { storyId, epicId, decision, round, cap, capReached, totalCriteria,
|
|
51
|
-
* metCount, unmetCriteria[], signalEmitted }
|
|
61
|
+
* metCount, unmetCriteria[], signalEmitted, replay, verdictFingerprint }
|
|
52
62
|
* (`epicId` is retained as a always-null field for envelope stability.)
|
|
53
63
|
*
|
|
64
|
+
* Reading is free (Story #4874): re-invoking the gate over a verdict the
|
|
65
|
+
* ledger has already scored replays that round (`replay: true`,
|
|
66
|
+
* `signalEmitted: false`) instead of consuming one, so an unchanged verdict
|
|
67
|
+
* can never escalate from `redraft` to `block` by being looked at twice.
|
|
68
|
+
*
|
|
54
69
|
* @see .agents/scripts/lib/orchestration/acceptance-eval-decision.js
|
|
55
70
|
* @see .agents/schemas/acceptance-eval-verdict.schema.json
|
|
56
71
|
*/
|
|
@@ -69,8 +84,9 @@ import { Logger } from './lib/Logger.js';
|
|
|
69
84
|
import { appendSignal } from './lib/observability/signals-writer.js';
|
|
70
85
|
import {
|
|
71
86
|
buildAcceptanceEvalSignal,
|
|
87
|
+
computeVerdictFingerprint,
|
|
72
88
|
decideAcceptanceEval,
|
|
73
|
-
|
|
89
|
+
resolveAcceptanceEvalRound,
|
|
74
90
|
} from './lib/orchestration/acceptance-eval-decision.js';
|
|
75
91
|
|
|
76
92
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -137,6 +153,7 @@ function parseCliArgs(argv) {
|
|
|
137
153
|
options: {
|
|
138
154
|
story: { type: 'string' },
|
|
139
155
|
verdict: { type: 'string' },
|
|
156
|
+
'expected-criteria': { type: 'string' },
|
|
140
157
|
'no-signal': { type: 'boolean', default: false },
|
|
141
158
|
},
|
|
142
159
|
strict: false,
|
|
@@ -145,10 +162,69 @@ function parseCliArgs(argv) {
|
|
|
145
162
|
return {
|
|
146
163
|
storyId: Number.isInteger(storyId) && storyId > 0 ? storyId : null,
|
|
147
164
|
verdictPath: values.verdict ?? null,
|
|
165
|
+
expectedCriteria: values['expected-criteria'] ?? null,
|
|
148
166
|
emitSignal: values['no-signal'] !== true,
|
|
149
167
|
};
|
|
150
168
|
}
|
|
151
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The merge contract, stated once so both the flag error and the coverage
|
|
172
|
+
* error name the same shape the caller has to produce.
|
|
173
|
+
*/
|
|
174
|
+
const MERGE_CONTRACT =
|
|
175
|
+
'One round = N parallel cluster critics -> ONE merged verdict -> ONE gate call: ' +
|
|
176
|
+
"merge every cluster's records into a single criteria[] in acceptance[] order, " +
|
|
177
|
+
'one per acceptance item, before scoring.';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the optional `--expected-criteria` flag to a positive integer, or
|
|
181
|
+
* `null` when the flag is absent (which preserves the pre-#4951 behaviour
|
|
182
|
+
* exactly — no coverage assertion is made).
|
|
183
|
+
*
|
|
184
|
+
* Exported for tests.
|
|
185
|
+
*
|
|
186
|
+
* @param {string|null|undefined} raw
|
|
187
|
+
* @returns {number|null}
|
|
188
|
+
*/
|
|
189
|
+
export function resolveExpectedCriteria(raw) {
|
|
190
|
+
if (raw === null || raw === undefined) return null;
|
|
191
|
+
// Digits only. `Number.parseInt` stops at the first non-digit, so `4abc`
|
|
192
|
+
// resolved to 4 — a guard whose entire job is to reject a wrong-sized
|
|
193
|
+
// verdict was itself accepting a malformed count, and a typo'd `--expected-
|
|
194
|
+
// criteria` would then wave through a verdict of the wrong length.
|
|
195
|
+
const text = String(raw).trim();
|
|
196
|
+
const expected = /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
197
|
+
if (!Number.isInteger(expected) || expected < 1) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`acceptance-eval: --expected-criteria must be a positive integer (the Story's acceptance[] count). ${MERGE_CONTRACT}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return expected;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reject a verdict that does not cover exactly `expectedCriteria` criteria.
|
|
207
|
+
*
|
|
208
|
+
* Called **before** `runAcceptanceEval`, which is where the round ledger is
|
|
209
|
+
* read and appended — so a partial cluster verdict handed to the gate by
|
|
210
|
+
* mistake costs no round and can never escalate a `redraft` into a `block`.
|
|
211
|
+
*
|
|
212
|
+
* Exported for tests.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} verdict — schema-validated verdict.
|
|
215
|
+
* @param {number|null} expectedCriteria — `null` disables the assertion.
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
218
|
+
export function assertCriteriaCoverage(verdict, expectedCriteria) {
|
|
219
|
+
if (expectedCriteria === null) return;
|
|
220
|
+
const actual = Array.isArray(verdict?.criteria) ? verdict.criteria.length : 0;
|
|
221
|
+
if (actual === expectedCriteria) return;
|
|
222
|
+
throw new Error(
|
|
223
|
+
`acceptance-eval: verdict covers ${actual} criteria but --expected-criteria is ${expectedCriteria}. ` +
|
|
224
|
+
`${MERGE_CONTRACT} No round was consumed.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
152
228
|
/**
|
|
153
229
|
* Compose the operator-facing envelope and emit the per-criterion signal.
|
|
154
230
|
*
|
|
@@ -166,7 +242,8 @@ function parseCliArgs(argv) {
|
|
|
166
242
|
* self-reported `round` is never load-bearing for the cap.
|
|
167
243
|
* @param {object} [deps]
|
|
168
244
|
* @param {Function} [deps.appendSignalFn]
|
|
169
|
-
* @param {Function} [deps.
|
|
245
|
+
* @param {Function} [deps.resolveRoundFn]
|
|
246
|
+
* @param {Function} [deps.fingerprintFn]
|
|
170
247
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
171
248
|
*/
|
|
172
249
|
export async function runAcceptanceEval(
|
|
@@ -175,13 +252,23 @@ export async function runAcceptanceEval(
|
|
|
175
252
|
) {
|
|
176
253
|
const {
|
|
177
254
|
appendSignalFn = appendSignal,
|
|
178
|
-
|
|
255
|
+
resolveRoundFn = resolveAcceptanceEvalRound,
|
|
256
|
+
fingerprintFn = computeVerdictFingerprint,
|
|
179
257
|
} = deps;
|
|
180
258
|
const { maxRounds } = getAcceptanceEval(config);
|
|
259
|
+
const verdictFingerprint = fingerprintFn(verdict);
|
|
260
|
+
// Story #4874: re-reading an already-scored verdict is a replay — it
|
|
261
|
+
// reports the round that verdict was scored under and appends nothing, so
|
|
262
|
+
// observation alone can never advance the counter or escalate a redraft.
|
|
263
|
+
const resolved = resolveRoundFn({
|
|
264
|
+
epicId: null,
|
|
265
|
+
storyId,
|
|
266
|
+
config,
|
|
267
|
+
verdictFingerprint,
|
|
268
|
+
});
|
|
269
|
+
const replay = resolved.replay === true;
|
|
181
270
|
const resolvedRound =
|
|
182
|
-
Number.isInteger(round) && round >= 1
|
|
183
|
-
? round
|
|
184
|
-
: deriveRoundFn({ epicId: null, storyId, config });
|
|
271
|
+
Number.isInteger(round) && round >= 1 ? round : resolved.round;
|
|
185
272
|
const outcome = decideAcceptanceEval({
|
|
186
273
|
verdict,
|
|
187
274
|
maxRounds,
|
|
@@ -189,9 +276,14 @@ export async function runAcceptanceEval(
|
|
|
189
276
|
});
|
|
190
277
|
|
|
191
278
|
let signalEmitted = false;
|
|
192
|
-
if (emitSignal) {
|
|
279
|
+
if (emitSignal && !replay) {
|
|
193
280
|
const signal = {
|
|
194
|
-
...buildAcceptanceEvalSignal({
|
|
281
|
+
...buildAcceptanceEvalSignal({
|
|
282
|
+
storyId,
|
|
283
|
+
epicId: null,
|
|
284
|
+
outcome,
|
|
285
|
+
verdictFingerprint,
|
|
286
|
+
}),
|
|
195
287
|
ts: new Date().toISOString(),
|
|
196
288
|
};
|
|
197
289
|
try {
|
|
@@ -228,6 +320,10 @@ export async function runAcceptanceEval(
|
|
|
228
320
|
evidence: c.evidence,
|
|
229
321
|
})),
|
|
230
322
|
signalEmitted,
|
|
323
|
+
// True when this invocation re-read a verdict the ledger had already
|
|
324
|
+
// scored: the round was replayed, not advanced, and nothing was appended.
|
|
325
|
+
replay,
|
|
326
|
+
verdictFingerprint,
|
|
231
327
|
};
|
|
232
328
|
|
|
233
329
|
// `block` is the only non-zero exit: the loop has exhausted its bounded
|
|
@@ -268,11 +364,13 @@ export async function runAcceptanceEvalCli(
|
|
|
268
364
|
runAcceptanceEvalImpl = runAcceptanceEval,
|
|
269
365
|
logger = Logger,
|
|
270
366
|
} = deps;
|
|
271
|
-
const { storyId, verdictPath, emitSignal } =
|
|
367
|
+
const { storyId, verdictPath, expectedCriteria, emitSignal } =
|
|
368
|
+
parseCliArgs(argv);
|
|
369
|
+
const expected = resolveExpectedCriteria(expectedCriteria);
|
|
272
370
|
|
|
273
371
|
if (!storyId) {
|
|
274
372
|
throw new Error(
|
|
275
|
-
'Usage: node acceptance-eval.js --story <id> --verdict <path> [--no-signal]',
|
|
373
|
+
'Usage: node acceptance-eval.js --story <id> --verdict <path> [--expected-criteria <n>] [--no-signal]',
|
|
276
374
|
);
|
|
277
375
|
}
|
|
278
376
|
if (!verdictPath) {
|
|
@@ -303,6 +401,11 @@ export async function runAcceptanceEvalCli(
|
|
|
303
401
|
|
|
304
402
|
const verdict = validateVerdictImpl(parsed);
|
|
305
403
|
|
|
404
|
+
// Story #4951: a merged verdict must cover every acceptance[] item. This
|
|
405
|
+
// runs before the round ledger is touched, so a partial cluster verdict is
|
|
406
|
+
// a free mistake.
|
|
407
|
+
assertCriteriaCoverage(verdict, expected);
|
|
408
|
+
|
|
306
409
|
// A verdict whose embedded storyId disagrees with the CLI flag is a
|
|
307
410
|
// wiring error worth failing on, not a silent mismatch.
|
|
308
411
|
if (Number.isInteger(verdict.storyId) && verdict.storyId !== storyId) {
|
|
@@ -346,12 +449,18 @@ runAsCli(import.meta.url, main, {
|
|
|
346
449
|
source: 'acceptance-eval',
|
|
347
450
|
usage: {
|
|
348
451
|
invocation:
|
|
349
|
-
'node .agents/scripts/acceptance-eval.js --story <id> --verdict <path> [--no-signal]',
|
|
452
|
+
'node .agents/scripts/acceptance-eval.js --story <id> --verdict <path> [--expected-criteria <n>] [--no-signal]',
|
|
350
453
|
summary:
|
|
351
454
|
"Score an authored acceptance verdict against the Story's acceptance[] criteria and emit the bounded loop's proceed / redraft / block decision.",
|
|
352
455
|
flags: [
|
|
353
456
|
['--story <id>', 'GitHub issue number of the Story (required).'],
|
|
354
457
|
['--verdict <path>', 'Path to the authored verdict JSON (required).'],
|
|
458
|
+
[
|
|
459
|
+
'--expected-criteria <n>',
|
|
460
|
+
'Reject — before scoring, consuming no round — a verdict whose criteria[] ' +
|
|
461
|
+
"length is not n. Pass the Story's acceptance[] count so a partial " +
|
|
462
|
+
'cluster verdict cannot be scored as the round.',
|
|
463
|
+
],
|
|
355
464
|
[
|
|
356
465
|
'--no-signal',
|
|
357
466
|
"Skip appending the per-criterion signal to the Story's signals ledger.",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI: baseline hotspot engine for the `/audit-baselines` lens (Story #4902).
|
|
5
|
+
*
|
|
6
|
+
* Turns the committed `baselines/` folder into ranked hotspot clusters,
|
|
7
|
+
* gate-surface health signals, trend deltas, and floor-tightening headroom —
|
|
8
|
+
* the deterministic evidence half of a baseline review, so the lens spends
|
|
9
|
+
* its judgment on findings instead of re-deriving the numbers by hand.
|
|
10
|
+
*
|
|
11
|
+
* Read-only by contract: nothing under `baselines/` is written, and no test,
|
|
12
|
+
* coverage, or mutation suite is run. The only file this process creates is
|
|
13
|
+
* the envelope at `--out`.
|
|
14
|
+
*
|
|
15
|
+
* Exit 0 whenever evidence was assembled — findings are evidence, not a gate.
|
|
16
|
+
* A missing git history, absent friction ledger, or unresolvable import graph
|
|
17
|
+
* are reported as degradations and still exit 0. Only an unwritable `--out`
|
|
18
|
+
* (or a missing one) is a failure.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node .agents/scripts/audit-baselines.js --out temp/audit-baselines/envelope.json
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
25
|
+
// first import so the check runs before any third-party-importing sibling
|
|
26
|
+
// module is evaluated (Story #3432).
|
|
27
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import process from 'node:process';
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_HOTSPOT_LIMIT,
|
|
33
|
+
runEngine,
|
|
34
|
+
summarize,
|
|
35
|
+
} from './lib/audit-baselines/engine.js';
|
|
36
|
+
import { DEFAULT_TOP_N } from './lib/audit-baselines/outliers.js';
|
|
37
|
+
import { buildBaselineSchemaAjv } from './lib/baseline-schema-registry.js';
|
|
38
|
+
import { defineFlags } from './lib/cli-args.js';
|
|
39
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
40
|
+
|
|
41
|
+
const ENVELOPE_SCHEMA_FILE = 'audit-baselines-envelope.schema.json';
|
|
42
|
+
|
|
43
|
+
const FLAG_SPEC = {
|
|
44
|
+
out: { type: 'string' },
|
|
45
|
+
cwd: { type: 'string' },
|
|
46
|
+
'top-n': { type: 'integer' },
|
|
47
|
+
'hotspot-limit': { type: 'integer' },
|
|
48
|
+
'trend-depth': { type: 'integer' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate the envelope against its shipped schema through the shared
|
|
53
|
+
* baseline schema registry. Throws with the AJV error list on mismatch —
|
|
54
|
+
* a malformed envelope is an engine bug, not evidence.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} envelope
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
export function assertEnvelope(envelope) {
|
|
60
|
+
const validate = buildBaselineSchemaAjv().getSchema(ENVELOPE_SCHEMA_FILE);
|
|
61
|
+
if (!validate) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[audit-baselines] ${ENVELOPE_SCHEMA_FILE} is not registered in the baseline schema registry`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (validate(envelope)) return;
|
|
67
|
+
const detail = (validate.errors ?? [])
|
|
68
|
+
.map((e) => `${e.instancePath || '/'} ${e.message}`)
|
|
69
|
+
.join('; ');
|
|
70
|
+
throw new Error(
|
|
71
|
+
`[audit-baselines] envelope failed schema validation: ${detail}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Assemble the envelope, validate it, write it, and return the stdout
|
|
77
|
+
* summary. Exported so tests drive the whole pipeline without spawning.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ argv?: string[], cwd?: string, stdout?: { write: (s: string) => void } }} [opts]
|
|
80
|
+
* @returns {Promise<number>} exit code
|
|
81
|
+
*/
|
|
82
|
+
export async function runCli({
|
|
83
|
+
argv = process.argv.slice(2),
|
|
84
|
+
cwd = process.cwd(),
|
|
85
|
+
stdout = process.stdout,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const { values } = defineFlags(FLAG_SPEC, argv);
|
|
88
|
+
if (!values.out) {
|
|
89
|
+
throw new Error('[audit-baselines] --out <path> is required');
|
|
90
|
+
}
|
|
91
|
+
const repoRoot = path.resolve(values.cwd ?? cwd);
|
|
92
|
+
const outPath = path.resolve(repoRoot, values.out);
|
|
93
|
+
|
|
94
|
+
const envelope = runEngine({
|
|
95
|
+
cwd: repoRoot,
|
|
96
|
+
topN: values.topN ?? DEFAULT_TOP_N,
|
|
97
|
+
hotspotLimit: values.hotspotLimit ?? DEFAULT_HOTSPOT_LIMIT,
|
|
98
|
+
trendDepth: values.trendDepth ?? 5,
|
|
99
|
+
});
|
|
100
|
+
assertEnvelope(envelope);
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
103
|
+
fs.writeFileSync(outPath, `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
|
|
104
|
+
|
|
105
|
+
stdout.write(`${JSON.stringify(summarize(envelope, outPath), null, 2)}\n`);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
runAsCli(import.meta.url, async () => runCli(), {
|
|
110
|
+
source: 'audit-baselines',
|
|
111
|
+
propagateExitCode: true,
|
|
112
|
+
errorPrefix: '[audit-baselines] ❌ Fatal error',
|
|
113
|
+
usage: {
|
|
114
|
+
invocation:
|
|
115
|
+
'node .agents/scripts/audit-baselines.js --out <path> [--cwd <dir>] [--top-n <n>] [--hotspot-limit <n>] [--trend-depth <n>]',
|
|
116
|
+
summary:
|
|
117
|
+
'Read-only baseline hotspot engine: extract bounded per-gate outliers, cluster them per file, rank by severity x churn x import in-degree x friction, and report gate-surface health, trend deltas, and floor headroom.',
|
|
118
|
+
flags: [
|
|
119
|
+
['--out <path>', 'Write the JSON envelope here (required).'],
|
|
120
|
+
['--cwd <dir>', 'Repository root to analyse (default: cwd).'],
|
|
121
|
+
[
|
|
122
|
+
'--top-n <n>',
|
|
123
|
+
`Outlier rows extracted per gate (default: ${DEFAULT_TOP_N}).`,
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
'--hotspot-limit <n>',
|
|
127
|
+
`Hotspot clusters emitted (default: ${DEFAULT_HOTSPOT_LIMIT}).`,
|
|
128
|
+
],
|
|
129
|
+
['--trend-depth <n>', 'Baseline commits sampled per kind (default: 5).'],
|
|
130
|
+
],
|
|
131
|
+
notes: [
|
|
132
|
+
'Never writes under baselines/ and never runs a test, coverage, or mutation suite.',
|
|
133
|
+
'Exit codes:\n 0 evidence assembled (including every degraded input)\n 1 the envelope could not be built or written',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* audit-labels-bootstrap.js — Idempotently create the
|
|
3
|
-
*
|
|
2
|
+
* audit-labels-bootstrap.js — Idempotently create the audit label taxonomy in
|
|
3
|
+
* the configured GitHub repo.
|
|
4
4
|
*
|
|
5
5
|
* Run this once per repo before `/audit-to-stories` opens its first
|
|
6
6
|
* Story. Re-runs are safe — existing labels are skipped, only missing
|
|
7
7
|
* ones are created. Story #2583 acceptance criterion #6.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (Story #
|
|
17
|
-
*
|
|
18
|
-
* `AUDIT_LENSES` and (optionally) a `LENS_META` entry.
|
|
9
|
+
* This CLI is a thin creator over
|
|
10
|
+
* [`lib/audit-to-stories/audit-label-taxonomy.js`](lib/audit-to-stories/audit-label-taxonomy.js),
|
|
11
|
+
* which is the SSOT for **every** label an audit sweep creates or generates:
|
|
12
|
+
* the `audit::<lens>` set (derived from the shared `AUDIT_LENSES` list, one per
|
|
13
|
+
* `/audit-<lens>` workflow) plus the story-axis labels the filer applies. The
|
|
14
|
+
* creator and the generator (`build-story-body.js`) read that one list, so the
|
|
15
|
+
* bootstrap cannot fall behind the filer — the drift that left `risk::high`
|
|
16
|
+
* generated but defined nowhere (Story #4877), and that made `audit::<dimension>`
|
|
17
|
+
* labels mint from free-form prose before Story #4195.
|
|
19
18
|
*
|
|
20
19
|
* Delegates to `gh label create` so the script works without any
|
|
21
20
|
* provider plumbing — `gh auth status` is the only prerequisite. Per
|
|
@@ -26,88 +25,11 @@
|
|
|
26
25
|
import process from 'node:process';
|
|
27
26
|
import { parseArgs } from 'node:util';
|
|
28
27
|
|
|
29
|
-
import {
|
|
28
|
+
import { AUDIT_LABEL_TAXONOMY } from './lib/audit-to-stories/audit-label-taxonomy.js';
|
|
30
29
|
import { runAsCli } from './lib/cli-utils.js';
|
|
31
30
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
32
31
|
import { gh as defaultGh, GhExecError } from './lib/gh-exec.js';
|
|
33
32
|
|
|
34
|
-
/**
|
|
35
|
-
* Per-lens label presentation. Keyed by canonical lens name. A lens absent
|
|
36
|
-
* from this map falls back to {@link DEFAULT_LENS_META} so a newly-added
|
|
37
|
-
* `AUDIT_LENSES` entry still gets a label without a hard requirement to
|
|
38
|
-
* register colour/description here first.
|
|
39
|
-
*/
|
|
40
|
-
const LENS_META = Object.freeze({
|
|
41
|
-
accessibility: {
|
|
42
|
-
color: 'c5def5',
|
|
43
|
-
description: 'Audit-sourced finding: WCAG accessibility conformance',
|
|
44
|
-
},
|
|
45
|
-
architecture: {
|
|
46
|
-
color: '6f42c1',
|
|
47
|
-
description: 'Audit-sourced finding: architectural concerns',
|
|
48
|
-
},
|
|
49
|
-
'clean-code': {
|
|
50
|
-
color: '0e8a16',
|
|
51
|
-
description: 'Audit-sourced finding: clean-code / maintainability',
|
|
52
|
-
},
|
|
53
|
-
dependencies: {
|
|
54
|
-
color: 'd4c5f9',
|
|
55
|
-
description: 'Audit-sourced finding: dependencies / supply chain',
|
|
56
|
-
},
|
|
57
|
-
devops: {
|
|
58
|
-
color: 'fbca04',
|
|
59
|
-
description: 'Audit-sourced finding: DevOps / CI / CD',
|
|
60
|
-
},
|
|
61
|
-
documentation: {
|
|
62
|
-
color: '1d76db',
|
|
63
|
-
description: 'Audit-sourced finding: documentation staleness / gaps',
|
|
64
|
-
},
|
|
65
|
-
navigability: {
|
|
66
|
-
color: 'bfdadc',
|
|
67
|
-
description: 'Audit-sourced finding: route / nav reachability',
|
|
68
|
-
},
|
|
69
|
-
performance: {
|
|
70
|
-
color: 'b60205',
|
|
71
|
-
description: 'Audit-sourced finding: performance / latency',
|
|
72
|
-
},
|
|
73
|
-
privacy: {
|
|
74
|
-
color: 'fef2c0',
|
|
75
|
-
description: 'Audit-sourced finding: privacy / data handling',
|
|
76
|
-
},
|
|
77
|
-
quality: {
|
|
78
|
-
color: '0052cc',
|
|
79
|
-
description: 'Audit-sourced finding: test quality / coverage gaps',
|
|
80
|
-
},
|
|
81
|
-
security: {
|
|
82
|
-
color: 'b60205',
|
|
83
|
-
description: 'Audit-sourced finding: security / OWASP',
|
|
84
|
-
},
|
|
85
|
-
seo: {
|
|
86
|
-
color: 'fbca04',
|
|
87
|
-
description: 'Audit-sourced finding: SEO / discoverability',
|
|
88
|
-
},
|
|
89
|
-
sre: {
|
|
90
|
-
color: '0052cc',
|
|
91
|
-
description: 'Audit-sourced finding: SRE / observability / reliability',
|
|
92
|
-
},
|
|
93
|
-
'ux-ui': {
|
|
94
|
-
color: 'd4c5f9',
|
|
95
|
-
description: 'Audit-sourced finding: UX / UI concerns',
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const DEFAULT_LENS_META = Object.freeze({
|
|
100
|
-
color: 'ededed',
|
|
101
|
-
description: 'Audit-sourced finding',
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const DIMENSIONS = Object.freeze(
|
|
105
|
-
AUDIT_LENSES.map((name) => ({
|
|
106
|
-
name,
|
|
107
|
-
...(LENS_META[name] ?? DEFAULT_LENS_META),
|
|
108
|
-
})),
|
|
109
|
-
);
|
|
110
|
-
|
|
111
33
|
async function labelExists(gh, owner, repo, name) {
|
|
112
34
|
try {
|
|
113
35
|
const list = await gh.label.list(
|
|
@@ -166,9 +88,8 @@ export async function bootstrapAuditLabels({
|
|
|
166
88
|
const skipped = [];
|
|
167
89
|
const failed = [];
|
|
168
90
|
|
|
169
|
-
for (const
|
|
170
|
-
const labelName =
|
|
171
|
-
const candidate = { ...dim, name: labelName };
|
|
91
|
+
for (const candidate of AUDIT_LABEL_TAXONOMY) {
|
|
92
|
+
const labelName = candidate.name;
|
|
172
93
|
|
|
173
94
|
if (dryRun) {
|
|
174
95
|
created.push(labelName);
|
|
@@ -190,7 +111,7 @@ export async function bootstrapAuditLabels({
|
|
|
190
111
|
}
|
|
191
112
|
}
|
|
192
113
|
|
|
193
|
-
return { created, skipped, failed, total:
|
|
114
|
+
return { created, skipped, failed, total: AUDIT_LABEL_TAXONOMY.length };
|
|
194
115
|
}
|
|
195
116
|
|
|
196
117
|
/**
|
|
@@ -241,7 +162,7 @@ export function formatBootstrapReport(result) {
|
|
|
241
162
|
};
|
|
242
163
|
}
|
|
243
164
|
|
|
244
|
-
export const __testing = {
|
|
165
|
+
export const __testing = { AUDIT_LABEL_TAXONOMY };
|
|
245
166
|
|
|
246
167
|
async function main() {
|
|
247
168
|
const { values } = parseArgs({
|
|
@@ -47,17 +47,34 @@ import { parseAuditReports } from './lib/audit-to-stories/parse-audit-md.js';
|
|
|
47
47
|
import { buildPlanSeedMarkdown } from './lib/audit-to-stories/seed-from-findings.js';
|
|
48
48
|
import { runAsCli } from './lib/cli-utils.js';
|
|
49
49
|
import { searchSemanticCandidates } from './lib/findings/semantic-issue-search.js';
|
|
50
|
+
import { SEVERITIES, SEVERITY_RANK } from './lib/findings/severity.js';
|
|
50
51
|
import { Logger } from './lib/Logger.js';
|
|
51
52
|
import { parse as parseStoryBody } from './lib/story-body/story-body.js';
|
|
52
53
|
|
|
53
|
-
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
54
54
|
const DEFAULT_GLOB = 'temp/audits/audit-*-results.md';
|
|
55
55
|
const FAN_OUT_REPORT = 'audit-fan-out-results.md';
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Does `finding` clear the `threshold` severity floor?
|
|
59
|
+
*
|
|
60
|
+
* `SEVERITY_RANK` is imported from the canonical scale rather than declared
|
|
61
|
+
* here (Story #4877). The local copy this replaces ranked only four levels
|
|
62
|
+
* (`critical|high|medium|low`), so `info` — the canonical floor — ranked `0`,
|
|
63
|
+
* below even `--severity low`, and every informational finding was silently
|
|
64
|
+
* dropped from every filtered run. Sourcing the ranking from the SSOT means a
|
|
65
|
+
* level cannot exist in the vocabulary and be invisible to the filter.
|
|
66
|
+
*
|
|
67
|
+
* An unrecognised or absent severity still ranks below every real floor: it
|
|
68
|
+
* failed to parse, so it is not evidence that a threshold was met.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ severity?: string }} finding
|
|
71
|
+
* @param {string} [threshold] — a canonical level, `'all'`, or falsy for no floor.
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
57
74
|
function meetsSeverity(finding, threshold) {
|
|
58
75
|
if (!threshold || threshold === 'all') return true;
|
|
59
76
|
const minRank = SEVERITY_RANK[threshold] ?? 0;
|
|
60
|
-
const fRank = SEVERITY_RANK[finding.severity] ??
|
|
77
|
+
const fRank = SEVERITY_RANK[finding.severity] ?? -1;
|
|
61
78
|
return fRank >= minRank;
|
|
62
79
|
}
|
|
63
80
|
|
|
@@ -77,8 +94,21 @@ function readReports(paths) {
|
|
|
77
94
|
}));
|
|
78
95
|
}
|
|
79
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Count findings per severity bucket. The buckets are the canonical levels plus
|
|
99
|
+
* `unknown` for a finding whose severity did not parse — kept as a visible
|
|
100
|
+
* bucket so an unparseable severity is reported rather than absorbed into a
|
|
101
|
+
* real level. Derived from `SEVERITIES` so a new level appears in the tally
|
|
102
|
+
* automatically instead of falling into `unknown` (Story #4877).
|
|
103
|
+
*
|
|
104
|
+
* @param {Array<{ severity?: string }>} findings
|
|
105
|
+
* @returns {Record<string, number>}
|
|
106
|
+
*/
|
|
80
107
|
function tallyBySeverity(findings) {
|
|
81
|
-
const t = {
|
|
108
|
+
const t = {
|
|
109
|
+
...Object.fromEntries(SEVERITIES.map((s) => [s, 0])),
|
|
110
|
+
unknown: 0,
|
|
111
|
+
};
|
|
82
112
|
for (const f of findings) {
|
|
83
113
|
if (Object.hasOwn(t, f.severity)) t[f.severity] += 1;
|
|
84
114
|
else t.unknown += 1;
|