mandrel 2.16.0 → 2.17.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 +1 -0
- package/.agents/docs/quality-gates.md +137 -0
- package/.agents/schemas/agentrc.schema.json +6 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +52 -12
- package/.agents/scripts/audit-to-stories.js +92 -25
- package/.agents/scripts/boot-sweep.js +28 -6
- package/.agents/scripts/check-baseline-drift.js +138 -0
- package/.agents/scripts/coverage-capture.js +74 -25
- package/.agents/scripts/deliver-recover.js +45 -18
- package/.agents/scripts/drain-pending-cleanup.js +67 -23
- package/.agents/scripts/generate-lens-checklists.js +81 -30
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +88 -17
- package/.agents/scripts/lib/baselines/drift-detector.js +351 -0
- package/.agents/scripts/lib/baselines/envelope.js +7 -0
- package/.agents/scripts/lib/baselines/kernel.js +31 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +76 -0
- package/.agents/scripts/lib/baselines/reader.js +12 -1
- package/.agents/scripts/lib/baselines/refresh-service.js +7 -1
- package/.agents/scripts/lib/baselines/writer.js +10 -0
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -8
- package/.agents/scripts/lib/cli-utils.js +48 -13
- package/.agents/scripts/lib/close-validation/projections/advisories.js +184 -0
- package/.agents/scripts/lib/close-validation/projections/crap.js +303 -0
- package/.agents/scripts/lib/close-validation/runner.js +68 -0
- package/.agents/scripts/lib/config/gates/crap.schema.js +7 -0
- package/.agents/scripts/lib/config/quality.js +40 -0
- package/.agents/scripts/lib/coverage-utils.js +92 -9
- package/.agents/scripts/lib/crap-engine.js +113 -23
- package/.agents/scripts/lib/crap-utils.js +159 -93
- package/.agents/scripts/lib/dynamic-workflow/audit-orchestrator.js +97 -10
- package/.agents/scripts/lib/dynamic-workflow/degraded-coverage.js +81 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +15 -8
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +35 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +13 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +16 -1
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +122 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +14 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal-schema.js +166 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +21 -50
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +26 -12
- package/.agents/scripts/lib/stdio-flush.js +71 -0
- package/.agents/scripts/lib/transpile.js +133 -6
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +47 -101
- package/.agents/scripts/lib/workers/crap-worker.js +49 -76
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +81 -8
- package/.agents/scripts/nav-registry-diff.js +30 -8
- package/.agents/scripts/plan-run-epilogue.js +27 -11
- package/.agents/scripts/resolve-doc-tiers.js +18 -8
- package/.agents/scripts/single-story-close.js +9 -92
- package/.agents/scripts/update-crap-baseline.js +13 -0
- package/README.md +14 -6
- package/docs/CHANGELOG.md +24 -0
- package/lib/cli/version-helpers.js +7 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +15 -8
- package/package.json +5 -1
|
@@ -50,24 +50,34 @@ export function parseArgv(argv = []) {
|
|
|
50
50
|
* Top-level CLI entry. Exported so tests can drive it against a fixture root
|
|
51
51
|
* with an injected sink and config.
|
|
52
52
|
*
|
|
53
|
+
* The optional final `deps` parameter is the module's injectable seam
|
|
54
|
+
* (`.agents/rules/test-seams.md` rules 1-2): every entry defaults to the real
|
|
55
|
+
* implementation, so the CLI path below — and any production caller — needs no
|
|
56
|
+
* configuration change.
|
|
57
|
+
*
|
|
53
58
|
* @param {{
|
|
54
59
|
* argv?: string[],
|
|
55
60
|
* config?: object,
|
|
56
61
|
* root?: string,
|
|
57
62
|
* stdout?: { write: (s: string) => void },
|
|
58
63
|
* }} [opts]
|
|
64
|
+
* @param {{
|
|
65
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
66
|
+
* resolveDocTiersImpl?: typeof resolveDocTiers,
|
|
67
|
+
* }} [deps]
|
|
59
68
|
* @returns {Promise<number>} always 0
|
|
60
69
|
*/
|
|
61
|
-
export async function runCli(
|
|
62
|
-
argv = process.argv.slice(2),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} = {}
|
|
70
|
+
export async function runCli(
|
|
71
|
+
{ argv = process.argv.slice(2), config, root, stdout = process.stdout } = {},
|
|
72
|
+
{
|
|
73
|
+
resolveConfigImpl = resolveConfig,
|
|
74
|
+
resolveDocTiersImpl = resolveDocTiers,
|
|
75
|
+
} = {},
|
|
76
|
+
) {
|
|
67
77
|
const { rootPath } = parseArgv(argv);
|
|
68
|
-
const resolvedConfig = config ??
|
|
78
|
+
const resolvedConfig = config ?? resolveConfigImpl();
|
|
69
79
|
const resolvedRoot = root ?? rootPath ?? PROJECT_ROOT;
|
|
70
|
-
const result =
|
|
80
|
+
const result = resolveDocTiersImpl(resolvedConfig, { root: resolvedRoot });
|
|
71
81
|
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
72
82
|
return 0;
|
|
73
83
|
}
|
|
@@ -90,6 +90,10 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
90
90
|
import { formatCliError } from './lib/error-redactor.js';
|
|
91
91
|
import { Logger } from './lib/Logger.js';
|
|
92
92
|
import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
|
|
93
|
+
import {
|
|
94
|
+
failedTerminalFor,
|
|
95
|
+
gatesForFailedPhase,
|
|
96
|
+
} from './lib/orchestration/single-story-close/failed-terminal.js';
|
|
93
97
|
import { enableAutoMergeWith } from './lib/orchestration/single-story-close/phases/auto-merge.js';
|
|
94
98
|
import {
|
|
95
99
|
buildSyncFailureCommentBody,
|
|
@@ -102,10 +106,8 @@ import {
|
|
|
102
106
|
} from './lib/orchestration/single-story-close/phases/code-review.js';
|
|
103
107
|
import { ensurePullRequestWith } from './lib/orchestration/single-story-close/phases/pull-request.js';
|
|
104
108
|
import {
|
|
105
|
-
buildTerminalEnvelope,
|
|
106
109
|
emitTerminalEnvelope,
|
|
107
110
|
exitCodeForTerminal,
|
|
108
|
-
NEXT_COMMANDS,
|
|
109
111
|
} from './lib/orchestration/story-deliver-terminal.js';
|
|
110
112
|
|
|
111
113
|
// Story #2990 moved the `gh`-spawn boundary into the `lib/gh-exec.js`
|
|
@@ -118,9 +120,13 @@ export const enableAutoMerge = enableAutoMergeWith;
|
|
|
118
120
|
|
|
119
121
|
// Re-export pure helpers verbatim — they don't touch `execFileSync`
|
|
120
122
|
// or any URL-mocked module, so the phase exports work unmodified.
|
|
123
|
+
// `gatesForFailedPhase` now lives beside the envelope it feeds
|
|
124
|
+
// (`single-story-close/failed-terminal.js`); it is re-exported here so the
|
|
125
|
+
// CLI's public surface is unchanged by that move.
|
|
121
126
|
export {
|
|
122
127
|
buildStoryReviewCrossRefBody,
|
|
123
128
|
buildSyncFailureCommentBody,
|
|
129
|
+
gatesForFailedPhase,
|
|
124
130
|
handleSyncFailure,
|
|
125
131
|
parsePrNumber,
|
|
126
132
|
runStoryScopeReview,
|
|
@@ -134,95 +140,6 @@ export async function runSingleStoryClose(opts) {
|
|
|
134
140
|
return mod.runSingleStoryClose(opts);
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
/**
|
|
138
|
-
* The close pipeline's phase order, as `setPhase` walks it. Only used to
|
|
139
|
-
* decide whether a gate had already run when a later phase died.
|
|
140
|
-
*/
|
|
141
|
-
const PHASE_ORDER = Object.freeze([
|
|
142
|
-
'init',
|
|
143
|
-
'wrong-tree-guard',
|
|
144
|
-
'close-validation',
|
|
145
|
-
'base-sync',
|
|
146
|
-
'push',
|
|
147
|
-
'pull-request',
|
|
148
|
-
'code-review',
|
|
149
|
-
'auto-merge',
|
|
150
|
-
'confirm-merge',
|
|
151
|
-
'post-land',
|
|
152
|
-
'done',
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
/** Each reported gate and the pipeline phase that decides it. */
|
|
156
|
-
const GATE_PHASES = Object.freeze([
|
|
157
|
-
['validation', 'close-validation'],
|
|
158
|
-
['baseSync', 'base-sync'],
|
|
159
|
-
['codeReview', 'code-review'],
|
|
160
|
-
]);
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Report every gate's outcome for a run that died at `phase`.
|
|
164
|
-
*
|
|
165
|
-
* The schema's contract: "A gate the run skipped … reports `skipped` rather
|
|
166
|
-
* than being omitted, so a missing gate is never mistaken for a passing one."
|
|
167
|
-
* The previous shape named only the gate that died and omitted the rest
|
|
168
|
-
* entirely — exactly the ambiguity the contract forbids.
|
|
169
|
-
*
|
|
170
|
-
* Reconstructed from the phase order, which is sound because the pipeline is
|
|
171
|
-
* strictly sequential: reaching phase N means every gate before it completed.
|
|
172
|
-
* A gate whose phase the run never reached is `skipped`; one the operator
|
|
173
|
-
* turned off via `--skip-validation` / `--skip-sync` is `skipped` too (it did
|
|
174
|
-
* not pass — it never ran).
|
|
175
|
-
*
|
|
176
|
-
* @param {string} phase The phase the run died in.
|
|
177
|
-
* @param {{ skipValidation?: boolean, skipSync?: boolean }} args Parsed CLI args.
|
|
178
|
-
* @returns {Record<string, 'passed'|'failed'|'skipped'>}
|
|
179
|
-
*/
|
|
180
|
-
export function gatesForFailedPhase(phase, args = {}) {
|
|
181
|
-
const skipped = { validation: args.skipValidation, baseSync: args.skipSync };
|
|
182
|
-
const failedAt = PHASE_ORDER.indexOf(phase);
|
|
183
|
-
const gates = {};
|
|
184
|
-
for (const [gate, gatePhase] of GATE_PHASES) {
|
|
185
|
-
const at = PHASE_ORDER.indexOf(gatePhase);
|
|
186
|
-
if (gatePhase === phase) gates[gate] = 'failed';
|
|
187
|
-
else if (failedAt < 0 || at > failedAt) gates[gate] = 'skipped';
|
|
188
|
-
else gates[gate] = skipped[gate] ? 'skipped' : 'passed';
|
|
189
|
-
}
|
|
190
|
-
return gates;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Build the `failed` terminal for a phase that crashed.
|
|
195
|
-
*
|
|
196
|
-
* The runner deliberately throws rather than returning a failure (a red gate
|
|
197
|
-
* must not look like a return value), so without this the most common
|
|
198
|
-
* non-happy ending — a failing close-validation gate — would emit **no
|
|
199
|
-
* envelope at all**, exiting 1 with only a stderr line while the workflow
|
|
200
|
-
* docs promise the agent a `failed` envelope naming the phase. Every close
|
|
201
|
-
* invocation emits exactly one envelope; this is the path that keeps that
|
|
202
|
-
* true when a phase dies.
|
|
203
|
-
*
|
|
204
|
-
* `err.closePhase` is tagged by the runner's phase tracker.
|
|
205
|
-
*
|
|
206
|
-
* @param {unknown} err
|
|
207
|
-
* @returns {object|null} A validated envelope, or null when even the story id
|
|
208
|
-
* is unknown (a usage error — there is nothing to report an envelope about).
|
|
209
|
-
*/
|
|
210
|
-
function failedTerminalFor(err) {
|
|
211
|
-
const phase = err?.closePhase ?? 'init';
|
|
212
|
-
const args = parseSprintArgs();
|
|
213
|
-
const storyId = Number(args.storyId);
|
|
214
|
-
if (!Number.isInteger(storyId) || storyId <= 0) return null;
|
|
215
|
-
return buildTerminalEnvelope({
|
|
216
|
-
storyId,
|
|
217
|
-
status: 'failed',
|
|
218
|
-
phase,
|
|
219
|
-
gates: gatesForFailedPhase(phase, args),
|
|
220
|
-
failure: { reason: String(err?.message ?? err) },
|
|
221
|
-
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
222
|
-
elapsedSeconds: 0,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
143
|
/**
|
|
227
144
|
* CLI entry — resolves the process exit code from the terminal envelope's
|
|
228
145
|
* status rather than from a thrown/not-thrown distinction, so `pending`
|
|
@@ -234,7 +151,7 @@ async function main() {
|
|
|
234
151
|
const outcome = await runSingleStoryClose();
|
|
235
152
|
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|
|
236
153
|
} catch (err) {
|
|
237
|
-
const terminal = failedTerminalFor(err);
|
|
154
|
+
const terminal = failedTerminalFor(err, parseSprintArgs());
|
|
238
155
|
if (!terminal) throw err;
|
|
239
156
|
// Mirror runAsCli's default error line (which this catch pre-empts) so the
|
|
240
157
|
// human-facing failure text is unchanged, then emit the envelope.
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from './lib/config-resolver.js';
|
|
14
14
|
import { loadCoverage } from './lib/coverage-utils.js';
|
|
15
15
|
import {
|
|
16
|
+
checkResolutionFloor,
|
|
16
17
|
resolveEscomplexVersion,
|
|
17
18
|
resolveTsTranspilerVersion,
|
|
18
19
|
scanAndScore,
|
|
@@ -71,6 +72,7 @@ async function main() {
|
|
|
71
72
|
const crap = getQuality(config).crap;
|
|
72
73
|
const targetDirs = Array.isArray(crap.targetDirs) ? crap.targetDirs : [];
|
|
73
74
|
const requireCoverage = crap.requireCoverage !== false;
|
|
75
|
+
const minMethodResolutionRate = crap.minMethodResolutionRate ?? 0.75;
|
|
74
76
|
const coveragePath =
|
|
75
77
|
args.coveragePath ?? crap.coveragePath ?? 'coverage/coverage-final.json';
|
|
76
78
|
const baselinePath = args.baselinePath ?? getBaselines(config).crap.path;
|
|
@@ -117,6 +119,7 @@ async function main() {
|
|
|
117
119
|
scannedFiles,
|
|
118
120
|
skippedFilesNoCoverage,
|
|
119
121
|
skippedMethodsNoCoverage,
|
|
122
|
+
resolution,
|
|
120
123
|
} = await scanAndScore({
|
|
121
124
|
targetDirs,
|
|
122
125
|
coverage,
|
|
@@ -137,6 +140,16 @@ async function main() {
|
|
|
137
140
|
`[CRAP] Skipped ${skippedMethodsNoCoverage} method(s) whose per-method coverage was unresolved.`,
|
|
138
141
|
);
|
|
139
142
|
}
|
|
143
|
+
if (resolution) {
|
|
144
|
+
Logger.info(
|
|
145
|
+
`[CRAP] Method resolution: ${resolution.resolvedMethods}/${resolution.joinableMethods} ` +
|
|
146
|
+
`(${(resolution.rate * 100).toFixed(1)}%) in files with coverage.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
// Fail closed BEFORE the service persists anything — a thin baseline is
|
|
150
|
+
// never written and then apologised for.
|
|
151
|
+
const refusal = checkResolutionFloor(resolution, minMethodResolutionRate);
|
|
152
|
+
if (refusal) throw new Error(refusal);
|
|
140
153
|
|
|
141
154
|
return (rows ?? []).filter(
|
|
142
155
|
(r) => typeof r?.crap === 'number' && Number.isFinite(r.crap),
|
package/README.md
CHANGED
|
@@ -181,10 +181,14 @@ dimensions, run model, and how to benchmark a new version.
|
|
|
181
181
|
|
|
182
182
|
## Contributors
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
`
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
The published `mandrel` package ships three directories — `.agents/`, `bin/`,
|
|
185
|
+
and `lib/` (see the `files` array in [`package.json`](package.json)).
|
|
186
|
+
`.agents/` is the payload `mandrel sync` materializes into a consumer's
|
|
187
|
+
`./.agents/` directory; `bin/mandrel.js` and its `lib/` implementation stay
|
|
188
|
+
inside `node_modules/mandrel/` and back the `npx mandrel …` CLI used
|
|
189
|
+
throughout this README. Everything else in this repository — `docs/`,
|
|
190
|
+
`tests/`, `.github/`, the root tooling configs — is internal development
|
|
191
|
+
tooling and is not published.
|
|
188
192
|
|
|
189
193
|
Common commands while developing the framework itself:
|
|
190
194
|
|
|
@@ -204,8 +208,12 @@ Deeper reference material lives in `docs/` rather than inline here:
|
|
|
204
208
|
- [`.agents/docs/workflows.md`](.agents/docs/workflows.md) — slash-command
|
|
205
209
|
index (auto-generated from the workflow set).
|
|
206
210
|
- [`docs/CHANGELOG.md`](docs/CHANGELOG.md) — release history.
|
|
207
|
-
- [`AGENTS.md`](AGENTS.md) — repository
|
|
208
|
-
|
|
211
|
+
- [`AGENTS.md`](AGENTS.md) — the repository-level orientation pointer; it
|
|
212
|
+
links on to [`docs/onboarding.md`](docs/onboarding.md) for the layout,
|
|
213
|
+
commands, and development standards.
|
|
214
|
+
- [`docs/release-operations.md`](docs/release-operations.md) — the Release
|
|
215
|
+
Checklist, the Install Matrix release gate, the single-package release
|
|
216
|
+
topology, PAT / npm-token setup, and the major-version policy. Releases are
|
|
209
217
|
automated by `release-please`: land Conventional Commits on `main` and it
|
|
210
218
|
opens a combined `chore: release main` PR that squash-merges itself once
|
|
211
219
|
CI is green, tags `main`, and publishes `mandrel` to npm.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.17.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.16.0...mandrel-v2.17.0) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* **crap:** wire the baseline-refresh projection layer and add CRAP freshness + full-scope drift detection ([#4776](https://github.com/dsj1984/mandrel/issues/4776)) ([#4778](https://github.com/dsj1984/mandrel/issues/4778)) ([8c49a12](https://github.com/dsj1984/mandrel/commit/8c49a12be6ca5ecaca60d8f0bc868f47c9a02534))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **audit-to-stories:** anchor findings to their real primary file (refs [#4781](https://github.com/dsj1984/mandrel/issues/4781)) ([#4782](https://github.com/dsj1984/mandrel/issues/4782)) ([a5794c0](https://github.com/dsj1984/mandrel/commit/a5794c02c3443c39ce116699c311b4cdd8001788))
|
|
16
|
+
* **cli:** drain stdio before exit and settle the audit fan-out per dimension (refs [#4783](https://github.com/dsj1984/mandrel/issues/4783)) ([#4787](https://github.com/dsj1984/mandrel/issues/4787)) ([3422fd5](https://github.com/dsj1984/mandrel/commit/3422fd564773d0e79ac4d16af5bbc5414d06c65b))
|
|
17
|
+
* **crap:** join per-method coverage in original-source coordinates — TS scoring resolves ~5% of methods ([#4775](https://github.com/dsj1984/mandrel/issues/4775)) ([#4777](https://github.com/dsj1984/mandrel/issues/4777)) ([1464c75](https://github.com/dsj1984/mandrel/commit/1464c75c061960aaa9ee1af88175d9706b044c26))
|
|
18
|
+
* **deliver:** keep the terminal envelope alive after the worktree reap (refs [#4784](https://github.com/dsj1984/mandrel/issues/4784)) ([#4791](https://github.com/dsj1984/mandrel/issues/4791)) ([907b7bd](https://github.com/dsj1984/mandrel/commit/907b7bdb951a70a7155e5a9a50afdbea7a81d6a6))
|
|
19
|
+
* **deps:** decouple the js-yaml override from markdownlint-cli2, close Node-engine drift, and re-enable knip dependency rules ([#4784](https://github.com/dsj1984/mandrel/issues/4784)) ([#4788](https://github.com/dsj1984/mandrel/issues/4788)) ([9ed687b](https://github.com/dsj1984/mandrel/commit/9ed687bead90d3e0edf76e7d0e700b0f51e8cb5d))
|
|
20
|
+
* **docs:** re-sync the reference docs to the contracts the code actually implements ([#4785](https://github.com/dsj1984/mandrel/issues/4785)) ([#4790](https://github.com/dsj1984/mandrel/issues/4790)) ([00d9270](https://github.com/dsj1984/mandrel/commit/00d92708b3d3ac0978c073c5a31fd6a80a25767e))
|
|
21
|
+
* **docs:** supersede the stale ADR chain in place and archive the retired pattern history ([#4786](https://github.com/dsj1984/mandrel/issues/4786)) ([#4789](https://github.com/dsj1984/mandrel/issues/4789)) ([5ba2e34](https://github.com/dsj1984/mandrel/commit/5ba2e3486973b87603fd3f2f23f750f18e75e240))
|
|
22
|
+
* **git-cleanup:** report the refs the prune phase actually dropped (refs [#4772](https://github.com/dsj1984/mandrel/issues/4772)) ([#4773](https://github.com/dsj1984/mandrel/issues/4773)) ([16424d5](https://github.com/dsj1984/mandrel/commit/16424d53305549841b6294ceb1d7651baf1f98e7))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
* **quality:** floor CRAP on methodsAbove20 instead of a fitted max ceiling ([#4779](https://github.com/dsj1984/mandrel/issues/4779)) ([647130b](https://github.com/dsj1984/mandrel/commit/647130b313a85add64978205d4bb300e90e3c9a3)), closes [#4775](https://github.com/dsj1984/mandrel/issues/4775)
|
|
28
|
+
|
|
5
29
|
## [2.16.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.15.0...mandrel-v2.16.0) (2026-07-25)
|
|
6
30
|
|
|
7
31
|
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* Builtins only — this module is imported from both the CLI surface
|
|
17
17
|
* (`lib/cli/`) and the doctor registry which runs before third-party
|
|
18
18
|
* packages are guaranteed to be present.
|
|
19
|
+
*
|
|
20
|
+
* The two filesystem-touching helpers below take the module's injectable
|
|
21
|
+
* seam as their optional final `fsImpl` parameter, defaulting to the real
|
|
22
|
+
* `node:fs` (`.agents/rules/test-seams.md` rules 1-2);
|
|
23
|
+
* `resolveConsumerPinVersion` forwards that seam to
|
|
24
|
+
* `resolveConsumerPinSpec` rather than re-acquiring `fs` itself (rule 4).
|
|
25
|
+
* Every other export here is pure, so it needs no seam of its own.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import nodeFs from 'node:fs';
|
|
@@ -40,10 +40,11 @@ const TAG_LINE_RE = /^(\s*)(@\S+(?:\s+@\S+)*)\s*$/;
|
|
|
40
40
|
* Recursively collect `.feature` file paths under `root`.
|
|
41
41
|
*
|
|
42
42
|
* @param {string} root
|
|
43
|
-
* @param {typeof nodeFs} fsImpl
|
|
43
|
+
* @param {typeof nodeFs} [fsImpl] Filesystem seam; defaults to the real
|
|
44
|
+
* `node:fs` per `.agents/rules/test-seams.md` rule 1.
|
|
44
45
|
* @returns {string[]}
|
|
45
46
|
*/
|
|
46
|
-
function collectFeatureFiles(root, fsImpl) {
|
|
47
|
+
function collectFeatureFiles(root, fsImpl = nodeFs) {
|
|
47
48
|
/** @type {string[]} */
|
|
48
49
|
const found = [];
|
|
49
50
|
/** @type {string[]} */
|
|
@@ -71,11 +72,13 @@ function collectFeatureFiles(root, fsImpl) {
|
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* @param {unknown} ctx
|
|
74
|
-
* @param {typeof nodeFs} fsImpl
|
|
75
|
+
* @param {typeof nodeFs} [fsImpl] Filesystem seam; forwarded to
|
|
76
|
+
* {@link collectFeatureFiles} rather than re-acquired there
|
|
77
|
+
* (`.agents/rules/test-seams.md` rule 4).
|
|
75
78
|
* @returns {string[]} Absolute paths of every `.feature` file under the
|
|
76
79
|
* canonical feature roots that exist in the consumer tree.
|
|
77
80
|
*/
|
|
78
|
-
function resolveFeatureFiles(ctx, fsImpl) {
|
|
81
|
+
function resolveFeatureFiles(ctx, fsImpl = nodeFs) {
|
|
79
82
|
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
80
83
|
return CANONICAL_FEATURE_ROOTS.flatMap((root) =>
|
|
81
84
|
collectFeatureFiles(path.join(projectRoot, root), fsImpl),
|
|
@@ -119,10 +122,12 @@ export const retireEpicAcTags = {
|
|
|
119
122
|
'(their reconciler consumer was deleted in the v2 Epic removal)',
|
|
120
123
|
/**
|
|
121
124
|
* @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
|
|
125
|
+
* @param {typeof nodeFs} [fsImpl] Optional final filesystem seam; defaults
|
|
126
|
+
* to `ctx.fs` when the migration runner threads one, else the real
|
|
127
|
+
* `node:fs` (`.agents/rules/test-seams.md` rule 1).
|
|
122
128
|
* @returns {boolean}
|
|
123
129
|
*/
|
|
124
|
-
detect(ctx) {
|
|
125
|
-
const fsImpl = ctx?.fs ?? nodeFs;
|
|
130
|
+
detect(ctx, fsImpl = ctx?.fs ?? nodeFs) {
|
|
126
131
|
return resolveFeatureFiles(ctx, fsImpl).some((file) => {
|
|
127
132
|
try {
|
|
128
133
|
return EPIC_AC_TAG_RE.test(fsImpl.readFileSync(file, 'utf8'));
|
|
@@ -133,10 +138,12 @@ export const retireEpicAcTags = {
|
|
|
133
138
|
},
|
|
134
139
|
/**
|
|
135
140
|
* @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
|
|
141
|
+
* @param {typeof nodeFs} [fsImpl] Optional final filesystem seam; defaults
|
|
142
|
+
* to `ctx.fs` when the migration runner threads one, else the real
|
|
143
|
+
* `node:fs` (`.agents/rules/test-seams.md` rule 1).
|
|
136
144
|
* @returns {void}
|
|
137
145
|
*/
|
|
138
|
-
apply(ctx) {
|
|
139
|
-
const fsImpl = ctx?.fs ?? nodeFs;
|
|
146
|
+
apply(ctx, fsImpl = ctx?.fs ?? nodeFs) {
|
|
140
147
|
for (const file of resolveFeatureFiles(ctx, fsImpl)) {
|
|
141
148
|
/** @type {string} */
|
|
142
149
|
let content;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
|
|
5
5
|
"files": [
|
|
6
6
|
".agents/",
|
|
@@ -110,6 +110,10 @@
|
|
|
110
110
|
"optional": true
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
|
+
"//": {
|
|
114
|
+
"overrides.js-yaml": "COUPLED to devDependencies.markdownlint-cli2 — do not bump either alone, and do NOT drop this override. It is load-bearing: markdownlint-cli2 0.22.x pulls js-yaml 4.1.1, which carries GHSA-52cp-r559-cp3m (high) and GHSA-h67p-54hq-rp68 (moderate); removing the override was measured to reintroduce both (1 high + 1 moderate), while with it in place `npm audit` is clean. An npm override also wins over a transitive package's own pin, so this tree-wide ^4.2.0 is imposed on every js-yaml consumer regardless of what they declare — and markdownlint-cli2 0.23.x declares an exact js-yaml 5.2.1. A dry-run bump confirmed the trap: markdownlint-cli2 0.23.1 resolves against js-yaml 4.3.0, two majors off what it declares, silently. The only safe move is to raise this override and bump markdownlint-cli2 in ONE reviewed commit (first confirming cosmiconfig, under @commitlint/cli, tolerates the same major). renovate.json excludes markdownlint-cli2 from devDependency auto-merge so that pair cannot drift apart unattended.",
|
|
115
|
+
"dependencies.js-yaml": "States the SAME range as overrides.js-yaml above. The two are deliberate duplicates — npm has no way to reference the direct range from the overrides block — so they MUST move in lockstep; changing one without the other silently splits the direct and transitive resolutions."
|
|
116
|
+
},
|
|
113
117
|
"overrides": {
|
|
114
118
|
"js-yaml": "^4.2.0",
|
|
115
119
|
"markdown-it": "^14.2.0"
|