mandrel 2.36.0 → 2.37.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/docs/configuration.md +25 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/docs/CHANGELOG.md +32 -0
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +7 -3
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- package/.agents/scripts/lib/orchestration/recut.js +0 -56
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI: keep the committed `.agentrc` validator in step with its schema
|
|
4
|
+
* (Story #5109).
|
|
5
|
+
*
|
|
6
|
+
* `getAgentrcValidator()` used to compile `AGENTRC_SCHEMA` with AJV in every
|
|
7
|
+
* process that touched configuration — 36 of the 79 entry scripts, at ~35 ms
|
|
8
|
+
* each — to produce a function that is a pure derivative of a literal already
|
|
9
|
+
* committed to the repository. The compile is now done **once, here**, and its
|
|
10
|
+
* output is committed as `lib/generated/agentrc-validator.js`; the runtime
|
|
11
|
+
* imports that module and only falls back to a live compile behind
|
|
12
|
+
* `MANDREL_AGENTRC_VALIDATOR=dynamic`.
|
|
13
|
+
*
|
|
14
|
+
* A generated artifact that nothing re-derives is a lie waiting to happen, so
|
|
15
|
+
* this script joins the standalone-ratchet family — `check-arch-cycles.js`,
|
|
16
|
+
* `check-cyclomatic.js`, `check-dead-exports.js` — and follows their contract:
|
|
17
|
+
*
|
|
18
|
+
* - `--check` regenerates in memory and compares against the committed
|
|
19
|
+
* file. Exit 0 when they agree, exit 1 (naming the fix) when they do not.
|
|
20
|
+
* `npm run lint` runs this, so a schema edit that skips regeneration is
|
|
21
|
+
* refused before it can ship a validator that accepts the wrong config.
|
|
22
|
+
* - No flag rewrites the file. That is the sanctioned motion after a
|
|
23
|
+
* deliberate schema change.
|
|
24
|
+
*
|
|
25
|
+
* The comparison is over the exact bytes, which is what makes the gate
|
|
26
|
+
* meaningful: AJV's standalone emit is deterministic for a given schema and
|
|
27
|
+
* AJV version, so a diff means either the schema moved or the compiler did,
|
|
28
|
+
* and both demand a fresh artifact.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import process from 'node:process';
|
|
35
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
36
|
+
import { AGENTRC_SCHEMA } from './lib/config-settings-schema.js';
|
|
37
|
+
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
|
|
40
|
+
/** Repo-relative home of the committed artifact. */
|
|
41
|
+
export const GENERATED_VALIDATOR_PATH =
|
|
42
|
+
'.agents/scripts/lib/generated/agentrc-validator.js';
|
|
43
|
+
|
|
44
|
+
/** The command that rewrites it — quoted in every failure message. */
|
|
45
|
+
const REGEN_COMMAND = 'npm run validator:gen';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AJV options for the generated validator.
|
|
49
|
+
*
|
|
50
|
+
* `allErrors: true` MUST match the runtime fallback in
|
|
51
|
+
* `config-settings-schema.js#getAgentrcValidator` — it is the difference
|
|
52
|
+
* between reporting one error and reporting all of them, and every caller
|
|
53
|
+
* that renders `validate.errors` would change behaviour if the two diverged.
|
|
54
|
+
* `code.source` is what makes the instance emittable; `code.esm` selects the
|
|
55
|
+
* ES-module emit. Neither touches validation semantics.
|
|
56
|
+
*/
|
|
57
|
+
const AJV_OPTIONS = Object.freeze({
|
|
58
|
+
allErrors: true,
|
|
59
|
+
code: { source: true, esm: true },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Banner prepended to the emitted module.
|
|
64
|
+
*
|
|
65
|
+
* The `createRequire` shim is load-bearing: AJV's `esm` emit still reaches for
|
|
66
|
+
* its shared runtime helpers (`ajv/dist/runtime/*`) with `require(...)`, which
|
|
67
|
+
* is not defined inside an ES module. Handing the generated code a real
|
|
68
|
+
* `require` is the smallest fix that keeps the emit verbatim — rewriting those
|
|
69
|
+
* calls into imports would mean re-deriving AJV's own codegen by hand, and
|
|
70
|
+
* would silently rot the next time it changes shape.
|
|
71
|
+
*
|
|
72
|
+
* The coverage pragma matches `config-settings-schema.js`: generated code has
|
|
73
|
+
* no branches anyone authored, so a coverage figure over it measures nothing.
|
|
74
|
+
*/
|
|
75
|
+
const BANNER = `/* node:coverage ignore file -- generated artifact; see check-generated-validator.js */
|
|
76
|
+
/**
|
|
77
|
+
* GENERATED FILE — DO NOT EDIT.
|
|
78
|
+
*
|
|
79
|
+
* Precompiled AJV validator for \`AGENTRC_SCHEMA\`
|
|
80
|
+
* (.agents/scripts/lib/config-settings-schema.js), emitted by
|
|
81
|
+
* .agents/scripts/check-generated-validator.js.
|
|
82
|
+
*
|
|
83
|
+
* Regenerate with \`${REGEN_COMMAND}\`; \`npm run lint\` fails when this file
|
|
84
|
+
* and the schema disagree.
|
|
85
|
+
*/
|
|
86
|
+
import { createRequire } from 'node:module';
|
|
87
|
+
|
|
88
|
+
const require = createRequire(import.meta.url);
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Emit the standalone validator source for `AGENTRC_SCHEMA`.
|
|
93
|
+
*
|
|
94
|
+
* Exported so the parity test can assert the committed artifact and a fresh
|
|
95
|
+
* emit agree without shelling out to this CLI.
|
|
96
|
+
*
|
|
97
|
+
* @returns {string} Complete module source, newline-terminated.
|
|
98
|
+
*/
|
|
99
|
+
export function generateValidatorSource() {
|
|
100
|
+
const ajvModule = require('ajv');
|
|
101
|
+
const Ajv = ajvModule.default ?? ajvModule;
|
|
102
|
+
const standaloneModule = require('ajv/dist/standalone');
|
|
103
|
+
const standaloneCode = standaloneModule.default ?? standaloneModule;
|
|
104
|
+
const ajv = new Ajv(AJV_OPTIONS);
|
|
105
|
+
const validate = ajv.compile(AGENTRC_SCHEMA);
|
|
106
|
+
const body = standaloneCode(ajv, validate);
|
|
107
|
+
return `${BANNER}\n${body.trim()}\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read the committed artifact, or `null` when it is absent.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} absPath
|
|
114
|
+
* @returns {string | null}
|
|
115
|
+
*/
|
|
116
|
+
function readCommitted(absPath) {
|
|
117
|
+
try {
|
|
118
|
+
return fs.readFileSync(absPath, 'utf-8');
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Top-level CLI entry. Exported so tests can drive both modes through the
|
|
126
|
+
* injected seams rather than by spawning a process.
|
|
127
|
+
*
|
|
128
|
+
* @param {{
|
|
129
|
+
* argv?: string[],
|
|
130
|
+
* cwd?: string,
|
|
131
|
+
* stdout?: { write: (s: string) => void },
|
|
132
|
+
* stderr?: { write: (s: string) => void },
|
|
133
|
+
* generateImpl?: () => string,
|
|
134
|
+
* writeFileImpl?: (p: string, data: string) => void,
|
|
135
|
+
* }} [opts]
|
|
136
|
+
* @returns {number} 0 = in step (or written); 1 = stale artifact.
|
|
137
|
+
*/
|
|
138
|
+
export function runCli({
|
|
139
|
+
argv = process.argv.slice(2),
|
|
140
|
+
cwd = process.cwd(),
|
|
141
|
+
stdout = process.stdout,
|
|
142
|
+
stderr = process.stderr,
|
|
143
|
+
generateImpl = generateValidatorSource,
|
|
144
|
+
writeFileImpl = (p, data) => fs.writeFileSync(p, data),
|
|
145
|
+
} = {}) {
|
|
146
|
+
const checkOnly = argv.includes('--check');
|
|
147
|
+
const absPath = path.resolve(cwd, GENERATED_VALIDATOR_PATH);
|
|
148
|
+
const expected = generateImpl();
|
|
149
|
+
|
|
150
|
+
if (!checkOnly) {
|
|
151
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
152
|
+
writeFileImpl(absPath, expected);
|
|
153
|
+
stdout.write(
|
|
154
|
+
`[generated-validator] wrote ${expected.length} bytes to ${GENERATED_VALIDATOR_PATH}\n`,
|
|
155
|
+
);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const actual = readCommitted(absPath);
|
|
160
|
+
if (actual === null) {
|
|
161
|
+
stderr.write(
|
|
162
|
+
`[generated-validator] ❌ ${GENERATED_VALIDATOR_PATH} is missing — run \`${REGEN_COMMAND}\`\n`,
|
|
163
|
+
);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
if (actual !== expected) {
|
|
167
|
+
stderr.write(
|
|
168
|
+
`[generated-validator] ❌ ${GENERATED_VALIDATOR_PATH} is stale against AGENTRC_SCHEMA ` +
|
|
169
|
+
`(committed ${actual.length} bytes, regenerated ${expected.length}) — run \`${REGEN_COMMAND}\`\n`,
|
|
170
|
+
);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
stdout.write(
|
|
174
|
+
`[generated-validator] ✅ ${GENERATED_VALIDATOR_PATH} is in step with AGENTRC_SCHEMA\n`,
|
|
175
|
+
);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
return runCli();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
runAsCli(import.meta.url, main, {
|
|
184
|
+
source: 'generated-validator',
|
|
185
|
+
propagateExitCode: true,
|
|
186
|
+
errorPrefix: '[generated-validator] ❌ Fatal error',
|
|
187
|
+
usage: {
|
|
188
|
+
invocation: 'node .agents/scripts/check-generated-validator.js [--check]',
|
|
189
|
+
summary:
|
|
190
|
+
'Regenerate the committed AJV validator for `AGENTRC_SCHEMA`, or (with --check) fail when the committed artifact is stale against the schema.',
|
|
191
|
+
flags: [
|
|
192
|
+
[
|
|
193
|
+
'--check',
|
|
194
|
+
'Compare only: exit 1 when the committed artifact differs from a fresh emit. Writes nothing.',
|
|
195
|
+
],
|
|
196
|
+
],
|
|
197
|
+
notes: [
|
|
198
|
+
'Exit codes:\n 0 artifact written, or already in step\n 1 committed artifact is missing or stale',
|
|
199
|
+
`Regenerate with \`${REGEN_COMMAND}\`. The runtime falls back to a live AJV compile when MANDREL_AGENTRC_VALIDATOR=dynamic.`,
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
@@ -78,6 +78,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
78
78
|
|
|
79
79
|
import { runAsCli } from './lib/cli-utils.js';
|
|
80
80
|
import { walkFilesByExtension } from './lib/fs-walk.js';
|
|
81
|
+
import { stripJsComments } from './lib/source-text/strip-js-comments.js';
|
|
81
82
|
|
|
82
83
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
83
84
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -162,73 +163,6 @@ export function findPromiseAllViolations(
|
|
|
162
163
|
return violations;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
/**
|
|
166
|
-
* Strip block (`/* … */`) and line (`// …`) comments from a source
|
|
167
|
-
* string. Pure — exported for tests so the comment-stripping contract
|
|
168
|
-
* is explicit. Defends against the corner case where the literal
|
|
169
|
-
* `'gh pr merge'` appears INSIDE a justification comment at the
|
|
170
|
-
* deletion site (Story #2253 deliberately leaves a prose explanation
|
|
171
|
-
* referencing the removed CLI call).
|
|
172
|
-
*
|
|
173
|
-
* The implementation is a tiny state machine rather than a regex so it
|
|
174
|
-
* correctly handles the (legal) case of a string literal containing
|
|
175
|
-
* `//` or `/*` characters.
|
|
176
|
-
*
|
|
177
|
-
* @param {string} src
|
|
178
|
-
* @returns {string} source with comments replaced by spaces (line
|
|
179
|
-
* numbers preserved so violations report the original line).
|
|
180
|
-
*/
|
|
181
|
-
export function stripComments(src) {
|
|
182
|
-
let out = '';
|
|
183
|
-
let i = 0;
|
|
184
|
-
const n = src.length;
|
|
185
|
-
while (i < n) {
|
|
186
|
-
const ch = src[i];
|
|
187
|
-
const nx = src[i + 1];
|
|
188
|
-
// line comment
|
|
189
|
-
if (ch === '/' && nx === '/') {
|
|
190
|
-
while (i < n && src[i] !== '\n') {
|
|
191
|
-
i += 1;
|
|
192
|
-
}
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
// block comment
|
|
196
|
-
if (ch === '/' && nx === '*') {
|
|
197
|
-
i += 2;
|
|
198
|
-
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
199
|
-
// preserve newlines so line numbers in violation reports stay
|
|
200
|
-
// aligned with the original file.
|
|
201
|
-
if (src[i] === '\n') out += '\n';
|
|
202
|
-
i += 1;
|
|
203
|
-
}
|
|
204
|
-
i += 2; // skip closing */
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
// string literal — copy through unchanged (we WANT to keep these
|
|
208
|
-
// so Rule 3 can flag them).
|
|
209
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
210
|
-
const quote = ch;
|
|
211
|
-
out += ch;
|
|
212
|
-
i += 1;
|
|
213
|
-
while (i < n) {
|
|
214
|
-
const c = src[i];
|
|
215
|
-
out += c;
|
|
216
|
-
if (c === '\\' && i + 1 < n) {
|
|
217
|
-
out += src[i + 1];
|
|
218
|
-
i += 2;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
i += 1;
|
|
222
|
-
if (c === quote) break;
|
|
223
|
-
}
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
out += ch;
|
|
227
|
-
i += 1;
|
|
228
|
-
}
|
|
229
|
-
return out;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
166
|
/**
|
|
233
167
|
* Rule 3 enforcement (Story #2253 / Task #2255, Epic #2172 review
|
|
234
168
|
* High-1). Returns an array of `{ file, line, hint }` violations for
|
|
@@ -263,7 +197,7 @@ export function findMergeLockoutViolations(
|
|
|
263
197
|
|
|
264
198
|
const raw = read(file, 'utf8');
|
|
265
199
|
const rawLines = raw.split('\n');
|
|
266
|
-
const stripped =
|
|
200
|
+
const stripped = stripJsComments(raw);
|
|
267
201
|
const lines = stripped.split('\n');
|
|
268
202
|
for (let i = 0; i < lines.length; i += 1) {
|
|
269
203
|
const line = lines[i];
|
|
@@ -88,6 +88,7 @@ import fs from 'node:fs';
|
|
|
88
88
|
import path from 'node:path';
|
|
89
89
|
import process from 'node:process';
|
|
90
90
|
import { runAsCli } from './lib/cli-utils.js';
|
|
91
|
+
import { stripJsComments } from './lib/source-text/strip-js-comments.js';
|
|
91
92
|
|
|
92
93
|
/** Schema root, relative to the repository root. */
|
|
93
94
|
const SCHEMA_ROOT = path.join('.agents', 'schemas');
|
|
@@ -114,33 +115,6 @@ const EXEMPTION_KEY = 'x-mandrel-uncompiled';
|
|
|
114
115
|
*/
|
|
115
116
|
const COMPUTED_PATH_RE = /\$\{[^}]*\}\.schema\.json/;
|
|
116
117
|
|
|
117
|
-
/** Block comments, including the JSDoc prose every module here opens with. */
|
|
118
|
-
const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Line comments. Anchored to start-of-line or whitespace so a `//` inside a
|
|
122
|
-
* URL (`http://…`) is left alone — truncating that line could delete a real
|
|
123
|
-
* reference sitting after it.
|
|
124
|
-
*/
|
|
125
|
-
const LINE_COMMENT_RE = /(^|\s)\/\/[^\n]*/g;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Strip comments from a source before matching.
|
|
129
|
-
*
|
|
130
|
-
* Prose is not a compile. This module's own header names
|
|
131
|
-
* `friction-event.schema.json` a dozen times while compiling nothing, and
|
|
132
|
-
* every schema-loading module documents the path it loads directly above the
|
|
133
|
-
* line that loads it — matching raw text would let a gate certify itself and
|
|
134
|
-
* would count a docblock as enforcement, which is precisely the confusion
|
|
135
|
-
* Story #4938 exists to end.
|
|
136
|
-
*
|
|
137
|
-
* @param {string} text raw source
|
|
138
|
-
* @returns {string} source with comment bodies removed
|
|
139
|
-
*/
|
|
140
|
-
function stripComments(text) {
|
|
141
|
-
return text.replace(BLOCK_COMMENT_RE, ' ').replace(LINE_COMMENT_RE, '$1');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
118
|
/**
|
|
145
119
|
* Parse argv. Exported so a unit test can pin the parser without spawning.
|
|
146
120
|
*
|
|
@@ -194,7 +168,7 @@ function walk(dir, extensions, acc = []) {
|
|
|
194
168
|
function readSources(root) {
|
|
195
169
|
return walk(root, JS_EXTENSIONS).map((file) => ({
|
|
196
170
|
rel: path.relative(root, file),
|
|
197
|
-
text:
|
|
171
|
+
text: stripJsComments(fs.readFileSync(file, 'utf8')),
|
|
198
172
|
}));
|
|
199
173
|
}
|
|
200
174
|
|
|
@@ -39,6 +39,18 @@ const progress = Logger.createProgress('drain-pending-cleanup', {
|
|
|
39
39
|
stderr: false,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The escalation posture, as a real boolean. `allowNegative` resolves
|
|
44
|
+
* `--no-escalate` to boolean `false`, but under `strict: false` the other
|
|
45
|
+
* documented spelling `--escalate=false` arrives as the *string* `'false'`,
|
|
46
|
+
* which is truthy while `forceDrainPendingCleanup` gates on `!escalate`.
|
|
47
|
+
* Both must reach the engine as a boolean or the flag is advertised and inert.
|
|
48
|
+
*
|
|
49
|
+
* @param {boolean|string|undefined} value
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
const escalateFrom = (value) => value !== false && value !== 'false';
|
|
53
|
+
|
|
42
54
|
/**
|
|
43
55
|
* The drain core, extracted from the CLI shell so the whole decision table —
|
|
44
56
|
* empty manifest, dry-run report, drain + escalate reporting — is reachable
|
|
@@ -83,6 +95,11 @@ export async function runDrainPendingCleanup(
|
|
|
83
95
|
'worktree-root': { type: 'string' },
|
|
84
96
|
},
|
|
85
97
|
strict: false,
|
|
98
|
+
// `node:util.parseArgs` honours a `--no-<flag>` spelling ONLY when this is
|
|
99
|
+
// set. Without it `--no-escalate` landed in `values['no-escalate']` and
|
|
100
|
+
// left `values.escalate` at its `true` default, so the documented passive
|
|
101
|
+
// drain performed the forced removal it promises to suppress.
|
|
102
|
+
allowNegative: true,
|
|
86
103
|
});
|
|
87
104
|
|
|
88
105
|
const config = resolveConfigImpl();
|
|
@@ -126,7 +143,7 @@ export async function runDrainPendingCleanup(
|
|
|
126
143
|
repoRoot: projectRoot,
|
|
127
144
|
worktreeRoot,
|
|
128
145
|
git: gitImpl,
|
|
129
|
-
escalate: values.escalate,
|
|
146
|
+
escalate: escalateFrom(values.escalate),
|
|
130
147
|
logger: {
|
|
131
148
|
info: (m) => progressImpl('DRAIN', m),
|
|
132
149
|
warn: (m) => progressImpl('DRAIN', `⚠️ ${m}`),
|
|
@@ -19,11 +19,41 @@
|
|
|
19
19
|
// the literal `*` are treated as the whole-repo rollup and always match
|
|
20
20
|
// every row regardless of declared globs.
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { Minimatch } from 'minimatch';
|
|
23
23
|
|
|
24
24
|
/** The default components map, used when a gate omits `components`. */
|
|
25
25
|
const DEFAULT_COMPONENTS = Object.freeze({ '*': Object.freeze(['**']) });
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Compiled-matcher cache for `groupRows`, keyed on the glob string.
|
|
29
|
+
*
|
|
30
|
+
* `groupRows` is called once per gate over the whole baseline row set — tens
|
|
31
|
+
* of thousands of rows for `crap.json` — and the functional `minimatch()` it
|
|
32
|
+
* used re-parsed each component glob for every single row. The component
|
|
33
|
+
* globs come from config, so the distinct set is small and immutable; caching
|
|
34
|
+
* the compiled matcher turns an O(rows × globs) parse into an O(globs) one
|
|
35
|
+
* (Story #5109). Matching semantics are unchanged: same patterns, same
|
|
36
|
+
* `{ dot: true }`.
|
|
37
|
+
*
|
|
38
|
+
* @type {Map<string, import('minimatch').Minimatch>}
|
|
39
|
+
*/
|
|
40
|
+
const GLOB_MATCHER_CACHE = new Map();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compile (once) the `Minimatch` for one glob.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} glob
|
|
46
|
+
* @returns {import('minimatch').Minimatch}
|
|
47
|
+
*/
|
|
48
|
+
function matcherFor(glob) {
|
|
49
|
+
let matcher = GLOB_MATCHER_CACHE.get(glob);
|
|
50
|
+
if (!matcher) {
|
|
51
|
+
matcher = new Minimatch(glob, { dot: true });
|
|
52
|
+
GLOB_MATCHER_CACHE.set(glob, matcher);
|
|
53
|
+
}
|
|
54
|
+
return matcher;
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/**
|
|
28
58
|
* Resolve the components map for a single gate config.
|
|
29
59
|
*
|
|
@@ -112,7 +142,7 @@ export function groupRows(rows, components, keyField = 'path') {
|
|
|
112
142
|
const list = Array.isArray(globs) ? globs : [];
|
|
113
143
|
for (const glob of list) {
|
|
114
144
|
if (typeof glob !== 'string' || glob.length === 0) continue;
|
|
115
|
-
if (
|
|
145
|
+
if (matcherFor(glob).match(normalized)) {
|
|
116
146
|
buckets[name].push(row);
|
|
117
147
|
break;
|
|
118
148
|
}
|
|
@@ -16,7 +16,7 @@ import { Logger } from '../Logger.js';
|
|
|
16
16
|
// rounding) routinely drifts +/- 0.05 to 0.3 on otherwise-unchanged files —
|
|
17
17
|
// well below the threshold of "actually less maintainable." A 0.5 floor
|
|
18
18
|
// stops the pre-push hook from auto-ratcheting the baseline on noise.
|
|
19
|
-
|
|
19
|
+
const MI_DEFAULT_TOLERANCE = 0.5;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Pure helper: resolve the effective CRAP config by layering env-var
|
|
@@ -43,7 +43,7 @@ const TARGET_APPROVAL_COUNT = 0;
|
|
|
43
43
|
* return value is a structured diff suitable for `hitlConfirm` so the
|
|
44
44
|
* operator sees exactly what would flip.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
function diffProtection(current, targetContexts) {
|
|
47
47
|
if (!current) return null; // create-from-scratch path; no diff needed.
|
|
48
48
|
|
|
49
49
|
const diff = {};
|
|
@@ -80,7 +80,7 @@ const TARGET_TO_PHASE = Object.freeze({
|
|
|
80
80
|
* @param {Record<string, { action?: string }>} [report]
|
|
81
81
|
* @returns {string|undefined}
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
function resolveExecutedAction(entry, report) {
|
|
84
84
|
if (!report) return undefined;
|
|
85
85
|
const phaseName = TARGET_TO_PHASE[entry.target];
|
|
86
86
|
if (!phaseName) return undefined;
|
|
@@ -28,7 +28,7 @@ import readline from 'node:readline/promises';
|
|
|
28
28
|
* Flags the bootstrap CLI accepts. Keep this list in sync with the
|
|
29
29
|
* `--help` text in bootstrap.js.
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
const KNOWN_FLAGS = Object.freeze({
|
|
32
32
|
string: [
|
|
33
33
|
'owner',
|
|
34
34
|
'repo',
|
|
@@ -41,7 +41,7 @@ import { deepEqual } from '../json-utils.js';
|
|
|
41
41
|
* the hook-installer can detect a verbatim framework hook (overwrite-safe)
|
|
42
42
|
* vs a custom hook (preserve and warn).
|
|
43
43
|
*/
|
|
44
|
-
|
|
44
|
+
const FRAMEWORK_PRE_COMMIT = `node scripts/check-version-sync.js
|
|
45
45
|
npx lint-staged
|
|
46
46
|
# Story #1395 / Epic #1386: catch MI/CRAP drift at git-commit time so the
|
|
47
47
|
# agent refactors before the diff is closed. quality:preview wraps both gates
|
|
@@ -78,7 +78,7 @@ export const PRE_COMMIT_MARKER =
|
|
|
78
78
|
* when the keys are absent. Mirrors `.agents/docs/agentrc-reference.json` — keep in
|
|
79
79
|
* sync when those numbers move.
|
|
80
80
|
*/
|
|
81
|
-
|
|
81
|
+
const QUALITY_CONFIG_DEFAULTS = Object.freeze({
|
|
82
82
|
codingGuardrails: Object.freeze({
|
|
83
83
|
cyclomaticFlag: 8,
|
|
84
84
|
cyclomaticMustFix: 12,
|
|
@@ -42,7 +42,7 @@ import { validateSignal } from '../observability/signal-validator.js';
|
|
|
42
42
|
* a health probe, not an exhaustive audit — the most recent window is a
|
|
43
43
|
* representative sample that keeps the read bounded on long streams.
|
|
44
44
|
*/
|
|
45
|
-
|
|
45
|
+
const MAX_SAMPLE_LINES = 200;
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Locate the most-recently-touched `run-<id>` temp tree under
|
|
@@ -210,7 +210,6 @@ export function parseSprintArgs(
|
|
|
210
210
|
'recut-of': { type: 'string' },
|
|
211
211
|
resume: { type: 'boolean', default: false },
|
|
212
212
|
restart: { type: 'boolean', default: false },
|
|
213
|
-
'no-evidence': { type: 'boolean', default: false },
|
|
214
213
|
},
|
|
215
214
|
allowPositionals: true,
|
|
216
215
|
strict: false,
|
|
@@ -260,7 +259,6 @@ export function parseSprintArgs(
|
|
|
260
259
|
// CLI compatibility).
|
|
261
260
|
resume: values.resume ?? false,
|
|
262
261
|
restart: values.restart ?? false,
|
|
263
|
-
noEvidence: values['no-evidence'] ?? false,
|
|
264
262
|
};
|
|
265
263
|
|
|
266
264
|
parsed.ticketId =
|
|
@@ -392,7 +392,7 @@ export const DEFAULT_GATES = buildDefaultGates();
|
|
|
392
392
|
* state, no overlapping ports/sockets). Safe to run concurrently — see
|
|
393
393
|
* `runCloseValidation` for the Promise.all + AbortController plumbing.
|
|
394
394
|
*/
|
|
395
|
-
|
|
395
|
+
const INDEPENDENT_GATE_NAMES = new Set(['lint', 'format', 'typecheck']);
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
398
|
* Partition a gate list into the parallel-safe set and the order-sensitive
|
|
@@ -113,7 +113,7 @@ const MARKER_PROBE_TAIL_LINES = 32;
|
|
|
113
113
|
* @param {string} output - Combined stdout/stderr captured from the gate child.
|
|
114
114
|
* @returns {boolean}
|
|
115
115
|
*/
|
|
116
|
-
|
|
116
|
+
function isBiomeNoFilesProcessed(output) {
|
|
117
117
|
return (
|
|
118
118
|
typeof output === 'string' && output.includes(BIOME_NO_FILES_PROCESSED)
|
|
119
119
|
);
|
|
@@ -173,7 +173,7 @@ export function collectRedundantAdvisories(
|
|
|
173
173
|
* @param {string} dottedPath
|
|
174
174
|
* @returns {boolean}
|
|
175
175
|
*/
|
|
176
|
-
|
|
176
|
+
function isLeafSchemaRemovable(rootSchema, dottedPath) {
|
|
177
177
|
if (!rootSchema || typeof rootSchema !== 'object') return true;
|
|
178
178
|
const parts = dottedPath.split('.');
|
|
179
179
|
let cursor = rootSchema;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* node:coverage ignore file -- AJV schema declaration (data-as-code); MI < 70 is inherent to large flat schema literals, no business logic to test */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import process from 'node:process';
|
|
5
5
|
import { COMMANDS_DEFAULTS } from './config/commands.js';
|
|
6
6
|
import {
|
|
7
7
|
BRANCH_PROTECTION_DEFAULTS,
|
|
@@ -15,6 +15,7 @@ import { SHELL_INJECTION_PATTERN_STRING } from './config-schema-shared.js';
|
|
|
15
15
|
// to keep this aggregate module above the maintainability floor. The
|
|
16
16
|
// resolved AGENTRC_SCHEMA is unchanged.
|
|
17
17
|
import { DELIVERY_SCHEMA } from './config-settings-schema-delivery.js';
|
|
18
|
+
import compiledAgentrcValidator from './generated/agentrc-validator.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Annotation contract (Story #5007). These schema literals are the SINGLE
|
|
@@ -861,10 +862,49 @@ export const AGENTRC_SCHEMA = {
|
|
|
861
862
|
};
|
|
862
863
|
|
|
863
864
|
let _agentrcValidator = null;
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Compile `AGENTRC_SCHEMA` with a live AJV instance.
|
|
868
|
+
*
|
|
869
|
+
* `ajv` is pulled in through `createRequire` rather than a top-level
|
|
870
|
+
* `import` so the module never enters the graph on the fast path
|
|
871
|
+
* (Story #5109). Loading AJV was itself a measurable share of the ~35 ms
|
|
872
|
+
* this function used to cost in each of the 36 config-touching entry
|
|
873
|
+
* scripts, and the precompiled validator needs none of it.
|
|
874
|
+
*
|
|
875
|
+
* @returns {import('ajv').ValidateFunction}
|
|
876
|
+
*/
|
|
877
|
+
function compileAgentrcValidatorDynamically() {
|
|
878
|
+
const require = createRequire(import.meta.url);
|
|
879
|
+
const ajvModule = require('ajv');
|
|
880
|
+
const Ajv = ajvModule.default ?? ajvModule;
|
|
881
|
+
const ajv = new Ajv({ allErrors: true });
|
|
882
|
+
return ajv.compile(AGENTRC_SCHEMA);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The `.agentrc.json` validator.
|
|
887
|
+
*
|
|
888
|
+
* Returns the **precompiled** validator committed at
|
|
889
|
+
* `lib/generated/agentrc-validator.js` — AJV's standalone emit for the exact
|
|
890
|
+
* `AGENTRC_SCHEMA` literal above, kept in step by
|
|
891
|
+
* `check-generated-validator.js --check` inside `npm run lint`. The verdicts
|
|
892
|
+
* are AJV's own, produced by AJV's own generated code with the same
|
|
893
|
+
* `allErrors: true` option the dynamic path uses, so nothing about what a
|
|
894
|
+
* config is allowed to contain changes — only when the codegen runs.
|
|
895
|
+
*
|
|
896
|
+
* Set `MANDREL_AGENTRC_VALIDATOR=dynamic` to compile at runtime instead. That
|
|
897
|
+
* escape hatch exists so a consumer who has hand-edited the schema (or hit a
|
|
898
|
+
* platform where the generated module will not load) is never stuck with a
|
|
899
|
+
* validator they cannot regenerate; it costs the ~35 ms the artifact removes.
|
|
900
|
+
*
|
|
901
|
+
* @returns {import('ajv').ValidateFunction}
|
|
902
|
+
*/
|
|
864
903
|
export function getAgentrcValidator() {
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
904
|
+
if (_agentrcValidator) return _agentrcValidator;
|
|
905
|
+
_agentrcValidator =
|
|
906
|
+
process.env.MANDREL_AGENTRC_VALIDATOR === 'dynamic'
|
|
907
|
+
? compileAgentrcValidatorDynamically()
|
|
908
|
+
: compiledAgentrcValidator;
|
|
869
909
|
return _agentrcValidator;
|
|
870
910
|
}
|
|
@@ -22,7 +22,7 @@ import path from 'node:path';
|
|
|
22
22
|
import picomatch from 'picomatch';
|
|
23
23
|
import { write, writeFile } from './baselines/writer.js';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const COVERAGE_FINAL_PATH = 'coverage/coverage-final.json';
|
|
26
26
|
export const COVERAGE_BASELINE_PATH = 'baselines/coverage.json';
|
|
27
27
|
// Absolute floating-point tolerance (percentage points). Values in the
|
|
28
28
|
// baseline are stored to two decimals, so anything below 0.01 is noise.
|
|
@@ -32,7 +32,7 @@ export const COVERAGE_TOLERANCE = 0.01;
|
|
|
32
32
|
// branch flipping covered↔uncovered between runs is the natural noise floor
|
|
33
33
|
// under non-deterministic Windows/Node 22 V8 instrumentation. We absorb up
|
|
34
34
|
// to one event of slack per axis. Anything beyond one event is real signal.
|
|
35
|
-
|
|
35
|
+
const NOISE_EVENT_HEADROOM = 1.0;
|
|
36
36
|
|
|
37
37
|
function toForwardSlash(p) {
|
|
38
38
|
return p.replace(/\\/g, '/');
|