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,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* .agents/scripts/generate-lens-checklists.js — per-lens authoring checklists
|
|
4
|
+
*
|
|
5
|
+
* Distils each canonical audit lens's own `.agents/workflows/audit-<lens>.md`
|
|
6
|
+
* workflow body into one compact authoring checklist under
|
|
7
|
+
* `.agents/audit-checklists/<lens>.md`. The distillation logic is the pure
|
|
8
|
+
* `lib/audit-suite/lens-checklist.js` seam; this entry point owns only the
|
|
9
|
+
* file read/write/prune and the `--check` drift gate.
|
|
10
|
+
*
|
|
11
|
+
* Why (Epic #4405 — shift-left audit): the audit lenses used to surface their
|
|
12
|
+
* concerns only when `/audit-<lens>` ran. These checklists move the concerns to
|
|
13
|
+
* the innermost, write-time tier as committed build artifacts, gated for
|
|
14
|
+
* staleness by `npm run docs:check` exactly like every other generated doc so
|
|
15
|
+
* they can never silently drift from their source workflow.
|
|
16
|
+
*
|
|
17
|
+
* The lens taxonomy is the SSOT `AUDIT_LENSES` list. A lens whose
|
|
18
|
+
* `audit-<lens>.md` workflow is absent produces **no** checklist and is
|
|
19
|
+
* reported (never a silent skip). Stray `.md` files under the checklist
|
|
20
|
+
* directory that no longer map to a lens are pruned in write mode and flagged
|
|
21
|
+
* in `--check` mode, keeping the directory a pure function of its sources.
|
|
22
|
+
*
|
|
23
|
+
* Modes:
|
|
24
|
+
* (default) — writes one `<lens>.md` per lens with a workflow, prunes
|
|
25
|
+
* strays, reports lenses missing a workflow.
|
|
26
|
+
* --check — exits 0 when every on-disk file matches the freshly generated
|
|
27
|
+
* content and no strays exist, throws (→ exit 1) otherwise.
|
|
28
|
+
*
|
|
29
|
+
* Per `.agents/rules/orchestration-error-handling.md`, unrecoverable failures
|
|
30
|
+
* surface via `throw new Error(...)` so `runAsCli` maps the throw to
|
|
31
|
+
* `process.exit(1)` deterministically (no `Logger.fatal`).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
import { parseArgs } from 'node:util';
|
|
38
|
+
import { renderLensChecklist } from './lib/audit-suite/lens-checklist.js';
|
|
39
|
+
import { AUDIT_LENSES } from './lib/audit-to-stories/audit-lenses.js';
|
|
40
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
41
|
+
import { Logger } from './lib/Logger.js';
|
|
42
|
+
|
|
43
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
44
|
+
const __dirname = path.dirname(__filename);
|
|
45
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
46
|
+
const WORKFLOWS_DIR = path.join(PROJECT_ROOT, '.agents', 'workflows');
|
|
47
|
+
const CHECKLISTS_DIR = path.join(PROJECT_ROOT, '.agents', 'audit-checklists');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pure: plan the expected checklist set from the lens taxonomy, given a
|
|
51
|
+
* workflow-existence predicate and a reader. A lens whose workflow is absent
|
|
52
|
+
* yields no checklist and is recorded in `missing` (never silently skipped).
|
|
53
|
+
* Kept side-effect-free so it is unit-testable with fabricated inputs.
|
|
54
|
+
*
|
|
55
|
+
* @param {ReadonlyArray<string>} lenses — canonical lens names.
|
|
56
|
+
* @param {(lens: string) => boolean} workflowExists
|
|
57
|
+
* @param {(lens: string) => string} readWorkflow
|
|
58
|
+
* @returns {{ expected: Map<string, string>, missing: string[] }}
|
|
59
|
+
*/
|
|
60
|
+
export function planChecklists(lenses, workflowExists, readWorkflow) {
|
|
61
|
+
const expected = new Map();
|
|
62
|
+
const missing = [];
|
|
63
|
+
for (const lens of lenses) {
|
|
64
|
+
if (!workflowExists(lens)) {
|
|
65
|
+
missing.push(lens);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
expected.set(`${lens}.md`, renderLensChecklist(lens, readWorkflow(lens)));
|
|
69
|
+
}
|
|
70
|
+
return { expected, missing };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the full expected checklist set from the lens taxonomy and the on-disk
|
|
75
|
+
* workflow bodies, plus the strays (on-disk checklist files mapping to no
|
|
76
|
+
* current lens).
|
|
77
|
+
*
|
|
78
|
+
* @returns {{
|
|
79
|
+
* expected: Map<string, string>,
|
|
80
|
+
* missing: string[],
|
|
81
|
+
* strays: string[],
|
|
82
|
+
* }} `expected` maps a checklist basename (`<lens>.md`) to its generated
|
|
83
|
+
* content; `missing` lists lenses with no `audit-<lens>.md`; `strays` lists
|
|
84
|
+
* on-disk checklist basenames that map to no current lens.
|
|
85
|
+
*/
|
|
86
|
+
export function buildExpected() {
|
|
87
|
+
const workflowPath = (lens) => path.join(WORKFLOWS_DIR, `audit-${lens}.md`);
|
|
88
|
+
const { expected, missing } = planChecklists(
|
|
89
|
+
AUDIT_LENSES,
|
|
90
|
+
(lens) => fs.existsSync(workflowPath(lens)),
|
|
91
|
+
(lens) => fs.readFileSync(workflowPath(lens), 'utf8'),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const onDisk = fs.existsSync(CHECKLISTS_DIR)
|
|
95
|
+
? fs.readdirSync(CHECKLISTS_DIR).filter((name) => name.endsWith('.md'))
|
|
96
|
+
: [];
|
|
97
|
+
const strays = onDisk.filter((name) => !expected.has(name));
|
|
98
|
+
|
|
99
|
+
return { expected, missing, strays };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} basename — e.g. `security.md`
|
|
104
|
+
* @returns {string} repo-relative POSIX path for messages.
|
|
105
|
+
*/
|
|
106
|
+
function relChecklist(basename) {
|
|
107
|
+
return path
|
|
108
|
+
.relative(PROJECT_ROOT, path.join(CHECKLISTS_DIR, basename))
|
|
109
|
+
.split(path.sep)
|
|
110
|
+
.join('/');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string[]} argv
|
|
115
|
+
*/
|
|
116
|
+
async function main(argv = process.argv.slice(2)) {
|
|
117
|
+
const { values } = parseArgs({
|
|
118
|
+
args: argv,
|
|
119
|
+
options: { check: { type: 'boolean', default: false } },
|
|
120
|
+
allowPositionals: false,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { expected, missing, strays } = buildExpected();
|
|
124
|
+
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
Logger.info(
|
|
127
|
+
`generate-lens-checklists: no audit-<lens>.md for: ${missing.join(', ')} — no checklist emitted.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (values.check) {
|
|
132
|
+
const drifted = [];
|
|
133
|
+
for (const [basename, content] of expected) {
|
|
134
|
+
const target = path.join(CHECKLISTS_DIR, basename);
|
|
135
|
+
const original = fs.existsSync(target)
|
|
136
|
+
? fs.readFileSync(target, 'utf8')
|
|
137
|
+
: null;
|
|
138
|
+
if (original !== content) drifted.push(relChecklist(basename));
|
|
139
|
+
}
|
|
140
|
+
if (drifted.length === 0 && strays.length === 0) {
|
|
141
|
+
Logger.info(
|
|
142
|
+
`generate-lens-checklists: ${expected.size} checklist(s) up to date.`,
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const problems = [
|
|
147
|
+
...drifted.map((p) => `out of date: ${p}`),
|
|
148
|
+
...strays.map((s) => `stray (no lens): ${relChecklist(s)}`),
|
|
149
|
+
];
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Lens checklists are out of sync:\n ${problems.join('\n ')}\n` +
|
|
152
|
+
'Run `node .agents/scripts/generate-lens-checklists.js` to regenerate.',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(CHECKLISTS_DIR, { recursive: true });
|
|
157
|
+
let wrote = 0;
|
|
158
|
+
for (const [basename, content] of expected) {
|
|
159
|
+
const target = path.join(CHECKLISTS_DIR, basename);
|
|
160
|
+
const original = fs.existsSync(target)
|
|
161
|
+
? fs.readFileSync(target, 'utf8')
|
|
162
|
+
: null;
|
|
163
|
+
if (original === content) continue;
|
|
164
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
165
|
+
wrote += 1;
|
|
166
|
+
}
|
|
167
|
+
for (const stray of strays) {
|
|
168
|
+
fs.rmSync(path.join(CHECKLISTS_DIR, stray));
|
|
169
|
+
Logger.info(
|
|
170
|
+
`generate-lens-checklists: pruned stray ${relChecklist(stray)}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
Logger.info(
|
|
174
|
+
`generate-lens-checklists: wrote ${wrote} of ${expected.size} checklist(s) (${strays.length} pruned).`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export { CHECKLISTS_DIR, WORKFLOWS_DIR };
|
|
179
|
+
|
|
180
|
+
runAsCli(import.meta.url, main, { source: 'generate-lens-checklists' });
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-suite/checklist-threading.js — write-time local-lens checklist
|
|
3
|
+
* threading (Epic #4405, Story #4410).
|
|
4
|
+
*
|
|
5
|
+
* Given a Story's predicted footprint (its `changes[]` / `references[]` path
|
|
6
|
+
* list), select the distilled authoring checklists for the **footprint-matched
|
|
7
|
+
* LOCAL lenses only** and assemble them into one payload the dispatch/hydrator
|
|
8
|
+
* seam threads into the Story maker prompt. This moves each local-lens concern
|
|
9
|
+
* to the innermost, write-time tier so makers author against it on fresh
|
|
10
|
+
* context instead of paying an Epic-close remediation loop on stale context.
|
|
11
|
+
*
|
|
12
|
+
* Matching is the **light** path, deliberately NOT `selectAudits`:
|
|
13
|
+
* - tier gate — a lens is in scope only when `resolveLensTier(lens) ===
|
|
14
|
+
* 'local'` (the pure `scope`-field read; cumulative/global
|
|
15
|
+
* lenses are owned by the Epic-close tier, never threaded
|
|
16
|
+
* here).
|
|
17
|
+
* - file gate — the pure `matchesAnyFilePattern` matcher runs the lens's
|
|
18
|
+
* own `triggers.filePatterns` (from `audit-rules.json`)
|
|
19
|
+
* against the predicted footprint.
|
|
20
|
+
*
|
|
21
|
+
* `selectAudits` is the wrong tool here: it is gate-aware and provider-backed
|
|
22
|
+
* (it reads the ticket through an `ITicketingProvider` and diffs the working
|
|
23
|
+
* tree via `git`). The write-time path has a *predicted* footprint, not a git
|
|
24
|
+
* diff, and no ticket to fetch — so this module reaches for the two pure
|
|
25
|
+
* primitives directly and never touches a provider or `git`. That purity is
|
|
26
|
+
* load-bearing (asserted by the test): the threading path must be a pure
|
|
27
|
+
* function of the footprint and the on-disk manifest/checklists.
|
|
28
|
+
*
|
|
29
|
+
* A hard token budget caps the assembled payload. When the matched lenses'
|
|
30
|
+
* checklists together exceed the budget, the payload is truncated
|
|
31
|
+
* deterministically (a stable prefix in {@link AUDIT_LENSES} order is kept;
|
|
32
|
+
* the overflowing tail is dropped) and every dropped lens is logged so the
|
|
33
|
+
* elision is never silent.
|
|
34
|
+
*
|
|
35
|
+
* Pure (modulo the default disk reads for `audit-rules.json` and the checklist
|
|
36
|
+
* artifacts, both injectable seams). No git, no provider, no network.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { AUDIT_LENSES } from '../audit-to-stories/audit-lenses.js';
|
|
42
|
+
import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
|
|
43
|
+
import { Logger } from '../Logger.js';
|
|
44
|
+
import { estimateTokens } from '../orchestration/context-envelope.js';
|
|
45
|
+
import { matchesAnyFilePattern, resolveLensTier } from './selector.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hard cap on the assembled checklist payload, in the ≈4-char/token estimate
|
|
49
|
+
* shared with the rest of the hydrator ({@link estimateTokens}). Generous
|
|
50
|
+
* relative to the real checklist sizes (each distilled lens checklist is
|
|
51
|
+
* ~130–190 tokens, and at most the seven local lenses can match), so a normal
|
|
52
|
+
* Story is never truncated — the cap is a safety ceiling against a pathological
|
|
53
|
+
* footprint that matches every local lens, not a routine squeeze. Callers may
|
|
54
|
+
* override per-invocation via `tokenBudget`.
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_CHECKLIST_TOKEN_BUDGET = 4000;
|
|
57
|
+
|
|
58
|
+
/** Separator between concatenated per-lens checklist sections. */
|
|
59
|
+
const SECTION_SEPARATOR = '\n\n';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The `audit-rules.json` / `resolveLensTier` lens key for a canonical lens
|
|
63
|
+
* name. The taxonomy in {@link AUDIT_LENSES} carries bare names (`clean-code`);
|
|
64
|
+
* the manifest and the tier resolver key off the `audit-`-prefixed form
|
|
65
|
+
* (`audit-clean-code`).
|
|
66
|
+
*
|
|
67
|
+
* @param {string} lens
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function lensKeyFor(lens) {
|
|
71
|
+
return `audit-${lens}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Absolute path to the committed distilled-checklist directory. Mirrors the
|
|
76
|
+
* SSOT the generator writes to (`generate-lens-checklists.js` →
|
|
77
|
+
* `<agentRoot>/audit-checklists/<lens>.md`), resolved through the same
|
|
78
|
+
* `agentRoot` the rest of the config surface uses so a consumer that relocates
|
|
79
|
+
* `.agents` reads its checklists from the relocated tree.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} [config] resolved config wrapper (defaults to a fresh read).
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function checklistsDir(config = resolveConfig()) {
|
|
85
|
+
return path.join(
|
|
86
|
+
PROJECT_ROOT,
|
|
87
|
+
getPaths(config).agentRoot,
|
|
88
|
+
'audit-checklists',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read and parse `audit-rules.json` (the same manifest {@link selectAudits}
|
|
94
|
+
* and {@link resolveLensTier} consume), resolved through the configured
|
|
95
|
+
* `schemasRoot`. Synchronous — this is a pure-ish read with no git and no
|
|
96
|
+
* provider.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} [config] resolved config wrapper (defaults to a fresh read).
|
|
99
|
+
* @returns {{ audits?: Record<string, { triggers?: { filePatterns?: string[] } }> }}
|
|
100
|
+
*/
|
|
101
|
+
export function readAuditRules(config = resolveConfig()) {
|
|
102
|
+
const rulesPath = path.join(
|
|
103
|
+
PROJECT_ROOT,
|
|
104
|
+
getPaths(config).schemasRoot,
|
|
105
|
+
'audit-rules.json',
|
|
106
|
+
);
|
|
107
|
+
return JSON.parse(fs.readFileSync(rulesPath, 'utf8'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The `triggers.filePatterns` a lens declares in the manifest, or `[]` when
|
|
112
|
+
* the lens declares none. (`audit-clean-code` carries the universal
|
|
113
|
+
* match-everything glob, so it footprint-matches every non-empty change
|
|
114
|
+
* set — the selector.js twin comment documents the same post-#4405
|
|
115
|
+
* follow-up fix.)
|
|
116
|
+
*
|
|
117
|
+
* @param {object} rules parsed `audit-rules.json`.
|
|
118
|
+
* @param {string} lens canonical lens name.
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
function filePatternsFor(rules, lens) {
|
|
122
|
+
return rules?.audits?.[lensKeyFor(lens)]?.triggers?.filePatterns ?? [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize a predicted footprint into a clean path list. Accepts a plain
|
|
127
|
+
* `string[]` (the shape the caller derives from a Story's `changes[]` /
|
|
128
|
+
* `references[]` entries); drops empty and non-string entries.
|
|
129
|
+
*
|
|
130
|
+
* @param {unknown} footprint
|
|
131
|
+
* @returns {string[]}
|
|
132
|
+
*/
|
|
133
|
+
function normalizeFootprint(footprint) {
|
|
134
|
+
if (!Array.isArray(footprint)) return [];
|
|
135
|
+
return footprint
|
|
136
|
+
.filter((p) => typeof p === 'string')
|
|
137
|
+
.map((p) => p.trim())
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Select, in stable {@link AUDIT_LENSES} order, the LOCAL lenses whose
|
|
143
|
+
* `filePatterns` match the predicted footprint. This is the whole matching
|
|
144
|
+
* contract — `resolveLensTier(lens) === 'local'` for the tier gate plus the
|
|
145
|
+
* pure `matchesAnyFilePattern` matcher for the file gate — with no provider and
|
|
146
|
+
* no git diff.
|
|
147
|
+
*
|
|
148
|
+
* A lens whose tier cannot be resolved (not registered / malformed scope) is
|
|
149
|
+
* skipped rather than allowed to throw the whole selection; the generator and
|
|
150
|
+
* schema keep the manifest well-formed, so this only guards against drift.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} params
|
|
153
|
+
* @param {string[]} params.footprint predicted footprint path list.
|
|
154
|
+
* @param {object} [params.rules] parsed `audit-rules.json` (injectable seam).
|
|
155
|
+
* @param {(lens: string) => string} [params.resolveTier] tier resolver
|
|
156
|
+
* (injectable seam; defaults to {@link resolveLensTier}).
|
|
157
|
+
* @returns {string[]} matched local lens names, in taxonomy order.
|
|
158
|
+
*/
|
|
159
|
+
export function matchLocalLenses({
|
|
160
|
+
footprint,
|
|
161
|
+
rules = readAuditRules(),
|
|
162
|
+
resolveTier = resolveLensTier,
|
|
163
|
+
} = {}) {
|
|
164
|
+
const paths = normalizeFootprint(footprint);
|
|
165
|
+
if (paths.length === 0) return [];
|
|
166
|
+
|
|
167
|
+
const matched = [];
|
|
168
|
+
for (const lens of AUDIT_LENSES) {
|
|
169
|
+
let tier;
|
|
170
|
+
try {
|
|
171
|
+
tier = resolveTier(lensKeyFor(lens));
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (tier !== 'local') continue;
|
|
176
|
+
|
|
177
|
+
const patterns = filePatternsFor(rules, lens);
|
|
178
|
+
if (patterns.length === 0) continue;
|
|
179
|
+
if (matchesAnyFilePattern(patterns, paths)) matched.push(lens);
|
|
180
|
+
}
|
|
181
|
+
return matched;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Default checklist reader: read `<agentRoot>/audit-checklists/<lens>.md`.
|
|
186
|
+
* Returns `null` when the artifact is absent (a matched lens with no committed
|
|
187
|
+
* checklist), so the caller skips it rather than crashing.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} lens
|
|
190
|
+
* @param {object} [config]
|
|
191
|
+
* @returns {string|null}
|
|
192
|
+
*/
|
|
193
|
+
function readChecklistFile(lens, config = resolveConfig()) {
|
|
194
|
+
const file = path.join(checklistsDir(config), `${lens}.md`);
|
|
195
|
+
try {
|
|
196
|
+
return fs.readFileSync(file, 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the write-time checklist payload for a Story's predicted footprint.
|
|
204
|
+
*
|
|
205
|
+
* Returns the assembled payload string plus the accounting the caller (and the
|
|
206
|
+
* test) asserts against: which lenses were included, which were dropped by the
|
|
207
|
+
* budget, the full matched set, and the payload's estimated token count.
|
|
208
|
+
*
|
|
209
|
+
* Truncation is deterministic prefix truncation: lenses are considered in
|
|
210
|
+
* stable {@link AUDIT_LENSES} order and appended while the running payload
|
|
211
|
+
* estimate stays within `tokenBudget`; the first lens that would overflow — and
|
|
212
|
+
* every remaining matched lens — is dropped, and the drop is logged. The
|
|
213
|
+
* returned `payload` therefore always satisfies
|
|
214
|
+
* `estimateTokens(payload) <= tokenBudget`.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} params
|
|
217
|
+
* @param {string[]} params.footprint predicted footprint (`changes[]` /
|
|
218
|
+
* `references[]` path list).
|
|
219
|
+
* @param {number} [params.tokenBudget] hard cap (defaults to
|
|
220
|
+
* {@link DEFAULT_CHECKLIST_TOKEN_BUDGET}).
|
|
221
|
+
* @param {object} [params.rules] parsed `audit-rules.json` (injectable seam).
|
|
222
|
+
* @param {(lens: string) => string} [params.resolveTier] tier resolver
|
|
223
|
+
* (injectable seam).
|
|
224
|
+
* @param {(lens: string) => (string|null)} [params.readChecklist] checklist
|
|
225
|
+
* reader (injectable seam; defaults to the on-disk reader).
|
|
226
|
+
* @param {{ warn?: (msg: string) => void }} [params.logger] logger for the drop
|
|
227
|
+
* record (defaults to {@link Logger}).
|
|
228
|
+
* @returns {{
|
|
229
|
+
* payload: string,
|
|
230
|
+
* includedLenses: string[],
|
|
231
|
+
* droppedLenses: string[],
|
|
232
|
+
* matchedLenses: string[],
|
|
233
|
+
* estimatedTokens: number,
|
|
234
|
+
* tokenBudget: number,
|
|
235
|
+
* }}
|
|
236
|
+
*/
|
|
237
|
+
export function buildChecklistPayload({
|
|
238
|
+
footprint,
|
|
239
|
+
tokenBudget = DEFAULT_CHECKLIST_TOKEN_BUDGET,
|
|
240
|
+
rules,
|
|
241
|
+
resolveTier,
|
|
242
|
+
readChecklist = readChecklistFile,
|
|
243
|
+
logger = Logger,
|
|
244
|
+
} = {}) {
|
|
245
|
+
const resolvedRules = rules ?? readAuditRules();
|
|
246
|
+
const matchedLenses = matchLocalLenses({
|
|
247
|
+
footprint,
|
|
248
|
+
rules: resolvedRules,
|
|
249
|
+
resolveTier,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const includedLenses = [];
|
|
253
|
+
const droppedLenses = [];
|
|
254
|
+
const sections = [];
|
|
255
|
+
let payload = '';
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < matchedLenses.length; i++) {
|
|
258
|
+
const lens = matchedLenses[i];
|
|
259
|
+
const content = readChecklist(lens);
|
|
260
|
+
if (content == null) {
|
|
261
|
+
// Matched a lens with no committed checklist artifact — skip it (never a
|
|
262
|
+
// budget drop). The generator keeps every lens's checklist in tree, so
|
|
263
|
+
// this only trips on drift, which we surface rather than swallow.
|
|
264
|
+
logger?.warn?.(
|
|
265
|
+
`[checklist-threading] no checklist artifact for matched local lens '${lens}' — skipping`,
|
|
266
|
+
);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const trimmed = content.trim();
|
|
271
|
+
const candidate = [...sections, trimmed].join(SECTION_SEPARATOR);
|
|
272
|
+
if (estimateTokens(candidate) > tokenBudget) {
|
|
273
|
+
// Deterministic prefix truncation: this lens and every remaining matched
|
|
274
|
+
// lens are dropped, keeping the payload within budget.
|
|
275
|
+
droppedLenses.push(...matchedLenses.slice(i));
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
sections.push(trimmed);
|
|
280
|
+
includedLenses.push(lens);
|
|
281
|
+
payload = candidate;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (droppedLenses.length > 0) {
|
|
285
|
+
logger?.warn?.(
|
|
286
|
+
`[checklist-threading] token budget ${tokenBudget} exceeded — dropped ` +
|
|
287
|
+
`${droppedLenses.length} footprint-matched local-lens checklist(s): ` +
|
|
288
|
+
`${droppedLenses.join(', ')}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
payload,
|
|
294
|
+
includedLenses,
|
|
295
|
+
droppedLenses,
|
|
296
|
+
matchedLenses,
|
|
297
|
+
estimatedTokens: estimateTokens(payload),
|
|
298
|
+
tokenBudget,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -35,6 +35,33 @@ export function aggregateSummary(findings) {
|
|
|
35
35
|
return summary;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The single halting rule for the unified `verification-results` findings
|
|
40
|
+
* contract (Story #4411, Epic #4405): a **surviving** (unfixed) Critical
|
|
41
|
+
* finding halts the delivery gate. Every code consumer of the contract —
|
|
42
|
+
* the in-process code-review producer (`runCodeReview`) and the auto-merge
|
|
43
|
+
* integration gate (`evaluateAutoMergePredicate`) — routes its
|
|
44
|
+
* halt-on-critical decision through this one predicate so the rule has a
|
|
45
|
+
* single definition rather than a re-derived `critical > 0` expression at
|
|
46
|
+
* each site.
|
|
47
|
+
*
|
|
48
|
+
* Accepts either the severity **count object** produced by
|
|
49
|
+
* `countBySeverity` / {@link aggregateSummary} (`{ critical: n, ... }`) or a
|
|
50
|
+
* raw `Finding[]`. A non-numeric / absent `critical` count (e.g. the
|
|
51
|
+
* auto-merge gate's "unparseable body" sentinel `null`) is **not** a halt —
|
|
52
|
+
* the caller owns that fail-open path separately. Pure; never throws.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ critical?: unknown }|Array<{ severity?: string }>|null|undefined} input
|
|
55
|
+
* @returns {boolean} `true` when at least one surviving Critical is present.
|
|
56
|
+
*/
|
|
57
|
+
export function hasSurvivingCritical(input) {
|
|
58
|
+
if (Array.isArray(input)) {
|
|
59
|
+
return input.some((finding) => finding?.severity === 'critical');
|
|
60
|
+
}
|
|
61
|
+
const critical = input?.critical;
|
|
62
|
+
return typeof critical === 'number' && critical > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
/**
|
|
39
66
|
* Resolve the per-component rollup map for an envelope. When the envelope
|
|
40
67
|
* already carries a `rollup` block (every writer-produced baseline does), we
|
|
@@ -14,14 +14,23 @@
|
|
|
14
14
|
* import { runAuditSuite, selectAudits } from './lib/audit-suite/index.js';
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
export {
|
|
18
|
+
buildChecklistPayload,
|
|
19
|
+
DEFAULT_CHECKLIST_TOKEN_BUDGET,
|
|
20
|
+
matchLocalLenses,
|
|
21
|
+
readAuditRules,
|
|
22
|
+
} from './checklist-threading.js';
|
|
17
23
|
export { runAuditSuite } from './runner.js';
|
|
18
24
|
export {
|
|
19
25
|
GLOBAL_LENS_ALLOWLIST,
|
|
20
26
|
isGlobalLens,
|
|
27
|
+
LENS_TIERS,
|
|
21
28
|
matchesAnyFilePattern,
|
|
22
29
|
matchesFilePattern,
|
|
23
30
|
NAVIGABILITY_LENS,
|
|
31
|
+
resolveLensTier,
|
|
24
32
|
resolveNavigabilityRouteGlobs,
|
|
25
33
|
routesNavigabilityLens,
|
|
26
34
|
selectAudits,
|
|
35
|
+
selectLocalLenses,
|
|
27
36
|
} from './selector.js';
|