mandrel 1.87.0 → 1.89.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/README.md +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +63 -16
- package/.agents/docs/configuration.md +5 -3
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-suite/lens-checklist.js — distill an `audit-<lens>.md` workflow
|
|
3
|
+
* body into a compact, drift-gated authoring checklist.
|
|
4
|
+
*
|
|
5
|
+
* Epic #4405 (shift-left audit) moves each lens's concerns to the innermost,
|
|
6
|
+
* write-time tier: instead of an engineer only learning what a lens checks
|
|
7
|
+
* when `/audit-<lens>` runs, a compact per-lens checklist ships as a committed
|
|
8
|
+
* build artifact under `.agents/audit-checklists/`. This module is the pure
|
|
9
|
+
* distillation seam — no IO — so it is unit-testable and the generator
|
|
10
|
+
* (`generate-lens-checklists.js`) owns only the file read/write/prune and the
|
|
11
|
+
* `--check` drift gate.
|
|
12
|
+
*
|
|
13
|
+
* The distillation is a deterministic transform of the workflow markdown: it
|
|
14
|
+
* lifts the lens's *concern* labels — the bold lead-ins of the analysis /
|
|
15
|
+
* evaluation list items — from the audit region of the workflow (everything
|
|
16
|
+
* before the `Output Requirements` / report-template boundary), skipping the
|
|
17
|
+
* recurring boilerplate sections (Role, Context, Scope, Execution strategy,
|
|
18
|
+
* Configuration). When a lens exposes no bold concern items it falls back to
|
|
19
|
+
* its audit-step headings so every checklist is non-empty. The output is
|
|
20
|
+
* hard-capped at {@link MAX_CHECKLIST_LINES} lines.
|
|
21
|
+
*
|
|
22
|
+
* Pure: no IO, no provider calls, safe to unit-test in isolation.
|
|
23
|
+
*
|
|
24
|
+
* @see Story #4408 — per-lens authoring checklists as drift-gated artifacts.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { clampSummary, extractFrontmatter } from './frontmatter.js';
|
|
28
|
+
|
|
29
|
+
// All RegExp instances are built via the constructor (rather than literal
|
|
30
|
+
// `/.../`) so the maintainability engine's AST walker (typhonjs-escomplex) can
|
|
31
|
+
// score this file — see the note in lib/audit-suite/frontmatter.js.
|
|
32
|
+
// biome-ignore-start lint/complexity/useRegexLiterals: typhonjs-escomplex MI workaround
|
|
33
|
+
const LINE_SPLIT_RE = new RegExp(String.raw`\r?\n`);
|
|
34
|
+
const FRONTMATTER_BLOCK_RE = new RegExp(
|
|
35
|
+
String.raw`^---\r?\n([\s\S]*?)\r?\n---`,
|
|
36
|
+
);
|
|
37
|
+
const BLOCK_SCALAR_INDICATOR_RE = new RegExp('^[|>][+-]?$');
|
|
38
|
+
const DESCRIPTION_KEY_RE = new RegExp(String.raw`^description\s*:`);
|
|
39
|
+
const INDENTED_LINE_RE = new RegExp(String.raw`^\s+\S`);
|
|
40
|
+
const COLLAPSE_WS_RE = new RegExp(String.raw`\s+`, 'g');
|
|
41
|
+
const HEADING_RE = new RegExp(String.raw`^(#{1,6})\s+(.*\S)\s*$`);
|
|
42
|
+
const BOLD_LEAD_ITEM_RE = new RegExp(
|
|
43
|
+
String.raw`^\s*(?:[-*+]|\d+\.)\s+\*\*(.+?)\*\*`,
|
|
44
|
+
);
|
|
45
|
+
const STEP_PREFIX_RE = new RegExp(String.raw`^Step\s+\d+\s*[:.]?\s*`, 'i');
|
|
46
|
+
const TRAILING_COLON_RE = new RegExp(String.raw`\s*:\s*$`);
|
|
47
|
+
// A `##` section whose heading opens with one of these is workflow boilerplate,
|
|
48
|
+
// not a lens concern — its list items are skipped during concern extraction.
|
|
49
|
+
const BOILERPLATE_SECTION_RE = new RegExp(
|
|
50
|
+
String.raw`^(Role|Context & Objective|Scope|Execution strategy|Configuration|Run Context|Target set)\b`,
|
|
51
|
+
'i',
|
|
52
|
+
);
|
|
53
|
+
// The report-template / output-contract boundary. Everything at or after the
|
|
54
|
+
// first heading matching this is template scaffolding, never a concern.
|
|
55
|
+
const OUTPUT_BOUNDARY_RE = new RegExp(
|
|
56
|
+
'(Output Requirements|Generate the Report)',
|
|
57
|
+
'i',
|
|
58
|
+
);
|
|
59
|
+
// biome-ignore-end lint/complexity/useRegexLiterals: typhonjs-escomplex MI workaround
|
|
60
|
+
|
|
61
|
+
/** Hard cap on generated checklist lines (asserted by the drift test). */
|
|
62
|
+
export const MAX_CHECKLIST_LINES = 40;
|
|
63
|
+
|
|
64
|
+
/** Fixed header/intro line budget consumed before the checklist items. */
|
|
65
|
+
const HEADER_LINE_BUDGET = 12;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pure: walk the workflow body once, collecting the lens's concern labels (bold
|
|
69
|
+
* lead-ins of analysis/evaluation list items) and, as a fallback, its audit
|
|
70
|
+
* step headings. Extraction stops at the report-template boundary and ignores
|
|
71
|
+
* boilerplate sections.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} content — raw `audit-<lens>.md` markdown.
|
|
74
|
+
* @returns {{ title: string|null, concerns: string[], stepHeadings: string[] }}
|
|
75
|
+
*/
|
|
76
|
+
export function extractLensConcerns(content) {
|
|
77
|
+
const lines = String(content ?? '').split(LINE_SPLIT_RE);
|
|
78
|
+
const concerns = [];
|
|
79
|
+
const stepHeadings = [];
|
|
80
|
+
let title = null;
|
|
81
|
+
let inBoilerplate = false;
|
|
82
|
+
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const heading = HEADING_RE.exec(line);
|
|
85
|
+
if (heading) {
|
|
86
|
+
const level = heading[1].length;
|
|
87
|
+
const text = heading[2].trim();
|
|
88
|
+
if (level === 1) {
|
|
89
|
+
// First H1 is the lens title; a later H1 opens the report template.
|
|
90
|
+
if (title === null) {
|
|
91
|
+
title = text;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (OUTPUT_BOUNDARY_RE.test(text)) break;
|
|
97
|
+
if (level === 2) {
|
|
98
|
+
inBoilerplate = BOILERPLATE_SECTION_RE.test(text);
|
|
99
|
+
if (!inBoilerplate) {
|
|
100
|
+
stepHeadings.push(text.replace(STEP_PREFIX_RE, '').trim());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (title === null || inBoilerplate) continue;
|
|
106
|
+
const item = BOLD_LEAD_ITEM_RE.exec(line);
|
|
107
|
+
if (item) {
|
|
108
|
+
const label = item[1].replace(TRAILING_COLON_RE, '').trim();
|
|
109
|
+
if (label) concerns.push(label);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { title, concerns, stepHeadings };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pure: resolve a workflow's `description` to a single clamped line, folding a
|
|
118
|
+
* YAML block scalar (`description: >-` / `|` with indented continuation lines)
|
|
119
|
+
* back into one line. `extractFrontmatter` only sees the indicator token
|
|
120
|
+
* (`>-`) for a block scalar, so this reads the continuation lines directly.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} content — raw workflow markdown.
|
|
123
|
+
* @param {Record<string, string>} fm — parsed frontmatter map.
|
|
124
|
+
* @returns {string} a single-line description (possibly empty).
|
|
125
|
+
*/
|
|
126
|
+
export function resolveDescription(content, fm) {
|
|
127
|
+
const raw = (fm.description ?? '').trim();
|
|
128
|
+
if (raw && !BLOCK_SCALAR_INDICATOR_RE.test(raw)) {
|
|
129
|
+
return clampSummary(raw).replace(COLLAPSE_WS_RE, ' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const block = FRONTMATTER_BLOCK_RE.exec(String(content ?? ''));
|
|
133
|
+
if (!block) return '';
|
|
134
|
+
const lines = block[1].split(LINE_SPLIT_RE);
|
|
135
|
+
const start = lines.findIndex((line) => DESCRIPTION_KEY_RE.test(line));
|
|
136
|
+
if (start === -1) return '';
|
|
137
|
+
const collected = [];
|
|
138
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
139
|
+
if (INDENTED_LINE_RE.test(lines[i])) {
|
|
140
|
+
collected.push(lines[i].trim());
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
break; // a blank line or a dedented next key ends the scalar.
|
|
144
|
+
}
|
|
145
|
+
return clampSummary(collected.join(' ')).replace(COLLAPSE_WS_RE, ' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pure: dedupe a list case-insensitively, preserving first-seen order.
|
|
150
|
+
*
|
|
151
|
+
* @param {string[]} items
|
|
152
|
+
* @returns {string[]}
|
|
153
|
+
*/
|
|
154
|
+
function dedupePreserveOrder(items) {
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
const key = item.toLowerCase();
|
|
159
|
+
if (seen.has(key)) continue;
|
|
160
|
+
seen.add(key);
|
|
161
|
+
out.push(item);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Pure: render the authoring checklist for a single lens from its workflow
|
|
168
|
+
* markdown. The output is deterministic (a pure function of `content`) and
|
|
169
|
+
* hard-capped at {@link MAX_CHECKLIST_LINES} lines including the trailing
|
|
170
|
+
* newline, so the same input always regenerates byte-identically.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} lens — canonical lens name (e.g. `security`).
|
|
173
|
+
* @param {string} content — raw `audit-<lens>.md` markdown.
|
|
174
|
+
* @returns {string} the checklist markdown (ends with a single newline).
|
|
175
|
+
*/
|
|
176
|
+
export function renderLensChecklist(lens, content) {
|
|
177
|
+
const fm = extractFrontmatter(content);
|
|
178
|
+
const description = resolveDescription(content, fm);
|
|
179
|
+
const { title, concerns, stepHeadings } = extractLensConcerns(content);
|
|
180
|
+
const displayTitle = title || `audit-${lens}`;
|
|
181
|
+
|
|
182
|
+
let items = dedupePreserveOrder(concerns);
|
|
183
|
+
if (items.length === 0) items = dedupePreserveOrder(stepHeadings);
|
|
184
|
+
if (items.length === 0) items = ['Review the full lens workflow'];
|
|
185
|
+
|
|
186
|
+
const header = [
|
|
187
|
+
'<!-- GENERATED FILE — do not edit by hand.',
|
|
188
|
+
` Source of truth: .agents/workflows/audit-${lens}.md`,
|
|
189
|
+
' Regenerate: node .agents/scripts/generate-lens-checklists.js',
|
|
190
|
+
' Drift is gated by: npm run docs:check',
|
|
191
|
+
'-->',
|
|
192
|
+
'',
|
|
193
|
+
`# ${displayTitle} — authoring checklist`,
|
|
194
|
+
'',
|
|
195
|
+
`> ${description || `Self-check your change against the ${lens} lens.`}`,
|
|
196
|
+
'',
|
|
197
|
+
"Self-check your change against this lens's concerns before you ship:",
|
|
198
|
+
'',
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
// Reserve room for a truncation marker so the cap is never exceeded.
|
|
202
|
+
const maxItems = MAX_CHECKLIST_LINES - HEADER_LINE_BUDGET;
|
|
203
|
+
let itemLines;
|
|
204
|
+
if (items.length > maxItems) {
|
|
205
|
+
itemLines = items.slice(0, maxItems - 1).map((c) => `- [ ] ${c}`);
|
|
206
|
+
itemLines.push('- [ ] …see the full lens for the remaining concerns');
|
|
207
|
+
} else {
|
|
208
|
+
itemLines = items.map((c) => `- [ ] ${c}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return `${[...header, ...itemLines].join('\n')}\n`;
|
|
212
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* mapping. All rule-matching lives here.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
19
20
|
import fs from 'node:fs/promises';
|
|
20
21
|
import path from 'node:path';
|
|
21
22
|
import picomatch from 'picomatch';
|
|
@@ -62,6 +63,141 @@ export function isGlobalLens(lens) {
|
|
|
62
63
|
return GLOBAL_LENS_ALLOWLIST.includes(lens);
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The canonical concern-ownership tiers a lens can declare via its
|
|
68
|
+
* `scope` field in [`audit-rules.json`](../../../schemas/audit-rules.json).
|
|
69
|
+
* This frozen tuple is the single source of truth for the tier vocabulary the
|
|
70
|
+
* schema's `scope` enum enforces and {@link resolveLensTier} returns:
|
|
71
|
+
*
|
|
72
|
+
* - `local` — decidable from a single Story's diff; verified at
|
|
73
|
+
* write-time and Story-scope review, not re-run at Epic close.
|
|
74
|
+
* - `cumulative` — only decidable across the Epic's combined diff; verified
|
|
75
|
+
* at Epic close.
|
|
76
|
+
* - `global` — evaluates a whole-product property regardless of the diff;
|
|
77
|
+
* verified at Epic close, exempt from change-set narrowing.
|
|
78
|
+
*
|
|
79
|
+
* @type {readonly ['local', 'cumulative', 'global']}
|
|
80
|
+
*/
|
|
81
|
+
export const LENS_TIERS = Object.freeze(['local', 'cumulative', 'global']);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the concern-ownership tier a lens declares in `audit-rules.json`.
|
|
85
|
+
* This is the pure read-side of the `scope` field (Epic #4405, Story #4407)
|
|
86
|
+
* that replaced the former `alwaysRun` special case: every downstream tier —
|
|
87
|
+
* write-time checklist threading, Story-scope review, the Epic-close roster
|
|
88
|
+
* split — routes off this one field instead of a maintained prose constraint.
|
|
89
|
+
*
|
|
90
|
+
* Deterministic given the on-disk manifest: it reads the same
|
|
91
|
+
* `audit-rules.json` that {@link selectAudits} consumes (resolved through the
|
|
92
|
+
* project's configured `schemasRoot`), looks up the lens, and returns its
|
|
93
|
+
* `scope`. It takes no ticket, runs no git, and has no side effects.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} lens Lens key registered in `audit-rules.json`
|
|
96
|
+
* (e.g. `audit-clean-code`).
|
|
97
|
+
* @returns {'local' | 'cumulative' | 'global'} The lens's declared tier.
|
|
98
|
+
* @throws {Error} When `lens` is not registered in the manifest, or the
|
|
99
|
+
* manifest cannot be read, or the registered entry carries a scope outside
|
|
100
|
+
* {@link LENS_TIERS}.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Read and parse the `audit-rules.json` manifest synchronously from the
|
|
104
|
+
* project's configured `schemasRoot`. Shared by the synchronous, ticket-free
|
|
105
|
+
* readers ({@link resolveLensTier}, {@link selectLocalLenses}) so the path
|
|
106
|
+
* resolution and read-failure handling live in one place rather than being
|
|
107
|
+
* duplicated per reader.
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ audits?: Record<string, object> }} Parsed manifest.
|
|
110
|
+
* @throws {Error} When the manifest cannot be read or parsed.
|
|
111
|
+
*/
|
|
112
|
+
function readAuditRulesSync() {
|
|
113
|
+
const { agentSettings } = resolveConfig();
|
|
114
|
+
const rulesPath = path.join(
|
|
115
|
+
PROJECT_ROOT,
|
|
116
|
+
getPaths({ agentSettings }).schemasRoot,
|
|
117
|
+
'audit-rules.json',
|
|
118
|
+
);
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(readFileSync(rulesPath, 'utf8'));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`audit-suite: failed to read audit-rules from ${rulesPath}: ${err.message}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resolveLensTier(lens) {
|
|
129
|
+
const rulesData = readAuditRulesSync();
|
|
130
|
+
|
|
131
|
+
const entry = rulesData.audits?.[lens];
|
|
132
|
+
if (!entry) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`resolveLensTier: unknown lens '${lens}' — not registered in audit-rules.json`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { scope } = entry;
|
|
139
|
+
if (!LENS_TIERS.includes(scope)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`resolveLensTier: lens '${lens}' declares invalid scope '${scope}'; expected one of ${LENS_TIERS.join(', ')}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return scope;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Select the LOCAL-tier lenses whose `filePatterns` triggers match a change
|
|
150
|
+
* set. This is the Story-scope roster used by the maker-blind story-close
|
|
151
|
+
* review (Epic #4405, Story #4409): a lens is selected iff
|
|
152
|
+
* `resolveLensTier(lens) === 'local'` **and** the pure
|
|
153
|
+
* {@link matchesAnyFilePattern} matcher hits at least one of `changedFiles`
|
|
154
|
+
* against the lens's registered `triggers.filePatterns`.
|
|
155
|
+
*
|
|
156
|
+
* This deliberately does **not** call {@link selectAudits}: `selectAudits`
|
|
157
|
+
* unions in keyword-matched and gate-scoped lenses and has no per-tier gate,
|
|
158
|
+
* so it would widen the roster beyond the local, footprint-matched set the
|
|
159
|
+
* shift-left Story-scope tier owns. A local lens with a universal
|
|
160
|
+
* `filePatterns` glob (e.g. `audit-clean-code`, whose sole pattern matches
|
|
161
|
+
* every path) matches every change set here, so its concern is verified at
|
|
162
|
+
* BOTH innermost tiers — the
|
|
163
|
+
* write-time checklist threading and this Story-scope lens pass — and excluded
|
|
164
|
+
* from Epic close (a local lens is dropped by {@link selectEpicCloseLenses}).
|
|
165
|
+
* A local lens with an empty `filePatterns` list matches nothing here, so a
|
|
166
|
+
* diff matching no local lens's patterns yields an empty roster and adds no
|
|
167
|
+
* lens work.
|
|
168
|
+
*
|
|
169
|
+
* Pure over its injected seams: `injectedRules` skips the disk read of the
|
|
170
|
+
* manifest and `resolveLensTierFn` overrides the tier resolver, so callers can
|
|
171
|
+
* exercise the selection without touching the filesystem. Selection order
|
|
172
|
+
* follows the manifest's declaration order, which is deterministic.
|
|
173
|
+
*
|
|
174
|
+
* @param {{
|
|
175
|
+
* changedFiles?: string[],
|
|
176
|
+
* injectedRules?: { audits?: Record<string, object> },
|
|
177
|
+
* resolveLensTierFn?: typeof resolveLensTier,
|
|
178
|
+
* }} [params]
|
|
179
|
+
* @returns {string[]} The matched local-lens identifiers, in manifest order.
|
|
180
|
+
*/
|
|
181
|
+
export function selectLocalLenses({
|
|
182
|
+
changedFiles,
|
|
183
|
+
injectedRules,
|
|
184
|
+
resolveLensTierFn = resolveLensTier,
|
|
185
|
+
} = {}) {
|
|
186
|
+
const files = Array.isArray(changedFiles) ? changedFiles : [];
|
|
187
|
+
if (files.length === 0) return [];
|
|
188
|
+
|
|
189
|
+
const rules = injectedRules ?? readAuditRulesSync();
|
|
190
|
+
const selected = [];
|
|
191
|
+
for (const [lens, entry] of Object.entries(rules.audits ?? {})) {
|
|
192
|
+
if (resolveLensTierFn(lens) !== 'local') continue;
|
|
193
|
+
const patterns = entry?.triggers?.filePatterns ?? [];
|
|
194
|
+
if (matchesAnyFilePattern(patterns, files)) {
|
|
195
|
+
selected.push(lens);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return selected;
|
|
199
|
+
}
|
|
200
|
+
|
|
65
201
|
/**
|
|
66
202
|
* Resolve the consumer's navigability route globs from the resolved config.
|
|
67
203
|
* Reads `delivery.quality.navigability.routeGlobs` — the route-tree SSOT the
|
|
@@ -264,11 +400,6 @@ export async function selectAudits({
|
|
|
264
400
|
const gateMatch = triggers.gates?.includes(gate);
|
|
265
401
|
if (!gateMatch) continue;
|
|
266
402
|
|
|
267
|
-
if (triggers.alwaysRun) {
|
|
268
|
-
selectedAudits.push(auditName);
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
403
|
const keywords = triggers.keywords || [];
|
|
273
404
|
let keywordMatch = false;
|
|
274
405
|
for (const kw of keywords) {
|