mandrel 2.56.0 → 2.58.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 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -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/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- 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/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- 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 +16 -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/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -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/code-review.js +7 -3
- 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/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -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/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- 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 +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- 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 +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -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 +277 -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/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/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 +46 -55
- 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 +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- 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
|
@@ -24,198 +24,31 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Output: human-readable text by default; pass `--json` for the raw
|
|
26
26
|
* report envelope.
|
|
27
|
+
*
|
|
28
|
+
* **Shell only (Story #5316).** Nothing imports this file, so nothing here is
|
|
29
|
+
* reachable from a test — which is why every function it used to hold scored
|
|
30
|
+
* the CRAP formula's untested maximum. Argv parsing, report rendering,
|
|
31
|
+
* progress logging and the run orchestration now live under
|
|
32
|
+
* `lib/test-isolate/`, beside the `list-files` / `parse-tap` / `runner`
|
|
33
|
+
* modules that were always there. Keep this file a shell: anything with a
|
|
34
|
+
* branch in it belongs next door, where a test can reach it.
|
|
27
35
|
*/
|
|
28
36
|
|
|
29
37
|
import path from 'node:path';
|
|
30
38
|
import { fileURLToPath } from 'node:url';
|
|
31
39
|
import { runAsCli } from './lib/cli-utils.js';
|
|
32
|
-
import {
|
|
33
|
-
import { diagnoseIsolation } from './lib/test-isolate/runner.js';
|
|
40
|
+
import { runTestIsolate } from './lib/test-isolate/run-isolate.js';
|
|
34
41
|
|
|
35
42
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
43
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
37
44
|
|
|
38
|
-
/**
|
|
39
|
-
* @param {string[]} argv
|
|
40
|
-
*/
|
|
41
|
-
export function parseIsolateArgv(argv) {
|
|
42
|
-
const options = {
|
|
43
|
-
pattern: undefined,
|
|
44
|
-
workers: undefined,
|
|
45
|
-
maxBisectDepth: 8,
|
|
46
|
-
maxBisectTargets: 5,
|
|
47
|
-
suiteConcurrency: 8,
|
|
48
|
-
json: false,
|
|
49
|
-
quiet: false,
|
|
50
|
-
};
|
|
51
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
52
|
-
const arg = argv[i];
|
|
53
|
-
if (arg === '--workers' && argv[i + 1]) {
|
|
54
|
-
options.workers = Number(argv[++i]);
|
|
55
|
-
} else if (arg === '--max-bisect-depth' && argv[i + 1]) {
|
|
56
|
-
options.maxBisectDepth = Number(argv[++i]);
|
|
57
|
-
} else if (arg === '--max-bisect-targets' && argv[i + 1]) {
|
|
58
|
-
options.maxBisectTargets = Number(argv[++i]);
|
|
59
|
-
} else if (arg === '--suite-concurrency' && argv[i + 1]) {
|
|
60
|
-
options.suiteConcurrency = Number(argv[++i]);
|
|
61
|
-
} else if (arg === '--json') {
|
|
62
|
-
options.json = true;
|
|
63
|
-
} else if (arg === '--quiet') {
|
|
64
|
-
options.quiet = true;
|
|
65
|
-
} else if (!arg.startsWith('--') && !options.pattern) {
|
|
66
|
-
options.pattern = arg;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return options;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {import('./lib/test-isolate/runner.js').IsolateReport} report
|
|
74
|
-
*/
|
|
75
|
-
export function renderReport(report) {
|
|
76
|
-
const lines = [];
|
|
77
|
-
lines.push('');
|
|
78
|
-
lines.push('=== test-isolate diagnostic report ===');
|
|
79
|
-
lines.push(`Files scanned: ${report.files.length}`);
|
|
80
|
-
lines.push(`Wall duration: ${(report.durationMs / 1000).toFixed(1)}s`);
|
|
81
|
-
lines.push('');
|
|
82
|
-
|
|
83
|
-
if (report.flippers.length === 0) {
|
|
84
|
-
lines.push('✓ No flippers detected — every file that passed alone');
|
|
85
|
-
lines.push(' also passed in the full suite run.');
|
|
86
|
-
} else {
|
|
87
|
-
lines.push(`✗ ${report.flippers.length} flipper(s) detected:`);
|
|
88
|
-
for (const f of report.flippers) {
|
|
89
|
-
lines.push(` - ${f}`);
|
|
90
|
-
}
|
|
91
|
-
lines.push('');
|
|
92
|
-
if (report.bisections.length > 0) {
|
|
93
|
-
lines.push('Likely polluters (bisection suspects):');
|
|
94
|
-
for (const b of report.bisections) {
|
|
95
|
-
const tag = b.inconclusive ? ' [inconclusive]' : '';
|
|
96
|
-
lines.push(` ${b.file}${tag}`);
|
|
97
|
-
for (const s of b.suspects) {
|
|
98
|
-
lines.push(` ← ${s}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
lines.push('');
|
|
105
|
-
if (report.envMutators.length === 0) {
|
|
106
|
-
lines.push('✓ No env-var leaks detected across isolated runs.');
|
|
107
|
-
} else {
|
|
108
|
-
lines.push(
|
|
109
|
-
`⚠ ${report.envMutators.length} file(s) left process.env mutated:`,
|
|
110
|
-
);
|
|
111
|
-
for (const m of report.envMutators) {
|
|
112
|
-
const parts = [];
|
|
113
|
-
if (m.envDiff.added.length > 0) {
|
|
114
|
-
parts.push(`added=[${m.envDiff.added.join(', ')}]`);
|
|
115
|
-
}
|
|
116
|
-
if (m.envDiff.removed.length > 0) {
|
|
117
|
-
parts.push(`removed=[${m.envDiff.removed.join(', ')}]`);
|
|
118
|
-
}
|
|
119
|
-
if (m.envDiff.changed.length > 0) {
|
|
120
|
-
parts.push(`changed=[${m.envDiff.changed.join(', ')}]`);
|
|
121
|
-
}
|
|
122
|
-
lines.push(` ${m.file}`);
|
|
123
|
-
lines.push(` ${parts.join(' ')}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
lines.push('');
|
|
128
|
-
return lines.join('\n');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Programmatic entry — wired up by the CLI and exported for tests.
|
|
133
|
-
*
|
|
134
|
-
* @param {object} [opts]
|
|
135
|
-
* @param {string[]} [opts.argv]
|
|
136
|
-
* @param {string} [opts.repoRoot]
|
|
137
|
-
* @param {(line: string) => void} [opts.onLog]
|
|
138
|
-
* @returns {Promise<{ exitCode: number, report: import('./lib/test-isolate/runner.js').IsolateReport }>}
|
|
139
|
-
*/
|
|
140
|
-
export async function runTestIsolate({
|
|
141
|
-
argv = process.argv.slice(2),
|
|
142
|
-
repoRoot = ROOT,
|
|
143
|
-
onLog = (s) => process.stdout.write(`${s}\n`),
|
|
144
|
-
} = {}) {
|
|
145
|
-
const options = parseIsolateArgv(argv);
|
|
146
|
-
const files = resolveTestFiles({
|
|
147
|
-
pattern: options.pattern,
|
|
148
|
-
repoRoot,
|
|
149
|
-
});
|
|
150
|
-
if (files.length === 0) {
|
|
151
|
-
onLog(
|
|
152
|
-
`[test-isolate] no test files matched pattern: ${options.pattern ?? '<default>'}`,
|
|
153
|
-
);
|
|
154
|
-
return { exitCode: 0, report: emptyReport() };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!options.quiet) {
|
|
158
|
-
onLog(`[test-isolate] scanning ${files.length} file(s)...`);
|
|
159
|
-
}
|
|
160
|
-
const report = await diagnoseIsolation({
|
|
161
|
-
repoRoot,
|
|
162
|
-
files,
|
|
163
|
-
workers: options.workers,
|
|
164
|
-
suiteConcurrency: options.suiteConcurrency,
|
|
165
|
-
maxBisectDepth: options.maxBisectDepth,
|
|
166
|
-
maxBisectTargets: options.maxBisectTargets,
|
|
167
|
-
onProgress: options.quiet
|
|
168
|
-
? undefined
|
|
169
|
-
: (stage, payload) => {
|
|
170
|
-
if (stage === 'isolated:start') {
|
|
171
|
-
onLog(`[test-isolate] isolated phase: ${payload.count} file(s)`);
|
|
172
|
-
} else if (stage === 'isolated:done') {
|
|
173
|
-
onLog('[test-isolate] isolated phase: done');
|
|
174
|
-
} else if (stage === 'suite:start') {
|
|
175
|
-
onLog(`[test-isolate] suite phase: ${payload.count} file(s)`);
|
|
176
|
-
} else if (stage === 'suite:done') {
|
|
177
|
-
onLog('[test-isolate] suite phase: done');
|
|
178
|
-
} else if (stage === 'bisect:start') {
|
|
179
|
-
onLog(`[test-isolate] bisecting flipper: ${payload.target}`);
|
|
180
|
-
} else if (stage === 'bisect:done') {
|
|
181
|
-
const list = payload.suspects.slice(0, 3).join(', ');
|
|
182
|
-
const more =
|
|
183
|
-
payload.suspects.length > 3
|
|
184
|
-
? ` (+${payload.suspects.length - 3} more)`
|
|
185
|
-
: '';
|
|
186
|
-
onLog(`[test-isolate] suspects: ${list}${more}`);
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
if (options.json) {
|
|
192
|
-
onLog(JSON.stringify(report, null, 2));
|
|
193
|
-
} else {
|
|
194
|
-
onLog(renderReport(report));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const exitCode =
|
|
198
|
-
report.flippers.length === 0 && report.envMutators.length === 0 ? 0 : 1;
|
|
199
|
-
return { exitCode, report };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function emptyReport() {
|
|
203
|
-
return {
|
|
204
|
-
pattern: null,
|
|
205
|
-
files: [],
|
|
206
|
-
isolated: [],
|
|
207
|
-
suite: [],
|
|
208
|
-
flippers: [],
|
|
209
|
-
bisections: [],
|
|
210
|
-
envMutators: [],
|
|
211
|
-
durationMs: 0,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
45
|
runAsCli(
|
|
216
46
|
import.meta.url,
|
|
217
47
|
async () => {
|
|
218
|
-
const { exitCode } = await runTestIsolate(
|
|
48
|
+
const { exitCode } = await runTestIsolate({
|
|
49
|
+
argv: process.argv.slice(2),
|
|
50
|
+
repoRoot: ROOT,
|
|
51
|
+
});
|
|
219
52
|
if (exitCode !== 0) process.exit(exitCode);
|
|
220
53
|
},
|
|
221
54
|
{ source: 'test-isolate' },
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
buildCoverageUpdaterScorer,
|
|
24
|
+
resolveCoverageUpdaterScope,
|
|
25
|
+
} from './lib/baselines/coverage-updater-cli.js';
|
|
23
26
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
24
27
|
import { runAsCli } from './lib/cli-utils.js';
|
|
25
28
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
@@ -57,90 +60,42 @@ const USAGE = {
|
|
|
57
60
|
],
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
/** CommonJS `require`, for the `.c8rc.cjs` scope config below. */
|
|
60
64
|
const require = createRequire(import.meta.url);
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Load the c8 include/exclude scope. Kept in the CLI rather than the extracted
|
|
68
|
+
* scorer because it is the one genuinely environment-bound step — a CJS
|
|
69
|
+
* `require` of a config file resolved against the working tree — and the
|
|
70
|
+
* scorer takes it as a seam so a test never touches the real `.c8rc.cjs`.
|
|
71
|
+
*/
|
|
62
72
|
function loadC8Scope(cwd) {
|
|
63
73
|
return require(path.resolve(cwd, '.c8rc.cjs'));
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
function parseFullScopeFlag(argv = []) {
|
|
67
|
-
return argv.includes('--full-scope');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
76
|
function main() {
|
|
71
|
-
const argv = process.argv.slice(2);
|
|
72
|
-
const diffScopeRef = parseDiffScopeFlag(argv);
|
|
73
|
-
const fullScope = parseFullScopeFlag(argv);
|
|
74
77
|
const cwd = process.cwd();
|
|
78
|
+
const { fullScope, diffScopeRef } = resolveCoverageUpdaterScope(
|
|
79
|
+
process.argv.slice(2),
|
|
80
|
+
);
|
|
75
81
|
Logger.info('[Coverage] Updating baseline from coverage-final.json...');
|
|
76
82
|
|
|
77
|
-
if (fullScope && diffScopeRef !== null) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
'[Coverage] --full-scope is incompatible with --diff-scope; pick one',
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Build the per-kind scorer the service will invoke. The scorer receives
|
|
84
|
-
// `(files, { fullScope })` and returns rows in the `{path, lines,
|
|
85
|
-
// branches, functions}` shape the writer expects.
|
|
86
|
-
const scorer = (files, opts) => {
|
|
87
|
-
const effectiveCwd = opts?.cwd ?? cwd;
|
|
88
|
-
let raw;
|
|
89
|
-
try {
|
|
90
|
-
raw = readCoverageFinal(effectiveCwd);
|
|
91
|
-
} catch (err) {
|
|
92
|
-
Logger.error(`[Coverage] ❌ ${err.message}`);
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const c8Config = loadC8Scope(effectiveCwd);
|
|
97
|
-
const c8Scope = buildScopePredicate({
|
|
98
|
-
include: c8Config.include ?? [],
|
|
99
|
-
exclude: c8Config.exclude ?? [],
|
|
100
|
-
});
|
|
101
|
-
const scores = scoreCoverageFinal({
|
|
102
|
-
raw,
|
|
103
|
-
cwd: effectiveCwd,
|
|
104
|
-
scope: c8Scope,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// In diff mode, further narrow to the service-resolved in-scope file list.
|
|
108
|
-
const inScope =
|
|
109
|
-
!opts?.fullScope && Array.isArray(files) && files.length > 0
|
|
110
|
-
? new Set(files)
|
|
111
|
-
: null;
|
|
112
|
-
|
|
113
|
-
const rows = Object.entries(scores)
|
|
114
|
-
.filter(([relPath]) => inScope === null || inScope.has(relPath))
|
|
115
|
-
.map(([relPath, score]) => ({
|
|
116
|
-
path: relPath,
|
|
117
|
-
lines: score?.lines ?? 0,
|
|
118
|
-
branches: score?.branches ?? 0,
|
|
119
|
-
functions: score?.functions ?? 0,
|
|
120
|
-
}));
|
|
121
|
-
|
|
122
|
-
const fileCount = Object.keys(scores).length;
|
|
123
|
-
Logger.info(
|
|
124
|
-
`[Coverage] Scored ${fileCount} file(s)${inScope ? ` (${rows.length} in scope)` : ''}.`,
|
|
125
|
-
);
|
|
126
|
-
return rows;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
83
|
const absBaselinePath = path.resolve(cwd, COVERAGE_BASELINE_PATH);
|
|
130
|
-
const epsilon = getBaselineEpsilon('coverage', null);
|
|
131
84
|
const refreshOpts = {
|
|
132
85
|
kind: 'coverage',
|
|
133
86
|
writePath: absBaselinePath,
|
|
134
|
-
epsilon,
|
|
135
|
-
scorer,
|
|
87
|
+
epsilon: getBaselineEpsilon('coverage', null),
|
|
88
|
+
scorer: buildCoverageUpdaterScorer(cwd, {
|
|
89
|
+
readCoverage: readCoverageFinal,
|
|
90
|
+
loadScope: loadC8Scope,
|
|
91
|
+
buildScope: buildScopePredicate,
|
|
92
|
+
score: scoreCoverageFinal,
|
|
93
|
+
}),
|
|
136
94
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
// No flag → scopeFiles=null + fullScope=false → service derives the diff
|
|
143
|
-
// via `origin/main..HEAD` (its default baseRef/headRef).
|
|
95
|
+
// No flag -> scopeFiles=null + fullScope=false -> the service derives the
|
|
96
|
+
// diff via `origin/main..HEAD` (its default baseRef/headRef).
|
|
97
|
+
if (fullScope) refreshOpts.fullScope = true;
|
|
98
|
+
else if (diffScopeRef) refreshOpts.baseRef = diffScopeRef;
|
|
144
99
|
|
|
145
100
|
return refreshBaseline(refreshOpts).then((result) => {
|
|
146
101
|
Logger.info(
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// first import so the check runs before any third-party-importing sibling
|
|
3
3
|
// module is evaluated (Story #3432).
|
|
4
4
|
import './lib/runtime-deps/ensure-installed.js';
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
buildCrapUpdaterScorer,
|
|
7
|
+
parseCrapUpdaterArgs,
|
|
8
|
+
resolveCrapUpdaterOptions,
|
|
9
|
+
} from './lib/baselines/crap-updater-cli.js';
|
|
7
10
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
8
11
|
import { runAsCli } from './lib/cli-utils.js';
|
|
9
12
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
@@ -14,10 +17,8 @@ import {
|
|
|
14
17
|
} from './lib/config-resolver.js';
|
|
15
18
|
import { loadCoverage } from './lib/coverage-utils.js';
|
|
16
19
|
import {
|
|
17
|
-
checkResolutionFloor,
|
|
18
20
|
resolveEscomplexVersion,
|
|
19
21
|
resolveTsTranspilerVersion,
|
|
20
|
-
scanAndScore,
|
|
21
22
|
} from './lib/crap-utils.js';
|
|
22
23
|
|
|
23
24
|
import { Logger } from './lib/Logger.js';
|
|
@@ -79,139 +80,34 @@ const USAGE = {
|
|
|
79
80
|
],
|
|
80
81
|
};
|
|
81
82
|
|
|
82
|
-
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
83
|
-
const out = {
|
|
84
|
-
baselinePath: undefined,
|
|
85
|
-
coveragePath: undefined,
|
|
86
|
-
fullScope: false,
|
|
87
|
-
diffScopeRef: null,
|
|
88
|
-
};
|
|
89
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
90
|
-
if (argv[i] === '--baseline' && argv[i + 1]) {
|
|
91
|
-
out.baselinePath = argv[i + 1];
|
|
92
|
-
i += 1;
|
|
93
|
-
} else if (argv[i] === '--coverage' && argv[i + 1]) {
|
|
94
|
-
out.coveragePath = argv[i + 1];
|
|
95
|
-
i += 1;
|
|
96
|
-
} else if (argv[i] === '--full-scope') {
|
|
97
|
-
out.fullScope = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
out.diffScopeRef = parseDiffScopeFlag(argv);
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
83
|
async function main() {
|
|
105
|
-
const args = parseCliArgs();
|
|
106
84
|
const config = resolveConfig();
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const coveragePath =
|
|
112
|
-
args.coveragePath ?? crap.coveragePath ?? 'coverage/coverage-final.json';
|
|
113
|
-
const baselinePath = args.baselinePath ?? getBaselines(config).crap.path;
|
|
114
|
-
const ignoreGlobs = Array.isArray(crap.ignoreGlobs) ? crap.ignoreGlobs : [];
|
|
85
|
+
const options = resolveCrapUpdaterOptions(
|
|
86
|
+
parseCrapUpdaterArgs(process.argv.slice(2)),
|
|
87
|
+
{ crap: getQuality(config).crap, baselines: getBaselines(config) },
|
|
88
|
+
);
|
|
115
89
|
|
|
116
90
|
Logger.info('[CRAP] Updating baseline...');
|
|
117
|
-
Logger.info(`[CRAP] Target dirs: ${targetDirs.join(', ')}`);
|
|
91
|
+
Logger.info(`[CRAP] Target dirs: ${options.targetDirs.join(', ')}`);
|
|
118
92
|
Logger.info(
|
|
119
|
-
`[CRAP] Coverage source: ${coveragePath}${requireCoverage ? ' (required)' : ' (optional)'}`,
|
|
93
|
+
`[CRAP] Coverage source: ${options.coveragePath}${options.requireCoverage ? ' (required)' : ' (optional)'}`,
|
|
120
94
|
);
|
|
121
95
|
|
|
122
|
-
const absBaselinePath = path.isAbsolute(baselinePath)
|
|
123
|
-
? baselinePath
|
|
124
|
-
: path.resolve(process.cwd(), baselinePath);
|
|
125
|
-
|
|
126
|
-
if (args.fullScope && args.diffScopeRef !== null) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
'[CRAP] --full-scope is incompatible with --diff-scope; pick one',
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Build the per-kind scorer the service will invoke. The scorer receives
|
|
133
|
-
// `(files, { fullScope, cwd })`:
|
|
134
|
-
// - fullScope === true: walk all target dirs; scopeFiles=null passed to scanAndScore.
|
|
135
|
-
// - fullScope === false: score only the diff-derived in-scope files.
|
|
136
|
-
const scorer = async (files, opts) => {
|
|
137
|
-
const effectiveCwd = opts?.cwd ?? process.cwd();
|
|
138
|
-
const coverageAbs = path.isAbsolute(coveragePath)
|
|
139
|
-
? coveragePath
|
|
140
|
-
: path.resolve(effectiveCwd, coveragePath);
|
|
141
|
-
const coverage = loadCoverage(coverageAbs);
|
|
142
|
-
if (!coverage && requireCoverage) {
|
|
143
|
-
Logger.warn(
|
|
144
|
-
`[CRAP] ⚠ No coverage artifact at ${coveragePath}. All files will be skipped under requireCoverage=true.`,
|
|
145
|
-
);
|
|
146
|
-
Logger.warn(
|
|
147
|
-
"[CRAP] ⚠ Run 'npm run test:coverage' before 'npm run crap:update'.",
|
|
148
|
-
);
|
|
149
|
-
return [];
|
|
150
|
-
}
|
|
151
|
-
const scopeFiles = opts?.fullScope ? null : (files ?? null);
|
|
152
|
-
const {
|
|
153
|
-
rows,
|
|
154
|
-
scannedFiles,
|
|
155
|
-
skippedFilesNoCoverage,
|
|
156
|
-
skippedMethodsNoCoverage,
|
|
157
|
-
resolution,
|
|
158
|
-
} = await scanAndScore({
|
|
159
|
-
targetDirs,
|
|
160
|
-
coverage,
|
|
161
|
-
requireCoverage,
|
|
162
|
-
cwd: effectiveCwd,
|
|
163
|
-
ignoreGlobs,
|
|
164
|
-
scopeFiles,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
Logger.info(`[CRAP] Scanned ${scannedFiles} file(s).`);
|
|
168
|
-
if (skippedFilesNoCoverage > 0) {
|
|
169
|
-
Logger.info(
|
|
170
|
-
`[CRAP] Skipped ${skippedFilesNoCoverage} file(s) without coverage entries.`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
if (skippedMethodsNoCoverage > 0) {
|
|
174
|
-
Logger.info(
|
|
175
|
-
`[CRAP] Skipped ${skippedMethodsNoCoverage} method(s) whose per-method coverage was unresolved.`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (resolution) {
|
|
179
|
-
Logger.info(
|
|
180
|
-
`[CRAP] Method resolution: ${resolution.resolvedMethods}/${resolution.joinableMethods} ` +
|
|
181
|
-
`(${(resolution.rate * 100).toFixed(1)}%) in files with coverage.`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
// Fail closed BEFORE the service persists anything — a thin baseline is
|
|
185
|
-
// never written and then apologised for.
|
|
186
|
-
const refusal = checkResolutionFloor(resolution, minMethodResolutionRate);
|
|
187
|
-
if (refusal) throw new Error(refusal);
|
|
188
|
-
|
|
189
|
-
return (rows ?? []).filter(
|
|
190
|
-
(r) => typeof r?.crap === 'number' && Number.isFinite(r.crap),
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const epsilon = getBaselineEpsilon('crap', config);
|
|
195
96
|
const refreshOpts = {
|
|
196
97
|
kind: 'crap',
|
|
197
|
-
writePath: absBaselinePath,
|
|
198
|
-
epsilon,
|
|
199
|
-
scorer,
|
|
98
|
+
writePath: options.absBaselinePath,
|
|
99
|
+
epsilon: getBaselineEpsilon('crap', config),
|
|
100
|
+
scorer: buildCrapUpdaterScorer(options, { loadCoverage }),
|
|
200
101
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
// No flag → scopeFiles=null + fullScope=false → service derives the diff
|
|
207
|
-
// via `origin/main..HEAD` (its default baseRef/headRef).
|
|
102
|
+
// No flag -> scopeFiles=null + fullScope=false -> the service derives the
|
|
103
|
+
// diff via `origin/main..HEAD` (its default baseRef/headRef).
|
|
104
|
+
if (options.fullScope) refreshOpts.fullScope = true;
|
|
105
|
+
else if (options.diffScopeRef) refreshOpts.baseRef = options.diffScopeRef;
|
|
208
106
|
|
|
209
107
|
const result = await refreshBaseline(refreshOpts);
|
|
210
108
|
|
|
211
|
-
const escomplexVersion = resolveEscomplexVersion();
|
|
212
|
-
const tsTranspilerVersion = resolveTsTranspilerVersion();
|
|
213
109
|
Logger.info(
|
|
214
|
-
`[CRAP] ✅ Baseline updated (kernelVersion=${result.envelope.kernelVersion}, escomplexVersion=${
|
|
110
|
+
`[CRAP] ✅ Baseline updated (kernelVersion=${result.envelope.kernelVersion}, escomplexVersion=${resolveEscomplexVersion()}, tsTranspilerVersion=${resolveTsTranspilerVersion()}). Wrote to ${options.absBaselinePath}.`,
|
|
215
111
|
);
|
|
216
112
|
Logger.info(
|
|
217
113
|
`[CRAP] Wrote ${result.envelope.rows.length} row(s). scope=${result.scope.mode}, wrote=${result.wrote}.`,
|
|
@@ -15,10 +15,10 @@ description:
|
|
|
15
15
|
is a single path that emits **one Story by default**.
|
|
16
16
|
- This skill is an optional **split advisory**: should the author keep one
|
|
17
17
|
Story, or does the seed clear the default-single split policy?
|
|
18
|
-
- Anchor sizing judgment to `
|
|
19
|
-
`DELIVERABLE_GRANULARITY_GUIDANCE` in
|
|
18
|
+
- Anchor sizing judgment to `DELIVERABLE_GRANULARITY_GUIDANCE` in
|
|
20
19
|
[`ticket-validator-sizing.js`](../../../scripts/lib/orchestration/ticket-validator-sizing.js).
|
|
21
|
-
|
|
20
|
+
There is no numeric threshold to restate — Story #5312 deleted the
|
|
21
|
+
plan-time capacity ceilings.
|
|
22
22
|
- Lead with **cohesion**: one Story is one coherent change with one reason
|
|
23
23
|
to exist. Coupled work stays one Story and uses `## Slicing` for
|
|
24
24
|
intra-session checkpoints.
|
|
@@ -46,7 +46,8 @@ run the tools first.
|
|
|
46
46
|
`check-baselines.js` gate enforces. Treat any per-file MI drop beyond
|
|
47
47
|
`delivery.quality.gates.maintainability.tolerance` (default 0.5pt) as a
|
|
48
48
|
grounded must-fix finding, and any cyclomatic reading over the
|
|
49
|
-
`codingGuardrails`
|
|
49
|
+
`codingGuardrails.cyclomaticFlag` (> 8) or the fixed ratchet ceiling
|
|
50
|
+
(≥ 12, reported by `quality:preview` as an advisory) as measured, not
|
|
50
51
|
guessed.
|
|
51
52
|
|
|
52
53
|
2. **Committed baselines (codebase-wide mode).** Read the committed metric
|
|
@@ -98,8 +99,8 @@ Analyze the repository with a focus on:
|
|
|
98
99
|
from
|
|
99
100
|
[`helpers/code-quality-guardrails.md`](helpers/code-quality-guardrails.md):
|
|
100
101
|
cyclomatic complexity > 8 (`delivery.quality.codingGuardrails.cyclomaticFlag`)
|
|
101
|
-
is **flag in review** (annotate or split);
|
|
102
|
-
|
|
102
|
+
is **flag in review** (annotate or split); ≥ 12 is the fixed ratchet
|
|
103
|
+
ceiling `check-cyclomatic.js` enforces on new breaches.
|
|
103
104
|
A per-file MI drop beyond the configured
|
|
104
105
|
`delivery.quality.gates.maintainability.tolerance` (default 0.5pt) requires
|
|
105
106
|
a refactor in the same Story rather than a baseline bump.
|