@sdd-method/maintain-cli 0.11.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.
- package/dist/lib/behaviour-store/archive.js +13 -0
- package/dist/lib/catalogue/aggregate.js +62 -0
- package/dist/lib/catalogue/build.js +12 -0
- package/dist/lib/catalogue/builders/capability-spec.js +1 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +1 -0
- package/dist/lib/catalogue/builders/features.js +23 -2
- package/dist/lib/catalogue/builders/journey-requirement.js +144 -0
- package/dist/lib/catalogue/builders/requirements.js +43 -16
- package/dist/lib/catalogue/canonical-schema.yaml +81 -14
- package/dist/lib/catalogue/manifest-loader.js +13 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +18 -0
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/metamodel/tql-subset-parser.js +35 -2
- package/dist/lib/validate/agreement-evidence.js +204 -0
- package/dist/lib/validate/behaviour-store.js +475 -28
- package/dist/lib/validate/dispatch.js +14 -0
- package/dist/lib/validate/feature-spec-pointers.js +175 -0
- package/dist/lib/validate/journey-impact.js +193 -0
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -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: "
|
|
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
|
-
|
|
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
|
|
670
|
+
description: Agreed; the change has not yet archived on a passing judge.
|
|
668
671
|
- value: completed
|
|
669
|
-
description:
|
|
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:
|
|
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)
|
|
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
|
|
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:
|
|
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: "
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|