mandrel 2.24.0 → 2.26.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 +19 -21
- 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/baselines.md +21 -0
- 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/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -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/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -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/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- 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/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- 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/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- 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-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -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 +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weights.js — the three ranking multipliers, and their degradations
|
|
3
|
+
* (Story #4902).
|
|
4
|
+
*
|
|
5
|
+
* A hotspot's severity says how bad the code measures. These three say how
|
|
6
|
+
* much that badness costs: how often the file changes (churn), how much of
|
|
7
|
+
* the repository depends on it (import in-degree), and how often agents have
|
|
8
|
+
* actually tripped over it (friction signals).
|
|
9
|
+
*
|
|
10
|
+
* All three are **optional inputs**. A shallow clone has no git history, a
|
|
11
|
+
* fresh checkout has no friction ledger, and a consumer whose sources live
|
|
12
|
+
* outside the scanned roots has no resolvable import graph. Each degrades to
|
|
13
|
+
* a neutral multiplier of exactly 1.0 — never 0 (which would erase the
|
|
14
|
+
* hotspot) and never a guess. The engine exits 0 in every degraded case.
|
|
15
|
+
*
|
|
16
|
+
* @module lib/audit-baselines/weights
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { computeInDegree, resolveRepoGraph } from '../import-graph.js';
|
|
23
|
+
|
|
24
|
+
/** Neutral multiplier every degraded weight collapses to. */
|
|
25
|
+
const NEUTRAL_WEIGHT = 1.0;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Saturating count → multiplier in `[1, 2)`. Zero observations gives exactly
|
|
29
|
+
* `NEUTRAL_WEIGHT`, so "no signal" and "signal says nothing notable" are the
|
|
30
|
+
* same number — the degradation is indistinguishable from an honest zero,
|
|
31
|
+
* which is the point: neither should move the ranking.
|
|
32
|
+
*
|
|
33
|
+
* @param {number} count
|
|
34
|
+
* @param {number} half count at which the multiplier reaches 1.5
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
function saturate(count, half) {
|
|
38
|
+
if (!Number.isFinite(count) || count <= 0) return NEUTRAL_WEIGHT;
|
|
39
|
+
return NEUTRAL_WEIGHT + count / (count + half);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Count commits touching each file in the recent history window.
|
|
44
|
+
*
|
|
45
|
+
* @param {{ cwd: string, windowDays?: number, run?: Function }} args
|
|
46
|
+
* @returns {{ counts: Map<string, number>, degraded: boolean }}
|
|
47
|
+
* `degraded` is true when git could not answer at all — not a git work
|
|
48
|
+
* tree, no commits yet, or the binary is unavailable.
|
|
49
|
+
*/
|
|
50
|
+
export function readChurn({ cwd, windowDays = 180, run = execFileSync }) {
|
|
51
|
+
let stdout;
|
|
52
|
+
try {
|
|
53
|
+
stdout = run(
|
|
54
|
+
'git',
|
|
55
|
+
[
|
|
56
|
+
'log',
|
|
57
|
+
`--since=${windowDays}.days.ago`,
|
|
58
|
+
'--name-only',
|
|
59
|
+
'--pretty=format:',
|
|
60
|
+
'--no-renames',
|
|
61
|
+
],
|
|
62
|
+
{
|
|
63
|
+
cwd,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
66
|
+
// git narrates "not a git repository" on stderr; the degradation is
|
|
67
|
+
// reported in the envelope, not shouted at the operator.
|
|
68
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
return { counts: new Map(), degraded: true };
|
|
73
|
+
}
|
|
74
|
+
const counts = new Map();
|
|
75
|
+
for (const line of String(stdout).split('\n')) {
|
|
76
|
+
const file = line.trim();
|
|
77
|
+
if (file.length === 0) continue;
|
|
78
|
+
counts.set(file, (counts.get(file) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
return { counts, degraded: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Import in-degree per module, keyed by repo-relative posix path.
|
|
85
|
+
*
|
|
86
|
+
* @param {{ cwd: string, graph?: Map<string, string[]> | null }} args
|
|
87
|
+
* @returns {{ degrees: Map<string, number>, degraded: boolean }}
|
|
88
|
+
*/
|
|
89
|
+
export function readCentrality({ cwd, graph }) {
|
|
90
|
+
const resolved = graph === undefined ? resolveRepoGraph(cwd) : graph;
|
|
91
|
+
if (!resolved) return { degrees: new Map(), degraded: true };
|
|
92
|
+
return { degrees: computeInDegree(resolved), degraded: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Path tokens that look like repository files, harvested from signal text. */
|
|
96
|
+
const PATH_TOKEN_RE = /[\w@][\w./@-]*\.(?:js|mjs|cjs|ts|tsx|json|md)\b/g;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Collect every `signals.ndjson` under `tempRoot`. Absent tree → empty list.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} tempRootAbs
|
|
102
|
+
* @returns {string[]} absolute paths
|
|
103
|
+
*/
|
|
104
|
+
function findSignalStreams(tempRootAbs) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const walk = (dir, depth) => {
|
|
107
|
+
if (depth > 6) return;
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const child = path.join(dir, entry.name);
|
|
116
|
+
if (entry.isDirectory()) walk(child, depth + 1);
|
|
117
|
+
else if (entry.name === 'signals.ndjson') out.push(child);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
walk(tempRootAbs, 0);
|
|
121
|
+
return out.sort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Count friction signals blaming each file.
|
|
126
|
+
*
|
|
127
|
+
* Signal records carry no dedicated path field — the blamed file surfaces
|
|
128
|
+
* inside free-form `details` / `emitter.command` text — so paths are
|
|
129
|
+
* harvested by token scan over each record's serialized form. A malformed
|
|
130
|
+
* line is skipped, never fatal.
|
|
131
|
+
*
|
|
132
|
+
* @param {{ tempRootAbs: string, kinds?: Set<string> }} args
|
|
133
|
+
* @returns {{ counts: Map<string, number>, degraded: boolean, streams: number }}
|
|
134
|
+
*/
|
|
135
|
+
export function readFriction({
|
|
136
|
+
tempRootAbs,
|
|
137
|
+
kinds = new Set(['friction', 'hotspot', 'rework', 'churn', 'retry']),
|
|
138
|
+
}) {
|
|
139
|
+
const streams = findSignalStreams(tempRootAbs);
|
|
140
|
+
if (streams.length === 0) {
|
|
141
|
+
return { counts: new Map(), degraded: true, streams: 0 };
|
|
142
|
+
}
|
|
143
|
+
const counts = new Map();
|
|
144
|
+
for (const stream of streams) {
|
|
145
|
+
let raw;
|
|
146
|
+
try {
|
|
147
|
+
raw = fs.readFileSync(stream, 'utf8');
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
for (const line of raw.split('\n')) {
|
|
152
|
+
if (line.trim().length === 0) continue;
|
|
153
|
+
let record;
|
|
154
|
+
try {
|
|
155
|
+
record = JSON.parse(line);
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!kinds.has(record?.kind)) continue;
|
|
160
|
+
const text =
|
|
161
|
+
JSON.stringify(record.details ?? {}) + (record?.emitter?.command ?? '');
|
|
162
|
+
for (const token of text.match(PATH_TOKEN_RE) ?? []) {
|
|
163
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { counts, degraded: false, streams: streams.length };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Bundle the three weight lookups into one resolver the hotspot builder can
|
|
172
|
+
* call per cluster id, plus the degradation flags the envelope reports.
|
|
173
|
+
*
|
|
174
|
+
* @param {{
|
|
175
|
+
* churn: { counts: Map<string, number>, degraded: boolean },
|
|
176
|
+
* centrality: { degrees: Map<string, number>, degraded: boolean },
|
|
177
|
+
* friction: { counts: Map<string, number>, degraded: boolean },
|
|
178
|
+
* }} sources
|
|
179
|
+
* @returns {(id: string) => { churnWeight: number, centralityWeight: number, frictionWeight: number }}
|
|
180
|
+
*/
|
|
181
|
+
export function makeWeightResolver({ churn, centrality, friction }) {
|
|
182
|
+
return (id) => ({
|
|
183
|
+
churnWeight: churn.degraded
|
|
184
|
+
? NEUTRAL_WEIGHT
|
|
185
|
+
: saturate(churn.counts.get(id) ?? 0, 12),
|
|
186
|
+
centralityWeight: centrality.degraded
|
|
187
|
+
? NEUTRAL_WEIGHT
|
|
188
|
+
: saturate(centrality.degrees.get(id) ?? 0, 8),
|
|
189
|
+
frictionWeight: friction.degraded
|
|
190
|
+
? NEUTRAL_WEIGHT
|
|
191
|
+
: saturate(friction.counts.get(id) ?? 0, 4),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -27,15 +27,10 @@ export {
|
|
|
27
27
|
} from './lens-diff-floor.js';
|
|
28
28
|
export { runAuditSuite } from './runner.js';
|
|
29
29
|
export {
|
|
30
|
-
GLOBAL_LENS_ALLOWLIST,
|
|
31
|
-
isGlobalLens,
|
|
32
30
|
LENS_TIERS,
|
|
33
31
|
matchesAnyFilePattern,
|
|
34
32
|
matchesFilePattern,
|
|
35
|
-
NAVIGABILITY_LENS,
|
|
36
33
|
resolveLensTier,
|
|
37
|
-
resolveNavigabilityRouteGlobs,
|
|
38
|
-
routesNavigabilityLens,
|
|
39
34
|
selectAudits,
|
|
40
35
|
selectLocalLenses,
|
|
41
36
|
} from './selector.js';
|
|
@@ -30,42 +30,6 @@ import { readAuditRulesSync } from './audit-rules-reader.js';
|
|
|
30
30
|
|
|
31
31
|
const DEFAULT_GIT_TIMEOUT_MS = 30000;
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* The audit-lens identifier for the navigability lens (Epic #4131, F2/F3).
|
|
35
|
-
* Authored as `.agents/workflows/audit-navigability.md`; registered here so the
|
|
36
|
-
* roster, the global-lens allowlist, and the route-added routing seam all
|
|
37
|
-
* reference one symbol rather than a hard-coded string.
|
|
38
|
-
*/
|
|
39
|
-
export const NAVIGABILITY_LENS = 'audit-navigability';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The **global-lens allowlist** — lenses that evaluate a property of the
|
|
43
|
-
* **whole** product (not just the Epic's change set) and are therefore exempt
|
|
44
|
-
* from the cross-epic-leak guard (`#3362`) that narrows every other lens's
|
|
45
|
-
* evidence to the Epic's `changedFiles`. A lens in this set still runs through
|
|
46
|
-
* the SAME `runAuditSuite` / `selectAuditStrategy` engine; only the
|
|
47
|
-
* change-set narrowing is bypassed, and only for the listed lenses. The guard
|
|
48
|
-
* is **not** weakened for any lens absent from this set.
|
|
49
|
-
*
|
|
50
|
-
* Navigability is the founding member: reachability is a global property — a
|
|
51
|
-
* change can orphan a route it never touched — so the lens must read the whole
|
|
52
|
-
* route tree + nav registry regardless of which file triggered it.
|
|
53
|
-
*/
|
|
54
|
-
export const GLOBAL_LENS_ALLOWLIST = Object.freeze([NAVIGABILITY_LENS]);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* True when `lens` is on the global-lens allowlist and is therefore exempt
|
|
58
|
-
* from the cross-epic-leak guard's change-set narrowing. Pure; the single
|
|
59
|
-
* read-side of {@link GLOBAL_LENS_ALLOWLIST} so callers never hard-code the
|
|
60
|
-
* membership test.
|
|
61
|
-
*
|
|
62
|
-
* @param {string} lens
|
|
63
|
-
* @returns {boolean}
|
|
64
|
-
*/
|
|
65
|
-
export function isGlobalLens(lens) {
|
|
66
|
-
return GLOBAL_LENS_ALLOWLIST.includes(lens);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
33
|
/**
|
|
70
34
|
* The canonical concern-ownership tiers a lens can declare via its
|
|
71
35
|
* `scope` field in [`audit-rules.json`](../../../schemas/audit-rules.json).
|
|
@@ -222,40 +186,23 @@ export function selectSensitivePathClasses({
|
|
|
222
186
|
/**
|
|
223
187
|
* Resolve the consumer's navigability route globs from the resolved config.
|
|
224
188
|
* Reads `delivery.quality.navigability.routeGlobs` — the route-tree SSOT the
|
|
225
|
-
* navigability lens enumerates
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
189
|
+
* navigability lens enumerates. Returns an empty array when the block (or any
|
|
190
|
+
* ancestor) is absent, so an unconfigured consumer contributes no web-surface
|
|
191
|
+
* evidence (Epic #4131 — "no-op when unconfigured").
|
|
192
|
+
*
|
|
193
|
+
* Module-local: the sole reader is {@link hasWebSurface}. It was previously
|
|
194
|
+
* exported alongside a route-added routing seam that Story #4926 removed —
|
|
195
|
+
* lens routing is decided by the `scope` field in `audit-rules.json`, never by
|
|
196
|
+
* a second predicate here.
|
|
229
197
|
*
|
|
230
198
|
* @param {object|null|undefined} config Resolved `.agentrc.json` wrapper.
|
|
231
199
|
* @returns {string[]} Route globs, or `[]` when unconfigured.
|
|
232
200
|
*/
|
|
233
|
-
|
|
201
|
+
function resolveNavigabilityRouteGlobs(config) {
|
|
234
202
|
const globs = config?.delivery?.quality?.navigability?.routeGlobs;
|
|
235
203
|
return Array.isArray(globs) ? globs.filter((g) => typeof g === 'string') : [];
|
|
236
204
|
}
|
|
237
205
|
|
|
238
|
-
/**
|
|
239
|
-
* Decide whether a change set routes the navigability lens. The lens is routed
|
|
240
|
-
* when any `changedFiles` entry matches a consumer-configured route glob
|
|
241
|
-
* (`delivery.quality.navigability.routeGlobs`) — i.e. the change set adds or
|
|
242
|
-
* touches a route file. When no route globs are configured, this returns
|
|
243
|
-
* `false` (the unconfigured no-op), so the existing change-set-scoped lens
|
|
244
|
-
* selection is unchanged.
|
|
245
|
-
*
|
|
246
|
-
* A pure predicate over a change set — the same input {@link selectAudits} and
|
|
247
|
-
* {@link selectLocalLenses} already match against. The caller unions its result
|
|
248
|
-
* into the lens roster it is assembling; no new routing machinery is added.
|
|
249
|
-
*
|
|
250
|
-
* @param {{ changedFiles?: string[], config?: object|null }} params
|
|
251
|
-
* @returns {boolean}
|
|
252
|
-
*/
|
|
253
|
-
export function routesNavigabilityLens({ changedFiles, config } = {}) {
|
|
254
|
-
const globs = resolveNavigabilityRouteGlobs(config);
|
|
255
|
-
if (globs.length === 0) return false;
|
|
256
|
-
return matchesAnyFilePattern(globs, changedFiles ?? []);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
206
|
/**
|
|
260
207
|
* Package names (or scope/name segments of them) that declare a **web
|
|
261
208
|
* rendering surface**. Matched against the consumer's root `package.json`
|
|
@@ -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
|