mandrel 2.24.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/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 +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- 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/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/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/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/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- 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/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +50 -13
- 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/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-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 +29 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +1 -1
|
@@ -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
|