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.
Files changed (114) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -33
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -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/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/evidence-gate.js +17 -1
  16. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  17. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  18. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  19. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  20. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  21. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  22. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  23. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  24. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  25. package/.agents/scripts/lib/config/explain.js +0 -19
  26. package/.agents/scripts/lib/config/limits.js +18 -78
  27. package/.agents/scripts/lib/config/quality.js +6 -3
  28. package/.agents/scripts/lib/config/runners.js +3 -2
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  30. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  31. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  32. package/.agents/scripts/lib/crap-engine.js +35 -4
  33. package/.agents/scripts/lib/crap-utils.js +17 -1
  34. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  35. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  36. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  37. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  38. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  39. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  40. package/.agents/scripts/lib/orchestration/code-review.js +7 -3
  41. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  42. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  43. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  44. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  45. package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
  46. package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
  47. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  48. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  49. package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
  51. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
  52. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
  53. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  54. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  55. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  56. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
  57. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  58. package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
  59. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  60. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
  61. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  62. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
  63. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  64. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  65. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  66. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  67. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  68. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  69. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  70. package/.agents/scripts/lib/story-body/story-body.js +54 -240
  71. package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
  72. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  73. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  74. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  75. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  76. package/.agents/scripts/lib/test-run-credit.js +277 -0
  77. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  78. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  79. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  80. package/.agents/scripts/plan-context.js +7 -9
  81. package/.agents/scripts/plan-critics.js +28 -54
  82. package/.agents/scripts/plan-persist.js +25 -68
  83. package/.agents/scripts/quality-preview.js +51 -0
  84. package/.agents/scripts/run-tests.js +12 -0
  85. package/.agents/scripts/stories-wave-tick.js +23 -45
  86. package/.agents/scripts/test-isolate.js +13 -180
  87. package/.agents/scripts/update-coverage-baseline.js +25 -70
  88. package/.agents/scripts/update-crap-baseline.js +19 -123
  89. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  90. package/.agents/workflows/audit-clean-code.md +4 -3
  91. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  92. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  93. package/.agents/workflows/helpers/code-review.md +2 -3
  94. package/.agents/workflows/helpers/deliver-digest.md +46 -55
  95. package/.agents/workflows/helpers/deliver-light.md +40 -105
  96. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  97. package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
  98. package/.agents/workflows/helpers/deliver-story.md +10 -13
  99. package/.agents/workflows/helpers/plan-reference.md +163 -221
  100. package/.agents/workflows/mandrel-plan.md +31 -40
  101. package/.agents/workflows/memory-consolidate.md +9 -13
  102. package/docs/CHANGELOG.md +36 -0
  103. package/lib/cli/registry.js +98 -2
  104. package/lib/migrations/index.js +4 -0
  105. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  106. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  107. package/package.json +1 -1
  108. package/.agents/scripts/lib/framework-version.js +0 -39
  109. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  110. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  111. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  112. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  113. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  114. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -18,8 +18,6 @@
18
18
  * verify: string[], // exact commands / tier annotation
19
19
  * references: PathEntry[], // read-only paths (optional)
20
20
  * non_goals: string[], // negative-scope bullets (optional, advisory)
21
- * wide: { reason } | null,// declared-wide footprint (optional)
22
- * reason_to_exist: string | null, // one-sentence cohesion reason (optional)
23
21
  * depends_on: string[], // blocker story slugs or #ids
24
22
  * }
25
23
  * ```
@@ -42,10 +40,6 @@
42
40
  * @module story-body
43
41
  */
44
42
 
45
- import {
46
- AUTHORED_MARKER_LINE_RE,
47
- authoredMarkerLine,
48
- } from '../framework-version.js';
49
43
  import { FILE_ASSUMPTION_VALUES } from '../orchestration/file-assumption-enum.js';
50
44
  import { suggestPathEntryFix } from './body-format-lints.js';
51
45
  import { isFooterSeparator, parseFooterBlockedByRefs } from './footer-block.js';
@@ -73,14 +67,10 @@ import { isFooterSeparator, parseFooterBlockedByRefs } from './footer-block.js';
73
67
  * @property {string} spec - Optional folded Tech Spec text block; '' when absent.
74
68
  * @property {PathEntry[]} changes - Files / globs this Story modifies.
75
69
  * @property {string[]} acceptance - Observable acceptance criteria.
76
- * @property {string[]} verify - Exact commands with tier annotation.
70
+ * @property {string[]} verify - Exact commands the deliverer runs.
77
71
  * @property {PathEntry[]} references - Read-only paths (may be empty).
78
72
  * @property {string[]} non_goals - Negative-scope bullets (advisory; may be empty).
79
- * @property {{ reason: string }|null} wide - Declared-wide footprint (reason), or null.
80
- * @property {string|null} reason_to_exist - One-sentence cohesion reason ("why this Story exists"), or null.
81
73
  * @property {string[]} depends_on - Blocking story slugs / issue refs.
82
- * @property {string|null} mandrel_version - Framework version stamped at authoring, or null.
83
- * @property {string|null} authored_at - Authoring date (YYYY-MM-DD) stamped at authoring, or null.
84
74
  */
85
75
 
86
76
  /**
@@ -174,13 +164,27 @@ const HUMANIZED_PATH_ENTRY_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
174
164
  // AC-<n> presentation prefix on acceptance checkboxes (Story #4600). The
175
165
  // numbering is a stable 1-based human handle only — parse() strips it so the
176
166
  // top-level acceptance[] machine contract round-trips byte-identical.
177
- const AC_PREFIX_RE = /^AC-\d+:\s+/;
178
-
179
- // Visible wide-rationale line (Story #4600): `> **Wide:** <reason>` rendered
180
- // under `## Goal`. Presentation only — the `<!-- meta -->` block stays the
181
- // canonical machine carrier, so the parser skips this line wherever it
182
- // appears (same treatment as the authored-provenance marker).
167
+ //
168
+ // The lettered form (`AC-14a:`) is accepted too (Story #5323). Nothing emits
169
+ // one — `serialize()` numbers from the array index — but a Story planned from
170
+ // an existing ticket can copy one out of the source issue's rendered
171
+ // checkboxes, and a body that already carries one must still parse to the
172
+ // handle-free text or the round-trip invariant breaks for it alone. Trailing
173
+ // whitespace is optional so `AC-3:text` normalises as readily as `AC-3: text`.
174
+ const AC_PREFIX_RE = /^AC-\d+[a-z]?:\s*/i;
175
+
176
+ // Machine-managed marker lines a body authored before Story #5312 may still
177
+ // carry: the `> **Wide:** <reason>` rationale line (Story #4600), the
178
+ // `> 🏷️ Authored with Mandrel …` provenance line, and the trailing
179
+ // `<!-- meta: {...} -->` block. Nothing writes them any more — the `wide` /
180
+ // `reason_to_exist` fields and the provenance stamp went with the plan-time
181
+ // sizing model — but live issue bodies are never rewritten, so the parser
182
+ // still skips them wherever they appear rather than absorbing a stray line
183
+ // into the last structured section. A skipped block is dropped on
184
+ // re-serialize, which is the cutover working as intended.
183
185
  const WIDE_MARKER_LINE_RE = /^>\s*\*\*Wide:\*\*/;
186
+ const AUTHORED_MARKER_LINE_RE = /^\s*>\s*🏷️\s+Authored with Mandrel\b/;
187
+ const META_BLOCK_RE = /<!--\s*meta:[\s\S]*?-->/;
184
188
 
185
189
  /**
186
190
  * Parse a single `changes` / `references` bullet into a `PathEntry`.
@@ -325,99 +329,6 @@ function extractBlockedBy(footerBlock) {
325
329
  return parseFooterBlockedByRefs(footerBlock);
326
330
  }
327
331
 
328
- // Matches any trailing `<!-- meta: … -->` block. Object payloads are the
329
- // canonical serialize() shape; non-object / malformed payloads are still
330
- // recognized so section parsing can skip them and extractMeta can degrade.
331
- const META_BLOCK_RE = /<!--\s*meta:\s*([\s\S]*?)\s*-->/;
332
- const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
333
-
334
- /**
335
- * Extract the `wide` / `reason_to_exist` fields from the trailing
336
- * `<!-- meta: {...} -->` comment block written by {@link serialize}. Returns
337
- * canonical-shaped values (null when absent or malformed) so the parser
338
- * round-trips the meta block faithfully.
339
- *
340
- * Failing closed here would be wrong: the meta block is an optional,
341
- * machine-written convenience and a malformed comment must not corrupt an
342
- * otherwise-valid Story body. A parse failure degrades to the absent-meta
343
- * defaults instead of throwing.
344
- *
345
- * The `mandrel_version` / `authored_at` provenance stamp (written once at
346
- * authoring time by the ticket-creation path) is recovered here too so a later
347
- * `parse → serialize` preserves the originally-authored version verbatim
348
- * rather than dropping or re-deriving it.
349
- *
350
- * @param {string} markdown
351
- * @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, mandrel_version: string|null, authored_at: string|null }}
352
- */
353
- function extractMeta(markdown) {
354
- const result = {
355
- wide: null,
356
- reason_to_exist: null,
357
- mandrel_version: null,
358
- authored_at: null,
359
- };
360
- const match = markdown.match(META_OBJECT_RE) ?? markdown.match(META_BLOCK_RE);
361
- if (!match) return result;
362
-
363
- let parsed;
364
- try {
365
- parsed = JSON.parse(match[1]);
366
- } catch {
367
- // Malformed meta comment — degrade to defaults rather than corrupt the body.
368
- return result;
369
- }
370
- // Non-object JSON (array / scalar / null) is treated as absent meta.
371
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
372
- return result;
373
- }
374
-
375
- result.wide = normalizeWide(parsed.wide);
376
- result.reason_to_exist = normalizeReasonToExist(parsed.reason_to_exist);
377
- if (
378
- typeof parsed.mandrel_version === 'string' &&
379
- parsed.mandrel_version.trim()
380
- ) {
381
- result.mandrel_version = parsed.mandrel_version.trim();
382
- }
383
- if (typeof parsed.authored_at === 'string' && parsed.authored_at.trim()) {
384
- result.authored_at = parsed.authored_at.trim();
385
- }
386
- return result;
387
- }
388
-
389
- /**
390
- * Normalize a raw `reason_to_exist` value to a non-empty trimmed string or
391
- * `null`. The field is the machine-checkable form of the cohesion rule
392
- * ("one Story = one coherent change with one reason to exist"): the
393
- * `epic-plan-consolidate` critic flags any Story whose body carries no
394
- * non-empty reason. An empty or non-string value is treated as absent.
395
- *
396
- * @param {unknown} raw
397
- * @returns {string|null}
398
- */
399
- function normalizeReasonToExist(raw) {
400
- if (typeof raw !== 'string') return null;
401
- const reason = raw.trim();
402
- return reason.length === 0 ? null : reason;
403
- }
404
-
405
- /**
406
- * Normalize a raw `wide` declaration to the canonical `{ reason }` shape or
407
- * `null`. A `wide` declaration is only honoured when it carries a non-empty
408
- * one-line reason — the reason is the whole point of the field (it states why
409
- * a Story is legitimately broad and lifts the hard file-width ceiling).
410
- *
411
- * @param {unknown} raw
412
- * @returns {{ reason: string }|null}
413
- */
414
- function normalizeWide(raw) {
415
- if (raw === null || typeof raw !== 'object') return null;
416
- const reason = typeof raw.reason === 'string' ? raw.reason.trim() : '';
417
- if (reason.length === 0) return null;
418
- return { reason };
419
- }
420
-
421
332
  /**
422
333
  * Split markdown into named sections plus a footer block.
423
334
  *
@@ -515,18 +426,10 @@ function isSectionTerminatorHeading(line, inPreamble, currentSection) {
515
426
  }
516
427
 
517
428
  /**
518
- * True for machine-managed marker lines section parsing skips wherever they
519
- * appear:
520
- * - the trailing `<!-- meta: {...} -->` block — machine metadata, not
521
- * section content, read separately by `extractMeta`, and skipped so a
522
- * `## References` section immediately followed by it does not swallow
523
- * the comment as a references entry;
524
- * - the visible `> 🏷️ Authored with Mandrel …` provenance marker (emitted
525
- * alongside the meta block by the authoring path; the value round-trips
526
- * via the meta block);
527
- * - the visible `> **Wide:** <reason>` rationale line (Story #4600):
528
- * presentation only — the meta block remains the canonical carrier for
529
- * `wide.reason`.
429
+ * True for the legacy machine-managed marker lines section parsing skips
430
+ * wherever they appear (see the regexes above): a `## References` section
431
+ * immediately followed by a retired meta block must not swallow the comment
432
+ * as a references entry.
530
433
  *
531
434
  * @param {string} line
532
435
  * @returns {boolean}
@@ -567,11 +470,7 @@ function parseUnstructuredBody(input, preamble, footer) {
567
470
  verify: [],
568
471
  references: [],
569
472
  non_goals: [],
570
- wide: null,
571
- reason_to_exist: null,
572
473
  depends_on: extractBlockedBy(footer),
573
- mandrel_version: null,
574
- authored_at: null,
575
474
  };
576
475
  return {
577
476
  body,
@@ -668,6 +567,33 @@ function parseTextListSection(lines) {
668
567
  * @returns {ParseResult}
669
568
  * @throws {StoryBodyParseError} When the body is structurally unrecoverable.
670
569
  */
570
+ /**
571
+ * Strip the presentation `AC-<n>:` handle off one acceptance item.
572
+ *
573
+ * The handle belongs to {@link serialize}, which numbers every checkbox from
574
+ * its position in `acceptance[]`; an authored item that already carries one
575
+ * would render doubled (`- [ ] AC-1: AC-1: …`) and a lettered handle copied
576
+ * from a source ticket would survive into the machine contract. Both parse
577
+ * and the persist-side normalisation resolve the grammar here so the two can
578
+ * never disagree about what a handle is (Story #5323).
579
+ *
580
+ * Stacked handles are peeled in full — a body persisted while the doubling
581
+ * was live carries two, and leaving the inner one would normalise to
582
+ * something that still is not the authored text.
583
+ *
584
+ * @param {string} item
585
+ * @returns {{ text: string, stripped: boolean }} The handle-free text, and
586
+ * whether anything was removed.
587
+ */
588
+ export function stripAcceptanceHandle(item) {
589
+ const original = String(item ?? '');
590
+ let text = original;
591
+ while (AC_PREFIX_RE.test(text)) {
592
+ text = text.replace(AC_PREFIX_RE, '');
593
+ }
594
+ return { text, stripped: text !== original };
595
+ }
596
+
671
597
  export function parse(input) {
672
598
  if (input === null || input === undefined) {
673
599
  throw new StoryBodyParseError('Story body is null or undefined', {
@@ -726,7 +652,7 @@ export function parse(input) {
726
652
  // The AC-<n> checkbox prefix is presentation-only (Story #4600): strip it
727
653
  // so acceptance[] round-trips byte-identical to the authored array.
728
654
  const acceptance = parseTextListSection(sections.get('acceptance') ?? []).map(
729
- (a) => a.replace(AC_PREFIX_RE, ''),
655
+ (a) => stripAcceptanceHandle(a).text,
730
656
  );
731
657
  const verify = parseTextListSection(sections.get('verify') ?? []);
732
658
  const references = parsePathEntrySection(
@@ -736,17 +662,6 @@ export function parse(input) {
736
662
  const non_goals = parseTextListSection(sections.get('non_goals') ?? []);
737
663
  const dependsOn = extractBlockedBy(footer);
738
664
 
739
- // --- Recover wide / reason_to_exist / provenance from the meta block ---
740
- // serialize() writes these into a trailing `<!-- meta: {...} -->` comment
741
- // so round-trips preserve them. Absent meta block → canonical null defaults.
742
- // Unknown keys are ignored, so a body carrying a retired meta field parses
743
- // clean and simply drops it.
744
- const meta = extractMeta(input);
745
- const wide = meta.wide;
746
- const reason_to_exist = meta.reason_to_exist;
747
- const mandrel_version = meta.mandrel_version;
748
- const authored_at = meta.authored_at;
749
-
750
665
  const body = {
751
666
  goal,
752
667
  slicing,
@@ -756,11 +671,7 @@ export function parse(input) {
756
671
  verify,
757
672
  references,
758
673
  non_goals,
759
- wide,
760
- reason_to_exist,
761
674
  depends_on: dependsOn,
762
- mandrel_version,
763
- authored_at,
764
675
  };
765
676
 
766
677
  return {
@@ -800,10 +711,6 @@ function parseStructuredObject(obj) {
800
711
  body[name] = STRUCTURED_FIELD_NORMALIZERS[kind](obj[name], warnings);
801
712
  }
802
713
 
803
- // Provenance stamp (preserved verbatim; never re-derived here).
804
- body.mandrel_version = normalizeProvenanceString(obj.mandrel_version);
805
- body.authored_at = normalizeProvenanceString(obj.authored_at);
806
-
807
714
  return {
808
715
  body,
809
716
  warnings,
@@ -829,8 +736,6 @@ function parseStructuredObject(obj) {
829
736
  * - `stringList` — array filtered to non-empty strings, else `[]`.
830
737
  * - `pathEntryList` — array normalized entry-wise via `parsePathEntry`
831
738
  * (fails closed on a malformed entry), else `[]`.
832
- * - `wide` / `reasonToExist` — the dedicated normalizers shared with the
833
- * markdown parse path's meta-block recovery.
834
739
  *
835
740
  * @type {Array<{ name: string, kind: keyof typeof STRUCTURED_FIELD_NORMALIZERS }>}
836
741
  */
@@ -846,8 +751,6 @@ const STRUCTURED_FIELD_SPECS = [
846
751
  { name: 'references', kind: 'pathEntryList' },
847
752
  // non_goals — advisory negative-scope bullets.
848
753
  { name: 'non_goals', kind: 'stringList' },
849
- { name: 'wide', kind: 'wide' },
850
- { name: 'reason_to_exist', kind: 'reasonToExist' },
851
754
  // depends_on — may be at top level or in body.
852
755
  { name: 'depends_on', kind: 'stringList' },
853
756
  ];
@@ -872,21 +775,8 @@ const STRUCTURED_FIELD_NORMALIZERS = {
872
775
  }
873
776
  return entries;
874
777
  },
875
- wide: (raw) => normalizeWide(raw),
876
- reasonToExist: (raw) => normalizeReasonToExist(raw),
877
778
  };
878
779
 
879
- /**
880
- * Normalize a provenance stamp field (`mandrel_version` / `authored_at`) to
881
- * a non-empty trimmed string or `null`.
882
- *
883
- * @param {unknown} raw
884
- * @returns {string|null}
885
- */
886
- function normalizeProvenanceString(raw) {
887
- return typeof raw === 'string' && raw.trim() ? raw.trim() : null;
888
- }
889
-
890
780
  // ---------------------------------------------------------------------------
891
781
  // Serializer
892
782
  // ---------------------------------------------------------------------------
@@ -926,18 +816,6 @@ const SERIALIZE_SECTIONS = [
926
816
  ? `## Goal\n${goal.trim()}`
927
817
  : null,
928
818
  },
929
- {
930
- // Visible wide-rationale line (Story #4600), rendered directly under
931
- // `## Goal`. Presentation only: the `<!-- meta -->` block remains the
932
- // canonical machine carrier and the parser skips this line, so `wide`
933
- // round-trips through the meta block alone. Absent/invalid wide emits
934
- // nothing, keeping every non-wide body byte-identical to before.
935
- field: 'wide',
936
- render: (wide) => {
937
- const normalized = normalizeWide(wide);
938
- return normalized === null ? null : `> **Wide:** ${normalized.reason}`;
939
- },
940
- },
941
819
  {
942
820
  // Optional v2 intra-Story delivery slice plan. Single-token `## Slicing`
943
821
  // heading (recognized by the `[\w-]+` field-heading regex). Verbatim text
@@ -1004,61 +882,6 @@ const SERIALIZE_SECTIONS = [
1004
882
  },
1005
883
  ];
1006
884
 
1007
- /**
1008
- * Build the trailing `<!-- meta: {...} -->` block carrying the fields that
1009
- * have no human-readable section (`wide`, `reason_to_exist`). Returns the
1010
- * empty string when no meta field is present so {@link serialize} appends
1011
- * nothing.
1012
- *
1013
- * Key insertion order (`wide` → `reason_to_exist` →
1014
- * `mandrel_version` → `authored_at`) is load-bearing: it fixes the serialized
1015
- * JSON byte sequence the parser's meta round-trip and the unit suite assert
1016
- * against. The provenance stamp keys are appended **last** so every
1017
- * pre-existing (stamp-less) body serialises byte-identically to before.
1018
- *
1019
- * @param {StoryBody} body
1020
- * @returns {string}
1021
- */
1022
- function serializeMetaBlock(body) {
1023
- const metaFields = {};
1024
- const wide = normalizeWide(body.wide);
1025
- if (wide !== null) {
1026
- metaFields.wide = wide;
1027
- }
1028
- const reasonToExist = normalizeReasonToExist(body.reason_to_exist);
1029
- if (reasonToExist !== null) {
1030
- metaFields.reason_to_exist = reasonToExist;
1031
- }
1032
- if (typeof body.mandrel_version === 'string' && body.mandrel_version.trim()) {
1033
- metaFields.mandrel_version = body.mandrel_version.trim();
1034
- }
1035
- if (typeof body.authored_at === 'string' && body.authored_at.trim()) {
1036
- metaFields.authored_at = body.authored_at.trim();
1037
- }
1038
- if (Object.keys(metaFields).length === 0) return '';
1039
- return `\n\n<!-- meta: ${JSON.stringify(metaFields)} -->`;
1040
- }
1041
-
1042
- /**
1043
- * Build the visible `> 🏷️ Authored with Mandrel v<version> · <date>` marker
1044
- * line when the body carries a complete provenance stamp
1045
- * (`mandrel_version` + `authored_at`). Emitted just above the meta block so it
1046
- * round-trips with the hidden field. Returns the empty string when either
1047
- * field is absent, so every pre-existing (stamp-less) body serialises
1048
- * byte-identically to before.
1049
- *
1050
- * @param {StoryBody} body
1051
- * @returns {string}
1052
- */
1053
- function serializeAuthoredMarker(body) {
1054
- const version =
1055
- typeof body.mandrel_version === 'string' ? body.mandrel_version.trim() : '';
1056
- const authoredAt =
1057
- typeof body.authored_at === 'string' ? body.authored_at.trim() : '';
1058
- if (!version || !authoredAt) return '';
1059
- return `\n\n${authoredMarkerLine({ version, authoredAt })}`;
1060
- }
1061
-
1062
885
  /**
1063
886
  * Build the optional `---` footer block (`parent` / `blocked by` lines).
1064
887
  * Returns the empty string when `opts.includeFooter` is falsy.
@@ -1090,10 +913,6 @@ function serializeFooter(body, opts) {
1090
913
  * `## Goal`, `## Slicing`, `## Spec`, `## Changes`, `## Acceptance`,
1091
914
  * `## Verify`, `## References`, `## Non-Goals` (each omitted when empty).
1092
915
  *
1093
- * `wide` and `reason_to_exist` are emitted as a fenced `<!-- meta -->`
1094
- * comment block so round-trips preserve them without polluting the
1095
- * human-readable body.
1096
- *
1097
916
  * @param {StoryBody} body
1098
917
  * @param {SerializeOptions} [opts]
1099
918
  * @returns {string}
@@ -1111,12 +930,7 @@ export function serialize(body, opts = {}) {
1111
930
  if (block !== null) sections.push(block);
1112
931
  }
1113
932
 
1114
- return (
1115
- sections.join('\n\n') +
1116
- serializeAuthoredMarker(body) +
1117
- serializeMetaBlock(body) +
1118
- serializeFooter(body, opts)
1119
- );
933
+ return sections.join('\n\n') + serializeFooter(body, opts);
1120
934
  }
1121
935
 
1122
936
  // ---------------------------------------------------------------------------