mandrel 2.23.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/acceptance-critic.md +13 -19
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/workflows.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +5 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/acceptance-eval.js +35 -9
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-test-temp-hygiene.js +83 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +22 -45
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +10 -25
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +396 -51
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +20 -32
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/writer.js +7 -3
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +97 -19
- package/.agents/scripts/lib/crap-utils.js +73 -13
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/escomplex-ast-compat.js +360 -0
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/maintainability-engine.js +83 -11
- package/.agents/scripts/lib/maintainability-unscorable.js +60 -0
- package/.agents/scripts/lib/maintainability-utils.js +14 -5
- package/.agents/scripts/lib/observability/runtime-friction.js +8 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +283 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +124 -0
- package/.agents/scripts/lib/orchestration/light-escalation.js +248 -0
- package/.agents/scripts/lib/orchestration/light-suitability.js +254 -51
- package/.agents/scripts/lib/orchestration/plan-context.js +12 -13
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +7 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +13 -1
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -34
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +70 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +73 -18
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +73 -9
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +6 -3
- package/.agents/scripts/lib/workers/crap-worker.js +6 -3
- package/.agents/scripts/lib/workers/maintainability-worker.js +14 -9
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/quality-preview.js +25 -0
- package/.agents/scripts/run-tests.js +10 -0
- package/.agents/scripts/single-story-close.js +6 -0
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +95 -10
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +81 -95
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/deliver-light.md +21 -4
- package/.agents/workflows/helpers/deliver-reference.md +18 -3
- package/.agents/workflows/helpers/deliver-story-reference.md +127 -46
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +34 -0
- package/.agents/workflows/plan.md +60 -76
- package/docs/CHANGELOG.md +38 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +1 -1
|
@@ -61,10 +61,10 @@ import { parseArgs } from 'node:util';
|
|
|
61
61
|
import { runAsCli } from './lib/cli-utils.js';
|
|
62
62
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
63
63
|
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
64
|
-
import {
|
|
64
|
+
import { resolveBackstopOutcome } from './lib/orchestration/light-backstop.js';
|
|
65
|
+
import { recordGateRefusal } from './lib/orchestration/light-escalation.js';
|
|
65
66
|
import {
|
|
66
67
|
buildReceiptStoryTicket,
|
|
67
|
-
checkLightDiffBackstop,
|
|
68
68
|
deriveLightSuitability,
|
|
69
69
|
resolveLightGateOutcome,
|
|
70
70
|
} from './lib/orchestration/light-suitability.js';
|
|
@@ -121,7 +121,11 @@ Gate options:
|
|
|
121
121
|
envelope and ENDS the session (no prompt, no fallback).
|
|
122
122
|
|
|
123
123
|
Backstop options:
|
|
124
|
-
--backstop Re-check the ACTUAL diff after implementation.
|
|
124
|
+
--backstop Re-check the ACTUAL diff after implementation. Bounds the
|
|
125
|
+
change's IMPLEMENTATION half by magnitude (changed lines +
|
|
126
|
+
file sprawl); test/doc/baseline companions are exempt from
|
|
127
|
+
the counts but still matched for sensitive paths. A block
|
|
128
|
+
emits a nextCommand recycling the receipt through /plan.
|
|
125
129
|
--story <id> Story issue number whose story-<id> branch to diff.
|
|
126
130
|
|
|
127
131
|
--pretty Pretty-print the JSON envelope.
|
|
@@ -130,8 +134,6 @@ Backstop options:
|
|
|
130
134
|
|
|
131
135
|
/** Exit code when the gate did not resolve to proceed-light. */
|
|
132
136
|
const EXIT_NOT_PROCEED = 2;
|
|
133
|
-
/** Exit code when the diff backstop blocked the land. */
|
|
134
|
-
const EXIT_BACKSTOP_BLOCKED = 3;
|
|
135
137
|
|
|
136
138
|
/**
|
|
137
139
|
* Split a comma-separated path list into trimmed, non-empty entries.
|
|
@@ -292,33 +294,6 @@ export function buildNextCommands(storyId) {
|
|
|
292
294
|
};
|
|
293
295
|
}
|
|
294
296
|
|
|
295
|
-
/**
|
|
296
|
-
* Run the diff backstop against a Story branch's actual change set.
|
|
297
|
-
*
|
|
298
|
-
* @param {{
|
|
299
|
-
* storyId: number,
|
|
300
|
-
* baseRef?: string,
|
|
301
|
-
* cwd?: string,
|
|
302
|
-
* computeFn?: typeof computeChangeSet,
|
|
303
|
-
* injectedRules?: object,
|
|
304
|
-
* }} args
|
|
305
|
-
* @returns {ReturnType<typeof checkLightDiffBackstop>}
|
|
306
|
-
*/
|
|
307
|
-
export function runDiffBackstop({
|
|
308
|
-
storyId,
|
|
309
|
-
baseRef = 'main',
|
|
310
|
-
cwd = process.cwd(),
|
|
311
|
-
computeFn = computeChangeSet,
|
|
312
|
-
injectedRules,
|
|
313
|
-
} = {}) {
|
|
314
|
-
const { files } = computeFn({
|
|
315
|
-
baseRef,
|
|
316
|
-
headRef: `story-${storyId}`,
|
|
317
|
-
cwd,
|
|
318
|
-
});
|
|
319
|
-
return checkLightDiffBackstop({ changedFiles: files, injectedRules });
|
|
320
|
-
}
|
|
321
|
-
|
|
322
297
|
/**
|
|
323
298
|
* Was a non-blank `--operator-proceed-light` supplied? The gate core decides
|
|
324
299
|
* whether it *applies*; this only asks whether the operator typed one, so the
|
|
@@ -348,27 +323,27 @@ function emit(envelope, pretty) {
|
|
|
348
323
|
}
|
|
349
324
|
|
|
350
325
|
/**
|
|
351
|
-
* Backstop mode — re-check the actual diff.
|
|
326
|
+
* Backstop mode — re-check the actual diff. The decision lives in
|
|
327
|
+
* {@link module:lib/orchestration/light-backstop}; this branches and prints.
|
|
352
328
|
*
|
|
353
|
-
* @param {
|
|
329
|
+
* @param {object} values Parsed CLI values.
|
|
330
|
+
* @param {{ resolveFn?: typeof resolveBackstopOutcome }} [deps]
|
|
354
331
|
* @returns {Promise<number>}
|
|
355
332
|
*/
|
|
356
|
-
async function runBackstopMode(values) {
|
|
333
|
+
async function runBackstopMode(values, deps = {}) {
|
|
334
|
+
const { resolveFn = resolveBackstopOutcome } = deps;
|
|
357
335
|
const storyId = Number.parseInt(String(values.story ?? ''), 10);
|
|
358
336
|
if (!Number.isInteger(storyId) || storyId <= 0) {
|
|
359
337
|
process.stderr.write(HELP);
|
|
360
338
|
throw new Error('[deliver-light] --backstop requires --story <id>');
|
|
361
339
|
}
|
|
362
|
-
const result
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
Logger.info(`[deliver-light] diff backstop clean for Story #${storyId}.`);
|
|
371
|
-
return 0;
|
|
340
|
+
const { result, nextCommand, preservation, exitCode, message } =
|
|
341
|
+
await resolveFn({ storyId });
|
|
342
|
+
const extra = nextCommand === null ? {} : { nextCommand, preservation };
|
|
343
|
+
emit({ mode: 'backstop', storyId, ...result, ...extra }, values.pretty);
|
|
344
|
+
if (result.blocked) Logger.warn(message);
|
|
345
|
+
else Logger.info(message);
|
|
346
|
+
return exitCode;
|
|
372
347
|
}
|
|
373
348
|
|
|
374
349
|
/**
|
|
@@ -407,6 +382,7 @@ export async function runGateMode(values, deps = {}) {
|
|
|
407
382
|
createReceiptFn = createLightReceipt,
|
|
408
383
|
emitFn = emit,
|
|
409
384
|
emitTerminalFn = emitTerminalEnvelope,
|
|
385
|
+
recordRefusalFn = recordGateRefusal,
|
|
410
386
|
} = deps;
|
|
411
387
|
|
|
412
388
|
if (!values.prompt || String(values.prompt).trim() === '') {
|
|
@@ -457,6 +433,7 @@ export async function runGateMode(values, deps = {}) {
|
|
|
457
433
|
{ mode: 'gate', action: gate.action, outcome: gate.outcome },
|
|
458
434
|
values.pretty,
|
|
459
435
|
);
|
|
436
|
+
await recordRefusalFn({ gate, amends: values.amends });
|
|
460
437
|
Logger.warn(
|
|
461
438
|
`[deliver-light] gate did not proceed light (${gate.action}): ${gate.outcome.reasons.join('; ')}`,
|
|
462
439
|
);
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* `single-story-init.js` hard-errors on an already-closed Story. Before this
|
|
17
17
|
* surface, that Story had no automated way back.
|
|
18
18
|
*
|
|
19
|
+
* Every command it prints **resumes** — it re-enters the in-flight worker or
|
|
20
|
+
* close, or closes the branch that is already pushed. None of them re-dispatch
|
|
21
|
+
* the Story, and that is deliberate: since Story #4876 the close-and-land tail
|
|
22
|
+
* belongs to the dispatching orchestrator, so a worker returning without a
|
|
23
|
+
* terminal envelope is the expected shape rather than evidence the Story never
|
|
24
|
+
* ran. Answering it with a fresh dispatch re-runs `single-story-init.js`
|
|
25
|
+
* underneath live work and puts a second close on one PR.
|
|
26
|
+
*
|
|
19
27
|
* Usage:
|
|
20
28
|
* node .agents/scripts/deliver-recover.js --story <STORY_ID> [--cwd <main-repo>]
|
|
21
29
|
* [--json]
|
|
@@ -46,6 +54,11 @@ Probes a Story's live delivery state — labels, lease, branch, worktree, PR
|
|
|
46
54
|
state and checks — and prints the single next command that resumes it, with
|
|
47
55
|
the evidence it was derived from. Read-only: mutates nothing.
|
|
48
56
|
|
|
57
|
+
The command always resumes the existing worker or close (or closes an already
|
|
58
|
+
pushed branch); it never re-dispatches the Story, which would re-run init
|
|
59
|
+
underneath live work. A worker that returned no terminal envelope is expected —
|
|
60
|
+
the orchestrator owns the close-and-land tail.
|
|
61
|
+
|
|
49
62
|
Mid-flight shapes (executing-*/closing-*) get a stability re-probe after a
|
|
50
63
|
short settle window: matching shapes return the fresher verdict; diverging
|
|
51
64
|
shapes report \`in-transition\` (a live delivery process is mutating the
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { gitSpawn } from '../git-utils.js';
|
|
31
|
+
import { readNumstatRows } from '../orchestration/diff-magnitude.js';
|
|
31
32
|
import { selectSensitivePathClasses } from './selector.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -58,6 +59,12 @@ export function resolveLensDiffFloor(config) {
|
|
|
58
59
|
* Count the changed lines (additions + deletions) in the
|
|
59
60
|
* `baseRef...headRef` diff via `git diff --numstat`.
|
|
60
61
|
*
|
|
62
|
+
* The read and the parse are shared with the light path's magnitude backstop
|
|
63
|
+
* ({@link module:lib/orchestration/diff-magnitude.readNumstatRows}) so the two
|
|
64
|
+
* cannot disagree about how a diff is measured. This one keeps a whole-diff
|
|
65
|
+
* total: the lens floor asks "is this diff small", not "is its implementation
|
|
66
|
+
* half small", so it deliberately does **not** apply the companion exemption.
|
|
67
|
+
*
|
|
61
68
|
* Total — never throws. Returns `null` (the neutral "count unknown" signal
|
|
62
69
|
* the floor fails open on) for any git failure or unparseable output, and
|
|
63
70
|
* `0` for a genuinely empty diff. Binary rows (`-\t-\tpath`) contribute 0
|
|
@@ -77,31 +84,9 @@ export function countChangedLines({
|
|
|
77
84
|
cwd = process.cwd(),
|
|
78
85
|
gitSpawnFn = gitSpawn,
|
|
79
86
|
} = {}) {
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
const result = gitSpawnFn(
|
|
84
|
-
cwd,
|
|
85
|
-
'diff',
|
|
86
|
-
'--numstat',
|
|
87
|
-
`${baseRef}...${headRef}`,
|
|
88
|
-
);
|
|
89
|
-
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
let total = 0;
|
|
93
|
-
for (const line of result.stdout.split('\n')) {
|
|
94
|
-
const trimmedEnd = line.replace(/\s+$/, '');
|
|
95
|
-
if (trimmedEnd.length === 0) continue;
|
|
96
|
-
const match = /^(\d+|-)\t(\d+|-)\t/.exec(trimmedEnd);
|
|
97
|
-
if (!match) return null; // Unexpected format — the count is not trustworthy.
|
|
98
|
-
if (match[1] !== '-') total += Number(match[1]);
|
|
99
|
-
if (match[2] !== '-') total += Number(match[2]);
|
|
100
|
-
}
|
|
101
|
-
return total;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
87
|
+
const rows = readNumstatRows({ baseRef, headRef, cwd, gitSpawnFn });
|
|
88
|
+
if (rows === null) return null;
|
|
89
|
+
return rows.reduce((total, row) => total + row.additions + row.deletions, 0);
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
/**
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-to-stories/audit-label-taxonomy.js — the closed set of GitHub
|
|
3
|
+
* labels an audit sweep may create or generate (Story #4877).
|
|
4
|
+
*
|
|
5
|
+
* There are two sides to a label axis and they used to be written down in
|
|
6
|
+
* different places, which is how they drifted:
|
|
7
|
+
*
|
|
8
|
+
* - the **creator** — `audit-labels-bootstrap.js`, which ran `gh label create`
|
|
9
|
+
* over the `audit::<lens>` list and nothing else;
|
|
10
|
+
* - the **generator** — `audit-to-stories/build-story-body.js`, which emitted
|
|
11
|
+
* `type::story`, `agent::ready`, `audit::<lens>` and — as a bare string
|
|
12
|
+
* literal — `risk::high`.
|
|
13
|
+
*
|
|
14
|
+
* `risk::high` was therefore generated by the filer and defined by **no**
|
|
15
|
+
* taxonomy: not `LABEL_TAXONOMY` (the repo-wide bootstrap), not the audit
|
|
16
|
+
* bootstrap. A generated label the repository has never created is the exact
|
|
17
|
+
* failure shape that made every `gh issue create` in the feedback loop fail in
|
|
18
|
+
* Story #4828. This module is the one list both sides read, so the creator
|
|
19
|
+
* cannot fall behind the generator again.
|
|
20
|
+
*
|
|
21
|
+
* It lives under `lib/` rather than inside the bootstrap CLI so the generator
|
|
22
|
+
* can import it without a lib → top-level-script edge (and without dragging the
|
|
23
|
+
* CLI's config/`gh` dependencies into a pure body builder).
|
|
24
|
+
*
|
|
25
|
+
* Label *names* come from `lib/label-constants.js` — the repo-wide SSOT for
|
|
26
|
+
* label spelling — so a rename still lands in one place.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
AGENT_LABELS,
|
|
31
|
+
LABEL_COLORS,
|
|
32
|
+
RISK_LABELS,
|
|
33
|
+
TYPE_LABELS,
|
|
34
|
+
} from '../label-constants.js';
|
|
35
|
+
import { AUDIT_LENSES } from './audit-lenses.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-lens label presentation, keyed by canonical lens name. A lens absent from
|
|
39
|
+
* this map falls back to {@link DEFAULT_LENS_META}, so adding an `audit-*`
|
|
40
|
+
* workflow to `AUDIT_LENSES` still yields a label without registering
|
|
41
|
+
* colour/description here first.
|
|
42
|
+
*/
|
|
43
|
+
const LENS_META = Object.freeze({
|
|
44
|
+
accessibility: {
|
|
45
|
+
color: 'c5def5',
|
|
46
|
+
description: 'Audit-sourced finding: WCAG accessibility conformance',
|
|
47
|
+
},
|
|
48
|
+
architecture: {
|
|
49
|
+
color: '6f42c1',
|
|
50
|
+
description: 'Audit-sourced finding: architectural concerns',
|
|
51
|
+
},
|
|
52
|
+
'clean-code': {
|
|
53
|
+
color: '0e8a16',
|
|
54
|
+
description: 'Audit-sourced finding: clean-code / maintainability',
|
|
55
|
+
},
|
|
56
|
+
dependencies: {
|
|
57
|
+
color: 'd4c5f9',
|
|
58
|
+
description: 'Audit-sourced finding: dependencies / supply chain',
|
|
59
|
+
},
|
|
60
|
+
devops: {
|
|
61
|
+
color: 'fbca04',
|
|
62
|
+
description: 'Audit-sourced finding: DevOps / CI / CD',
|
|
63
|
+
},
|
|
64
|
+
documentation: {
|
|
65
|
+
color: '1d76db',
|
|
66
|
+
description: 'Audit-sourced finding: documentation staleness / gaps',
|
|
67
|
+
},
|
|
68
|
+
navigability: {
|
|
69
|
+
color: 'bfdadc',
|
|
70
|
+
description: 'Audit-sourced finding: route / nav reachability',
|
|
71
|
+
},
|
|
72
|
+
performance: {
|
|
73
|
+
color: 'b60205',
|
|
74
|
+
description: 'Audit-sourced finding: performance / latency',
|
|
75
|
+
},
|
|
76
|
+
privacy: {
|
|
77
|
+
color: 'fef2c0',
|
|
78
|
+
description: 'Audit-sourced finding: privacy / data handling',
|
|
79
|
+
},
|
|
80
|
+
quality: {
|
|
81
|
+
color: '0052cc',
|
|
82
|
+
description: 'Audit-sourced finding: test quality / coverage gaps',
|
|
83
|
+
},
|
|
84
|
+
security: {
|
|
85
|
+
color: 'b60205',
|
|
86
|
+
description: 'Audit-sourced finding: security / OWASP',
|
|
87
|
+
},
|
|
88
|
+
seo: {
|
|
89
|
+
color: 'fbca04',
|
|
90
|
+
description: 'Audit-sourced finding: SEO / discoverability',
|
|
91
|
+
},
|
|
92
|
+
sre: {
|
|
93
|
+
color: '0052cc',
|
|
94
|
+
description: 'Audit-sourced finding: SRE / observability / reliability',
|
|
95
|
+
},
|
|
96
|
+
'ux-ui': {
|
|
97
|
+
color: 'd4c5f9',
|
|
98
|
+
description: 'Audit-sourced finding: UX / UI concerns',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const DEFAULT_LENS_META = Object.freeze({
|
|
103
|
+
color: 'ededed',
|
|
104
|
+
description: 'Audit-sourced finding',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** `gh label create --color` wants a bare hex triplet, not a CSS `#rrggbb`. */
|
|
108
|
+
function hex(color) {
|
|
109
|
+
return String(color).replace('#', '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The `audit::<lens>` half of the taxonomy, one per `/audit-<lens>` workflow.
|
|
114
|
+
* Derived from the same `AUDIT_LENSES` SSOT that `build-story-body.js` derives
|
|
115
|
+
* its labels from, so the creator and the deriver cannot drift (Story #4195).
|
|
116
|
+
*/
|
|
117
|
+
const AUDIT_LENS_LABELS = Object.freeze(
|
|
118
|
+
AUDIT_LENSES.map((name) => ({
|
|
119
|
+
name: `audit::${name}`,
|
|
120
|
+
...(LENS_META[name] ?? DEFAULT_LENS_META),
|
|
121
|
+
})),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The story-axis labels an audit-generated Story carries alongside its
|
|
126
|
+
* `audit::<lens>` labels.
|
|
127
|
+
*
|
|
128
|
+
* The `type::` / `agent::` entries are also in `LABEL_TAXONOMY` (the repo-wide
|
|
129
|
+
* bootstrap). Creating a label twice is a no-op, so the overlap costs nothing
|
|
130
|
+
* and means an audit sweep does not silently depend on the repo-wide bootstrap
|
|
131
|
+
* having run first.
|
|
132
|
+
*/
|
|
133
|
+
const AUDIT_STORY_AXIS_LABELS = Object.freeze([
|
|
134
|
+
{
|
|
135
|
+
name: TYPE_LABELS.STORY,
|
|
136
|
+
color: hex(LABEL_COLORS.TYPE),
|
|
137
|
+
description: 'Story work item',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: AGENT_LABELS.READY,
|
|
141
|
+
color: hex(LABEL_COLORS.AGENT),
|
|
142
|
+
description:
|
|
143
|
+
'Parking state — frozen dispatch manifest exists; awaiting local /deliver',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: RISK_LABELS.HIGH,
|
|
147
|
+
color: hex(LABEL_COLORS.RISK_HIGH),
|
|
148
|
+
description:
|
|
149
|
+
'Planning/audit metadata: review this first (Critical finding present)',
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every label the audit sweep defines — the closed set the bootstrap creates
|
|
155
|
+
* and {@link definesAuditLabel} answers against.
|
|
156
|
+
*/
|
|
157
|
+
export const AUDIT_LABEL_TAXONOMY = Object.freeze([
|
|
158
|
+
...AUDIT_LENS_LABELS,
|
|
159
|
+
...AUDIT_STORY_AXIS_LABELS,
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const DEFINED_NAMES = new Set(AUDIT_LABEL_TAXONOMY.map((l) => l.name));
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* True when `name` is a label this taxonomy defines.
|
|
166
|
+
*
|
|
167
|
+
* The enforcement point is the label **generator** (`build-story-body.js`),
|
|
168
|
+
* which routes every label it derives through this predicate and refuses to
|
|
169
|
+
* emit one the taxonomy does not define — closing the drift in the direction it
|
|
170
|
+
* actually happened: a generator inventing names the bootstrap never created.
|
|
171
|
+
*
|
|
172
|
+
* @param {unknown} name
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
export function definesAuditLabel(name) {
|
|
176
|
+
return typeof name === 'string' && DEFINED_NAMES.has(name);
|
|
177
|
+
}
|
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
* structured contract.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { AGENT_LABELS, RISK_LABELS, TYPE_LABELS } from '../label-constants.js';
|
|
25
26
|
import { serialize } from '../story-body/story-body.js';
|
|
27
|
+
import { definesAuditLabel } from './audit-label-taxonomy.js';
|
|
26
28
|
import { auditLabelsForFindings } from './audit-lenses.js';
|
|
27
29
|
import {
|
|
28
30
|
renderFingerprintFooter,
|
|
29
31
|
renderSemanticKeyFooter,
|
|
30
32
|
} from './finding-adapter.js';
|
|
31
33
|
|
|
32
|
-
const STATIC_LABELS = Object.freeze([
|
|
34
|
+
const STATIC_LABELS = Object.freeze([TYPE_LABELS.STORY, AGENT_LABELS.READY]);
|
|
33
35
|
|
|
34
36
|
// The verify[] contract every generated audit Story carries. These commands
|
|
35
37
|
// exist in this repo's harness (package.json scripts) so the Story satisfies
|
|
@@ -183,8 +185,38 @@ function labelsForGroup(group) {
|
|
|
183
185
|
const hasCritical = (group.findings ?? []).some(
|
|
184
186
|
(f) => f.severity === 'critical',
|
|
185
187
|
);
|
|
186
|
-
if (hasCritical) labels.push(
|
|
187
|
-
return uniq(labels);
|
|
188
|
+
if (hasCritical) labels.push(RISK_LABELS.HIGH);
|
|
189
|
+
return assertLabelsInTaxonomy(uniq(labels));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Refuse to generate a label the audit bootstrap taxonomy does not define
|
|
194
|
+
* (Story #4877).
|
|
195
|
+
*
|
|
196
|
+
* Story #4195 fixed half of this: `audit::<dimension>` labels minted from
|
|
197
|
+
* free-form dimension prose ("stale-description", "dry") named labels that did
|
|
198
|
+
* not exist, so derivation moved to the closed lens list. The other half stayed
|
|
199
|
+
* open — `risk::high` was a bare string literal here, defined by no taxonomy —
|
|
200
|
+
* and nothing checked the generated set against anything at all. Throwing is
|
|
201
|
+
* deliberate: a label the repo has never created is dropped or fails the create
|
|
202
|
+
* outright, and a filer that silently loses `risk::high` on a Critical merge is
|
|
203
|
+
* worse than a loud failure at the point of generation.
|
|
204
|
+
*
|
|
205
|
+
* @param {string[]} labels
|
|
206
|
+
* @returns {string[]} the same labels, when every one is defined.
|
|
207
|
+
* @throws {Error} naming the offending labels.
|
|
208
|
+
*/
|
|
209
|
+
function assertLabelsInTaxonomy(labels) {
|
|
210
|
+
const undefinedLabels = labels.filter((l) => !definesAuditLabel(l));
|
|
211
|
+
if (undefinedLabels.length > 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`buildStoryBody: generated label(s) ${undefinedLabels.join(', ')} are not ` +
|
|
214
|
+
'defined by the audit label taxonomy (audit-label-taxonomy.js). Add ' +
|
|
215
|
+
'them there — or stop generating them — rather than emitting a label ' +
|
|
216
|
+
'the repository does not have.',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return labels;
|
|
188
220
|
}
|
|
189
221
|
|
|
190
222
|
/**
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Pure: no I/O.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
import { highestSeverity as highestSeverityOf } from '../findings/severity.js';
|
|
25
25
|
|
|
26
26
|
function dirOf(filePath) {
|
|
27
27
|
if (typeof filePath !== 'string' || filePath.length === 0) return '';
|
|
@@ -37,17 +37,26 @@ function pickPrimaryFile(finding) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The highest severity across a group's findings, ranked by the severity SSOT
|
|
42
|
+
* ({@link highestSeverityOf}) rather than a local copy of the scale.
|
|
43
|
+
*
|
|
44
|
+
* The rank map this replaces knew four levels and not `info`, so an Info
|
|
45
|
+
* finding tied with a finding carrying no severity at all — the same partial
|
|
46
|
+
* vocabulary this Story removes everywhere else.
|
|
47
|
+
*
|
|
48
|
+
* A group whose findings all lack a usable severity still reports `null`, not
|
|
49
|
+
* the SSOT's `info` floor: absent is not the same claim as "graded lowest",
|
|
50
|
+
* and the callers that tally and threshold on this value distinguish them.
|
|
51
|
+
*
|
|
52
|
+
* @param {Array<{ severity?: string }>} findings
|
|
53
|
+
* @returns {string|null} a canonical severity, or null when none is stated.
|
|
54
|
+
*/
|
|
40
55
|
function highestSeverity(findings) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (r > bestRank) {
|
|
46
|
-
bestRank = r;
|
|
47
|
-
best = f.severity ?? null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return best;
|
|
56
|
+
const stated = findings
|
|
57
|
+
.map((f) => f?.severity)
|
|
58
|
+
.filter((value) => typeof value === 'string' && value.length > 0);
|
|
59
|
+
return stated.length === 0 ? null : highestSeverityOf(stated);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/**
|
|
@@ -17,14 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
critical: 'critical',
|
|
22
|
-
high: 'high',
|
|
23
|
-
medium: 'medium',
|
|
24
|
-
mod: 'medium',
|
|
25
|
-
moderate: 'medium',
|
|
26
|
-
low: 'low',
|
|
27
|
-
});
|
|
20
|
+
import { normalizeSeverity } from '../findings/severity.js';
|
|
28
21
|
|
|
29
22
|
const KEY_LINE = /^\s*-\s*\*\*([^:*]+):\*\*\s*(.*)$/;
|
|
30
23
|
const HEADING_FINDING = /^###\s+(.+?)\s*$/;
|
|
@@ -44,6 +37,27 @@ function unwrapInlineCode(value) {
|
|
|
44
37
|
return trimmed;
|
|
45
38
|
}
|
|
46
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a raw severity/impact token to a canonical level, or `null` when the
|
|
42
|
+
* token carries no recognisable severity at all.
|
|
43
|
+
*
|
|
44
|
+
* The vocabulary itself is NOT written down here (Story #4877). This module used
|
|
45
|
+
* to carry its own alias table covering `critical|high|medium|mod|moderate|low`
|
|
46
|
+
* — four of the canonical five levels, missing `info`. A lens that graded a
|
|
47
|
+
* finding `Info` or `Informational` (which the shared severity scale now
|
|
48
|
+
* sanctions) therefore parsed to `null`, tallied as `unknown`, and was dropped
|
|
49
|
+
* by every severity-filtered run, `--severity low` included. Delegating to the
|
|
50
|
+
* canonical normaliser in `lib/findings/severity.js` means this parser cannot
|
|
51
|
+
* know a narrower vocabulary than the rest of the pipeline.
|
|
52
|
+
*
|
|
53
|
+
* `null` — rather than the normaliser's `info` fallback — remains the
|
|
54
|
+
* no-severity answer, because {@link deriveSeverity} walks several candidate
|
|
55
|
+
* keys and needs to distinguish "this key had no severity" from "this key said
|
|
56
|
+
* `info`".
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} token
|
|
59
|
+
* @returns {string|null}
|
|
60
|
+
*/
|
|
47
61
|
function normaliseSeverity(token) {
|
|
48
62
|
if (typeof token !== 'string') return null;
|
|
49
63
|
const cleaned = token
|
|
@@ -53,7 +67,7 @@ function normaliseSeverity(token) {
|
|
|
53
67
|
.trim();
|
|
54
68
|
if (!cleaned) return null;
|
|
55
69
|
for (const word of cleaned.split(/[\s|/,]+/)) {
|
|
56
|
-
const hit =
|
|
70
|
+
const hit = normalizeSeverity(word, null);
|
|
57
71
|
if (hit) return hit;
|
|
58
72
|
}
|
|
59
73
|
return null;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Pure: returns a string. The caller decides where to persist it.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { SEVERITIES } from '../findings/severity.js';
|
|
20
21
|
import {
|
|
21
22
|
renderFingerprintFooter,
|
|
22
23
|
renderSemanticKeyFooter,
|
|
@@ -37,10 +38,16 @@ const DIMENSION_LABEL = {
|
|
|
37
38
|
architecture: 'Architecture',
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The severity profile in the seed's Problem Statement is ordered and bucketed
|
|
43
|
+
* by the canonical scale (Story #4877) rather than by a fourth local copy of
|
|
44
|
+
* it. The list this replaces omitted `info`, so an informational finding was
|
|
45
|
+
* absent from the profile the planner reads even when it survived the filter.
|
|
46
|
+
*/
|
|
47
|
+
const SEVERITY_ORDER = SEVERITIES;
|
|
41
48
|
|
|
42
49
|
function tallySeverities(findings) {
|
|
43
|
-
const tally =
|
|
50
|
+
const tally = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
|
|
44
51
|
for (const f of findings) {
|
|
45
52
|
if (Object.hasOwn(tally, f.severity)) tally[f.severity] += 1;
|
|
46
53
|
}
|
|
Binary file
|
|
Binary file
|