mandrel 2.56.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.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- 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/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/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/plan-context.js +181 -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/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -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 +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- 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 +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -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 +266 -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 +41 -57
- 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 +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- 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
|
@@ -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
|
|
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
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
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
|
|
519
|
-
* appear:
|
|
520
|
-
*
|
|
521
|
-
*
|
|
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
|
// ---------------------------------------------------------------------------
|