mandrel 2.35.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/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +27 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +11 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -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-doc-links.js +23 -2
- 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/git-cleanup.js +2 -0
- 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/ci.js +18 -0
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
- 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 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
- package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +164 -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/auto-merge.js +145 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -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/notify.js +4 -10
- 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/.agents/workflows/audit-documentation.md +5 -6
- package/docs/CHANGELOG.md +50 -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 +9 -5
- package/.agents/scripts/generate-lifecycle-docs.js +0 -237
- 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
package/lib/cli/migrate.js
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
import {
|
|
55
55
|
migrations as defaultRegistry,
|
|
56
56
|
runMigrations as defaultRunMigrations,
|
|
57
|
+
selectStepsInRange,
|
|
57
58
|
} from '../migrations/index.js';
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -77,36 +78,97 @@ function parseOption(argv, flag) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* exactly.
|
|
81
|
+
* The envelope shape every `runMigrate` exit returns. Centralised so the four
|
|
82
|
+
* exits cannot drift in which fields they populate.
|
|
83
83
|
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @returns {
|
|
84
|
+
* @param {object} fields
|
|
85
|
+
* @returns {object}
|
|
86
86
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
function migrateResult(fields) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
action: 'usage-error',
|
|
91
|
+
fromVersion: null,
|
|
92
|
+
toVersion: null,
|
|
93
|
+
dryRun: false,
|
|
94
|
+
applied: [],
|
|
95
|
+
skipped: [],
|
|
96
|
+
wouldApply: [],
|
|
97
|
+
wouldSkip: [],
|
|
98
|
+
...fields,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Both bounds are required: the runner filters `fromVersion < v <= toVersion`,
|
|
104
|
+
* so an absent bound is ambiguous rather than a sensible default.
|
|
105
|
+
*
|
|
106
|
+
* @param {{ writeErr: (s: string) => void, exit: (code: number) => void }} io
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
function reportUsageError({ writeErr, exit }) {
|
|
110
|
+
writeErr(
|
|
111
|
+
'mandrel migrate: both --from <version> and --to <version> are required.\n' +
|
|
112
|
+
' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
|
|
113
|
+
);
|
|
114
|
+
exit(1);
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
/**
|
|
97
|
-
*
|
|
118
|
+
* Report the in-range steps and whether each WOULD apply or be skipped,
|
|
119
|
+
* probing a throwaway context so no step's `apply` runs and nothing on disk
|
|
120
|
+
* changes. The ordering comes from `selectStepsInRange`, the same selector the
|
|
121
|
+
* live run uses, so the preview cannot disagree with what would happen.
|
|
98
122
|
*
|
|
99
|
-
* @param {
|
|
100
|
-
*
|
|
101
|
-
*
|
|
123
|
+
* @param {{
|
|
124
|
+
* registry: Array<object>,
|
|
125
|
+
* fromVersion: string,
|
|
126
|
+
* toVersion: string,
|
|
127
|
+
* write: (s: string) => void,
|
|
128
|
+
* }} params
|
|
129
|
+
* @returns {{ wouldApply: string[], wouldSkip: string[] }}
|
|
102
130
|
*/
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
function previewMigrations({ registry, fromVersion, toVersion, write }) {
|
|
132
|
+
const inRange = selectStepsInRange({ registry, fromVersion, toVersion });
|
|
133
|
+
const wouldApply = [];
|
|
134
|
+
const wouldSkip = [];
|
|
135
|
+
const probeCtx = {};
|
|
136
|
+
|
|
137
|
+
write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
|
|
138
|
+
if (inRange.length === 0) {
|
|
139
|
+
write(' (no migration steps in range)\n');
|
|
108
140
|
}
|
|
109
|
-
|
|
141
|
+
for (const step of inRange) {
|
|
142
|
+
const bucket = step.detect(probeCtx) ? wouldApply : wouldSkip;
|
|
143
|
+
const verb = bucket === wouldApply ? 'would apply ' : 'would skip ';
|
|
144
|
+
bucket.push(step.version);
|
|
145
|
+
write(` ${verb} ${step.version}: ${step.description}\n`);
|
|
146
|
+
}
|
|
147
|
+
write('Dry run: no migrations applied, nothing written.\n');
|
|
148
|
+
|
|
149
|
+
return { wouldApply, wouldSkip };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {{
|
|
154
|
+
* applied: string[],
|
|
155
|
+
* fromVersion: string,
|
|
156
|
+
* toVersion: string,
|
|
157
|
+
* write: (s: string) => void,
|
|
158
|
+
* }} params
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
function reportApplied({ applied, fromVersion, toVersion, write }) {
|
|
162
|
+
if (applied.length === 0) {
|
|
163
|
+
write(
|
|
164
|
+
`mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const plural = applied.length === 1 ? '' : 's';
|
|
169
|
+
write(
|
|
170
|
+
`✅ Applied ${applied.length} migration${plural} (v${fromVersion} → v${toVersion}).\n`,
|
|
171
|
+
);
|
|
110
172
|
}
|
|
111
173
|
|
|
112
174
|
/**
|
|
@@ -146,107 +208,51 @@ export function runMigrate({
|
|
|
146
208
|
const fromVersion = parseOption(argv, '--from');
|
|
147
209
|
const toVersion = parseOption(argv, '--to');
|
|
148
210
|
|
|
149
|
-
// --- Usage validation -----------------------------------------------------
|
|
150
|
-
// Both bounds are required: the runner filters fromVersion < v <= toVersion,
|
|
151
|
-
// so an absent bound is ambiguous rather than a sensible default.
|
|
152
211
|
if (!fromVersion || !toVersion) {
|
|
153
|
-
writeErr
|
|
154
|
-
|
|
155
|
-
' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
|
|
156
|
-
);
|
|
157
|
-
exit(1);
|
|
158
|
-
return {
|
|
159
|
-
ok: false,
|
|
160
|
-
action: 'usage-error',
|
|
212
|
+
reportUsageError({ writeErr, exit });
|
|
213
|
+
return migrateResult({
|
|
161
214
|
fromVersion: fromVersion ?? null,
|
|
162
215
|
toVersion: toVersion ?? null,
|
|
163
216
|
dryRun,
|
|
164
|
-
|
|
165
|
-
skipped: [],
|
|
166
|
-
wouldApply: [],
|
|
167
|
-
wouldSkip: [],
|
|
168
|
-
};
|
|
217
|
+
});
|
|
169
218
|
}
|
|
170
219
|
|
|
171
|
-
// --- Dry run --------------------------------------------------------------
|
|
172
|
-
// Report the in-range steps and whether each WOULD apply or be skipped,
|
|
173
|
-
// probing a throwaway context so no step's apply runs and nothing on disk
|
|
174
|
-
// changes.
|
|
175
220
|
if (dryRun) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const wouldApply = [];
|
|
185
|
-
const wouldSkip = [];
|
|
186
|
-
const probeCtx = {};
|
|
187
|
-
|
|
188
|
-
write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
|
|
189
|
-
if (inRange.length === 0) {
|
|
190
|
-
write(' (no migration steps in range)\n');
|
|
191
|
-
}
|
|
192
|
-
for (const step of inRange) {
|
|
193
|
-
const willApply = step.detect(probeCtx);
|
|
194
|
-
if (willApply) {
|
|
195
|
-
wouldApply.push(step.version);
|
|
196
|
-
write(` would apply ${step.version}: ${step.description}\n`);
|
|
197
|
-
} else {
|
|
198
|
-
wouldSkip.push(step.version);
|
|
199
|
-
write(` would skip ${step.version}: ${step.description}\n`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
write('Dry run: no migrations applied, nothing written.\n');
|
|
203
|
-
|
|
204
|
-
return {
|
|
221
|
+
const { wouldApply, wouldSkip } = previewMigrations({
|
|
222
|
+
registry,
|
|
223
|
+
fromVersion,
|
|
224
|
+
toVersion,
|
|
225
|
+
write,
|
|
226
|
+
});
|
|
227
|
+
return migrateResult({
|
|
205
228
|
ok: true,
|
|
206
229
|
action: 'dry-run',
|
|
207
230
|
fromVersion,
|
|
208
231
|
toVersion,
|
|
209
232
|
dryRun: true,
|
|
210
|
-
applied: [],
|
|
211
|
-
skipped: [],
|
|
212
233
|
wouldApply,
|
|
213
234
|
wouldSkip,
|
|
214
|
-
};
|
|
235
|
+
});
|
|
215
236
|
}
|
|
216
237
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// and the per-step `migrated …` log line.
|
|
238
|
+
// The runner owns ordering, range filtering, idempotency, and the per-step
|
|
239
|
+
// `migrated …` log line.
|
|
220
240
|
const { applied, skipped } = runMigrations({
|
|
221
241
|
fromVersion,
|
|
222
242
|
toVersion,
|
|
223
243
|
ctx,
|
|
224
244
|
registry,
|
|
225
245
|
});
|
|
246
|
+
reportApplied({ applied, fromVersion, toVersion, write });
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
write(
|
|
229
|
-
`mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
|
|
230
|
-
);
|
|
231
|
-
} else {
|
|
232
|
-
write(
|
|
233
|
-
`✅ Applied ${applied.length} migration${
|
|
234
|
-
applied.length === 1 ? '' : 's'
|
|
235
|
-
} (v${fromVersion} → v${toVersion}).\n`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
248
|
+
return migrateResult({
|
|
240
249
|
ok: true,
|
|
241
250
|
action: 'migrated',
|
|
242
251
|
fromVersion,
|
|
243
252
|
toVersion,
|
|
244
|
-
dryRun: false,
|
|
245
253
|
applied,
|
|
246
254
|
skipped,
|
|
247
|
-
|
|
248
|
-
wouldSkip: [],
|
|
249
|
-
};
|
|
255
|
+
});
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
/**
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// lib/migrations/helpers/retire-agentrc-key.js
|
|
2
|
+
/**
|
|
3
|
+
* The shared scaffold behind every "strip a retired `.agentrc.json` key"
|
|
4
|
+
* migration step.
|
|
5
|
+
*
|
|
6
|
+
* Retiring a config key is the framework's most common migration: a key is
|
|
7
|
+
* dropped from the runtime AJV schema, the block it lived under carries
|
|
8
|
+
* `additionalProperties: false`, and a consumer whose config still sets it
|
|
9
|
+
* hits a hard validation failure on upgrade rather than a warning. Every such
|
|
10
|
+
* step reads the config, decides whether the key is present, deletes it,
|
|
11
|
+
* prunes whatever containers that emptied, and writes the file back — the
|
|
12
|
+
* same mechanics each time, previously copy-pasted per step (jscpd recorded
|
|
13
|
+
* five of them between 48 and 58 percent duplicated).
|
|
14
|
+
*
|
|
15
|
+
* Only three things actually vary between steps, so only those three are
|
|
16
|
+
* declared:
|
|
17
|
+
*
|
|
18
|
+
* - **which config surfaces to sweep** — `.agentrc.json` alone, or that plus
|
|
19
|
+
* the gitignored `.agentrc.local.json`. `config-resolver.js` deep-merges
|
|
20
|
+
* the overlay over the base *before* the AJV gate runs, so a key surviving
|
|
21
|
+
* in the overlay fails exactly as a base one would. A step that swept only
|
|
22
|
+
* the base would report "nothing to migrate" and leave that consumer hard
|
|
23
|
+
* broken with no self-service remedy.
|
|
24
|
+
* - **which key paths are retired** — a step may retire more than one.
|
|
25
|
+
* - **how far to prune emptied ancestors** — this is deliberately per-key
|
|
26
|
+
* and not a global policy. `planning` is optional, so an emptied
|
|
27
|
+
* `planning` block is removed; `project` is required and an empty
|
|
28
|
+
* `commands` object is valid against the schema, so pruning it would be a
|
|
29
|
+
* cosmetic edit to a config the consumer owns.
|
|
30
|
+
*
|
|
31
|
+
* Builtins only, and the `fs` seam stays injectable via `ctx.fs`, because
|
|
32
|
+
* these steps run during `mandrel update` before third-party packages are
|
|
33
|
+
* guaranteed to be present.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import nodeFs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
/** The committed config every consumer has. */
|
|
40
|
+
export const AGENTRC_BASE_FILENAME = '.agentrc.json';
|
|
41
|
+
|
|
42
|
+
/** The operator-owned, gitignored overlay the resolver merges over the base. */
|
|
43
|
+
const AGENTRC_LOCAL_FILENAME = '.agentrc.local.json';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Both surfaces the resolver reads — the default sweep for a retired key.
|
|
47
|
+
*
|
|
48
|
+
* Module-local: a step that wants both surfaces takes the default and names
|
|
49
|
+
* nothing, so exporting this would ship a symbol with no importer.
|
|
50
|
+
*/
|
|
51
|
+
const AGENTRC_FILENAMES = Object.freeze([
|
|
52
|
+
AGENTRC_BASE_FILENAME,
|
|
53
|
+
AGENTRC_LOCAL_FILENAME,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} ctx
|
|
58
|
+
* @param {string} filename
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function resolveAgentrcPath(ctx, filename) {
|
|
62
|
+
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
63
|
+
return path.join(projectRoot, filename);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read and parse one config surface. An absent or unparseable file is the
|
|
68
|
+
* common case (no overlay, fresh checkout), not an error.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} ctx
|
|
71
|
+
* @param {string} filename
|
|
72
|
+
* @param {typeof nodeFs} fsImpl
|
|
73
|
+
* @returns {object | null}
|
|
74
|
+
*/
|
|
75
|
+
function readAgentrcConfig(ctx, filename, fsImpl) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
|
|
78
|
+
return JSON.parse(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk the container chain for a key path, returning every object along it.
|
|
86
|
+
* Returns `null` as soon as a level is missing or is not a plain object, which
|
|
87
|
+
* is what makes `hasKey` false for a config that never had the block.
|
|
88
|
+
*
|
|
89
|
+
* @param {object | null} config
|
|
90
|
+
* @param {string[]} keyPath
|
|
91
|
+
* @returns {object[] | null}
|
|
92
|
+
*/
|
|
93
|
+
function resolveContainers(config, keyPath) {
|
|
94
|
+
if (!config || typeof config !== 'object') return null;
|
|
95
|
+
const containers = [config];
|
|
96
|
+
let cursor = config;
|
|
97
|
+
for (const segment of keyPath.slice(0, -1)) {
|
|
98
|
+
const next = cursor[segment];
|
|
99
|
+
if (!next || typeof next !== 'object') return null;
|
|
100
|
+
containers.push(next);
|
|
101
|
+
cursor = next;
|
|
102
|
+
}
|
|
103
|
+
return containers;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {object | null} config
|
|
108
|
+
* @param {string[]} keyPath
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function hasKey(config, keyPath) {
|
|
112
|
+
const containers = resolveContainers(config, keyPath);
|
|
113
|
+
if (!containers) return false;
|
|
114
|
+
const leafOwner = containers[containers.length - 1];
|
|
115
|
+
return Object.hasOwn(leafOwner, keyPath[keyPath.length - 1]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Delete the leaf key, then remove up to `pruneDepth` ancestors that the
|
|
120
|
+
* deletion left empty. `pruneDepth: 0` deletes the key and nothing else.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} config
|
|
123
|
+
* @param {{ path: string[], pruneDepth?: number }} key
|
|
124
|
+
* @returns {void}
|
|
125
|
+
*/
|
|
126
|
+
function stripKey(config, key) {
|
|
127
|
+
const keyPath = key.path;
|
|
128
|
+
const containers = resolveContainers(config, keyPath);
|
|
129
|
+
if (!containers) return;
|
|
130
|
+
|
|
131
|
+
const leafOwner = containers[containers.length - 1];
|
|
132
|
+
delete leafOwner[keyPath[keyPath.length - 1]];
|
|
133
|
+
|
|
134
|
+
const pruneDepth = key.pruneDepth ?? 0;
|
|
135
|
+
for (let level = 0; level < pruneDepth; level += 1) {
|
|
136
|
+
const index = containers.length - 1 - level;
|
|
137
|
+
const emptied = containers[index];
|
|
138
|
+
const parent = containers[index - 1];
|
|
139
|
+
if (!parent || Object.keys(emptied).length > 0) break;
|
|
140
|
+
delete parent[keyPath[index - 1]];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build a migration step that strips one or more retired keys from the
|
|
146
|
+
* consumer's config surfaces.
|
|
147
|
+
*
|
|
148
|
+
* `detect` is true when any declared key is present in any swept surface;
|
|
149
|
+
* `apply` rewrites only the surfaces that actually carry one, which is what
|
|
150
|
+
* makes a repeat pass a genuine no-op rather than a reformat.
|
|
151
|
+
*
|
|
152
|
+
* @param {{
|
|
153
|
+
* version: string,
|
|
154
|
+
* description: string,
|
|
155
|
+
* filenames?: readonly string[],
|
|
156
|
+
* keys: Array<{ path: string[], pruneDepth?: number }>,
|
|
157
|
+
* }} spec
|
|
158
|
+
* @returns {{
|
|
159
|
+
* version: string,
|
|
160
|
+
* description: string,
|
|
161
|
+
* detect: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => boolean,
|
|
162
|
+
* apply: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => void,
|
|
163
|
+
* }}
|
|
164
|
+
*/
|
|
165
|
+
export function createRetireAgentrcKeyStep({
|
|
166
|
+
version,
|
|
167
|
+
description,
|
|
168
|
+
filenames = AGENTRC_FILENAMES,
|
|
169
|
+
keys,
|
|
170
|
+
}) {
|
|
171
|
+
const carriesRetiredKey = (config) =>
|
|
172
|
+
keys.some((key) => hasKey(config, key.path));
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
version,
|
|
176
|
+
description,
|
|
177
|
+
detect(ctx) {
|
|
178
|
+
const fsImpl = ctx?.fs ?? nodeFs;
|
|
179
|
+
return filenames.some((filename) =>
|
|
180
|
+
carriesRetiredKey(readAgentrcConfig(ctx, filename, fsImpl)),
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
apply(ctx) {
|
|
184
|
+
const fsImpl = ctx?.fs ?? nodeFs;
|
|
185
|
+
for (const filename of filenames) {
|
|
186
|
+
const config = readAgentrcConfig(ctx, filename, fsImpl);
|
|
187
|
+
if (!carriesRetiredKey(config)) continue;
|
|
188
|
+
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
if (hasKey(config, key.path)) stripKey(config, key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fsImpl.writeFileSync(
|
|
194
|
+
resolveAgentrcPath(ctx, filename),
|
|
195
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
package/lib/migrations/index.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* (ascending by version) with an idempotent `detect`/`apply`.
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
+
import { compareVersions } from '../cli/version-helpers.js';
|
|
55
56
|
import { retireMiDropKnobs } from './steps/2.1.0-retire-mi-drop-knobs.js';
|
|
56
57
|
import { retireVerifyConcurrencyCap } from './steps/2.1.0-retire-verify-concurrency-cap.js';
|
|
57
58
|
import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
|
|
@@ -80,38 +81,42 @@ export const migrations = [
|
|
|
80
81
|
];
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
*
|
|
84
|
-
* Non-numeric or missing segments coerce to 0 so a partial version
|
|
85
|
-
* (`'1.4'`) still compares sanely against a full one (`'1.4.0'`).
|
|
84
|
+
* The version ordering every migration decision uses.
|
|
86
85
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* Re-exported from `lib/cli/version-helpers.js` — the dependency-free leaf
|
|
87
|
+
* that owns the canonical `parseVersion` / `compareVersions` pair (Story
|
|
88
|
+
* #4048 B3). The runner previously carried its own copy, so an ordering fix
|
|
89
|
+
* had to be made in three places or the copies drifted apart. Re-exporting
|
|
90
|
+
* rather than moving keeps this module's public surface intact for the
|
|
91
|
+
* callers that import `compareVersions` from here.
|
|
89
92
|
*/
|
|
90
|
-
|
|
91
|
-
const [major, minor, patch] = String(version).split('.');
|
|
92
|
-
return [
|
|
93
|
-
Number.parseInt(major, 10) || 0,
|
|
94
|
-
Number.parseInt(minor, 10) || 0,
|
|
95
|
-
Number.parseInt(patch, 10) || 0,
|
|
96
|
-
];
|
|
97
|
-
}
|
|
93
|
+
export { compareVersions };
|
|
98
94
|
|
|
99
95
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
96
|
+
* The steps that apply for an upgrade, in the order they must run:
|
|
97
|
+
* `fromVersion < step.version <= toVersion`, ascending.
|
|
98
|
+
*
|
|
99
|
+
* Exported because `mandrel migrate --dry-run` has to preview exactly what the
|
|
100
|
+
* live run would do. That preview previously re-implemented this filter and
|
|
101
|
+
* carried a comment promising it "mirrors the runner's own comparator" — a
|
|
102
|
+
* sync obligation nothing enforced, and the reason a third `compareVersions`
|
|
103
|
+
* copy existed. One selector means the preview cannot drift from the run.
|
|
103
104
|
*
|
|
104
|
-
* @param {string}
|
|
105
|
-
* @
|
|
106
|
-
* @returns {number}
|
|
105
|
+
* @param {{ registry?: Array<object>, fromVersion: string, toVersion: string }} params
|
|
106
|
+
* @returns {Array<object>}
|
|
107
107
|
*/
|
|
108
|
-
export function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
export function selectStepsInRange({
|
|
109
|
+
registry = migrations,
|
|
110
|
+
fromVersion,
|
|
111
|
+
toVersion,
|
|
112
|
+
}) {
|
|
113
|
+
return registry
|
|
114
|
+
.filter(
|
|
115
|
+
(step) =>
|
|
116
|
+
compareVersions(step.version, fromVersion) > 0 &&
|
|
117
|
+
compareVersions(step.version, toVersion) <= 0,
|
|
118
|
+
)
|
|
119
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -152,13 +157,7 @@ export function runMigrations({
|
|
|
152
157
|
log = console.log,
|
|
153
158
|
registry = migrations,
|
|
154
159
|
} = {}) {
|
|
155
|
-
const inRange = registry
|
|
156
|
-
.filter(
|
|
157
|
-
(step) =>
|
|
158
|
-
compareVersions(step.version, fromVersion) > 0 &&
|
|
159
|
-
compareVersions(step.version, toVersion) <= 0,
|
|
160
|
-
)
|
|
161
|
-
.sort((a, b) => compareVersions(a.version, b.version));
|
|
160
|
+
const inRange = selectStepsInRange({ registry, fromVersion, toVersion });
|
|
162
161
|
|
|
163
162
|
const applied = [];
|
|
164
163
|
const skipped = [];
|
|
@@ -11,90 +11,31 @@
|
|
|
11
11
|
* sub-schemas with `additionalProperties: false`, so a consumer whose
|
|
12
12
|
* config still carries either key hits a hard AJV validation failure on
|
|
13
13
|
* upgrade, not a warning. This step strips them before that check runs.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the later retire-steps this one sweeps the committed config only:
|
|
16
|
+
* the knobs were written by the bootstrap script, never hand-pinned in an
|
|
17
|
+
* operator's `.agentrc.local.json`.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {unknown} ctx
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
function resolveAgentrcPath(ctx) {
|
|
26
|
-
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
27
|
-
return path.join(projectRoot, AGENTRC_FILENAME);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @param {unknown} ctx
|
|
32
|
-
* @param {typeof nodeFs} fsImpl
|
|
33
|
-
* @returns {object | null}
|
|
34
|
-
*/
|
|
35
|
-
function readAgentrcConfig(ctx, fsImpl) {
|
|
36
|
-
try {
|
|
37
|
-
const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx), 'utf8');
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {object | null} config
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
*/
|
|
48
|
-
function hasRetiredKey(config) {
|
|
49
|
-
const guardrails = config?.delivery?.quality?.codingGuardrails;
|
|
50
|
-
const autoRefresh = config?.delivery?.quality?.autoRefresh;
|
|
51
|
-
return (
|
|
52
|
-
(Boolean(guardrails) && Object.hasOwn(guardrails, 'miDropMustRefactor')) ||
|
|
53
|
-
(Boolean(autoRefresh) && Object.hasOwn(autoRefresh, 'miDropCap'))
|
|
54
|
-
);
|
|
55
|
-
}
|
|
20
|
+
import {
|
|
21
|
+
AGENTRC_BASE_FILENAME,
|
|
22
|
+
createRetireAgentrcKeyStep,
|
|
23
|
+
} from '../helpers/retire-agentrc-key.js';
|
|
56
24
|
|
|
57
|
-
export const retireMiDropKnobs = {
|
|
25
|
+
export const retireMiDropKnobs = createRetireAgentrcKeyStep({
|
|
58
26
|
version: '2.1.0',
|
|
59
27
|
description:
|
|
60
28
|
'strip retired delivery.quality.codingGuardrails.miDropMustRefactor ' +
|
|
61
29
|
'and delivery.quality.autoRefresh.miDropCap from .agentrc.json',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
apply(ctx) {
|
|
75
|
-
const fsImpl = ctx?.fs ?? nodeFs;
|
|
76
|
-
const config = readAgentrcConfig(ctx, fsImpl);
|
|
77
|
-
if (!config) return;
|
|
78
|
-
|
|
79
|
-
const guardrails = config.delivery?.quality?.codingGuardrails;
|
|
80
|
-
if (guardrails && Object.hasOwn(guardrails, 'miDropMustRefactor')) {
|
|
81
|
-
delete guardrails.miDropMustRefactor;
|
|
82
|
-
if (Object.keys(guardrails).length === 0) {
|
|
83
|
-
delete config.delivery.quality.codingGuardrails;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const autoRefresh = config.delivery?.quality?.autoRefresh;
|
|
88
|
-
if (autoRefresh && Object.hasOwn(autoRefresh, 'miDropCap')) {
|
|
89
|
-
delete autoRefresh.miDropCap;
|
|
90
|
-
if (Object.keys(autoRefresh).length === 0) {
|
|
91
|
-
delete config.delivery.quality.autoRefresh;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
fsImpl.writeFileSync(
|
|
96
|
-
resolveAgentrcPath(ctx),
|
|
97
|
-
`${JSON.stringify(config, null, 2)}\n`,
|
|
98
|
-
);
|
|
99
|
-
},
|
|
100
|
-
};
|
|
30
|
+
filenames: [AGENTRC_BASE_FILENAME],
|
|
31
|
+
keys: [
|
|
32
|
+
{
|
|
33
|
+
path: ['delivery', 'quality', 'codingGuardrails', 'miDropMustRefactor'],
|
|
34
|
+
pruneDepth: 1,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: ['delivery', 'quality', 'autoRefresh', 'miDropCap'],
|
|
38
|
+
pruneDepth: 1,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|