mandrel 2.56.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.
Files changed (106) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  16. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  17. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  18. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  19. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  20. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  21. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  22. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  23. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  24. package/.agents/scripts/lib/config/explain.js +0 -19
  25. package/.agents/scripts/lib/config/limits.js +18 -78
  26. package/.agents/scripts/lib/config/quality.js +6 -3
  27. package/.agents/scripts/lib/config/runners.js +3 -2
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  29. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  30. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  31. package/.agents/scripts/lib/crap-engine.js +35 -4
  32. package/.agents/scripts/lib/crap-utils.js +17 -1
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  34. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  35. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  36. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  37. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  39. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  41. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  42. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  43. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
  49. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  51. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  52. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  53. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  56. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  57. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  58. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  59. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  60. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  61. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  62. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  63. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  64. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  65. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  66. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  67. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  68. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  69. package/.agents/scripts/lib/test-run-credit.js +266 -0
  70. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  71. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  72. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  73. package/.agents/scripts/plan-context.js +7 -9
  74. package/.agents/scripts/plan-critics.js +28 -54
  75. package/.agents/scripts/plan-persist.js +25 -68
  76. package/.agents/scripts/quality-preview.js +51 -0
  77. package/.agents/scripts/run-tests.js +12 -0
  78. package/.agents/scripts/stories-wave-tick.js +23 -45
  79. package/.agents/scripts/test-isolate.js +13 -180
  80. package/.agents/scripts/update-coverage-baseline.js +25 -70
  81. package/.agents/scripts/update-crap-baseline.js +19 -123
  82. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  83. package/.agents/workflows/audit-clean-code.md +4 -3
  84. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  85. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  86. package/.agents/workflows/helpers/code-review.md +2 -3
  87. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  88. package/.agents/workflows/helpers/deliver-light.md +40 -105
  89. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  90. package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
  91. package/.agents/workflows/helpers/deliver-story.md +9 -13
  92. package/.agents/workflows/helpers/plan-reference.md +132 -219
  93. package/.agents/workflows/mandrel-plan.md +27 -40
  94. package/.agents/workflows/memory-consolidate.md +9 -13
  95. package/docs/CHANGELOG.md +23 -0
  96. package/lib/migrations/index.js +4 -0
  97. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  98. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  99. package/package.json +1 -1
  100. package/.agents/scripts/lib/framework-version.js +0 -39
  101. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  102. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  103. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  104. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  105. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  106. 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 { resolveTestFiles } from './lib/test-isolate/list-files.js';
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 { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
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
- if (fullScope) {
138
- refreshOpts.fullScope = true;
139
- } else if (diffScopeRef) {
140
- refreshOpts.baseRef = diffScopeRef;
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 path from 'node:path';
6
- import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
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 crap = getQuality(config).crap;
108
- const targetDirs = Array.isArray(crap.targetDirs) ? crap.targetDirs : [];
109
- const requireCoverage = crap.requireCoverage !== false;
110
- const minMethodResolutionRate = crap.minMethodResolutionRate ?? 0.75;
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
- if (args.fullScope) {
202
- refreshOpts.fullScope = true;
203
- } else if (args.diffScopeRef) {
204
- refreshOpts.baseRef = args.diffScopeRef;
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=${escomplexVersion}, tsTranspilerVersion=${tsTranspilerVersion}). Wrote to ${absBaselinePath}.`,
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 `DEFAULT_MODEL_CAPACITY` and
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
- Do not restate numeric thresholds.
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` ceilings (flag > 8, must-fix > 12) as measured, not
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); > 12
102
- (`codingGuardrails.cyclomaticMustFix`) is **must-fix** before the work merges.
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.