mandrel 2.56.0 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -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
|
/**
|
|
@@ -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
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
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
|
|
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`.
|
|
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.
|
|
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
|
// ---------------------------------------------------------------------------
|