@sdd-method/maintain-cli 0.10.0 → 0.12.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.
@@ -7,7 +7,7 @@
7
7
  # Emitted by sdd-cli's YAML emitter with no line folding; the three header scalars are quoted for the
8
8
  # textual readers downstream. The one deliberate difference from the authored form is schema_status:
9
9
  # the authored form says canonical; a projection never does.
10
- schema_version: "4.2.0"
10
+ schema_version: "5.0.0"
11
11
  schema_owner: "sdd-method"
12
12
  schema_status: "generated"
13
13
  enums:
@@ -620,6 +620,8 @@ enums:
620
620
  description: "In the living spec: shipped behaviour."
621
621
  - value: agreed
622
622
  description: Added by an open change whose agreement record is agreed; not yet shipped.
623
+ - value: retired
624
+ description: "Removed by an archived change's REMOVED delta: kept so its lineage shows the removal; no status."
623
625
  description: Where a catalogued requirement stands (M-ADR 0221 §4.4).
624
626
  requirement_change_kind:
625
627
  values:
@@ -633,7 +635,8 @@ enums:
633
635
  - value: ADDED
634
636
  - value: MODIFIED
635
637
  - value: RENAMED
636
- description: How a change's delta treated a requirement that is current now (a removed requirement has no row).
638
+ - value: REMOVED
639
+ description: How a change's delta treated a requirement.
637
640
  requirement_status_value:
638
641
  values:
639
642
  - value: agreed
@@ -664,10 +667,33 @@ enums:
664
667
  work_order_status:
665
668
  values:
666
669
  - value: agreed
667
- description: Agreed; the repository's work is not yet judged.
670
+ description: Agreed; the change has not yet archived on a passing judge.
668
671
  - value: completed
669
- description: Marked completed by a passing judge.
672
+ description: "Completed: the change archived on a passing judge, after every repository merged its judged commit."
670
673
  description: A work order's status in its change's agreement record (M-ADR 0223 §4.2, §4.6).
674
+ feature_status:
675
+ values:
676
+ - value: active
677
+ description: The feature's behaviour is live or being built.
678
+ - value: retired
679
+ description: Its behaviour was retired by an archived change; kept so its retired requirements resolve.
680
+ description: A catalogued feature's status (M-ADR 0221 §4.5).
681
+ feature_kind:
682
+ values:
683
+ - value: feature
684
+ description: "A product or capability feature: the default."
685
+ - value: obligation
686
+ description: Behaviour every feature in a population depends on, owned as a feature of its own (M-ADR 0226, Proposed).
687
+ - value: journey
688
+ description: A user journey's joint behaviour, held in the behaviour store (M-ADR 0229 §4.2, option B; Proposed).
689
+ description: What a catalogued feature is (M-ADR 0226 and M-ADR 0229, both Proposed).
690
+ journey_requirement_basis:
691
+ values:
692
+ - value: tag
693
+ description: A @req tag in the journey's end-to-end test file (option A).
694
+ - value: citation
695
+ description: A citation in the journey feature's living spec (option B).
696
+ description: Where a journey's citation of a requirement was read (M-ADR 0229 §4.4, Proposed).
671
697
  catalogues:
672
698
  domains:
673
699
  primary_key: id
@@ -1454,7 +1480,7 @@ catalogues:
1454
1480
  - name: status
1455
1481
  type: string
1456
1482
  nullable: false
1457
- description: Default 'active'.
1483
+ description: "Bound to feature_status: active, or retired when the feature spec says **Status:** retired (its behaviour retired by an archived change)."
1458
1484
  - name: source
1459
1485
  type: string
1460
1486
  nullable: false
@@ -1463,6 +1489,10 @@ catalogues:
1463
1489
  type: string
1464
1490
  nullable: true
1465
1491
  description: Optional adopter-owned lifecycle / migration marker. Free string at method level — the schema declares no enum and ships no controlled vocabulary. Adopters MAY declare a per-SDD `feature-runway-vocabulary.yaml` at their SDD root; when present, the canonical builder validates captured values against it and emits a `convention` finding for unknown values. Empty when the adopter does not track migration state. Comparability across SDDs requires ecosystem-level vocabulary alignment as an explicit pre-step. Added in schema 2.2.0.
1492
+ - name: kind
1493
+ type: string
1494
+ nullable: false
1495
+ description: "Bound to feature_kind: feature (the default), obligation (M-ADR 0226, Proposed) or journey (M-ADR 0229 §4.2, Proposed), from the feature spec's **Kind:** line."
1466
1496
  - name: source_sdd_id
1467
1497
  type: string
1468
1498
  nullable: true
@@ -1485,6 +1515,8 @@ catalogues:
1485
1515
  references: features.id
1486
1516
  nullable: true
1487
1517
  column_enums:
1518
+ status: enums.feature_status
1519
+ kind: enums.feature_kind
1488
1520
  source: enums.feature_source
1489
1521
  maturity: enums.artefact_maturity
1490
1522
  personas:
@@ -4188,7 +4220,7 @@ catalogues:
4188
4220
  - name: stage
4189
4221
  type: string
4190
4222
  nullable: false
4191
- description: "Bound to requirement_stage: living (in the living spec) or agreed (added by an open change whose agreement record is agreed)."
4223
+ description: "Bound to requirement_stage: living (in the living spec), agreed (added by an open change whose agreement record is agreed) or retired (removed by an archived change)."
4192
4224
  - name: spec_path
4193
4225
  type: string
4194
4226
  nullable: false
@@ -4225,7 +4257,7 @@ catalogues:
4225
4257
  - name: change_id
4226
4258
  type: string
4227
4259
  nullable: false
4228
- description: The archived change's directory name (its archive date prefix included); a changes.id.
4260
+ description: "The archived change's id: <sdd_id>::<archive directory> (its archive date prefix included); a changes.id."
4229
4261
  - name: change_kind
4230
4262
  type: string
4231
4263
  nullable: false
@@ -4233,7 +4265,7 @@ catalogues:
4233
4265
  - name: operation
4234
4266
  type: string
4235
4267
  nullable: false
4236
- description: "Bound to requirement_operation: how the change's delta treated the requirement."
4268
+ description: "Bound to requirement_operation: how the change's delta treated the requirement; REMOVED retires it."
4237
4269
  - name: archived_on
4238
4270
  type: string
4239
4271
  nullable: false
@@ -4278,7 +4310,7 @@ catalogues:
4278
4310
  - name: basis
4279
4311
  type: string
4280
4312
  nullable: false
4281
- description: "The change that produced the status: the open change's or the archived change's directory name."
4313
+ description: "The change that produced the status: its changes.id, <sdd_id>::<directory>."
4282
4314
  - name: source_sdd_id
4283
4315
  type: string
4284
4316
  nullable: true
@@ -4287,7 +4319,7 @@ catalogues:
4287
4319
  type: string
4288
4320
  nullable: true
4289
4321
  description: The platform SDD the row belongs to (stamped at aggregation).
4290
- description: "Edge: a requirement's status in a repository that implements it (M-ADR 0221 §4.5; M-ADR 0223 §4.7). Derived, never authored: agreed from an open agreed change's work orders, declared-shipped from an archived change's PASS verdict and its work orders, observed from a baseline's evidence. The latest event in archive order wins."
4322
+ description: "Edge: a requirement's status in a repository that implements it (M-ADR 0221 §4.5; M-ADR 0223 §4.7). Derived, never authored: agreed from an open agreed change's work orders, declared-shipped from an archived change's PASS verdict and its work orders, observed from a baseline's evidence. The latest event in archive order wins. Limitation: a requirement and repository hold one status. An agreed open change's status replaces the evidence about the revision that is live until the change archives, and evidence is not bound to the requirement revision it concerns."
4291
4323
  foreign_keys:
4292
4324
  - column: requirement_id
4293
4325
  references: requirements.id
@@ -4303,7 +4335,7 @@ catalogues:
4303
4335
  - name: id
4304
4336
  type: string
4305
4337
  nullable: false
4306
- description: "The change's directory name: the open change's name, or the archived change's directory (its archive date prefix included). requirement_lineage.change_id references it."
4338
+ description: "<sdd_id>::<directory>: the SDD the store belongs to and the change's directory name there, the open change's name or the archived change's directory (its archive date prefix included). A change name is its store's own, so the SDD is part of its identity: two SDDs' changes of one name are two changes. requirement_lineage.change_id references it."
4307
4339
  - name: name
4308
4340
  type: string
4309
4341
  nullable: false
@@ -4354,7 +4386,7 @@ catalogues:
4354
4386
  - name: status
4355
4387
  type: string
4356
4388
  nullable: false
4357
- description: "Bound to work_order_status: the record's status for the work order; a judge PASS marks it completed (M-ADR 0223 §4.6)."
4389
+ description: "Bound to work_order_status: the record's status for the work order; the change archiving on a judge PASS marks it completed (M-ADR 0223 §4.6)."
4358
4390
  - name: source_sdd_id
4359
4391
  type: string
4360
4392
  nullable: true
@@ -4379,11 +4411,11 @@ catalogues:
4379
4411
  - name: id
4380
4412
  type: string
4381
4413
  nullable: false
4382
- description: "<change_name>__<work_item_id>: keyed by the change's undated name, so the row is the same before and after archive."
4414
+ description: "<sdd_id>::<change_name>__<work_item_id>: keyed by the SDD and the change's undated name, so the row is the same before and after archive."
4383
4415
  - name: change_id
4384
4416
  type: string
4385
4417
  nullable: false
4386
- description: "The change as it stands now: its open name, or its archive directory."
4418
+ description: "The change as it stands now (a changes.id): <sdd_id>:: with its open name or its archive directory."
4387
4419
  - name: change_name
4388
4420
  type: string
4389
4421
  nullable: false
@@ -4405,3 +4437,38 @@ catalogues:
4405
4437
  - column: change_id
4406
4438
  references: changes.id
4407
4439
  nullable: false
4440
+ journey_requirement:
4441
+ primary_key: id
4442
+ columns:
4443
+ - name: id
4444
+ type: string
4445
+ nullable: false
4446
+ description: <journey_id>__<requirement_id>.
4447
+ - name: journey_id
4448
+ type: string
4449
+ nullable: false
4450
+ - name: requirement_id
4451
+ type: string
4452
+ nullable: false
4453
+ - name: basis
4454
+ type: string
4455
+ nullable: false
4456
+ description: "Bound to journey_requirement_basis: where the citation was read."
4457
+ - name: source_sdd_id
4458
+ type: string
4459
+ nullable: true
4460
+ description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
4461
+ - name: platform_sdd_id
4462
+ type: string
4463
+ nullable: true
4464
+ description: The platform SDD the row belongs to (stamped at aggregation).
4465
+ description: "Edge: a requirement a user journey cites for one of its steps (M-ADR 0229 §4.4, Proposed). Derived, never authored: from the @req tags in the journey's end-to-end test file, or from the citations in its journey feature's living spec. A journey with no row is untraced."
4466
+ foreign_keys:
4467
+ - column: journey_id
4468
+ references: user_journeys.id
4469
+ nullable: false
4470
+ - column: requirement_id
4471
+ references: requirements.id
4472
+ nullable: false
4473
+ column_enums:
4474
+ basis: enums.journey_requirement_basis
@@ -218,6 +218,16 @@ const ManifestSchema = z.object({
218
218
  // Shared-library trait (sdd-manifest-schema 1.12.0, M-ADR 0217 §4.4). Profile-agnostic,
219
219
  // like `exemplar`; absent is the unmarked norm, no finding.
220
220
  shared_library: z.boolean().optional(),
221
+ // Where this estate's journey behaviour is mastered (sdd-manifest-schema 1.13.0, M-ADR 0229 §4.2, Proposed).
222
+ // `master` is declared at the root, and journey-impact treats a member naming its platform as inheriting it.
223
+ // `test_paths` is never inherited: it is relative to this SDD, so an SDD that holds end-to-end files declares
224
+ // its own block, with the master and its paths. Unknown values fail at load.
225
+ journeys: z
226
+ .object({
227
+ master: z.enum(["test-file", "store"]),
228
+ test_paths: z.array(z.string()).optional(),
229
+ })
230
+ .optional(),
221
231
  // Application-product archetype trait (M-ADR 0191, sdd-manifest-schema
222
232
  // 1.2.0). RESOLVED enum, not fixed: the loader validates only the slug
223
233
  // shape here. Whether the value resolves against the active vocabulary
@@ -378,6 +388,9 @@ export async function loadSddManifest(sddPath, options) {
378
388
  ...(parsed.data.packaging !== undefined ? { packaging: parsed.data.packaging } : {}),
379
389
  ...(parsed.data.exemplar !== undefined ? { exemplar: parsed.data.exemplar } : {}),
380
390
  ...(parsed.data.shared_library !== undefined ? { sharedLibrary: parsed.data.shared_library } : {}),
391
+ ...(parsed.data.journeys !== undefined
392
+ ? { journeys: { master: parsed.data.journeys.master, testPaths: parsed.data.journeys.test_paths ?? [] } }
393
+ : {}),
381
394
  ...(parsed.data.application_archetype !== undefined
382
395
  ? { applicationArchetype: parsed.data.application_archetype }
383
396
  : {}),
@@ -933,3 +933,21 @@ coverage:
933
933
  kind: enum
934
934
  scoring: not-applicable
935
935
  reason: closed vocabulary, not an artefact
936
+ feature_status:
937
+ kind: enum
938
+ scoring: not-applicable
939
+ reason: closed vocabulary, not an artefact
940
+ journey_requirement:
941
+ kind: edge
942
+ scoring: derived-edge
943
+ class: dependency
944
+ impact_flow: toward-source
945
+ reason: derived from end-to-end test tags or journey features' living specs
946
+ feature_kind:
947
+ kind: enum
948
+ scoring: not-applicable
949
+ reason: closed vocabulary, not an artefact
950
+ journey_requirement_basis:
951
+ kind: enum
952
+ scoring: not-applicable
953
+ reason: closed vocabulary, not an artefact
@@ -27,7 +27,7 @@
27
27
  * M-ADR 0134 §4.6 (canonical builder + aggregator).
28
28
  */
29
29
  export const SCHEMA_COMPAT = {
30
- catalogue_schema: "^4.0.0",
30
+ catalogue_schema: "^5.0.0",
31
31
  sdd_manifest: "^1.0.0",
32
32
  aggregation_manifest: "^1.0.0",
33
33
  };
@@ -0,0 +1,77 @@
1
+ // The intent artefact's shape (M-ADR 0227 §4.1), shared by the migration that writes intents and the check
2
+ // that reads them. The section names and header lines are the shipped template's
3
+ // (docs/method/methods/templates/intent-template.md), so a change there is a change here.
4
+ export const INTENT_DIRS = ["content/method/intents", "content/product/intents"];
5
+ export const INTENT_SECTIONS = [
6
+ "Problem",
7
+ "Proposed outcome",
8
+ "Affected users and systems",
9
+ "Constraints",
10
+ "Open questions",
11
+ "Decision to resolve in exploration",
12
+ "ADR requirement",
13
+ "Where it went",
14
+ ];
15
+ /** Header fields of the retired Idea and the idea map: planning state, which belongs in the work item (§4.3). */
16
+ export const PLANNING_FIELDS = [
17
+ "Status",
18
+ "Priority",
19
+ "Blocked by",
20
+ "Enables",
21
+ "Parent Idea",
22
+ "Child Ideas",
23
+ "Idea ID",
24
+ "Epic ID",
25
+ "Theme",
26
+ ];
27
+ /** A "Where it went" line naming a path: "- `<path>` (<sdd-id>; <source>)". */
28
+ export const WHERE_PATH_LINE = /^-\s+`([^`]+)`\s+\(([^;)]+);\s*([^)]+)\)\s*$/;
29
+ /** "## 3. Why Explore This (Revised)" -> "why explore this". */
30
+ export function normaliseHeading(h) {
31
+ return h
32
+ .replace(/^\s*\d+[A-Za-z]?(\.\d+)*\.?\s*/, "")
33
+ .replace(/\((revised|updated)\)\s*$/i, "")
34
+ .trim()
35
+ .toLowerCase();
36
+ }
37
+ export function parseMarkdown(text) {
38
+ const lines = text.split("\n");
39
+ let title = "";
40
+ const fields = new Map();
41
+ const sections = new Map();
42
+ const headings = [];
43
+ let current = null;
44
+ let buf = [];
45
+ const flush = () => {
46
+ if (current !== null) {
47
+ const prev = sections.get(current);
48
+ const body = buf.join("\n").trim();
49
+ sections.set(current, prev ? `${prev}\n\n${body}` : body);
50
+ }
51
+ buf = [];
52
+ };
53
+ for (const line of lines) {
54
+ const h1 = /^#\s+(.+?)\s*$/.exec(line);
55
+ if (h1 && !title && current === null) {
56
+ title = h1[1];
57
+ continue;
58
+ }
59
+ const h2 = /^##\s+(.+?)\s*$/.exec(line);
60
+ if (h2) {
61
+ flush();
62
+ headings.push(h2[1]);
63
+ current = normaliseHeading(h2[1]);
64
+ continue;
65
+ }
66
+ if (current === null) {
67
+ const f = /^\*\*([^*:]+):\*\*\s*(.*?)\s*$/.exec(line);
68
+ if (f)
69
+ fields.set(f[1].trim(), f[2]);
70
+ continue;
71
+ }
72
+ buf.push(line);
73
+ }
74
+ flush();
75
+ return { title, fields, sections, headings };
76
+ }
77
+ //# sourceMappingURL=format.js.map
@@ -174,7 +174,13 @@ export function parseTypes(text, fileLabel = "catalogue-schema.tql", options = {
174
174
  throw new ParseError(`${where}: plays needs <relation>:<role>`);
175
175
  if (decl.plays.has(w[1]))
176
176
  throw new ParseError(`${where}: ${lab} plays ${w[1]} twice`);
177
- decl.plays.set(w[1], parseAnnotations(w.slice(2), where));
177
+ const ann = parseAnnotations(w.slice(2), where);
178
+ // A plays clause's cardinality constrains participation, which the relational projection has no place for:
179
+ // refused, so a constraint the model gained can never be dropped by the projection with the gate green.
180
+ if (ann.key || ann.required || ann.multi || ann.optional) {
181
+ throw new ParseError(`${where}: ${lab} plays ${w[1]} with a cardinality annotation; participation cardinality cannot be projected onto the catalogue schema, so it is not admitted`);
182
+ }
183
+ decl.plays.set(w[1], ann);
178
184
  }
179
185
  else if (head === "relates") {
180
186
  if (kind !== "relation")
@@ -225,6 +231,16 @@ export function parseTypes(text, fileLabel = "catalogue-schema.tql", options = {
225
231
  }
226
232
  const ROW_RE = /^insert \$m isa ([a-z][a-z0-9-]*), (.*);$/;
227
233
  const TOK_RE = /has ([a-z][a-z0-9-]*) ("(?:[^"\\]|\\.)*"|-?\d+|true|false)/y;
234
+ function valueFamily(v) {
235
+ return typeof v === "boolean" ? "boolean" : typeof v === "number" ? "integer" : "string";
236
+ }
237
+ function attrFamily(valueType) {
238
+ if (valueType === undefined)
239
+ return undefined;
240
+ if (valueType === "long" || valueType === "integer")
241
+ return "integer";
242
+ return valueType;
243
+ }
228
244
  export function parseMeta(text, fileLabel = "catalogue-schema-meta.tql", options = {}) {
229
245
  const metaTypes = options.metaTypes ?? META_TYPES;
230
246
  const rows = new Map();
@@ -268,9 +284,26 @@ export function parseMeta(text, fileLabel = "catalogue-schema-meta.tql", options
268
284
  else
269
285
  throw new ParseError(`${where}: expected ', has ...' at offset ${i}: ${JSON.stringify(body.slice(i, i + 40))}`);
270
286
  }
287
+ const decl = options.types?.entities.get(typ);
288
+ if (options.types && !decl)
289
+ throw new ParseError(`${where}: meta type ${typ} is not declared in the type file`);
290
+ if (decl) {
291
+ for (const [k, vs] of acc) {
292
+ const own = decl.owns.get(k);
293
+ if (!own)
294
+ throw new ParseError(`${where}: ${typ} does not own ${k}; an undeclared meta property is refused, not ignored`);
295
+ if (!own.multi && vs.length > 1)
296
+ throw new ParseError(`${where}: ${typ}.${k} is a single value and appears ${vs.length} times; a repeated scalar is refused, not reduced to its first value`);
297
+ const fam = attrFamily(options.types.attrs.get(k));
298
+ for (const v of vs) {
299
+ if (fam !== undefined && valueFamily(v) !== fam)
300
+ throw new ParseError(`${where}: ${typ}.${k} holds ${JSON.stringify(v)}, but ${k} is of value type ${options.types.attrs.get(k)}`);
301
+ }
302
+ }
303
+ }
271
304
  const row = {};
272
305
  for (const [k, vs] of acc)
273
- row[k] = LIST_KEYS.has(k) ? vs : vs[0];
306
+ row[k] = (decl ? decl.owns.get(k).multi : LIST_KEYS.has(k)) ? vs : vs[0];
274
307
  const list = rows.get(typ) ?? [];
275
308
  list.push(row);
276
309
  rows.set(typ, list);
@@ -0,0 +1,204 @@
1
+ // What an agreement binds and what a verdict must show (M-ADR 0222 §4.6, M-ADR 0223 §4.4-§4.6).
2
+ // Pure text helpers; behaviour-store.ts runs them against git.
3
+ //
4
+ // agreed content the change's proposal, deltas, Gate 0, design, tasks (checkboxes ignored) and the test plan's
5
+ // scenario-to-test rows (its Before and After cells ignored) are what was approved. Each is compared
6
+ // with its state at the record's pinned commit: the latest amendment's commit, else sdd.commit.
7
+ // requirement a requirement's block in a spec: its heading to the next requirement or section heading. The
8
+ // stale check compares the blocks a change targets, not the whole file.
9
+ // scenario kind changed (a scenario of an ADDED requirement, or one of a MODIFIED requirement that is new or
10
+ // differs from the living spec's scenario of the same title) goes red then green; carried (identical
11
+ // to the living spec's) is regression and stays green.
12
+ // verdict line verdict.md's own verdict: `Verdict: PASS|FAIL` or a leading `**PASS**`/`**FAIL**`.
13
+ /** The files under a change directory that agreement binds, relative to the change directory. */
14
+ export function boundFile(rel) {
15
+ return /^(proposal|design|gate-0|tasks|test-plan)\.md$/.test(rel) || /^specs\/.+\/spec\.md$/.test(rel);
16
+ }
17
+ /** A bound file's content as agreement compares it: all of it, with only the fields execution fills in masked. */
18
+ export function normaliseBound(rel, text) {
19
+ if (rel === "tasks.md")
20
+ return text.replace(/^(\s*-\s*\[)\s*[xX~]?\s*(\])/gm, "$1 $2").trimEnd();
21
+ if (rel === "test-plan.md")
22
+ return normaliseTestPlan(text);
23
+ return text.trimEnd();
24
+ }
25
+ /**
26
+ * The test plan as agreed. Masked, because execution fills them in: the last two cells (Before and After) of each
27
+ * row of the scenario table, the whole Ledger section, and the last two cells (Killed by and Result) of each row of
28
+ * the Mutants table. Everything else, the stand-ins and any strategy or limitation stated, stays bound.
29
+ */
30
+ export function normaliseTestPlan(text) {
31
+ const out = [];
32
+ let section = "";
33
+ for (const line of text.split("\n")) {
34
+ const h = /^##\s+(.+?)\s*$/.exec(line);
35
+ if (h) {
36
+ section = h[1].toLowerCase();
37
+ out.push(line);
38
+ continue;
39
+ }
40
+ if (section === "ledger")
41
+ continue;
42
+ if ((section === "" || section === "mutants") && /^\s*\|/.test(line)) {
43
+ const cells = line.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
44
+ out.push(cells.slice(0, Math.max(0, cells.length - 2)).join(" | "));
45
+ continue;
46
+ }
47
+ out.push(line);
48
+ }
49
+ return out.join("\n").trimEnd();
50
+ }
51
+ /** A tasks file's repository groups (`## <n>. <repository>`) and the requirements each says it implements. */
52
+ export function taskGroups(tasks) {
53
+ const out = [];
54
+ let current = null;
55
+ for (const line of tasks.split("\n")) {
56
+ const g = /^##\s+\d+\.\s+`?([A-Za-z0-9][A-Za-z0-9._-]*)`?\s*$/.exec(line);
57
+ if (g) {
58
+ current = { repo: g[1], implements: [] };
59
+ out.push(current);
60
+ continue;
61
+ }
62
+ if (/^##\s/.test(line)) {
63
+ current = null;
64
+ continue;
65
+ }
66
+ const m = /^\s*(?:[-*]\s+)?\**Implements:?\**:?\s*(.+)$/i.exec(line);
67
+ if (current && m)
68
+ current.implements.push(...m[1].split(",").map((x) => x.trim().replace(/^`|`$/g, "")).filter((x) => x && !/^none$/i.test(x)));
69
+ }
70
+ return out;
71
+ }
72
+ /** A pinned commit: 7 to 40 hex characters. */
73
+ export function isCommit(v) {
74
+ return typeof v === "string" && /^[0-9a-f]{7,40}$/i.test(v);
75
+ }
76
+ /** a >= b, for x.y.z versions; a missing or malformed version is lowest. */
77
+ export function versionAtLeast(a, b) {
78
+ const p = (v) => (v && /^\d+\.\d+\.\d+$/.test(v) ? v.split(".").map(Number) : [-1, -1, -1]);
79
+ const [x, y] = [p(a), p(b)];
80
+ for (let i = 0; i < 3; i++)
81
+ if (x[i] !== y[i])
82
+ return x[i] > y[i];
83
+ return true;
84
+ }
85
+ /** The test plan's scenario-to-test rows: every table row before `## Ledger`, as trimmed cells, with the first row (the header) and separator rows dropped. The template holds one table there; a second would be read as more rows. */
86
+ export function testPlanMapping(text) {
87
+ const head = text.split(/^##\s+Ledger\b/m)[0] ?? "";
88
+ const rows = head.split("\n").filter((l) => /^\s*\|/.test(l)).map((l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim()));
89
+ return rows.filter((r, i) => i > 0 && !r.every((c) => /^:?-{3,}:?$/.test(c) || c === ""));
90
+ }
91
+ /** The test plan table's header cells, lower-cased. */
92
+ export function testPlanHeader(text) {
93
+ const head = text.split(/^##\s+Ledger\b/m)[0] ?? "";
94
+ const first = head.split("\n").find((l) => /^\s*\|/.test(l));
95
+ return first ? first.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim().toLowerCase()) : [];
96
+ }
97
+ /** A requirement's block: `### Requirement: <name>` to the next requirement or `##` heading. Null when absent. */
98
+ export function requirementBlock(spec, name) {
99
+ const lines = spec.split("\n");
100
+ const start = lines.findIndex((l) => new RegExp(`^###\\s+Requirement:\\s*${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`).test(l));
101
+ if (start < 0)
102
+ return null;
103
+ let end = lines.length;
104
+ for (let i = start + 1; i < lines.length; i++) {
105
+ if (/^###\s+Requirement:/.test(lines[i]) || /^##\s/.test(lines[i])) {
106
+ end = i;
107
+ break;
108
+ }
109
+ }
110
+ return lines.slice(start, end).join("\n").trimEnd();
111
+ }
112
+ /** Scenario title -> its block text, within one requirement block. */
113
+ export function scenarios(block) {
114
+ const out = new Map();
115
+ let title = null;
116
+ let buf = [];
117
+ const flush = () => {
118
+ if (title !== null)
119
+ out.set(title, buf.join("\n").trim());
120
+ };
121
+ for (const line of block.split("\n")) {
122
+ const m = /^####\s+Scenario:\s*(.+?)\s*$/.exec(line);
123
+ if (m) {
124
+ flush();
125
+ title = m[1];
126
+ buf = [];
127
+ continue;
128
+ }
129
+ if (title !== null)
130
+ buf.push(line);
131
+ }
132
+ flush();
133
+ return out;
134
+ }
135
+ /** The section a requirement sits under in a delta: ADDED, MODIFIED, REMOVED or RENAMED. */
136
+ export function deltaOperation(delta, name) {
137
+ let op = null;
138
+ for (const line of delta.split("\n")) {
139
+ const s = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\b/.exec(line);
140
+ if (s)
141
+ op = s[1];
142
+ else if (/^##\s/.test(line))
143
+ op = null;
144
+ if (new RegExp(`^###\\s+Requirement:\\s*${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`).test(line))
145
+ return op;
146
+ }
147
+ return null;
148
+ }
149
+ /** changed or carried, for one scenario of a requirement in a delta, against the living spec (null when the delta has no such scenario). */
150
+ export function scenarioKind(delta, living, requirement, scenario) {
151
+ const op = deltaOperation(delta, requirement);
152
+ const block = requirementBlock(delta, requirement);
153
+ if (!op || !block)
154
+ return null;
155
+ const mine = scenarios(block).get(scenario);
156
+ if (mine === undefined)
157
+ return null;
158
+ if (op !== "MODIFIED")
159
+ return "changed";
160
+ const theirs = living === null ? null : requirementBlock(living, requirement);
161
+ const before = theirs === null ? undefined : scenarios(theirs).get(scenario);
162
+ return before !== undefined && before === mine ? "carried" : "changed";
163
+ }
164
+ /** verdict.md's own verdict, or null when it states none in a form this reads. */
165
+ export function verdictLine(text) {
166
+ const m = /^\s*(?:#+\s*)?Verdict:\s*\**\s*(PASS|FAIL)\b/im.exec(text) ?? /^\s*\*\*(PASS|FAIL)\*\*/m.exec(text);
167
+ return m?.[1] ?? null;
168
+ }
169
+ /** Two pins name one commit when one abbreviates the other. */
170
+ export function sameCommit(a, b) {
171
+ const [x, y] = [a.toLowerCase(), b.toLowerCase()];
172
+ return x.length >= 7 && y.length >= 7 && (x.startsWith(y) || y.startsWith(x));
173
+ }
174
+ function isoDate(v) {
175
+ if (v instanceof Date && !Number.isNaN(v.getTime()))
176
+ return v.toISOString().slice(0, 10);
177
+ if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v))
178
+ return v;
179
+ return null;
180
+ }
181
+ /**
182
+ * Problems with one agreement condition (M-ADR 0222 §4.6 as amended); `today` is YYYY-MM-DD. A withdrawn or
183
+ * superseded change has closed its obligations: its conditions are not overdue.
184
+ */
185
+ export function conditionProblems(c, today, recordStatus = "agreed") {
186
+ const out = [];
187
+ for (const k of ["assumption", "owner", "how", "release_limit"]) {
188
+ if (typeof c[k] !== "string" || c[k].trim() === "")
189
+ out.push(`no ${k}`);
190
+ }
191
+ const by = isoDate(c.validate_by);
192
+ if (by === null)
193
+ out.push("validate_by is not a YYYY-MM-DD date");
194
+ const status = typeof c.status === "string" ? c.status : "";
195
+ if (!["open", "validated", "failed"].includes(status))
196
+ out.push(`status ${JSON.stringify(c.status)} is not open | validated | failed`);
197
+ if ((status === "validated" || status === "failed") && (typeof c.evidence !== "string" || c.evidence.trim() === ""))
198
+ out.push(`${status} without evidence`);
199
+ if (status === "open" && by !== null && by < today && recordStatus !== "withdrawn" && recordStatus !== "superseded")
200
+ out.push(`OVERDUE: open past its validate_by ${by}; validate it, extend it by a ruling, or withdraw the change`);
201
+ return out;
202
+ }
203
+ export { isoDate };
204
+ //# sourceMappingURL=agreement-evidence.js.map