mandrel 2.55.0 → 2.57.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/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +4 -30
- package/.agents/docs/configuration.md +11 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +28 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/audit-to-stories.js +222 -75
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/file-ci-gap.js +306 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +49 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +38 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/pr-watch-with-update.js +3 -2
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/audit-to-stories.md +63 -27
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -196
- package/.agents/workflows/mandrel-plan.md +28 -41
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +33 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/baselines/crap-updater-cli.js — the `update-crap-baseline` CLI's own
|
|
3
|
+
* logic: flag parsing, option defaulting, and the bespoke scorer it hands
|
|
4
|
+
* `refreshBaseline`.
|
|
5
|
+
*
|
|
6
|
+
* Story #5316: all three lived inside `update-crap-baseline.js#main`, which no
|
|
7
|
+
* test imports. `parseCliArgs` scored CRAP 56, the inlined scorer 72, and
|
|
8
|
+
* `main` itself 90 — three of the ten methods Story #5311's honest re-anchor
|
|
9
|
+
* made visible, every one at 0% coverage. They sit here for the same reason
|
|
10
|
+
* `diff-scope-cli.js` does: a CLI shell is unreachable from a test, and the
|
|
11
|
+
* import from the CLI is what keeps these off the `dead-exports:production`
|
|
12
|
+
* ratchet.
|
|
13
|
+
*
|
|
14
|
+
* **Why this is NOT the `refresh-service.js` default scorer.** The service
|
|
15
|
+
* already resolves a `buildDefaultCrapScorer`, and Story #4293 made
|
|
16
|
+
* `update-maintainability-baseline.js` drop its bespoke scorer in favour of
|
|
17
|
+
* exactly that. This one cannot follow: it carries `checkResolutionFloor`, a
|
|
18
|
+
* fail-closed refusal that throws before anything is written when too few
|
|
19
|
+
* methods resolved a coverage entry — the guard that stops a broken join being
|
|
20
|
+
* persisted as a sparse baseline (Story #4775). Moving that into the shared
|
|
21
|
+
* default would change behaviour for `refresh-commit.js` and close-validation,
|
|
22
|
+
* which resolve the same default. So the scorer stays bespoke, and is tested
|
|
23
|
+
* here instead.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { checkResolutionFloor, scanAndScore } from '../crap-utils.js';
|
|
28
|
+
import { Logger } from '../Logger.js';
|
|
29
|
+
import { parseDiffScopeFlag } from './diff-scope-cli.js';
|
|
30
|
+
|
|
31
|
+
/** Coverage artifact read when neither the flag nor config names one. */
|
|
32
|
+
const DEFAULT_COVERAGE_PATH = 'coverage/coverage-final.json';
|
|
33
|
+
|
|
34
|
+
/** Resolution-rate floor applied when config does not set one. */
|
|
35
|
+
const DEFAULT_MIN_RESOLUTION_RATE = 0.75;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse the updater's argv.
|
|
39
|
+
*
|
|
40
|
+
* `--full-scope` and `--diff-scope <ref>` are read here but deliberately NOT
|
|
41
|
+
* reconciled — {@link resolveCrapUpdaterOptions} owns the refusal, so a caller
|
|
42
|
+
* cannot get a half-validated shape by calling only this.
|
|
43
|
+
*
|
|
44
|
+
* @param {string[]} [argv]
|
|
45
|
+
* @returns {{baselinePath: string|undefined, coveragePath: string|undefined,
|
|
46
|
+
* fullScope: boolean, diffScopeRef: string|null}}
|
|
47
|
+
*/
|
|
48
|
+
export function parseCrapUpdaterArgs(argv = []) {
|
|
49
|
+
const out = {
|
|
50
|
+
baselinePath: undefined,
|
|
51
|
+
coveragePath: undefined,
|
|
52
|
+
fullScope: false,
|
|
53
|
+
diffScopeRef: parseDiffScopeFlag(argv),
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
if (argv[i] === '--baseline' && argv[i + 1]) {
|
|
57
|
+
out.baselinePath = argv[i + 1];
|
|
58
|
+
i += 1;
|
|
59
|
+
} else if (argv[i] === '--coverage' && argv[i + 1]) {
|
|
60
|
+
out.coveragePath = argv[i + 1];
|
|
61
|
+
i += 1;
|
|
62
|
+
} else if (argv[i] === '--full-scope') {
|
|
63
|
+
out.fullScope = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fold parsed args over the project's quality config into the one shape the
|
|
71
|
+
* CLI and the scorer both read.
|
|
72
|
+
*
|
|
73
|
+
* Every flag wins over config, and config over the built-in default — the
|
|
74
|
+
* precedence that used to be a chain of `??` inside `main`, which is most of
|
|
75
|
+
* why `main` was cyclomatic 9.
|
|
76
|
+
*
|
|
77
|
+
* Throws on `--full-scope` together with `--diff-scope`: the two describe
|
|
78
|
+
* incompatible scopes and silently preferring one would write a baseline the
|
|
79
|
+
* operator did not ask for.
|
|
80
|
+
*
|
|
81
|
+
* @param {{baselinePath?: string, coveragePath?: string, fullScope?: boolean,
|
|
82
|
+
* diffScopeRef?: string|null}} args From {@link parseCrapUpdaterArgs}.
|
|
83
|
+
* @param {{crap?: object, baselines?: object}} sources Resolved config slices:
|
|
84
|
+
* `crap` is the quality block, `baselines` the baselines block.
|
|
85
|
+
* @param {string} [cwd] Root the baseline path is resolved against.
|
|
86
|
+
* @returns {{targetDirs: string[], ignoreGlobs: string[],
|
|
87
|
+
* requireCoverage: boolean, minMethodResolutionRate: number,
|
|
88
|
+
* coveragePath: string, baselinePath: string, absBaselinePath: string,
|
|
89
|
+
* fullScope: boolean, diffScopeRef: string|null}}
|
|
90
|
+
*/
|
|
91
|
+
export function resolveCrapUpdaterOptions(
|
|
92
|
+
args = {},
|
|
93
|
+
{ crap = {}, baselines = {} } = {},
|
|
94
|
+
cwd = process.cwd(),
|
|
95
|
+
) {
|
|
96
|
+
if (args.fullScope && args.diffScopeRef != null) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'[CRAP] --full-scope is incompatible with --diff-scope; pick one',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const baselinePath = args.baselinePath ?? baselines?.crap?.path;
|
|
102
|
+
const coveragePath =
|
|
103
|
+
args.coveragePath ?? crap.coveragePath ?? DEFAULT_COVERAGE_PATH;
|
|
104
|
+
return {
|
|
105
|
+
targetDirs: Array.isArray(crap.targetDirs) ? crap.targetDirs : [],
|
|
106
|
+
ignoreGlobs: Array.isArray(crap.ignoreGlobs) ? crap.ignoreGlobs : [],
|
|
107
|
+
requireCoverage: crap.requireCoverage !== false,
|
|
108
|
+
minMethodResolutionRate:
|
|
109
|
+
crap.minMethodResolutionRate ?? DEFAULT_MIN_RESOLUTION_RATE,
|
|
110
|
+
coveragePath,
|
|
111
|
+
baselinePath,
|
|
112
|
+
absBaselinePath: path.isAbsolute(baselinePath)
|
|
113
|
+
? baselinePath
|
|
114
|
+
: path.resolve(cwd, baselinePath),
|
|
115
|
+
fullScope: Boolean(args.fullScope),
|
|
116
|
+
diffScopeRef: args.diffScopeRef ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Report a scan's drop counters. Each earns a line only when it moved, so a
|
|
122
|
+
* clean scan stays quiet.
|
|
123
|
+
*
|
|
124
|
+
* `unscorableFiles` (Story #5311) is the one that must never be silent: a file
|
|
125
|
+
* the scan could not read, transpile or parse contributes no rows, so without
|
|
126
|
+
* a line of its own the run reads as a clean scan of a tree with fewer methods
|
|
127
|
+
* than it has.
|
|
128
|
+
*
|
|
129
|
+
* @param {{skippedFilesNoCoverage?: number, skippedMethodsNoCoverage?: number,
|
|
130
|
+
* unscorableFiles?: number, resolution?: object}} summary
|
|
131
|
+
* @param {{info: Function}} [logger]
|
|
132
|
+
*/
|
|
133
|
+
function reportScanSummary(summary, logger = Logger) {
|
|
134
|
+
const counters = [
|
|
135
|
+
[
|
|
136
|
+
summary.skippedFilesNoCoverage,
|
|
137
|
+
'file(s) skipped without coverage entries.',
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
summary.skippedMethodsNoCoverage,
|
|
141
|
+
'method(s) skipped — per-method coverage unresolved.',
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
summary.unscorableFiles,
|
|
145
|
+
'file(s) unscorable (read/transpile/parse failure) — no rows contributed.',
|
|
146
|
+
],
|
|
147
|
+
];
|
|
148
|
+
for (const [count, what] of counters) {
|
|
149
|
+
if (count > 0) logger.info(`[CRAP] ${count} ${what}`);
|
|
150
|
+
}
|
|
151
|
+
const r = summary.resolution;
|
|
152
|
+
if (r) {
|
|
153
|
+
logger.info(
|
|
154
|
+
`[CRAP] Method resolution: ${r.resolvedMethods}/${r.joinableMethods} ` +
|
|
155
|
+
`(${(r.rate * 100).toFixed(1)}%) in files with coverage.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the scorer `refreshBaseline` invokes.
|
|
162
|
+
*
|
|
163
|
+
* Fails closed twice, and both refusals are the point of keeping this bespoke:
|
|
164
|
+
*
|
|
165
|
+
* - **No coverage artifact under `requireCoverage`** — every file would be
|
|
166
|
+
* skipped, so the scan is abandoned with an operator-facing warning rather
|
|
167
|
+
* than returning a confidently empty row set.
|
|
168
|
+
* - **Resolution rate below the floor** — `checkResolutionFloor` throws
|
|
169
|
+
* BEFORE the service writes anything. A baseline built from a broken join
|
|
170
|
+
* is not sparse, it is wrong.
|
|
171
|
+
*
|
|
172
|
+
* `loadCoverage` and the logger are named seams so a test drives the whole
|
|
173
|
+
* scorer without a coverage artifact on disk (`rules/test-seams.md`).
|
|
174
|
+
*
|
|
175
|
+
* @param {ReturnType<typeof resolveCrapUpdaterOptions>} options
|
|
176
|
+
* @param {{loadCoverage: Function, scan?: Function, logger?: object}} deps
|
|
177
|
+
* @returns {(files: string[], opts: object) => Promise<object[]>}
|
|
178
|
+
*/
|
|
179
|
+
export function buildCrapUpdaterScorer(
|
|
180
|
+
options,
|
|
181
|
+
{ loadCoverage, scan = scanAndScore, logger = Logger } = {},
|
|
182
|
+
) {
|
|
183
|
+
return async (files, opts) => {
|
|
184
|
+
const effectiveCwd = opts?.cwd ?? process.cwd();
|
|
185
|
+
const coverageAbs = path.isAbsolute(options.coveragePath)
|
|
186
|
+
? options.coveragePath
|
|
187
|
+
: path.resolve(effectiveCwd, options.coveragePath);
|
|
188
|
+
const coverage = loadCoverage(coverageAbs);
|
|
189
|
+
if (!coverage && options.requireCoverage) {
|
|
190
|
+
logger.warn(
|
|
191
|
+
`[CRAP] ⚠ No coverage artifact at ${options.coveragePath}. All files will be skipped under requireCoverage=true.`,
|
|
192
|
+
);
|
|
193
|
+
logger.warn(
|
|
194
|
+
"[CRAP] ⚠ Run 'npm run test:coverage' before 'npm run crap:update'.",
|
|
195
|
+
);
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const summary = await scan({
|
|
200
|
+
targetDirs: options.targetDirs,
|
|
201
|
+
coverage,
|
|
202
|
+
requireCoverage: options.requireCoverage,
|
|
203
|
+
cwd: effectiveCwd,
|
|
204
|
+
ignoreGlobs: options.ignoreGlobs,
|
|
205
|
+
scopeFiles: opts?.fullScope ? null : (files ?? null),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
logger.info(`[CRAP] Scanned ${summary.scannedFiles} file(s).`);
|
|
209
|
+
reportScanSummary(summary, logger);
|
|
210
|
+
|
|
211
|
+
// Fail closed BEFORE the service persists anything — a thin baseline is
|
|
212
|
+
// never written and then apologised for.
|
|
213
|
+
const refusal = checkResolutionFloor(
|
|
214
|
+
summary.resolution,
|
|
215
|
+
options.minMethodResolutionRate,
|
|
216
|
+
);
|
|
217
|
+
if (refusal) throw new Error(refusal);
|
|
218
|
+
|
|
219
|
+
return (summary.rows ?? []).filter(
|
|
220
|
+
(r) => typeof r?.crap === 'number' && Number.isFinite(r.crap),
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -36,7 +36,9 @@ const BDD_SCENARIOS_BYTE_BUDGET = 24_000;
|
|
|
36
36
|
/**
|
|
37
37
|
* Truncate a scenario index to a byte budget, deterministically (scan
|
|
38
38
|
* order — file walk order, then in-file order — never re-sorted), and
|
|
39
|
-
* report what was dropped rather than truncating silently
|
|
39
|
+
* report what was dropped rather than truncating silently: `truncated` is
|
|
40
|
+
* `null` when everything fit, else a note naming how many scenarios were
|
|
41
|
+
* cut (Story #5312).
|
|
40
42
|
*
|
|
41
43
|
* @param {Array<object>} scenarios Full scan output (order preserved).
|
|
42
44
|
* @param {{ byteBudget?: number }} [opts]
|
|
@@ -44,7 +46,7 @@ const BDD_SCENARIOS_BYTE_BUDGET = 24_000;
|
|
|
44
46
|
* scenarios: Array<object>,
|
|
45
47
|
* totalScenarios: number,
|
|
46
48
|
* includedScenarios: number,
|
|
47
|
-
* truncated:
|
|
49
|
+
* truncated: null | { droppedScenarios: number, note: string },
|
|
48
50
|
* }}
|
|
49
51
|
*/
|
|
50
52
|
export function capBddScenarios(scenarios, opts = {}) {
|
|
@@ -63,6 +65,22 @@ export function capBddScenarios(scenarios, opts = {}) {
|
|
|
63
65
|
scenarios: list.slice(0, cut),
|
|
64
66
|
totalScenarios: list.length,
|
|
65
67
|
includedScenarios: cut,
|
|
66
|
-
truncated: cut
|
|
68
|
+
truncated: describeTruncation(list, cut, byteBudget),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The `truncated` note: `null` when everything fit, else what was cut.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<object>} list
|
|
76
|
+
* @param {number} cut
|
|
77
|
+
* @param {number} byteBudget
|
|
78
|
+
* @returns {null | { droppedScenarios: number, note: string }}
|
|
79
|
+
*/
|
|
80
|
+
function describeTruncation(list, cut, byteBudget) {
|
|
81
|
+
if (cut === list.length) return null;
|
|
82
|
+
return {
|
|
83
|
+
droppedScenarios: list.length - cut,
|
|
84
|
+
note: `bddScenarios cut to ${cut} of ${list.length} scenarios to fit the ${byteBudget}-byte envelope budget`,
|
|
67
85
|
};
|
|
68
86
|
}
|
|
@@ -14,6 +14,7 @@ import { getQuality } from '../config/quality.js';
|
|
|
14
14
|
import { filterFilesUnderTargets } from '../coverage-capture.js';
|
|
15
15
|
import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
|
|
16
16
|
import { KNOWN_KINDS } from '../orchestration/check-baselines/phases/parse-args.js';
|
|
17
|
+
import { predictsTestEvidenceCredit } from '../test-run-credit.js';
|
|
17
18
|
import {
|
|
18
19
|
buildFormatHint,
|
|
19
20
|
FORMAT_CHECK_FALLBACK,
|
|
@@ -415,6 +416,35 @@ function predictsIncrementalCaptureSkip({
|
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Is the `test` gate already credited for this build? Only consulted when
|
|
421
|
+
* coverage-capture is the active runner; a consumer without one gets the
|
|
422
|
+
* plain `test` gate regardless (Story #5313).
|
|
423
|
+
*
|
|
424
|
+
* @param {{ coverageCaptureActive: boolean } & Parameters<typeof predictsTestEvidenceCredit>[0]} opts
|
|
425
|
+
* @returns {boolean}
|
|
426
|
+
*/
|
|
427
|
+
function resolveTestCredited({ coverageCaptureActive, ...probe }) {
|
|
428
|
+
return coverageCaptureActive && predictsTestEvidenceCredit(probe);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Will the `coverage-capture` gate be the one that runs the suite for this
|
|
433
|
+
* build — so the plain `test` gate is dropped? It is not when it is inactive,
|
|
434
|
+
* when its incremental skip is pre-decided (Story #5278), or when a green
|
|
435
|
+
* bare `npm test` already deposited the test credit (Story #5313).
|
|
436
|
+
*
|
|
437
|
+
* @param {{ coverageCaptureActive: boolean, captureSkipPredicted: boolean, testCredited: boolean }} opts
|
|
438
|
+
* @returns {boolean}
|
|
439
|
+
*/
|
|
440
|
+
function coverageCaptureRunsSuite({
|
|
441
|
+
coverageCaptureActive,
|
|
442
|
+
captureSkipPredicted,
|
|
443
|
+
testCredited,
|
|
444
|
+
}) {
|
|
445
|
+
return coverageCaptureActive && !captureSkipPredicted && !testCredited;
|
|
446
|
+
}
|
|
447
|
+
|
|
418
448
|
/**
|
|
419
449
|
* The `coverage-capture` gate's argv.
|
|
420
450
|
*
|
|
@@ -510,10 +540,25 @@ export function buildDefaultGates({
|
|
|
510
540
|
presentBaselines,
|
|
511
541
|
log,
|
|
512
542
|
getChangedFilesImpl,
|
|
543
|
+
storyId,
|
|
544
|
+
evidenceCwd,
|
|
545
|
+
gitSpawnImpl,
|
|
546
|
+
shouldSkipImpl,
|
|
513
547
|
} = {}) {
|
|
514
548
|
const scripts = packageScripts ?? readPackageScripts(cwd);
|
|
515
549
|
const coverageCaptureActive =
|
|
516
550
|
isCrapGateEnabled(config) && hasNpmScript(scripts, 'test:coverage');
|
|
551
|
+
// Story #5313 — a credited bare `npm test` registers the plain `test` gate
|
|
552
|
+
// beside the capture so the credit is reported, never re-spent.
|
|
553
|
+
const testCredited = resolveTestCredited({
|
|
554
|
+
coverageCaptureActive,
|
|
555
|
+
storyId,
|
|
556
|
+
cwd,
|
|
557
|
+
evidenceCwd,
|
|
558
|
+
gitSpawnImpl,
|
|
559
|
+
shouldSkipImpl,
|
|
560
|
+
log,
|
|
561
|
+
});
|
|
517
562
|
// Story #5278 — a registered coverage-capture gate that is going to take
|
|
518
563
|
// its own incremental skip is not the test runner for this close, so the
|
|
519
564
|
// plain `test` gate comes back beside it and the capture gate registers as
|
|
@@ -563,7 +608,13 @@ export function buildDefaultGates({
|
|
|
563
608
|
// scoped pair does not shift the close-orchestrator log line, the
|
|
564
609
|
// evidence keyspace, or the parallel-partition membership below.
|
|
565
610
|
{ name: 'lint', cmd: lint.cmd, args: lint.args },
|
|
566
|
-
...buildTestGateEntry(
|
|
611
|
+
...buildTestGateEntry(
|
|
612
|
+
coverageCaptureRunsSuite({
|
|
613
|
+
coverageCaptureActive,
|
|
614
|
+
captureSkipPredicted,
|
|
615
|
+
testCredited,
|
|
616
|
+
}),
|
|
617
|
+
),
|
|
567
618
|
{
|
|
568
619
|
// Gate name kept generic ("format") so the close-orchestrator log line
|
|
569
620
|
// doesn't shift when a repo swaps biome for Prettier / dprint via
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Acceptance self-eval accessor (Story #3819).
|
|
2
|
+
* Acceptance self-eval accessor (Story #3819, Story #5313).
|
|
3
3
|
*
|
|
4
4
|
* Resolves `.agentrc.json → delivery.acceptanceEval` into the canonical
|
|
5
5
|
* shape the per-Story acceptance self-eval loop consumes. The loop scores
|
|
6
6
|
* the caller-injected change set against each inline `acceptance[]` item,
|
|
7
|
-
* redrafts the unmet items, and re-evaluates — capped at `maxRounds`
|
|
8
|
-
* then escalates to `agent::blocked` when criteria remain
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* would disable the loop) clamps up to 1; a pathological
|
|
18
|
-
* `maxRounds: 9999` clamps down to the ceiling.
|
|
19
|
-
* 2. Non-integer / non-finite / missing values fall back to the
|
|
20
|
-
* documented default. The AJV schema already rejects `maxRounds < 1`
|
|
21
|
-
* and non-integers before they reach this accessor, but the resolver
|
|
22
|
-
* stays defensive so unit-test fixtures and degraded configs never
|
|
23
|
-
* produce an unbounded or zero-round loop.
|
|
24
|
-
*
|
|
25
|
-
* There is intentionally no `enabled` flag — the loop is a hard cutover
|
|
26
|
-
* (always on) per `rules/git-conventions.md` (no parallel old-shape path,
|
|
27
|
-
* no toggle between "loop" and "no loop").
|
|
7
|
+
* redrafts the unmet items, and re-evaluates — capped at `maxRounds`
|
|
8
|
+
* redraft rounds, then escalates to `agent::blocked` when criteria remain
|
|
9
|
+
* unmet.
|
|
10
|
+
*
|
|
11
|
+
* Story #5313 dropped the hard ceiling (`ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING`)
|
|
12
|
+
* and the floor-of-one clamp: `maxRounds` is any non-negative integer, and
|
|
13
|
+
* `0` means the verdict is scored **once** with no redraft round. The
|
|
14
|
+
* scoring pass itself is always on — there is intentionally no `enabled`
|
|
15
|
+
* flag (hard cutover per `rules/git-conventions.md`); only the redraft
|
|
16
|
+
* budget is tunable.
|
|
28
17
|
*/
|
|
29
18
|
|
|
30
19
|
/**
|
|
31
|
-
* Default redraft-round
|
|
20
|
+
* Default redraft-round budget applied when `.agentrc.json` omits
|
|
32
21
|
* `delivery.acceptanceEval.maxRounds`. Frozen so downstream callers cannot
|
|
33
22
|
* mutate the resolver's defaults across processes.
|
|
34
23
|
*
|
|
@@ -41,55 +30,34 @@ export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
|
|
|
41
30
|
});
|
|
42
31
|
|
|
43
32
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* to it.
|
|
48
|
-
*
|
|
49
|
-
* @type {number}
|
|
50
|
-
*/
|
|
51
|
-
export const ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING = 5;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Clamp a candidate round count into the inviolable `[1, ceiling]` range.
|
|
55
|
-
* Non-integer / non-finite inputs fall back to the documented default.
|
|
33
|
+
* Normalize a candidate round count: a non-negative integer is taken as-is
|
|
34
|
+
* (including `0`); anything else — negative, non-integer, non-finite,
|
|
35
|
+
* missing — falls back to the documented default.
|
|
56
36
|
*
|
|
57
37
|
* @param {unknown} value
|
|
58
38
|
* @param {number} fallback
|
|
59
39
|
* @returns {number}
|
|
60
40
|
*/
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (candidate > ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING) {
|
|
66
|
-
return ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING;
|
|
67
|
-
}
|
|
68
|
-
return candidate;
|
|
41
|
+
function normalizeRounds(value, fallback) {
|
|
42
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0
|
|
43
|
+
? value
|
|
44
|
+
: fallback;
|
|
69
45
|
}
|
|
70
46
|
|
|
71
47
|
/**
|
|
72
48
|
* Read the merged acceptance-eval block. Returns the canonical shape:
|
|
73
49
|
*
|
|
74
|
-
* {
|
|
75
|
-
* maxRounds: number, // clamped into [1, ceiling]
|
|
76
|
-
* ceiling: number // the undisableable hard cap on rounds
|
|
77
|
-
* }
|
|
78
|
-
*
|
|
79
|
-
* `maxRounds` is always a positive integer no greater than `ceiling`,
|
|
80
|
-
* regardless of what the resolved config carried.
|
|
50
|
+
* { maxRounds: number } // non-negative integer; 0 = scored once
|
|
81
51
|
*
|
|
82
52
|
* @param {object | null | undefined} config
|
|
83
|
-
* @returns {{ maxRounds: number
|
|
53
|
+
* @returns {{ maxRounds: number }}
|
|
84
54
|
*/
|
|
85
55
|
export function getAcceptanceEval(config) {
|
|
86
56
|
const user = config?.delivery?.acceptanceEval ?? {};
|
|
87
|
-
const maxRounds = clampRounds(
|
|
88
|
-
user.maxRounds,
|
|
89
|
-
ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
|
|
90
|
-
);
|
|
91
57
|
return {
|
|
92
|
-
maxRounds
|
|
93
|
-
|
|
58
|
+
maxRounds: normalizeRounds(
|
|
59
|
+
user.maxRounds,
|
|
60
|
+
ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
|
|
61
|
+
),
|
|
94
62
|
};
|
|
95
63
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `delivery.routing` accessor + framework defaults — Epic #4478 (M7-B), the
|
|
3
|
-
* role-scoped-boot-context flip and the
|
|
3
|
+
* role-scoped-boot-context flip and the ceremony profile.
|
|
4
4
|
*
|
|
5
5
|
* Stage 6 dropped `delivery.routing.singleDelivery` (the v1 epic
|
|
6
|
-
* single-vs-fan-out kill-switch).
|
|
7
|
-
*
|
|
6
|
+
* single-vs-fan-out kill-switch). Story #5313 dropped
|
|
7
|
+
* `delivery.routing.freshCriticSampleRate` (the maker-checker sampling
|
|
8
|
+
* floor): the standard profile now routes purely off the derived change
|
|
9
|
+
* level — high or underivable → fresh critic, low → inline self-eval. v2 has
|
|
10
|
+
* one Story delivery path; routing here is only about spawn boot context and
|
|
11
|
+
* the ceremony profile.
|
|
8
12
|
*
|
|
9
13
|
* `delivery.routing.roleScopedAgents` is the **kill-switch for the role-scoped
|
|
10
14
|
* boot contexts** (Epic #4478, M7-B). It defaults to `true`: a converted spawn
|
|
@@ -17,22 +21,11 @@
|
|
|
17
21
|
* `.claude/agents/`. Flipping it off never drops a gate: the fallback is the
|
|
18
22
|
* full-closure agent that ran before M7-B.
|
|
19
23
|
*
|
|
20
|
-
* `delivery.routing.freshCriticSampleRate` is the **maker-checker sampling
|
|
21
|
-
* floor** (Epic #4478, M7-B, Part 2). Ceremony routing sends the acceptance
|
|
22
|
-
* clusters of a change set that touches no sensitive path down the
|
|
23
|
-
* contract-identical *inline* critic path, but a fraction of them are still
|
|
24
|
-
* forced through a *fresh-context* critic so a low derived level never degrades
|
|
25
|
-
* to zero independent checking. The rate is clamped into `[0, 1]`; `0` disables
|
|
26
|
-
* the floor (pure level routing), `1` forces every cluster fresh. The default is
|
|
27
|
-
* `0.2`. See `resolveCeremonyForRisk` in
|
|
28
|
-
* `lib/orchestration/ceremony-routing.js`.
|
|
29
|
-
*
|
|
30
24
|
* Framework-defaults pattern mirrors `lib/config/ci.js#getCiDelivery`.
|
|
31
25
|
*/
|
|
32
26
|
|
|
33
27
|
export const DELIVERY_ROUTING_DEFAULTS = Object.freeze({
|
|
34
28
|
roleScopedAgents: true,
|
|
35
|
-
freshCriticSampleRate: 0.2,
|
|
36
29
|
/** @type {'minimal'|'standard'|'strict'} */
|
|
37
30
|
ceremonyProfile: 'standard',
|
|
38
31
|
/**
|
|
@@ -43,23 +36,6 @@ export const DELIVERY_ROUTING_DEFAULTS = Object.freeze({
|
|
|
43
36
|
closeAndLand: true,
|
|
44
37
|
});
|
|
45
38
|
|
|
46
|
-
/**
|
|
47
|
-
* Clamp a candidate sample rate into `[0, 1]`. Non-finite / non-number inputs
|
|
48
|
-
* fall back to the framework default so a degraded config never yields a
|
|
49
|
-
* NaN-driven or out-of-range floor.
|
|
50
|
-
*
|
|
51
|
-
* @param {unknown} value
|
|
52
|
-
* @returns {number}
|
|
53
|
-
*/
|
|
54
|
-
function clampSampleRate(value) {
|
|
55
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
56
|
-
return DELIVERY_ROUTING_DEFAULTS.freshCriticSampleRate;
|
|
57
|
-
}
|
|
58
|
-
if (value < 0) return 0;
|
|
59
|
-
if (value > 1) return 1;
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
39
|
/**
|
|
64
40
|
* Normalize ceremony profile; unknown values → `standard`.
|
|
65
41
|
*
|
|
@@ -82,7 +58,6 @@ function normalizeCeremonyProfile(value) {
|
|
|
82
58
|
* @param {object | null | undefined} config
|
|
83
59
|
* @returns {{
|
|
84
60
|
* roleScopedAgents: boolean,
|
|
85
|
-
* freshCriticSampleRate: number,
|
|
86
61
|
* ceremonyProfile: 'minimal'|'standard'|'strict',
|
|
87
62
|
* closeAndLand: boolean,
|
|
88
63
|
* }}
|
|
@@ -94,7 +69,6 @@ export function getDeliveryRouting(config) {
|
|
|
94
69
|
typeof routing.roleScopedAgents === 'boolean'
|
|
95
70
|
? routing.roleScopedAgents
|
|
96
71
|
: DELIVERY_ROUTING_DEFAULTS.roleScopedAgents,
|
|
97
|
-
freshCriticSampleRate: clampSampleRate(routing.freshCriticSampleRate),
|
|
98
72
|
ceremonyProfile: normalizeCeremonyProfile(routing.ceremonyProfile),
|
|
99
73
|
closeAndLand:
|
|
100
74
|
typeof routing.closeAndLand === 'boolean'
|
|
@@ -112,20 +112,6 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
112
112
|
'Allowlist of events that fire a webhook notification.',
|
|
113
113
|
|
|
114
114
|
// planning.*
|
|
115
|
-
'planning.riskHeuristics':
|
|
116
|
-
'Phrases that flag a Story as high-risk for HITL escalation.',
|
|
117
|
-
'planning.failOnSharedEditors':
|
|
118
|
-
'Whether shared-editor conflict findings are promoted to hard errors.',
|
|
119
|
-
'planning.requireExplicitCrossStoryDeps':
|
|
120
|
-
'Whether implicit cross-Story dependencies are promoted to hard errors.',
|
|
121
|
-
'planning.failOnRegistryConflicts':
|
|
122
|
-
'Whether cross-cutting registry conflict findings are promoted to hard errors.',
|
|
123
|
-
'planning.failOnLargeFanOut':
|
|
124
|
-
'Whether large fan-out findings are promoted to hard errors.',
|
|
125
|
-
'planning.largeFanOutThreshold':
|
|
126
|
-
'Story count above which a plan is flagged as a large fan-out.',
|
|
127
|
-
'planning.crossCuttingRegistries':
|
|
128
|
-
'Glob patterns naming cross-cutting registry files the planner conflict-checks.',
|
|
129
115
|
'planning.navigation.routeGlobs':
|
|
130
116
|
'Glob patterns marking paths that add a user-facing route (plan-time reachability gate).',
|
|
131
117
|
'planning.navigation.navRegistry':
|
|
@@ -168,14 +154,10 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
168
154
|
'Ordered provider chain the code-review phase consults.',
|
|
169
155
|
'delivery.codeReview.maxFixAttempts':
|
|
170
156
|
'Maximum auto-fix attempts the code-review phase makes.',
|
|
171
|
-
'delivery.codeReview.maxFixScopeFiles':
|
|
172
|
-
'Maximum files an auto-fix may touch in one attempt.',
|
|
173
157
|
'delivery.codeReview.autoFixSeverity':
|
|
174
158
|
'Severity threshold for on-branch code-review remediation (medium fixes 🔴/🟠/🟡, high fixes 🔴/🟠 only; default medium).',
|
|
175
159
|
'delivery.routing.roleScopedAgents':
|
|
176
160
|
'Whether delivery spawns boot on role-scoped .claude/agents/<role>.md contexts.',
|
|
177
|
-
'delivery.routing.freshCriticSampleRate':
|
|
178
|
-
'Fraction of low-risk acceptance clusters forced through a fresh-context critic (maker-checker floor).',
|
|
179
161
|
'delivery.routing.ceremonyProfile':
|
|
180
162
|
'Acceptance-ceremony depth: minimal (always inline), standard (derived-level routed), or strict (always fresh).',
|
|
181
163
|
'delivery.routing.closeAndLand':
|
|
@@ -231,7 +213,6 @@ const PREFIX_MEANINGS = Object.freeze([
|
|
|
231
213
|
'Tolerance epsilon applied when comparing a quality baseline.',
|
|
232
214
|
],
|
|
233
215
|
['delivery.quality', 'Delivery-time quality configuration.'],
|
|
234
|
-
['delivery.signals', 'Threshold for a delivery friction/telemetry signal.'],
|
|
235
216
|
['delivery.mergeWatch', 'Merge-wait poll cadence and wall-clock budget.'],
|
|
236
217
|
[
|
|
237
218
|
'delivery.feedbackLoop',
|