mandrel 2.55.0 → 2.57.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.
Files changed (131) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +4 -30
  4. package/.agents/docs/configuration.md +11 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/rules/ci-remediation.md +39 -21
  9. package/.agents/schemas/agentrc.schema.json +28 -185
  10. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  11. package/.agents/scripts/acceptance-eval.js +107 -17
  12. package/.agents/scripts/audit-to-stories.js +222 -75
  13. package/.agents/scripts/ceremony-derive.js +191 -0
  14. package/.agents/scripts/check-context-budget.js +28 -33
  15. package/.agents/scripts/check-cyclomatic.js +4 -3
  16. package/.agents/scripts/deliver-light.js +31 -94
  17. package/.agents/scripts/file-ci-gap.js +306 -0
  18. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  19. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  20. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
  21. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  22. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  23. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  24. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
  25. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  26. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  27. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  28. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  29. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  30. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  31. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  32. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  33. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  34. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  35. package/.agents/scripts/lib/config/explain.js +0 -19
  36. package/.agents/scripts/lib/config/limits.js +18 -78
  37. package/.agents/scripts/lib/config/quality.js +6 -3
  38. package/.agents/scripts/lib/config/runners.js +3 -2
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  40. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  41. package/.agents/scripts/lib/config-settings-schema.js +49 -143
  42. package/.agents/scripts/lib/crap-engine.js +35 -4
  43. package/.agents/scripts/lib/crap-utils.js +17 -1
  44. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +38 -0
  50. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  51. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  52. package/.agents/scripts/lib/label-constants.js +6 -1
  53. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  54. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  55. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  56. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  57. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  58. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  59. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  60. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  61. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  62. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  63. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  64. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  65. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  66. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  67. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  68. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  69. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
  70. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  71. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  72. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  73. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  74. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  75. package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
  76. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  77. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  78. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  79. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  80. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  81. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  82. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  83. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  84. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  85. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  86. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  87. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  88. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  89. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  90. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  91. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  92. package/.agents/scripts/lib/test-run-credit.js +266 -0
  93. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  94. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  95. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  96. package/.agents/scripts/plan-context.js +7 -9
  97. package/.agents/scripts/plan-critics.js +28 -54
  98. package/.agents/scripts/plan-persist.js +25 -68
  99. package/.agents/scripts/pr-watch-with-update.js +3 -2
  100. package/.agents/scripts/quality-preview.js +51 -0
  101. package/.agents/scripts/run-tests.js +12 -0
  102. package/.agents/scripts/stories-wave-tick.js +23 -45
  103. package/.agents/scripts/test-isolate.js +13 -180
  104. package/.agents/scripts/update-coverage-baseline.js +25 -70
  105. package/.agents/scripts/update-crap-baseline.js +19 -123
  106. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  107. package/.agents/workflows/audit-clean-code.md +4 -3
  108. package/.agents/workflows/audit-to-stories.md +63 -27
  109. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  110. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  111. package/.agents/workflows/helpers/code-review.md +2 -3
  112. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  113. package/.agents/workflows/helpers/deliver-light.md +40 -105
  114. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  115. package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
  116. package/.agents/workflows/helpers/deliver-story.md +9 -13
  117. package/.agents/workflows/helpers/plan-reference.md +132 -196
  118. package/.agents/workflows/mandrel-plan.md +28 -41
  119. package/.agents/workflows/memory-consolidate.md +9 -13
  120. package/docs/CHANGELOG.md +33 -0
  121. package/lib/migrations/index.js +4 -0
  122. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  123. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  124. package/package.json +1 -1
  125. package/.agents/scripts/lib/framework-version.js +0 -39
  126. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  127. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  128. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  129. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  130. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  131. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -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
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * decomposer-context.js — system-prompt builder for the `/mandrel-plan` authoring
3
- * pass (Story #2466; v2 keeps the prompt helper, drops the Epic-fetch
4
- * decomposition envelope).
5
- *
6
- * `buildDecomposerSystemPrompt` is the live surface consumed by
7
- * `plan-context.js#buildSystemPrompts`. The retired
8
- * `buildDecompositionContext` Epic-fetch path lived here until the
9
- * Story-only cutover removed every caller.
10
- */
11
-
12
- import { renderDecomposerSystemPrompt } from '../../templates/decomposer-prompts.js';
13
-
14
- export function buildDecomposerSystemPrompt(
15
- heuristics = [],
16
- { maxTickets } = {},
17
- ) {
18
- const base = renderDecomposerSystemPrompt({
19
- maxTickets,
20
- });
21
- const heuristicsStr =
22
- heuristics.length > 0
23
- ? `### RISK HEURISTICS (planning metadata if any apply):\n- ${heuristics.join('\n- ')}`
24
- : '';
25
- return `${base}${heuristicsStr ? `\n\n${heuristicsStr}` : ''}`;
26
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * lib/orchestration/spec-budget.js — the soft `## Spec` word-budget pass
3
- * (Story #4723), extracted from `ticket-validator.js` so the advisory
4
- * length nudge lives beside neither the hard validators nor their error
5
- * channel: everything here is `'soft'` by construction and can never fail
6
- * a persist.
7
- */
8
-
9
- import { parse as parseStoryBody } from '../story-body/story-body.js';
10
-
11
- /**
12
- * Warn threshold (words) for a Story's inline `## Spec` (Story #4723,
13
- * retuned by Story #4907). The budget carries **two distinct numbers**, and
14
- * both belong in any surface that documents it:
15
- *
16
- * - **~250 words — the authoring target.** The #4707 contract-level-prose
17
- * aim: interfaces, invariants, and load-bearing constraints, not
18
- * route-by-route behavior narration. It lives in the story-author template
19
- * placeholder (`plan-context.js`), which is what an author actually reads.
20
- * - **350 words — this warn threshold.** Deliberately slack against the
21
- * target so the warning marks a real outlier rather than ordinary
22
- * authoring variance. Measured over the 45 most recent Stories carrying a
23
- * Spec, a 250-word threshold fired on 22% of *accepted* work (median 233,
24
- * p75 249) — a warning that common trains its readers to ignore it.
25
- *
26
- * Distinct again from the hard ~1500-token fail-closed ceiling in
27
- * `spec-spill.js`: this budget only warns; it never fails the persist.
28
- */
29
- export const SPEC_SOFT_WORD_BUDGET = 350;
30
-
31
- /**
32
- * Resolve a Story's Spec prose across both authoring shapes: the canonical
33
- * serialized string body (parsed; `## Spec` text block) and the
34
- * pre-serialize structured object body (`body.spec`). Returns `''` when
35
- * absent — or when a string body does not parse: this pass is advisory, so
36
- * an unreadable body contributes no finding here and is left to the hard
37
- * parse gate (`assertStoryBodiesParse`) to reject.
38
- *
39
- * @param {object} story
40
- * @returns {string}
41
- */
42
- function resolveSpecText(story) {
43
- const body = story?.body;
44
- if (typeof body === 'string' && body.trim().length > 0) {
45
- let spec;
46
- try {
47
- spec = parseStoryBody(body).body.spec;
48
- } catch {
49
- return '';
50
- }
51
- return typeof spec === 'string' ? spec : '';
52
- }
53
- if (body !== null && typeof body === 'object') {
54
- return typeof body?.spec === 'string' ? body.spec : '';
55
- }
56
- return '';
57
- }
58
-
59
- /**
60
- * Advisory `## Spec` length pass (Story #4723). Emits one `'soft'` finding
61
- * per Story whose Spec prose exceeds {@link SPEC_SOFT_WORD_BUDGET} words,
62
- * nudging the author toward contract-level prose (#4707). Soft only — the
63
- * findings never reach the validator's `errors[]` channel, so an
64
- * over-budget Spec never fails the persist.
65
- *
66
- * @param {{ stories: object[] }} opts
67
- * @returns {object[]} Zero or more `spec-word-budget` findings.
68
- */
69
- export function computeSpecBudgetFindings({ stories }) {
70
- const findings = [];
71
- for (const story of stories ?? []) {
72
- const words = resolveSpecText(story).split(/\s+/).filter(Boolean).length;
73
- if (words <= SPEC_SOFT_WORD_BUDGET) continue;
74
- findings.push({
75
- kind: 'spec-word-budget',
76
- severity: 'soft',
77
- ticketSlug: story.slug ?? '<unknown>',
78
- words,
79
- budget: SPEC_SOFT_WORD_BUDGET,
80
- message:
81
- `Story "${story.slug ?? '<unknown>'}" ## Spec is ~${words} words ` +
82
- `(soft budget ${SPEC_SOFT_WORD_BUDGET}). Prefer contract-level prose ` +
83
- '(interfaces, invariants, load-bearing constraints with their why) ' +
84
- 'over per-file behavior narration — advisory only; the persist ' +
85
- 'proceeds.',
86
- });
87
- }
88
- return findings;
89
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * v2 Story `## Spec` budget gate.
3
- *
4
- * The Story body is the single executable document: Tech Spec prose stays
5
- * inline under `## Spec`. There is no spill-to-`docs/` path — an over-budget
6
- * Spec is treated as a sizing smell (the Story should be split or the Spec
7
- * tightened), not as a reason to write temporary product docs.
8
- *
9
- * This module also **owns** the §2 FinOps token estimator
10
- * ({@link estimateTokens}, ~4 chars/token) — the one shared approximation every
11
- * fixed framework ceiling speaks in (the Spec budget below, the plan-time
12
- * sizing ceilings in `ticket-validator-sizing.js`, and the audit checklist
13
- * threading budget). It used to live in a `context-envelope.js` SDK whose
14
- * remaining surface had no live caller; the estimator is all that survived.
15
- *
16
- * @module lib/orchestration/spec-spill
17
- */
18
-
19
- /**
20
- * Rough token estimate: ~4 characters per token. Deliberately cheap and
21
- * deterministic — every ceiling that quotes "tokens" is quoting this number,
22
- * so the approximation matters far less than every caller sharing one.
23
- *
24
- * @param {string} text
25
- * @returns {number}
26
- */
27
- export function estimateTokens(text) {
28
- return Math.ceil(String(text ?? '').length / 4);
29
- }
30
-
31
- /**
32
- * Soft budget (estimated tokens) for an inline `## Spec`. ~1500 tokens ≈ 6KB —
33
- * enough for a real approach section, well under issue-body bloat.
34
- */
35
- export const DEFAULT_SPEC_BODY_TOKEN_BUDGET = 1500;
36
-
37
- /**
38
- * @typedef {object} SpecBudgetResult
39
- * @property {number} estimatedTokens
40
- * @property {string} content
41
- */
42
-
43
- /**
44
- * Keep Spec inline when it fits; throw when it exceeds the budget so the
45
- * planner splits or tightens instead of writing docs/.
46
- *
47
- * @param {object} args
48
- * @param {string} args.storyId Story slug/id for error context.
49
- * @param {string} args.spec Folded Tech Spec markdown.
50
- * @param {object} [opts]
51
- * @param {number} [opts.tokenBudget=DEFAULT_SPEC_BODY_TOKEN_BUDGET]
52
- * @returns {SpecBudgetResult}
53
- */
54
- export function assertSpecWithinBudget({ storyId, spec }, opts = {}) {
55
- const { tokenBudget = DEFAULT_SPEC_BODY_TOKEN_BUDGET } = opts;
56
- const content = typeof spec === 'string' ? spec : '';
57
- const estimatedTokens = estimateTokens(content);
58
-
59
- if (estimatedTokens <= tokenBudget) {
60
- return { estimatedTokens, content };
61
- }
62
-
63
- const label =
64
- typeof storyId === 'string' && storyId.trim() !== ''
65
- ? storyId.trim()
66
- : 'unknown';
67
-
68
- throw new Error(
69
- `[plan-persist] Story "${label}" ## Spec is ~${estimatedTokens} tokens ` +
70
- `(budget ${tokenBudget}). An over-budget Spec usually means the Story ` +
71
- `is too large — split it, or tighten ## Spec so the Story body stays ` +
72
- `the single executable document. Specs are never written to docs/.`,
73
- );
74
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * verify-tier-repair.js — repair-before-judging for the `verify[]` tier suffix
3
- * (Story #5005).
4
- *
5
- * The tier suffix (`… (unit)`) is a deterministic, mechanically-derivable
6
- * formality. `task-body-validator.js` already computed the corrected entry in
7
- * order to print it as a "Suggested fix" — then rejected the plan anyway and
8
- * charged the author a full re-drafting round to paste that exact string back.
9
- * This module applies the inference the validator already trusts, so the hard
10
- * error is reserved for the entries only the author can resolve.
11
- *
12
- * It lives beside the validator rather than inside it because the validator's
13
- * job is to *judge*: mixing a mutating repair pass into a module of pure
14
- * collectors muddies both. `persist-helpers.js#validateTickets` calls this
15
- * first, then `validateTaskBodies`.
16
- *
17
- * @module lib/orchestration/verify-tier-repair
18
- */
19
-
20
- import { suggestVerifyFix } from '../story-body/body-format-lints.js';
21
- import {
22
- parse as parseStoryBody,
23
- serialize as serializeStoryBody,
24
- } from '../story-body/story-body.js';
25
- import { VERIFY_TIER_VALUES } from './task-body-validator.js';
26
-
27
- /** A parenthesised tier drawn from the canonical vocabulary, at end of entry. */
28
- const VERIFY_TIER_RE = new RegExp(
29
- `\\((?:${VERIFY_TIER_VALUES.join('|')})\\)\\s*$`,
30
- );
31
-
32
- /**
33
- * Rewrite one `verify[]` list, appending the inferable testing tier to any
34
- * entry missing one. Returns `null` when nothing changed, so callers can keep
35
- * an already-compliant list byte-identical (and skip re-serializing the body
36
- * it came from).
37
- *
38
- * @param {unknown} rawVerify
39
- * @returns {string[]|null} The corrected list, or `null` when no fix applied.
40
- */
41
- function repairVerifyList(rawVerify) {
42
- if (!Array.isArray(rawVerify)) return null;
43
- let changed = false;
44
- const next = rawVerify.map((entry) => {
45
- if (typeof entry !== 'string') return entry;
46
- if (entry.startsWith('manual:') || VERIFY_TIER_RE.test(entry)) return entry;
47
- const fix = suggestVerifyFix(entry);
48
- if (fix === null) return entry;
49
- changed = true;
50
- return fix;
51
- });
52
- return changed ? next : null;
53
- }
54
-
55
- /**
56
- * Repair the `## Verify` section of a serialized (string) body, in place on
57
- * the ticket. Only re-serializes when a fix actually applied, so a compliant
58
- * body is never round-tripped through `parse → serialize`.
59
- *
60
- * Leaving the body untouched while the top level was repaired would be worse
61
- * than not repairing at all: `syncContractFieldFromTopLevel` fails closed on a
62
- * body section that disagrees with its top-level array, so both sides must be
63
- * repaired by the same rule or neither.
64
- *
65
- * @param {object} ticket
66
- */
67
- function repairStringBodyVerify(ticket) {
68
- let parsed;
69
- try {
70
- parsed = parseStoryBody(ticket.body).body;
71
- } catch {
72
- // An unparseable body is already a hard error in `validateTaskBodyShape`;
73
- // there is nothing to repair and nothing to report twice.
74
- return;
75
- }
76
- const repaired = repairVerifyList(parsed.verify);
77
- if (repaired === null) return;
78
- ticket.body = serializeStoryBody({ ...parsed, verify: repaired });
79
- }
80
-
81
- /**
82
- * Auto-append the testing tier to `verify[]` entries that omit one, wherever
83
- * `suggestVerifyFix` can infer it from the command. An entry whose tier cannot
84
- * be inferred is left untouched and still hard-errors downstream.
85
- *
86
- * Mutates `tickets` in place (the persist pipeline threads this same array on
87
- * to assembly) and returns it. Already-compliant tickets are untouched, so
88
- * their persisted output stays byte-identical. Total — a non-array argument
89
- * and non-Story tickets are no-ops.
90
- *
91
- * @param {object[]} tickets
92
- * @returns {object[]} `tickets`
93
- */
94
- export function normalizeVerifyTiers(tickets) {
95
- for (const ticket of Array.isArray(tickets) ? tickets : []) {
96
- if (!ticket || ticket.type !== 'story' || ticket.body == null) continue;
97
- const topLevel = repairVerifyList(ticket.verify);
98
- if (topLevel !== null) ticket.verify = topLevel;
99
- if (typeof ticket.body === 'string') {
100
- repairStringBodyVerify(ticket);
101
- continue;
102
- }
103
- const bodyLevel = repairVerifyList(ticket.body.verify);
104
- if (bodyLevel !== null) ticket.body.verify = bodyLevel;
105
- }
106
- return tickets;
107
- }