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
|
@@ -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.
|
|
@@ -181,6 +181,14 @@ persists, via `carryProvenanceFooters`
|
|
|
181
181
|
The carry is additive, union-preserving and idempotent, so a resumed persist
|
|
182
182
|
cannot stack footers and a hand-authored fingerprint is never dropped.
|
|
183
183
|
|
|
184
|
+
**Persist records the ledger and the labels too.** It stamps the seed's
|
|
185
|
+
`audit::<dimension>` labels — the dedup corpus is listed by them, and an indexed
|
|
186
|
+
run answers lookups from that pool without reaching the provider, so a footer
|
|
187
|
+
alone leaves a plan-path Story invisible — and records each Story's
|
|
188
|
+
**attributed** identities. Union-only identities are not recorded: every sibling
|
|
189
|
+
carries every fingerprint, so an owner would be a coin flip; persist says so on
|
|
190
|
+
stderr. Author per-Story `provenance` to record them.
|
|
191
|
+
|
|
184
192
|
This is deliberately not an authoring step. It used to be: the footers reached
|
|
185
193
|
the seed and stopped there, leaving the authoring agent to notice HTML comments
|
|
186
194
|
in a one-pager and copy them forward — a remembered step, which is to say no
|
|
@@ -243,6 +251,14 @@ every Story that has a resolvable blocker with a canonical
|
|
|
243
251
|
GitHub `blocked_by` relations. An edge whose target was never opened (deduped,
|
|
244
252
|
ledger-suppressed) drops rather than becoming a `blocked by #undefined`.
|
|
245
253
|
|
|
254
|
+
**This pass also records the ledger.** The `--ids` map is the only artifact
|
|
255
|
+
carrying the numbers just opened, so `--wire-edges` folds in the cross-run
|
|
256
|
+
record: each mapped group's findings are written `filed` against its Issue
|
|
257
|
+
(`--ledger <path>`, default `baselines/audit-ledger.json`; `--dry-run`
|
|
258
|
+
suppresses the write). The record runs **before** the provider loads, so a host
|
|
259
|
+
with no `gh` still remembers what it filed. Without it nothing ever writes
|
|
260
|
+
`filed` and the ledger suppresses nothing.
|
|
261
|
+
|
|
246
262
|
**Do not skip this.** `/mandrel-deliver` has no other source for this cohort's order:
|
|
247
263
|
its footprint guard ignores the shared provenance footers, so an unwired cohort
|
|
248
264
|
is genuinely unordered and `/mandrel-deliver` will co-dispatch Stories the edges say
|
|
@@ -277,31 +293,45 @@ the `classifications` array:
|
|
|
277
293
|
is skipped by default; flag in the Phase 7 summary so the operator
|
|
278
294
|
can decide whether to reopen.
|
|
279
295
|
|
|
280
|
-
`routeFinding`
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
Dedup runs in **two stages** when a provider resolves: a
|
|
290
|
-
meaning-first **semantic candidate** pass (`searchCandidates`, wired to
|
|
291
|
-
[`lib/findings/semantic-issue-search.js`](../scripts/lib/findings/semantic-issue-search.js))
|
|
292
|
-
runs FIRST and widens the net across open + closed issues; the exact
|
|
293
|
-
**fingerprint / semantic-key** confirmation runs SECOND. A finding whose title
|
|
294
|
-
was reworded but whose *location* is unchanged still confirms against the Issue
|
|
295
|
-
that already tracks that location, because the audit filers stamp a
|
|
296
|
-
location-based `audit-semantic-keys` footer alongside the `audit-fingerprints`
|
|
297
|
-
footer. Filings from the
|
|
296
|
+
`routeFinding` reads open + closed issues through two ports, which **both run
|
|
297
|
+
and union their pools** — the exact `searchIssues` lookup, and the meaning-first
|
|
298
|
+
`searchCandidates` pass wired to
|
|
299
|
+
[`lib/findings/semantic-issue-search.js`](../scripts/lib/findings/semantic-issue-search.js).
|
|
300
|
+
Confirmation then filters that union by footer, dropping hits whose body
|
|
301
|
+
mentions a sha in prose without the canonical marker. A finding reworded but
|
|
302
|
+
unmoved still confirms, because the filers stamp a location-based
|
|
303
|
+
`audit-semantic-keys` footer
|
|
304
|
+
beside `audit-fingerprints`; so do
|
|
298
305
|
[`retro-proposals-graduator`](../scripts/lib/feedback-loop/retro-proposals-graduator.js)
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
filings, which a sweep therefore never re-files.
|
|
307
|
+
|
|
308
|
+
Where the corpus is pre-fetched — either off the list endpoint or from
|
|
309
|
+
`--issues-file` — the exact lookup is answered from that local index and the
|
|
310
|
+
search API is spent only on findings with no exact hit. The workflow owns **no**
|
|
311
|
+
parallel dedup or footer-parsing code: fingerprint, footer round-trip, and
|
|
312
|
+
routing all live in that one shared module.
|
|
313
|
+
|
|
314
|
+
### When there is no `gh` CLI
|
|
315
|
+
|
|
316
|
+
**No GitHub access** (air-gapped): pass `--no-provider` to `--scan`. Every
|
|
317
|
+
group is classified `create` and the operator is told dedupe was skipped — a
|
|
318
|
+
re-run opens duplicates.
|
|
319
|
+
|
|
320
|
+
**Reachable, but not through `gh`** (a cloud sandbox: no `gh`, no direct API,
|
|
321
|
+
MCP fine): fetch the corpus yourself. List every issue labelled `audit::*` at
|
|
322
|
+
state `all` — `mcp__github__list_issues` or any other path — write the raw
|
|
323
|
+
result as a JSON array, and pass it:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
node .agents/scripts/audit-to-stories.js --scan --no-provider \
|
|
327
|
+
--issues-file temp/audits/issues.json --glob "temp/audits/audit-*-results.md"
|
|
328
|
+
```
|
|
301
329
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
Real `skip-open` / `skip-reoccurring` classifications come back. The corpus is
|
|
331
|
+
normalised on load, so a raw list result works as-is; only `number` and `body`
|
|
332
|
+
are read. An unreadable file is a hard error, never a silent fall-back to an
|
|
333
|
+
unchecked run; an **empty** array — a valid first sweep — is reported with its
|
|
334
|
+
count so it cannot pass for a failed fetch.
|
|
305
335
|
|
|
306
336
|
### Cross-run ledger
|
|
307
337
|
|
|
@@ -314,8 +344,12 @@ shape). Each entry is keyed by the finding's fingerprint plus a location-based
|
|
|
314
344
|
(`new | filed | fixed | accepted-risk | regressed`). A finding whose tracking
|
|
315
345
|
Issue was closed as `not_planned` becomes `accepted-risk` and is **suppressed**
|
|
316
346
|
on every later scan; a `fixed` finding that re-appears becomes `regressed`. The
|
|
317
|
-
ledger is written by the unattended `--auto` sweep
|
|
318
|
-
run
|
|
347
|
+
ledger is written by the unattended `--auto` sweep, by any `--scan --ledger`
|
|
348
|
+
run, by the Phase 5c `--wire-edges` pass, and by `plan-persist` on the Phase 5a
|
|
349
|
+
chained path — the two filing paths both record what they filed; the
|
|
350
|
+
plain `--scan` path leaves it untouched. A finding whose resolved Issue is open
|
|
351
|
+
is recorded `filed` and is known on re-detection; a closed Issue still outranks
|
|
352
|
+
that.
|
|
319
353
|
|
|
320
354
|
## Phase 7 — Summary & cleanup
|
|
321
355
|
|
|
@@ -391,8 +425,10 @@ The routine shape is **lenses full-scope → dry-run → live with a ledger PR**
|
|
|
391
425
|
from `delivery.auditToStories.severityFloor` (default `high`, overridable with
|
|
392
426
|
`--severity`), applies the two-stage dedup, reconciles the cross-run ledger,
|
|
393
427
|
and prints a run-summary JSON (create / skip-open / skip-reoccurring /
|
|
394
|
-
suppressed-by-ledger tallies,
|
|
395
|
-
|
|
428
|
+
suppressed-by-ledger tallies, the `create`-classified group keys the `--ids`
|
|
429
|
+
map is built from, plus the re-detected open Issue numbers an operator may want
|
|
430
|
+
a "re-detected" comment on). It opens no Issues itself, so run Phase 5c after
|
|
431
|
+
filing or the sweep's memory stays empty. `--dry-run` performs zero GitHub
|
|
396
432
|
writes and skips the ledger write, emitting only the summary.
|
|
397
433
|
|
|
398
434
|
`--auto` **fails closed on any `summary.reportFailures[]` entry** (Phase 1): an
|