mandrel 2.51.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/story-worker.md +4 -5
- package/.agents/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -0
- package/.agents/scripts/check-audit-attribution.js +245 -0
- package/.agents/scripts/check-pinned-override-notes.js +102 -0
- package/.agents/scripts/coverage-capture.js +36 -21
- package/.agents/scripts/lib/audit-attribution.js +112 -0
- package/.agents/scripts/lib/close-validation/commands.js +27 -1
- package/.agents/scripts/lib/close-validation/gates.js +33 -14
- package/.agents/scripts/lib/config/commands.js +14 -12
- package/.agents/scripts/lib/config-settings-schema-delivery.js +6 -0
- package/.agents/scripts/lib/config-settings-schema.js +9 -2
- package/.agents/scripts/lib/coverage-capture.js +70 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +34 -0
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/epic-container.js +27 -0
- package/.agents/scripts/lib/orchestration/epic-rollup.js +15 -4
- package/.agents/scripts/lib/orchestration/light-backstop.js +62 -6
- package/.agents/scripts/lib/orchestration/light-escalation.js +29 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +105 -19
- package/.agents/scripts/lib/orchestration/retro-proposals.js +36 -2
- package/.agents/scripts/lib/orchestration/worktree-dirty.js +80 -0
- package/.agents/scripts/lib/pinned-override-notes.js +100 -0
- package/.agents/scripts/resolve-stories.js +9 -3
- package/.agents/workflows/helpers/deliver-digest.md +4 -6
- package/.agents/workflows/helpers/deliver-light.md +13 -7
- package/docs/CHANGELOG.md +20 -0
- package/package.json +5 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/worktree-dirty.js — "does this branch's checkout carry
|
|
3
|
+
* uncommitted changes?" (Story #5238).
|
|
4
|
+
*
|
|
5
|
+
* Its own module because the light path's diff backstop is otherwise a pure
|
|
6
|
+
* join over two committed-state git reads, and this is the one question there
|
|
7
|
+
* that needs a third read of a *working tree*. Keeping it here leaves
|
|
8
|
+
* {@link module:lib/orchestration/light-backstop} the thin join it claims to
|
|
9
|
+
* be, and gives the probe its own place to be tested against every way a git
|
|
10
|
+
* read can fail.
|
|
11
|
+
*
|
|
12
|
+
* @module lib/orchestration/worktree-dirty
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { gitSpawn } from '../git-utils.js';
|
|
16
|
+
import { parseWorktreePorcelain } from '../worktree/inspector.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The stdout of a successful git read, or `null` when it cannot be trusted.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ status?: number, stdout?: unknown }|null|undefined} result
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
function readableStdout(result) {
|
|
25
|
+
if (result?.status !== 0) return null;
|
|
26
|
+
return typeof result.stdout === 'string' ? result.stdout : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Locate the checkout that has `branch` checked out, or `null` when no
|
|
31
|
+
* checkout does.
|
|
32
|
+
*
|
|
33
|
+
* `git worktree list --porcelain` enumerates **every** checkout including the
|
|
34
|
+
* main one, so a repository working on the branch directly (no separate
|
|
35
|
+
* worktree) is found by this same lookup rather than by a second fallback path.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ branch: string, cwd: string, gitFn: typeof gitSpawn }} args
|
|
38
|
+
* @returns {string|null}
|
|
39
|
+
*/
|
|
40
|
+
function resolveBranchCheckout({ branch, cwd, gitFn }) {
|
|
41
|
+
const listed = readableStdout(gitFn(cwd, 'worktree', 'list', '--porcelain'));
|
|
42
|
+
if (listed === null) return null;
|
|
43
|
+
const match = parseWorktreePorcelain(listed).find(
|
|
44
|
+
(record) => record.branch === branch,
|
|
45
|
+
);
|
|
46
|
+
return match ? match.path : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Does the checkout holding `branch` have uncommitted changes?
|
|
51
|
+
*
|
|
52
|
+
* **Why the light path asks.** Its diff backstop measures `base...head`, which
|
|
53
|
+
* is committed state, so a run that implemented and did not commit measures an
|
|
54
|
+
* empty change set — and the refusal then told the agent its scope was
|
|
55
|
+
* unverifiable and to escalate, when the actual fix was `git commit`. Measured
|
|
56
|
+
* in the consumer: the refusal signal is stamped 12:14:06Z and the branch's
|
|
57
|
+
* only commit 12:15:19Z (issue #5237).
|
|
58
|
+
*
|
|
59
|
+
* Total, and deliberately asymmetric: every unreadable surface — a failed
|
|
60
|
+
* `worktree list`, a branch no checkout holds, a failed `status`, a throwing
|
|
61
|
+
* git — answers `false`. A probe that cannot see the tree must not be able to
|
|
62
|
+
* talk a refusal into friendlier guidance than the evidence supports.
|
|
63
|
+
*
|
|
64
|
+
* @param {{ branch: string, cwd?: string, gitFn?: typeof gitSpawn }} args
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export function hasUncommittedWork({
|
|
68
|
+
branch,
|
|
69
|
+
cwd = process.cwd(),
|
|
70
|
+
gitFn = gitSpawn,
|
|
71
|
+
} = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const checkout = resolveBranchCheckout({ branch, cwd, gitFn });
|
|
74
|
+
if (checkout === null) return false;
|
|
75
|
+
const status = readableStdout(gitFn(checkout, 'status', '--porcelain'));
|
|
76
|
+
return status !== null && status.trim() !== '';
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pinned-override-notes.js — keep a load-bearing dependency note honest.
|
|
3
|
+
*
|
|
4
|
+
* `package.json` carries a `"//"` block of prose notes keyed by dotted config
|
|
5
|
+
* path. Two of them are safety notes about a pinned `overrides` entry: they
|
|
6
|
+
* record WHY the pin exists, which advisories removing it reintroduces, and
|
|
7
|
+
* that the `overrides` range and the direct `dependencies` range MUST move in
|
|
8
|
+
* lockstep because npm has no way to reference one from the other.
|
|
9
|
+
*
|
|
10
|
+
* A note like that is consulted precisely when someone is deciding whether a
|
|
11
|
+
* bump is safe — so a stale version inside it hands out wrong premises about
|
|
12
|
+
* a coupling the note itself calls a trap. Nothing enforced either claim, and
|
|
13
|
+
* both had drifted: the note described `^4.2.0` while the pin had moved twice.
|
|
14
|
+
*
|
|
15
|
+
* This module derives the checks from the `"//"` keys themselves rather than
|
|
16
|
+
* naming any package, so a second pinned override gets the same guarantee by
|
|
17
|
+
* writing its note.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** A `"//"` key that documents a pinned override, e.g. `overrides.js-yaml`. */
|
|
21
|
+
const OVERRIDE_NOTE_KEY = /^overrides\.(.+)$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract every semver range that appears literally in a note's prose.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately permissive about the surrounding words — a note is prose, and
|
|
27
|
+
* pinning its phrasing would make it unwritable. What matters is only that
|
|
28
|
+
* the range it quotes is the range in force.
|
|
29
|
+
*
|
|
30
|
+
* Not exported: it is an implementation detail of the audit below, and its
|
|
31
|
+
* behaviour is observable through that — a note quoting only bare versions
|
|
32
|
+
* yields no `stale-note`, a note quoting a mismatched range yields one.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} text
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
function quotedRanges(text) {
|
|
38
|
+
if (typeof text !== 'string') return [];
|
|
39
|
+
return [...text.matchAll(/[\^~]\d+\.\d+\.\d+/g)].map((m) => m[0]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Audit one package document's pinned-override notes.
|
|
44
|
+
*
|
|
45
|
+
* Two findings per documented override, each naming the drift rather than
|
|
46
|
+
* just asserting a mismatch:
|
|
47
|
+
* - `lockstep` — `overrides.<name>` and `dependencies.<name>` disagree. The
|
|
48
|
+
* companion note declares they must not; a split silently gives the
|
|
49
|
+
* direct and transitive resolutions different floors.
|
|
50
|
+
* - `stale-note` — the note quotes at least one range but not the one in
|
|
51
|
+
* force, so its stated version is behind the pin it describes. A note
|
|
52
|
+
* quoting no range at all is not scored: prose that names no version
|
|
53
|
+
* cannot go stale.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ '//'?: Record<string,string>, overrides?: Record<string,string>, dependencies?: Record<string,string> }} pkg
|
|
56
|
+
* @returns {{ findings: Array<{ kind: string, name: string, detail: string }>, checked: string[] }}
|
|
57
|
+
*/
|
|
58
|
+
export function auditPinnedOverrideNotes(pkg) {
|
|
59
|
+
const notes = pkg?.['//'] ?? {};
|
|
60
|
+
const overrides = pkg?.overrides ?? {};
|
|
61
|
+
const dependencies = pkg?.dependencies ?? {};
|
|
62
|
+
const findings = [];
|
|
63
|
+
const checked = [];
|
|
64
|
+
|
|
65
|
+
for (const [key, text] of Object.entries(notes)) {
|
|
66
|
+
const match = OVERRIDE_NOTE_KEY.exec(key);
|
|
67
|
+
if (!match) continue;
|
|
68
|
+
const name = match[1];
|
|
69
|
+
const pinned = overrides[name];
|
|
70
|
+
if (typeof pinned !== 'string') {
|
|
71
|
+
findings.push({
|
|
72
|
+
kind: 'orphan-note',
|
|
73
|
+
name,
|
|
74
|
+
detail: `"//"["${key}"] documents an override that no longer exists in the overrides block. Delete the note or restore the pin — a safety note for a pin nobody has is read as though the pin were still there.`,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
checked.push(name);
|
|
79
|
+
|
|
80
|
+
const direct = dependencies[name];
|
|
81
|
+
if (typeof direct === 'string' && direct !== pinned) {
|
|
82
|
+
findings.push({
|
|
83
|
+
kind: 'lockstep',
|
|
84
|
+
name,
|
|
85
|
+
detail: `overrides.${name} is "${pinned}" but dependencies.${name} is "${direct}". The "//" note declares these move in lockstep; a split gives the direct and transitive resolutions different floors.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const quoted = quotedRanges(text);
|
|
90
|
+
if (quoted.length > 0 && !quoted.includes(pinned)) {
|
|
91
|
+
findings.push({
|
|
92
|
+
kind: 'stale-note',
|
|
93
|
+
name,
|
|
94
|
+
detail: `"//"["${key}"] quotes ${quoted.map((q) => `"${q}"`).join(', ')} but the pin in force is "${pinned}". The note is what tells the next author whether a bump is safe, so it must state the version it is describing.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { findings, checked };
|
|
100
|
+
}
|
|
@@ -39,6 +39,7 @@ import { parseArgs } from 'node:util';
|
|
|
39
39
|
import { runAsCli } from './lib/cli-utils.js';
|
|
40
40
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
41
41
|
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
42
|
+
import { resolveEpicNodeId } from './lib/orchestration/epic-container.js';
|
|
42
43
|
import { expandEpicIds } from './lib/orchestration/epic-expansion.js';
|
|
43
44
|
import {
|
|
44
45
|
buildStoriesEnvelope,
|
|
@@ -98,15 +99,20 @@ export function resolveStoriesProvider({
|
|
|
98
99
|
* Injected into `expandEpicIds` so the lib layer stays provider-agnostic,
|
|
99
100
|
* exactly as `paginate` is injected into `readNativeBlockedBy`. A provider
|
|
100
101
|
* without the GraphQL surface yields `[]`, and the Epic body's checklist
|
|
101
|
-
* carries the children on its own
|
|
102
|
+
* carries the children on its own — as does an Epic carrying no resolvable
|
|
103
|
+
* node id, which `resolveEpicNodeId` reports rather than letting an
|
|
104
|
+
* `undefined` reach the API as a rejected `ID!` variable.
|
|
102
105
|
*
|
|
103
106
|
* @param {object} provider
|
|
104
107
|
* @returns {(epic: object) => Promise<number[]>}
|
|
105
108
|
*/
|
|
106
109
|
export function nativeChildReader(provider) {
|
|
107
110
|
return async (epic) => {
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const nodeId = resolveEpicNodeId(epic);
|
|
112
|
+
if (nodeId === null) return [];
|
|
113
|
+
return (
|
|
114
|
+
provider?._getNativeSubIssues?.(nodeId, epic?.number ?? epic?.id) ?? []
|
|
115
|
+
);
|
|
110
116
|
};
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
description: >-
|
|
3
3
|
The deliver path's one bundled framework read: dispatch decision, engine
|
|
4
4
|
invariants, the change-set/ceremony incantation, the acceptance-eval gate,
|
|
5
|
-
the credited full-suite run, and the terminal envelope contract
|
|
6
|
-
engine reads one file, not the helper/schema set, each session.
|
|
5
|
+
the credited full-suite run, and the terminal envelope contract.
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
# Deliver digest (read once per session)
|
|
@@ -123,14 +122,13 @@ node <main-repo>/.agents/scripts/evidence-gate.js --standalone \
|
|
|
123
122
|
--scope-id <storyId> --gate test --worktree <workCwd> -- npm test
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
Dispatch it in the **background**: it outruns the host's
|
|
127
|
-
ceiling, and its completion re-invokes you. Never spawn a task to poll or
|
|
125
|
+
Dispatch it in the **background**: it outruns the host's sync Bash ceiling, and its completion re-invokes you. Never spawn a task to poll or
|
|
128
126
|
`sleep`-loop against it ([`parallel-tooling.md`](parallel-tooling.md)
|
|
129
127
|
Rule 2).
|
|
130
128
|
|
|
131
129
|
Read the **output**, not the exit code: capture skips — no test run, no
|
|
132
|
-
credit — when nothing changed under the CRAP `targetDirs
|
|
133
|
-
|
|
130
|
+
credit — when nothing changed under the CRAP `targetDirs`. Run the scoped
|
|
131
|
+
projects for the roots you changed plus `verify[]`, not the whole suite.
|
|
134
132
|
|
|
135
133
|
`verify[]` is scoped entries **plus** this one run: an entry that is itself a
|
|
136
134
|
full-suite command is reported credited against the same stamp, never
|
|
@@ -153,13 +153,19 @@ answer).
|
|
|
153
153
|
number that then rejects the change. They are **not** exempt from
|
|
154
154
|
sensitive-path matching, which runs over the full change set.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
the
|
|
156
|
+
**Commit before you run it.** The backstop measures **committed** state, so
|
|
157
|
+
a run that implemented but has not committed measures an empty diff and is
|
|
158
|
+
refused for a scope it never had. That refusal names the real fix — commit
|
|
159
|
+
on `story-<id>`, then re-run the backstop — and its `nextCommand` is that
|
|
160
|
+
re-run, not an escalation.
|
|
161
|
+
|
|
162
|
+
Exit `3` (`blocked: true`) otherwise means the diff exceeds a light ceiling or
|
|
163
|
+
touches a sensitive-path class. STOP, flip `agent::blocked`, and **recycle the
|
|
164
|
+
receipt** through the envelope's `nextCommand` (`/mandrel-plan <storyId>`) —
|
|
165
|
+
tickets mode rewrites it into properly-planned Stories and closes it as
|
|
166
|
+
superseded. Do not land, and do not leave the receipt open with no successor:
|
|
167
|
+
it already carries the branch, the worktree, and the implementation, all of
|
|
168
|
+
which are evidence the plan should read.
|
|
163
169
|
|
|
164
170
|
5. **Close and land (same engine).** Exactly [`/mandrel-deliver`](../mandrel-deliver.md)'s close:
|
|
165
171
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,26 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.53.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.52.0...mandrel-v2.53.0) (2026-09-09)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
* a red SCA check says whose defect it is, and the js-yaml override's load-bearing note can no longer drift from the pin it describes ([#5248](https://github.com/dsj1984/mandrel/issues/5248)) ([#5250](https://github.com/dsj1984/mandrel/issues/5250)) ([38f4c3a](https://github.com/dsj1984/mandrel/commit/38f4c3a6acbf6e5a0bb6f9586c5a04c1f3abed02))
|
|
24
|
+
* close stops paying for whole-repo work already done: the lint gate resolves project.commands.lint, and coverage-capture announces (or refuses) an uncredited full-suite run before it spawns one ([#5244](https://github.com/dsj1984/mandrel/issues/5244)) ([#5245](https://github.com/dsj1984/mandrel/issues/5245)) ([bb4216c](https://github.com/dsj1984/mandrel/commit/bb4216cf1f443ebeb3bac6a22171fffa107d028d))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
* epic rollup's native sub-issue read passes an undefined node id, degrading every Epic's child list ([#5251](https://github.com/dsj1984/mandrel/issues/5251)) ([#5252](https://github.com/dsj1984/mandrel/issues/5252)) ([5da2df5](https://github.com/dsj1984/mandrel/commit/5da2df5322a8527c33cd4ebafa4584bdfedf867d))
|
|
30
|
+
|
|
31
|
+
## [2.52.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.51.0...mandrel-v2.52.0) (2026-09-08)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
* render deliver-light refusal reasons in retro follow-ups and file each backstop refusal class as its own friction category ([#5238](https://github.com/dsj1984/mandrel/issues/5238)) ([#5239](https://github.com/dsj1984/mandrel/issues/5239)) ([65ff0ff](https://github.com/dsj1984/mandrel/commit/65ff0ff4252ddc4fe34d4d8453159a80afae6223))
|
|
37
|
+
|
|
18
38
|
## [2.51.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.50.0...mandrel-v2.51.0) (2026-09-08)
|
|
19
39
|
|
|
20
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.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/",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"baselines:scope": "node .agents/scripts/check-baseline-scope.js",
|
|
34
34
|
"baselines:prune": "node .agents/scripts/prune-baseline-orphans.js",
|
|
35
35
|
"lint:md": "markdownlint-cli2 \".agents/**/*.md\" \"*.md\" \"!node_modules/**\" \"!.worktrees/**\"",
|
|
36
|
-
"lint": "node .agents/scripts/run-lint.js && node .agents/scripts/check-generated-validator.js --check && npm run docs:check",
|
|
36
|
+
"lint": "node .agents/scripts/run-lint.js && node .agents/scripts/check-generated-validator.js --check && node .agents/scripts/check-pinned-override-notes.js && npm run docs:check",
|
|
37
37
|
"docs:gen": "node .agents/scripts/generate-config-docs.js && node .agents/scripts/generate-workflows-doc.js && node .agents/scripts/generate-lens-checklists.js",
|
|
38
38
|
"docs:check": "node .agents/scripts/generate-config-docs.js --check && node .agents/scripts/generate-workflows-doc.js --check && node .agents/scripts/generate-lens-checklists.js --check && node .agents/scripts/check-doc-links.js && npm run skills:check",
|
|
39
39
|
"skills:index": "node .agents/scripts/generate-skills-index.js",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"dependencies": {
|
|
111
111
|
"ajv": "^8.20.0",
|
|
112
112
|
"ajv-formats": "^3.0.1",
|
|
113
|
-
"js-yaml": "^4.3.
|
|
113
|
+
"js-yaml": "^4.3.2",
|
|
114
114
|
"minimatch": "^10.0.0",
|
|
115
115
|
"picomatch": "^4.0.4",
|
|
116
116
|
"typhonjs-escomplex": "^0.1.0"
|
|
@@ -129,11 +129,11 @@
|
|
|
129
129
|
},
|
|
130
130
|
"//": {
|
|
131
131
|
"peerDependencies.@cucumber/gherkin": "OPTIONAL peer, mirroring the `typescript` precedent, and deliberately NOT a runtime dependency. `check-gherkin-corpus.js` is the only consumer and it is opt-in behind `qa.gherkinLint`, so a consumer with no BDD tier must gain nothing from an upgrade. The devDependency alongside it is what lets this repository's own suite drive the real parser. The gate resolves it through a require path anchored at the consumer project — `.agents/` reaches a consumer by plain file copy, so a bare specifier would resolve against the consumer's module chain, which under a non-hoisting linker need not hold it.",
|
|
132
|
-
"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
|
|
132
|
+
"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.3.2 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. The floor has since been raised twice for advisories against the pinned range itself (most recently to ^4.3.2 for GHSA-2883-xcg3-v3hh, whose fix is 4.3.2) — raising it is safe and does NOT touch the coupling above; check-pinned-override-notes.js now fails the build if this sentence and the pin disagree.",
|
|
133
133
|
"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."
|
|
134
134
|
},
|
|
135
135
|
"overrides": {
|
|
136
|
-
"js-yaml": "^4.3.
|
|
136
|
+
"js-yaml": "^4.3.2",
|
|
137
137
|
"markdown-it": "^14.2.0"
|
|
138
138
|
}
|
|
139
139
|
}
|