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
@@ -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
  /**
@@ -176,11 +166,18 @@ const HUMANIZED_PATH_ENTRY_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
176
166
  // top-level acceptance[] machine contract round-trips byte-identical.
177
167
  const AC_PREFIX_RE = /^AC-\d+:\s+/;
178
168
 
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).
169
+ // Machine-managed marker lines a body authored before Story #5312 may still
170
+ // carry: the `> **Wide:** <reason>` rationale line (Story #4600), the
171
+ // `> 🏷️ Authored with Mandrel …` provenance line, and the trailing
172
+ // `<!-- meta: {...} -->` block. Nothing writes them any more — the `wide` /
173
+ // `reason_to_exist` fields and the provenance stamp went with the plan-time
174
+ // sizing model — but live issue bodies are never rewritten, so the parser
175
+ // still skips them wherever they appear rather than absorbing a stray line
176
+ // into the last structured section. A skipped block is dropped on
177
+ // re-serialize, which is the cutover working as intended.
183
178
  const WIDE_MARKER_LINE_RE = /^>\s*\*\*Wide:\*\*/;
179
+ const AUTHORED_MARKER_LINE_RE = /^\s*>\s*🏷️\s+Authored with Mandrel\b/;
180
+ const META_BLOCK_RE = /<!--\s*meta:[\s\S]*?-->/;
184
181
 
185
182
  /**
186
183
  * Parse a single `changes` / `references` bullet into a `PathEntry`.
@@ -325,99 +322,6 @@ function extractBlockedBy(footerBlock) {
325
322
  return parseFooterBlockedByRefs(footerBlock);
326
323
  }
327
324
 
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
325
  /**
422
326
  * Split markdown into named sections plus a footer block.
423
327
  *
@@ -515,18 +419,10 @@ function isSectionTerminatorHeading(line, inPreamble, currentSection) {
515
419
  }
516
420
 
517
421
  /**
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`.
422
+ * True for the legacy machine-managed marker lines section parsing skips
423
+ * wherever they appear (see the regexes above): a `## References` section
424
+ * immediately followed by a retired meta block must not swallow the comment
425
+ * as a references entry.
530
426
  *
531
427
  * @param {string} line
532
428
  * @returns {boolean}
@@ -567,11 +463,7 @@ function parseUnstructuredBody(input, preamble, footer) {
567
463
  verify: [],
568
464
  references: [],
569
465
  non_goals: [],
570
- wide: null,
571
- reason_to_exist: null,
572
466
  depends_on: extractBlockedBy(footer),
573
- mandrel_version: null,
574
- authored_at: null,
575
467
  };
576
468
  return {
577
469
  body,
@@ -736,17 +628,6 @@ export function parse(input) {
736
628
  const non_goals = parseTextListSection(sections.get('non_goals') ?? []);
737
629
  const dependsOn = extractBlockedBy(footer);
738
630
 
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
631
  const body = {
751
632
  goal,
752
633
  slicing,
@@ -756,11 +637,7 @@ export function parse(input) {
756
637
  verify,
757
638
  references,
758
639
  non_goals,
759
- wide,
760
- reason_to_exist,
761
640
  depends_on: dependsOn,
762
- mandrel_version,
763
- authored_at,
764
641
  };
765
642
 
766
643
  return {
@@ -800,10 +677,6 @@ function parseStructuredObject(obj) {
800
677
  body[name] = STRUCTURED_FIELD_NORMALIZERS[kind](obj[name], warnings);
801
678
  }
802
679
 
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
680
  return {
808
681
  body,
809
682
  warnings,
@@ -829,8 +702,6 @@ function parseStructuredObject(obj) {
829
702
  * - `stringList` — array filtered to non-empty strings, else `[]`.
830
703
  * - `pathEntryList` — array normalized entry-wise via `parsePathEntry`
831
704
  * (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
705
  *
835
706
  * @type {Array<{ name: string, kind: keyof typeof STRUCTURED_FIELD_NORMALIZERS }>}
836
707
  */
@@ -846,8 +717,6 @@ const STRUCTURED_FIELD_SPECS = [
846
717
  { name: 'references', kind: 'pathEntryList' },
847
718
  // non_goals — advisory negative-scope bullets.
848
719
  { name: 'non_goals', kind: 'stringList' },
849
- { name: 'wide', kind: 'wide' },
850
- { name: 'reason_to_exist', kind: 'reasonToExist' },
851
720
  // depends_on — may be at top level or in body.
852
721
  { name: 'depends_on', kind: 'stringList' },
853
722
  ];
@@ -872,21 +741,8 @@ const STRUCTURED_FIELD_NORMALIZERS = {
872
741
  }
873
742
  return entries;
874
743
  },
875
- wide: (raw) => normalizeWide(raw),
876
- reasonToExist: (raw) => normalizeReasonToExist(raw),
877
744
  };
878
745
 
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
746
  // ---------------------------------------------------------------------------
891
747
  // Serializer
892
748
  // ---------------------------------------------------------------------------
@@ -926,18 +782,6 @@ const SERIALIZE_SECTIONS = [
926
782
  ? `## Goal\n${goal.trim()}`
927
783
  : null,
928
784
  },
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
785
  {
942
786
  // Optional v2 intra-Story delivery slice plan. Single-token `## Slicing`
943
787
  // heading (recognized by the `[\w-]+` field-heading regex). Verbatim text
@@ -1004,61 +848,6 @@ const SERIALIZE_SECTIONS = [
1004
848
  },
1005
849
  ];
1006
850
 
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
851
  /**
1063
852
  * Build the optional `---` footer block (`parent` / `blocked by` lines).
1064
853
  * Returns the empty string when `opts.includeFooter` is falsy.
@@ -1090,10 +879,6 @@ function serializeFooter(body, opts) {
1090
879
  * `## Goal`, `## Slicing`, `## Spec`, `## Changes`, `## Acceptance`,
1091
880
  * `## Verify`, `## References`, `## Non-Goals` (each omitted when empty).
1092
881
  *
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
882
  * @param {StoryBody} body
1098
883
  * @param {SerializeOptions} [opts]
1099
884
  * @returns {string}
@@ -1111,12 +896,7 @@ export function serialize(body, opts = {}) {
1111
896
  if (block !== null) sections.push(block);
1112
897
  }
1113
898
 
1114
- return (
1115
- sections.join('\n\n') +
1116
- serializeAuthoredMarker(body) +
1117
- serializeMetaBlock(body) +
1118
- serializeFooter(body, opts)
1119
- );
899
+ return sections.join('\n\n') + serializeFooter(body, opts);
1120
900
  }
1121
901
 
1122
902
  // ---------------------------------------------------------------------------