mandrel 2.56.0 → 2.58.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/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,42 @@ 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.58.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.57.0...mandrel-v2.58.0) (2026-09-12)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
* make the close test credit earnable on any test runner, and surface the gap at setup instead of mid-close ([#5324](https://github.com/dsj1984/mandrel/issues/5324)) ([#5329](https://github.com/dsj1984/mandrel/issues/5329)) ([04db145](https://github.com/dsj1984/mandrel/commit/04db145b8f1017121e129897f9207ab51db1c3f8))
|
|
24
|
+
* tickets-mode planning re-derives the Story instead of carrying the source ticket's shape: acceptance handles, tier suffixes, generated-artifact footprints and pinned identifiers ([#5323](https://github.com/dsj1984/mandrel/issues/5323)) ([#5326](https://github.com/dsj1984/mandrel/issues/5326)) ([5563e7a](https://github.com/dsj1984/mandrel/commit/5563e7a264864f78001beb19ce58160a1b483cda))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
* give base-sync and the Story-scope code review one base ref, so a stale local base branch cannot raise false blockers ([#5325](https://github.com/dsj1984/mandrel/issues/5325)) ([#5328](https://github.com/dsj1984/mandrel/issues/5328)) ([c4032b6](https://github.com/dsj1984/mandrel/commit/c4032b63a63c4cb8cc3f9c9956b6c646e5acd460))
|
|
30
|
+
|
|
31
|
+
## [2.57.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.56.0...mandrel-v2.57.0) (2026-09-12)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### ⚠ BREAKING CHANGES
|
|
35
|
+
|
|
36
|
+
* the .agentrc keys delivery.routing.freshCriticSampleRate, delivery.codeReview.maxFixScopeFiles, delivery.signals and delivery.quality.codingGuardrails.cyclomaticMustFix are removed; a 2.57.0 migration step strips them on upgrade.
|
|
37
|
+
* the .agentrc.json keys planning.complexityGate, planning.riskHeuristics, planning.failOnSharedEditors, planning.requireExplicitCrossStoryDeps, planning.failOnRegistryConflicts, planning.failOnLargeFanOut, planning.largeFanOutThreshold, planning.crossCuttingRegistries, planning.memoryPool.staleAfterDays and planning.memoryPool.growthDelta are removed; the planning block is strict, so a config still carrying one fails validation until the 2.57.0 migration step strips it on `mandrel update`. plan-persist.js no longer accepts --route-downgrade-reason, --allow-over-budget or --allow-large-fan-out, and a verify[] entry is a bare command with no tier suffix.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
* delivery diet: drop the sampling, round-ceiling, fix-scope, signal and cyclomatic knobs, script the ceremony derivation, let a bare test run earn credit, and make the light gate and footprint guard read the diff ([#5313](https://github.com/dsj1984/mandrel/issues/5313)) ([#5319](https://github.com/dsj1984/mandrel/issues/5319)) ([33fa9db](https://github.com/dsj1984/mandrel/commit/33fa9db4d01d8932bd886d380fe32b29db9a7bce))
|
|
42
|
+
* planning diet: delete the sizing, spec, lite-route and verify-tier limits, demote the footprint probes to warnings, and render the author prompt for the one-Story default ([#5312](https://github.com/dsj1984/mandrel/issues/5312)) ([#5318](https://github.com/dsj1984/mandrel/issues/5318)) ([cbcfdac](https://github.com/dsj1984/mandrel/commit/cbcfdacbca2cce9731e12c6cdfdab095d23b3e12))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
* score the CRAP worker path through the escomplex AST shim, and report an unscorable file as unscorable ([#5311](https://github.com/dsj1984/mandrel/issues/5311)) ([#5314](https://github.com/dsj1984/mandrel/issues/5314)) ([0a8e8c8](https://github.com/dsj1984/mandrel/commit/0a8e8c8cd8afb7af88b3d0d5a49e6359a91f3524))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
* free the CLI entrypoints' logic from `main` so it can be tested, and tighten the CRAP floor back to 2 ([#5316](https://github.com/dsj1984/mandrel/issues/5316)) ([#5317](https://github.com/dsj1984/mandrel/issues/5317)) ([f67cfb3](https://github.com/dsj1984/mandrel/commit/f67cfb3b6e531c38300690e4dcd16a4de7dd18f8))
|
|
53
|
+
|
|
18
54
|
## [2.56.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.55.0...mandrel-v2.56.0) (2026-09-11)
|
|
19
55
|
|
|
20
56
|
|
package/lib/cli/registry.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Exports an ordered array of check objects each shaped `{ name, run() }`.
|
|
6
6
|
* `run()` returns `{ ok, detail, remedy? }` — `remedy` is present and
|
|
7
|
-
* non-empty
|
|
8
|
-
*
|
|
7
|
+
* non-empty whenever `ok` is false, and on an `advisory` check that passes
|
|
8
|
+
* while still having something actionable to say (`test-credit-path`), which
|
|
9
|
+
* repeats it in `detail` because that is the field the doctor prints for a
|
|
10
|
+
* passing check. The registry is the single source of truth for which checks
|
|
11
|
+
* the doctor command runs and in what order.
|
|
9
12
|
*
|
|
10
13
|
* Checks run sequentially in the doctor runner (not in parallel) because
|
|
11
14
|
* some checks are meaningless without a prerequisite having passed first
|
|
@@ -1176,6 +1179,90 @@ function probeConfiguredDriver(command, runner, projectRoot) {
|
|
|
1176
1179
|
};
|
|
1177
1180
|
}
|
|
1178
1181
|
|
|
1182
|
+
// ---------------------------------------------------------------------------
|
|
1183
|
+
// check: test-credit-path
|
|
1184
|
+
// ---------------------------------------------------------------------------
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* The runner whose own green full run deposits the close `test` credit as a
|
|
1188
|
+
* side effect (`.agents/scripts/run-tests.js` → `lib/test-run-credit.js`).
|
|
1189
|
+
*/
|
|
1190
|
+
const MANDREL_TEST_RUNNER = 'run-tests.js';
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* The deposit that works whatever `npm test` resolves to: it spawns the
|
|
1194
|
+
* project's own suite and stamps the result, so it is honest by construction.
|
|
1195
|
+
*/
|
|
1196
|
+
const TEST_CREDIT_DEPOSIT_COMMAND =
|
|
1197
|
+
'node .agents/scripts/evidence-gate.js --standalone --scope-id <storyId> --gate test --worktree <workCwd> -- npm test';
|
|
1198
|
+
|
|
1199
|
+
/** Remedy shared by every shape that does not earn the credit on its own. */
|
|
1200
|
+
const TEST_CREDIT_REMEDY = `run the suite through the depositor instead of bare \`npm test\`: ${TEST_CREDIT_DEPOSIT_COMMAND}`;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* The project's `test` script — what `npm test` runs, and therefore what the
|
|
1204
|
+
* close `test` gate spawns. Deliberately read from `package.json` rather than
|
|
1205
|
+
* `project.commands.test`: the close gate's argv is the literal `npm test`, so
|
|
1206
|
+
* the npm script is the command whose shape decides whether a bare run
|
|
1207
|
+
* deposits anything.
|
|
1208
|
+
*
|
|
1209
|
+
* @param {string} projectRoot
|
|
1210
|
+
* @param {typeof fs.readFileSync} readFileImpl
|
|
1211
|
+
* @returns {string|null} The trimmed script, or null when there is none.
|
|
1212
|
+
*/
|
|
1213
|
+
function readProjectTestScript(projectRoot, readFileImpl) {
|
|
1214
|
+
try {
|
|
1215
|
+
const raw = readFileImpl(path.join(projectRoot, 'package.json'), 'utf8');
|
|
1216
|
+
const script = JSON.parse(raw)?.scripts?.test;
|
|
1217
|
+
return typeof script === 'string' && script.trim().length > 0
|
|
1218
|
+
? script.trim()
|
|
1219
|
+
: null;
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Report whether this project's test command reaches mandrel's own runner,
|
|
1227
|
+
* and therefore whether a bare `npm test` deposits the close `test` credit
|
|
1228
|
+
* by itself (Story #5324).
|
|
1229
|
+
*
|
|
1230
|
+
* **Always `ok: true`.** A project on `vitest`, `jest` or any other runner is
|
|
1231
|
+
* a supported setup, not a broken install — the only thing it lacks is the
|
|
1232
|
+
* runner-side bonus deposit, and the remedy command covers it. So the check
|
|
1233
|
+
* informs rather than gates, and (unlike every other entry here) carries its
|
|
1234
|
+
* `remedy` alongside a passing verdict; the same guidance also rides in
|
|
1235
|
+
* `detail`, which is the field the doctor prints for a passing check.
|
|
1236
|
+
*
|
|
1237
|
+
* The runner is detected by name, never by executing anything: a script that
|
|
1238
|
+
* reaches `run-tests.js` indirectly reads as "own runner", which errs toward
|
|
1239
|
+
* naming the deposit command — correct on either shape.
|
|
1240
|
+
*
|
|
1241
|
+
* @param {{ projectRoot?: string, readFile?: typeof fs.readFileSync }} [opts]
|
|
1242
|
+
* @returns {{ ok: boolean, detail: string, remedy?: string }}
|
|
1243
|
+
*/
|
|
1244
|
+
function runTestCreditPath({ projectRoot, readFile = fs.readFileSync } = {}) {
|
|
1245
|
+
const script = readProjectTestScript(projectRoot ?? process.cwd(), readFile);
|
|
1246
|
+
if (script === null) {
|
|
1247
|
+
return {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
detail: `no \`test\` script in package.json — close still spawns \`npm test\`, so ${TEST_CREDIT_REMEDY}`,
|
|
1250
|
+
remedy: TEST_CREDIT_REMEDY,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
if (script.includes(MANDREL_TEST_RUNNER)) {
|
|
1254
|
+
return {
|
|
1255
|
+
ok: true,
|
|
1256
|
+
detail: `\`npm test\` → \`${script}\` reaches mandrel's runner — a green full run on a story branch deposits the close \`test\` credit itself`,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
ok: true,
|
|
1261
|
+
detail: `\`npm test\` → \`${script}\` is this project's own runner and never reaches \`${MANDREL_TEST_RUNNER}\`, so it deposits no close \`test\` credit and prints nothing — ${TEST_CREDIT_REMEDY}`,
|
|
1262
|
+
remedy: TEST_CREDIT_REMEDY,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1179
1266
|
/**
|
|
1180
1267
|
* Ordered array of doctor checks. Each entry follows the
|
|
1181
1268
|
* `{ name: string, run(opts?): { ok: boolean, detail: string, remedy?: string } }` contract.
|
|
@@ -1243,6 +1330,15 @@ export const registry = [
|
|
|
1243
1330
|
advisory: true,
|
|
1244
1331
|
run: (opts) => runVersionCurrent(opts),
|
|
1245
1332
|
},
|
|
1333
|
+
{
|
|
1334
|
+
name: 'test-credit-path',
|
|
1335
|
+
// Non-fatal: `run()` always returns ok:true. A project on its own test
|
|
1336
|
+
// runner is a supported setup that simply has to deposit the close
|
|
1337
|
+
// `test` credit explicitly, so this reports a condition rather than
|
|
1338
|
+
// failing the install (Story #5324).
|
|
1339
|
+
advisory: true,
|
|
1340
|
+
run: (opts) => runTestCreditPath(opts),
|
|
1341
|
+
},
|
|
1246
1342
|
];
|
|
1247
1343
|
|
|
1248
1344
|
export default registry;
|
package/lib/migrations/index.js
CHANGED
|
@@ -59,6 +59,8 @@ import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
|
|
|
59
59
|
import { retireMaxSeedWords } from './steps/2.11.0-retire-max-seed-words.js';
|
|
60
60
|
import { retireCodebaseSnapshot } from './steps/2.20.0-retire-codebase-snapshot.js';
|
|
61
61
|
import { retireLintBaselineCommand } from './steps/2.32.0-retire-lint-baseline-command.js';
|
|
62
|
+
import { retireDeliveryLimitKnobs } from './steps/2.57.0-retire-delivery-limit-knobs.js';
|
|
63
|
+
import { retirePlanningLimitKnobs } from './steps/2.57.0-retire-planning-limit-knobs.js';
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Ordered registry of migration steps. MUST stay sorted ascending by
|
|
@@ -78,6 +80,8 @@ export const migrations = [
|
|
|
78
80
|
retireMaxSeedWords,
|
|
79
81
|
retireCodebaseSnapshot,
|
|
80
82
|
retireLintBaselineCommand,
|
|
83
|
+
retirePlanningLimitKnobs,
|
|
84
|
+
retireDeliveryLimitKnobs,
|
|
81
85
|
];
|
|
82
86
|
|
|
83
87
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js
|
|
2
|
+
/**
|
|
3
|
+
* Story #5313 — the delivery diet. Strip the retired `delivery.*` keys from a
|
|
4
|
+
* consumer's config:
|
|
5
|
+
*
|
|
6
|
+
* - `delivery.routing.freshCriticSampleRate` — the maker-checker sampling
|
|
7
|
+
* floor is gone; the standard profile routes purely off the derived
|
|
8
|
+
* change level (high or underivable → fresh critic, low → inline).
|
|
9
|
+
* - `delivery.codeReview.maxFixScopeFiles` — the auto-fix file-count
|
|
10
|
+
* ceiling bounded remediation by count rather than by risk.
|
|
11
|
+
* - `delivery.signals` (`rework.editsPerFile`, `retry.repeatCount`) — the
|
|
12
|
+
* detector thresholds and `SIGNALS_DEFAULTS` are retired wholesale.
|
|
13
|
+
* - `delivery.quality.codingGuardrails.cyclomaticMustFix` — the cyclomatic
|
|
14
|
+
* ratchet keeps its fixed ceiling of 12; `cyclomaticFlag` stays advisory.
|
|
15
|
+
*
|
|
16
|
+
* Every affected block carries `additionalProperties: false`, so a config
|
|
17
|
+
* still setting any of them fails validation on upgrade rather than warning.
|
|
18
|
+
* It sweeps **both** config surfaces (`.agentrc.json` and the gitignored
|
|
19
|
+
* `.agentrc.local.json`), because `config-resolver.js` deep-merges the
|
|
20
|
+
* overlay before the AJV gate runs.
|
|
21
|
+
*
|
|
22
|
+
* Pruning: `delivery.signals` is removed whole (`pruneDepth: 1` prunes an
|
|
23
|
+
* emptied `delivery`); the nested keys prune their emptied ancestors up to
|
|
24
|
+
* `delivery` itself. `delivery` is optional, so an emptied block is removed
|
|
25
|
+
* rather than left as `{}`. A sibling key that survives keeps its block.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
|
|
29
|
+
|
|
30
|
+
export const retireDeliveryLimitKnobs = createRetireAgentrcKeyStep({
|
|
31
|
+
version: '2.57.0',
|
|
32
|
+
description:
|
|
33
|
+
'strip the retired delivery.* limit knobs from .agentrc.json — ' +
|
|
34
|
+
'routing.freshCriticSampleRate, codeReview.maxFixScopeFiles, signals, ' +
|
|
35
|
+
'and quality.codingGuardrails.cyclomaticMustFix (Story #5313)',
|
|
36
|
+
keys: [
|
|
37
|
+
{ path: ['delivery', 'routing', 'freshCriticSampleRate'], pruneDepth: 2 },
|
|
38
|
+
{ path: ['delivery', 'codeReview', 'maxFixScopeFiles'], pruneDepth: 2 },
|
|
39
|
+
{ path: ['delivery', 'signals'], pruneDepth: 1 },
|
|
40
|
+
{
|
|
41
|
+
path: ['delivery', 'quality', 'codingGuardrails', 'cyclomaticMustFix'],
|
|
42
|
+
pruneDepth: 3,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js
|
|
2
|
+
/**
|
|
3
|
+
* Story #5312 — the planning diet. Strip the ten retired `planning.*` keys
|
|
4
|
+
* from a consumer's config:
|
|
5
|
+
*
|
|
6
|
+
* - `planning.complexityGate` — the plan-side lite claim and its persist
|
|
7
|
+
* backstop are gone; nothing reads the switch.
|
|
8
|
+
* - `planning.riskHeuristics` — the phrase list was empty in every consumer
|
|
9
|
+
* that resolved it, and the critic trigger that matched it went with the
|
|
10
|
+
* consolidation critic.
|
|
11
|
+
* - `planning.failOnSharedEditors`, `planning.requireExplicitCrossStoryDeps`,
|
|
12
|
+
* `planning.failOnRegistryConflicts`, `planning.failOnLargeFanOut`,
|
|
13
|
+
* `planning.largeFanOutThreshold`, `planning.crossCuttingRegistries` —
|
|
14
|
+
* every conflict finding is advisory now, and the registry and fan-out
|
|
15
|
+
* findings no longer exist.
|
|
16
|
+
* - `planning.memoryPool.staleAfterDays`, `planning.memoryPool.growthDelta`
|
|
17
|
+
* — the memory-hygiene advisory keeps only its index-byte arm.
|
|
18
|
+
*
|
|
19
|
+
* The `planning` block carries `additionalProperties: false`, so a config
|
|
20
|
+
* still setting any of them fails validation on upgrade rather than warning.
|
|
21
|
+
* It sweeps **both** config surfaces (`.agentrc.json` and the gitignored
|
|
22
|
+
* `.agentrc.local.json`), because `config-resolver.js` deep-merges the
|
|
23
|
+
* overlay before the AJV gate runs.
|
|
24
|
+
*
|
|
25
|
+
* Pruning: the two `memoryPool` keys prune the `memoryPool` object when it
|
|
26
|
+
* empties and then `planning` itself (`pruneDepth: 2`); the top-level keys
|
|
27
|
+
* prune `planning` (`pruneDepth: 1`). `planning` is optional, so an emptied
|
|
28
|
+
* block is removed rather than left as `{}`. A `memoryPool` that still
|
|
29
|
+
* carries `indexByteCeiling` survives untouched.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
|
|
33
|
+
|
|
34
|
+
const TOP_LEVEL_KEYS = [
|
|
35
|
+
'complexityGate',
|
|
36
|
+
'riskHeuristics',
|
|
37
|
+
'failOnSharedEditors',
|
|
38
|
+
'requireExplicitCrossStoryDeps',
|
|
39
|
+
'failOnRegistryConflicts',
|
|
40
|
+
'failOnLargeFanOut',
|
|
41
|
+
'largeFanOutThreshold',
|
|
42
|
+
'crossCuttingRegistries',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const retirePlanningLimitKnobs = createRetireAgentrcKeyStep({
|
|
46
|
+
version: '2.57.0',
|
|
47
|
+
description:
|
|
48
|
+
'strip the retired planning.* limit knobs from .agentrc.json — ' +
|
|
49
|
+
'complexityGate, riskHeuristics, the conflict-severity and fan-out ' +
|
|
50
|
+
'knobs, and memoryPool.{staleAfterDays, growthDelta} (Story #5312)',
|
|
51
|
+
keys: [
|
|
52
|
+
...TOP_LEVEL_KEYS.map((key) => ({
|
|
53
|
+
path: ['planning', key],
|
|
54
|
+
pruneDepth: 1,
|
|
55
|
+
})),
|
|
56
|
+
{ path: ['planning', 'memoryPool', 'staleAfterDays'], pruneDepth: 2 },
|
|
57
|
+
{ path: ['planning', 'memoryPool', 'growthDelta'], pruneDepth: 2 },
|
|
58
|
+
],
|
|
59
|
+
});
|
package/package.json
CHANGED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// .agents/scripts/lib/framework-version.js
|
|
2
|
-
/**
|
|
3
|
-
* framework-version.js — the visible authoring-marker surface for the legacy
|
|
4
|
-
* ticket-body provenance stamp.
|
|
5
|
-
*
|
|
6
|
-
* Tickets authored under v1 carry a hybrid stamp: a hidden
|
|
7
|
-
* `<!-- meta: {"mandrel_version":"…","authored_at":"…"} -->` block plus a
|
|
8
|
-
* single visible footer line
|
|
9
|
-
* `> 🏷️ Authored with Mandrel v<version> · <YYYY-MM-DD>`. The producer of
|
|
10
|
-
* new stamps was retired with the Epic write surface (#4574) — nothing stamps
|
|
11
|
-
* a new ticket — but bodies already stamped are live in the backlog, so the
|
|
12
|
-
* Story-body serializer/parser must keep round-tripping them:
|
|
13
|
-
*
|
|
14
|
-
* - {@link AUTHORED_MARKER_LINE_RE} lets the parser skip the marker line during
|
|
15
|
-
* section parsing so it never pollutes the last structured section.
|
|
16
|
-
* - {@link authoredMarkerLine} lets the serializer re-emit a byte-identical
|
|
17
|
-
* marker line for a stamp it parsed, preserving provenance verbatim.
|
|
18
|
-
*
|
|
19
|
-
* This module imports nothing so it can be pulled in from the story-body
|
|
20
|
-
* serializer without risking an import cycle.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The visible authoring marker line. A blockquote so GitHub renders it as a
|
|
25
|
-
* callout. Used in the Story-body parser to skip the line during section
|
|
26
|
-
* parsing so it never pollutes the last structured section.
|
|
27
|
-
*/
|
|
28
|
-
export const AUTHORED_MARKER_LINE_RE = /^\s*>\s*🏷️\s+Authored with Mandrel\b/;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build the visible authoring marker line for a given stamp. The Story-body
|
|
32
|
-
* serializer uses this to re-emit a legacy stamp it parsed, byte-identically.
|
|
33
|
-
*
|
|
34
|
-
* @param {{ version: string, authoredAt: string }} stamp
|
|
35
|
-
* @returns {string}
|
|
36
|
-
*/
|
|
37
|
-
export function authoredMarkerLine({ version, authoredAt }) {
|
|
38
|
-
return `> 🏷️ Authored with Mandrel v${version} · ${authoredAt}`;
|
|
39
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* consolidation-precondition.js — deterministic dispatch gate for the Phase
|
|
3
|
-
* 8.3 Holistic Consolidation sub-agent (Story #4431, Epic #4429).
|
|
4
|
-
*
|
|
5
|
-
* The Phase 8.3 consolidation critic (`epic-plan-consolidate`) is a genuine
|
|
6
|
-
* fresh-context `Agent` dispatch — every call re-pays the full always-loaded
|
|
7
|
-
* context (`.agents/instructions.md` and its always-on rules, § 4). When the
|
|
8
|
-
* decomposer's draft `tickets.json` already matches the Tech Spec's `##
|
|
9
|
-
* Delivery Slicing` target 1:1 (same shippable-Story count, and the
|
|
10
|
-
* `depends_on` shape already agrees with each slice's declared
|
|
11
|
-
* "Independent?" answer), there is nothing left for the critic to
|
|
12
|
-
* reconcile — dispatching it is pure token spend for a no-op. This module
|
|
13
|
-
* computes that decision **deterministically**, off the same two inputs the
|
|
14
|
-
* critic itself reads (the draft array and the Epic body's Delivery Slicing
|
|
15
|
-
* table), so the planning workflow can skip the sub-agent
|
|
16
|
-
* dispatch when it is provably safe to.
|
|
17
|
-
*
|
|
18
|
-
* **Fail-open by design.** Every ambiguous case — a missing or unparseable
|
|
19
|
-
* Delivery Slicing section, an unparseable "Independent?" cell — resolves to
|
|
20
|
-
* `dispatch: true`. This gate can only ever *save* a dispatch when it is
|
|
21
|
-
* confident the critic has nothing to do; it never disables the critic's
|
|
22
|
-
* ability to catch a real divergence. Since Epic #4474 PR6 this precondition
|
|
23
|
-
* is one input to the risk/size-conditional dispatch layer
|
|
24
|
-
* (`plan-critic-conditions.js`): reachability (8.4) is a deterministic
|
|
25
|
-
* persist-side check (`plan-reachability.js`) and the pre-mortem critic
|
|
26
|
-
* (8.5) is risk/size-gated; the deterministic ticket validator remains
|
|
27
|
-
* unconditional.
|
|
28
|
-
*
|
|
29
|
-
* Pure, synchronous, no I/O — callers own reading `tickets.json` and the
|
|
30
|
-
* Epic body off disk / the GitHub API.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { DELIVERY_SLICING_RE as DELIVERY_SLICING_HEADING_RE } from '../ticket-body-sections.js';
|
|
34
|
-
|
|
35
|
-
/** A row is a markdown table line: starts with `|` once trimmed. */
|
|
36
|
-
const TABLE_ROW_RE = /^\|/;
|
|
37
|
-
|
|
38
|
-
/** A markdown table separator row: `|---|:---:|---:|` (dashes, colons, pipes only). */
|
|
39
|
-
const TABLE_SEPARATOR_RE = /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/;
|
|
40
|
-
|
|
41
|
-
/** The literal goal-section token that marks the wave-0 BDD scaffold Story. */
|
|
42
|
-
const BDD_SCAFFOLD_GOAL_TOKEN = 'bdd-scaffold';
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Split one markdown table row into trimmed cell strings.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} line
|
|
48
|
-
* @returns {string[]}
|
|
49
|
-
*/
|
|
50
|
-
function splitTableRow(line) {
|
|
51
|
-
let trimmed = line.trim();
|
|
52
|
-
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
|
|
53
|
-
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
|
|
54
|
-
return trimmed.split('|').map((cell) => cell.trim());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Parse an "Independent?" cell per the pinned rule: match the cell's
|
|
59
|
-
* leading word case-insensitively as `Yes` or `No`. Any other leading word
|
|
60
|
-
* (or an empty cell) is unparseable and returns `null` — the caller must
|
|
61
|
-
* fail open (`dispatch: true`) rather than guess.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} cell
|
|
64
|
-
* @returns {boolean|null} `true` for Yes, `false` for No, `null` when unparseable.
|
|
65
|
-
*/
|
|
66
|
-
function parseIndependentCell(cell) {
|
|
67
|
-
const match = String(cell ?? '')
|
|
68
|
-
.trim()
|
|
69
|
-
.match(/^[A-Za-z]+/);
|
|
70
|
-
if (!match) return null;
|
|
71
|
-
const word = match[0].toLowerCase();
|
|
72
|
-
if (word === 'yes') return true;
|
|
73
|
-
if (word === 'no') return false;
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Locate and parse the `## Delivery Slicing` markdown table out of the Epic
|
|
79
|
-
* body (which carries the folded Tech Spec sections — Story #4324). Returns
|
|
80
|
-
* `null` when the heading is absent, no table follows it, the table has no
|
|
81
|
-
* "Independent?" column, or any data row's "Independent?" cell is
|
|
82
|
-
* unparseable — every one of those is a fail-open signal for the caller.
|
|
83
|
-
*
|
|
84
|
-
* @param {string} epicBody
|
|
85
|
-
* @returns {{ slice: string, independent: boolean }[] | null}
|
|
86
|
-
*/
|
|
87
|
-
export function parseDeliverySlicingTable(epicBody) {
|
|
88
|
-
if (typeof epicBody !== 'string' || epicBody.length === 0) return null;
|
|
89
|
-
|
|
90
|
-
const lines = epicBody.split(/\r?\n/);
|
|
91
|
-
const headingIdx = lines.findIndex((line) =>
|
|
92
|
-
DELIVERY_SLICING_HEADING_RE.test(line.trim()),
|
|
93
|
-
);
|
|
94
|
-
if (headingIdx === -1) return null;
|
|
95
|
-
|
|
96
|
-
let i = headingIdx + 1;
|
|
97
|
-
while (i < lines.length && lines[i].trim() === '') i++;
|
|
98
|
-
if (i >= lines.length || !TABLE_ROW_RE.test(lines[i].trim())) return null;
|
|
99
|
-
|
|
100
|
-
const headerCells = splitTableRow(lines[i]);
|
|
101
|
-
i++;
|
|
102
|
-
if (i >= lines.length || !TABLE_SEPARATOR_RE.test(lines[i].trim())) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
i++;
|
|
106
|
-
|
|
107
|
-
const independentIdx = headerCells.findIndex((cell) =>
|
|
108
|
-
/independent/i.test(cell),
|
|
109
|
-
);
|
|
110
|
-
if (independentIdx === -1) return null;
|
|
111
|
-
|
|
112
|
-
const rows = [];
|
|
113
|
-
while (i < lines.length && TABLE_ROW_RE.test(lines[i].trim())) {
|
|
114
|
-
const cells = splitTableRow(lines[i]);
|
|
115
|
-
const independent = parseIndependentCell(cells[independentIdx]);
|
|
116
|
-
if (independent === null) return null; // unparseable cell → fail open
|
|
117
|
-
rows.push({ slice: (cells[0] ?? '').trim(), independent });
|
|
118
|
-
i++;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return rows.length > 0 ? rows : null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* True when `story` is the recognized wave-0 BDD scaffold Story — identified
|
|
126
|
-
* by the literal `bdd-scaffold` goal token the decomposer prompt
|
|
127
|
-
* skill's WAVE-0 BDD SCAFFOLD STORY section requires. Scaffold Stories are
|
|
128
|
-
* not a Delivery Slicing slice, so they are excluded from the count
|
|
129
|
-
* comparison — BDD-adopting consumer repos still benefit from the
|
|
130
|
-
* precondition gate rather than always paying the 8.3 dispatch.
|
|
131
|
-
*
|
|
132
|
-
* @param {{ body?: unknown }} story
|
|
133
|
-
* @returns {boolean}
|
|
134
|
-
*/
|
|
135
|
-
function isBddScaffoldStory(story) {
|
|
136
|
-
const body = story?.body;
|
|
137
|
-
return (
|
|
138
|
-
typeof body === 'string' &&
|
|
139
|
-
body.toLowerCase().includes(BDD_SCAFFOLD_GOAL_TOKEN)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Evaluate whether the Phase 8.3 consolidation sub-agent needs to run.
|
|
145
|
-
*
|
|
146
|
-
* @param {object} input
|
|
147
|
-
* @param {object[]} input.draftStories - The draft `tickets.json` array
|
|
148
|
-
* (the decomposer's output) — raw Story ticket objects with
|
|
149
|
-
* top-level `slug` / `depends_on` / `body` (serialized string).
|
|
150
|
-
* @param {string} input.epicBody - The Epic body carrying the folded Tech
|
|
151
|
-
* Spec sections (`## Delivery Slicing` onward).
|
|
152
|
-
* @returns {{ dispatch: boolean, cause: 'match'|'divergence'|'fail-open', reasons: string[] }}
|
|
153
|
-
* `dispatch: false` only when the draft matches the Delivery Slicing table
|
|
154
|
-
* 1:1 in count and dependency shape; `dispatch: true` (with `reasons`)
|
|
155
|
-
* otherwise, including every fail-open case. `cause` distinguishes a
|
|
156
|
-
* **confirmed** divergence (count or dependency-shape mismatch) from the
|
|
157
|
-
* fail-open ambiguity (missing/unparseable table) — the #4474 PR6
|
|
158
|
-
* conditional-dispatch layer treats only the former as a firing condition
|
|
159
|
-
* on small drafts.
|
|
160
|
-
*/
|
|
161
|
-
export function evaluateConsolidationPrecondition({ draftStories, epicBody }) {
|
|
162
|
-
if (!Array.isArray(draftStories)) {
|
|
163
|
-
throw new TypeError(
|
|
164
|
-
'evaluateConsolidationPrecondition: draftStories must be an array',
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const slicing = parseDeliverySlicingTable(epicBody);
|
|
169
|
-
if (!slicing) {
|
|
170
|
-
return {
|
|
171
|
-
dispatch: true,
|
|
172
|
-
cause: 'fail-open',
|
|
173
|
-
reasons: [
|
|
174
|
-
'Delivery Slicing section is missing or unparseable — fail-open to the critic.',
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const slicedStories = draftStories.filter(
|
|
180
|
-
(story) => !isBddScaffoldStory(story),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (slicedStories.length !== slicing.length) {
|
|
184
|
-
return {
|
|
185
|
-
dispatch: true,
|
|
186
|
-
cause: 'divergence',
|
|
187
|
-
reasons: [
|
|
188
|
-
`Story count diverges from Delivery Slicing: ${slicing.length} proposed slice(s) vs ${slicedStories.length} non-scaffold draft Story(ies).`,
|
|
189
|
-
],
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const reasons = [];
|
|
194
|
-
for (let idx = 0; idx < slicing.length; idx++) {
|
|
195
|
-
const slice = slicing[idx];
|
|
196
|
-
const story = slicedStories[idx];
|
|
197
|
-
const dependsOn = Array.isArray(story?.depends_on) ? story.depends_on : [];
|
|
198
|
-
const hasDeps = dependsOn.length > 0;
|
|
199
|
-
const storyLabel = story?.slug ?? story?.title ?? `<story ${idx + 1}>`;
|
|
200
|
-
|
|
201
|
-
if (slice.independent === false && !hasDeps) {
|
|
202
|
-
reasons.push(
|
|
203
|
-
`Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: No but draft Story "${storyLabel}" declares no depends_on.`,
|
|
204
|
-
);
|
|
205
|
-
} else if (slice.independent === true && hasDeps) {
|
|
206
|
-
reasons.push(
|
|
207
|
-
`Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: Yes but draft Story "${storyLabel}" declares depends_on [${dependsOn.join(', ')}].`,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (reasons.length > 0) {
|
|
213
|
-
return { dispatch: true, cause: 'divergence', reasons };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
dispatch: false,
|
|
218
|
-
cause: 'match',
|
|
219
|
-
reasons: [
|
|
220
|
-
`Draft matches Delivery Slicing 1:1 in count and dependency shape (${slicing.length} slice(s)) — skipping the 8.3 consolidation dispatch.`,
|
|
221
|
-
],
|
|
222
|
-
};
|
|
223
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fan-out / soft-conflict gates used by plan-persist (extracted from the
|
|
3
|
-
* retired epic-plan-decompose persist phase in Stage 5).
|
|
4
|
-
*
|
|
5
|
-
* @module lib/orchestration/plan-persist/fan-out-gate
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { Logger } from '../../Logger.js';
|
|
9
|
-
import {
|
|
10
|
-
CONFLICT_KINDS,
|
|
11
|
-
renderFanOutEvidence,
|
|
12
|
-
renderFanOutRemedy,
|
|
13
|
-
renderHardConflictError,
|
|
14
|
-
} from '../ticket-validator-conflicts.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {object[]} findings
|
|
18
|
-
* @param {boolean} allowLargeFanOut
|
|
19
|
-
* @param {string} [tag]
|
|
20
|
-
*/
|
|
21
|
-
export function enforceFanOutGate(
|
|
22
|
-
findings,
|
|
23
|
-
allowLargeFanOut,
|
|
24
|
-
tag = 'plan-persist',
|
|
25
|
-
) {
|
|
26
|
-
const fanOut = (findings ?? []).filter((f) => f.kind === 'fan-out-warning');
|
|
27
|
-
if (fanOut.length === 0) return;
|
|
28
|
-
if (allowLargeFanOut) {
|
|
29
|
-
for (const f of fanOut) {
|
|
30
|
-
Logger.warn(
|
|
31
|
-
`[${tag}] Persisting a large-fan-out deletion: ` +
|
|
32
|
-
`Story "${f.storySlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
|
|
33
|
-
`importer(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.` +
|
|
34
|
-
renderFanOutEvidence(f),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const lines = fanOut
|
|
40
|
-
.map(
|
|
41
|
-
(f) =>
|
|
42
|
-
` - Story "${f.storySlug}" deletes "${f.path}" — ` +
|
|
43
|
-
`${f.callSiteCount} importer(s) (threshold ${f.threshold})` +
|
|
44
|
-
renderFanOutEvidence(f),
|
|
45
|
-
)
|
|
46
|
-
.join('\n');
|
|
47
|
-
// Each finding carries its own remedy — a rename-shaped deletion has no
|
|
48
|
-
// subsystems to split across, so a blanket "split it up" footer would be
|
|
49
|
-
// wrong advice for it (Story #4547).
|
|
50
|
-
const remedies = [...new Set(fanOut.map((f) => renderFanOutRemedy(f)))];
|
|
51
|
-
throw new Error(
|
|
52
|
-
`[${tag}] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
|
|
53
|
-
remedies.join('\n\n'),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Report every soft finding the validator produced, each under its own kind.
|
|
59
|
-
*
|
|
60
|
-
* Only the {@link CONFLICT_KINDS} are cross-Story conflicts. The advisory
|
|
61
|
-
* kinds — `spec-word-budget` and the sizing findings — are single-Story
|
|
62
|
-
* nudges, and announcing them as conflicts overstated them and taught readers
|
|
63
|
-
* to discount the whole channel (Story #4907). `fan-out-warning` is excluded
|
|
64
|
-
* throughout: {@link enforceFanOutGate} owns it and has already either thrown
|
|
65
|
-
* or logged the override.
|
|
66
|
-
*
|
|
67
|
-
* @param {object[]} findings
|
|
68
|
-
* @param {string} [tag]
|
|
69
|
-
*/
|
|
70
|
-
export function surfaceSoftConflictFindings(findings, tag = 'plan-persist') {
|
|
71
|
-
const soft = (findings ?? []).filter(
|
|
72
|
-
(f) => f?.severity === 'soft' && f?.kind !== 'fan-out-warning',
|
|
73
|
-
);
|
|
74
|
-
if (soft.length === 0) return;
|
|
75
|
-
const conflicts = soft.filter((f) => CONFLICT_KINDS.has(f?.kind));
|
|
76
|
-
const advisories = soft.filter((f) => !CONFLICT_KINDS.has(f?.kind));
|
|
77
|
-
if (conflicts.length > 0) {
|
|
78
|
-
Logger.warn(
|
|
79
|
-
`[${tag}] ${conflicts.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
|
|
80
|
-
);
|
|
81
|
-
for (const finding of conflicts) {
|
|
82
|
-
Logger.warn(
|
|
83
|
-
`[${tag}] soft conflict: ${renderHardConflictError(finding)}`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (advisories.length > 0) {
|
|
88
|
-
Logger.warn(
|
|
89
|
-
`[${tag}] ${advisories.length} advisory finding(s) — the persist proceeds:`,
|
|
90
|
-
);
|
|
91
|
-
for (const finding of advisories) {
|
|
92
|
-
Logger.warn(
|
|
93
|
-
`[${tag}] advisory (${finding.kind}): ${renderHardConflictError(finding)}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|