@sdd-method/verify-cli 4.0.1 → 5.1.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/catalogue/canonical-schema.yaml +50 -30
- package/dist/lib/catalogue/manifest-loader.js +1 -1
- package/dist/lib/catalogue/rubric-coverage-map.yaml +7 -3
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/catalogue/staleness-verdicts.js +3 -3
- package/dist/lib/structure/catalogue.js +31 -10
- package/dist/lib/structure/compose.js +1 -1
- package/dist/lib/structure/intended.js +194 -0
- package/dist/lib/structure/model.js +1 -1
- package/dist/lib/structure/render.js +1 -1
- package/dist/lib/structure/view.js +8 -3
- package/dist/lib/validate/agreement-evidence.js +2 -2
- package/dist/lib/validate/behaviour-store.js +49 -12
- package/dist/lib/validate/dispatch.js +2 -2
- package/dist/lib/validate/journey-impact.js +7 -7
- package/dist/lib/validate/structural-model.js +48 -35
- 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: "7.1.0"
|
|
11
11
|
schema_owner: "sdd-method"
|
|
12
12
|
schema_status: "generated"
|
|
13
13
|
enums:
|
|
@@ -645,7 +645,7 @@ enums:
|
|
|
645
645
|
description: Its change archived on a passing judge, with this repository's work order implementing it.
|
|
646
646
|
- value: observed
|
|
647
647
|
description: Independent evidence from this repository's code matches (a baseline's characterization tests or a reverse snapshot).
|
|
648
|
-
description: The kind of evidence an assertion is about one revision, in one repository (M-ADR 0230 §4.7
|
|
648
|
+
description: The kind of evidence an assertion is about one revision, in one repository (M-ADR 0230 §4.7).
|
|
649
649
|
change_status:
|
|
650
650
|
values:
|
|
651
651
|
- value: proposed
|
|
@@ -685,15 +685,15 @@ enums:
|
|
|
685
685
|
- value: obligation
|
|
686
686
|
description: Behaviour every feature in a population depends on, owned as a feature of its own (M-ADR 0226, Proposed).
|
|
687
687
|
- value: journey
|
|
688
|
-
description: A user journey's joint behaviour, held in the behaviour store (M-ADR 0229 §4.2, option B
|
|
689
|
-
description: What a catalogued feature is (M-ADR 0226 and M-ADR 0229
|
|
688
|
+
description: A user journey's joint behaviour, held in the behaviour store (M-ADR 0229 §4.2, option B).
|
|
689
|
+
description: What a catalogued feature is (M-ADR 0226, Proposed, and M-ADR 0229).
|
|
690
690
|
journey_requirement_basis:
|
|
691
691
|
values:
|
|
692
692
|
- value: tag
|
|
693
693
|
description: A @req tag in the journey's end-to-end test file (option A).
|
|
694
694
|
- value: citation
|
|
695
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
|
|
696
|
+
description: Where a journey's citation of a requirement was read (M-ADR 0229 §4.4).
|
|
697
697
|
source_of_truth:
|
|
698
698
|
values:
|
|
699
699
|
- value: structure
|
|
@@ -714,14 +714,14 @@ enums:
|
|
|
714
714
|
description: Observations, scores and findings, about a named revision.
|
|
715
715
|
- value: execution
|
|
716
716
|
description: "The change records and the build itself: changes, work orders, lineage, and the provenance the build or aggregation stamps."
|
|
717
|
-
description: Where a catalogue's facts, or a column's, are authored (M-ADR 0230 §4.1
|
|
717
|
+
description: Where a catalogue's facts, or a column's, are authored (M-ADR 0230 §4.1). Each catalogue carries one; a column carries one only where it differs.
|
|
718
718
|
relationship_end_cardinality:
|
|
719
719
|
values:
|
|
720
720
|
- value: exactly-one
|
|
721
721
|
- value: zero-or-one
|
|
722
722
|
- value: one-or-more
|
|
723
723
|
- value: zero-or-more
|
|
724
|
-
description: "How many entities one end of a relationship holds for one entity at the other end (M-ADR 0230 §4.3
|
|
724
|
+
description: "How many entities one end of a relationship holds for one entity at the other end (M-ADR 0230 §4.3): a Mermaid ER end marker (|| exactly one, |o or o| zero or one, }| or |{ one or more, }o or o{ zero or more). A TypeQL relationship is zero-or-more at both ends, because the subset admits no participation cardinality; a role's @card is a cardinality invariant (domain_invariants). Replaced entity_relationship_cardinality in schema 6.0.0."
|
|
725
725
|
domain_invariant_kind:
|
|
726
726
|
values:
|
|
727
727
|
- value: unique
|
|
@@ -732,7 +732,7 @@ enums:
|
|
|
732
732
|
description: A bound on how many entities the named relationship relates, stated in the statement.
|
|
733
733
|
- value: rule
|
|
734
734
|
description: Any other rule, stated in the statement.
|
|
735
|
-
description: The type of a domain invariant (M-ADR 0230 §4.3
|
|
735
|
+
description: The type of a domain invariant (M-ADR 0230 §4.3).
|
|
736
736
|
revision_state:
|
|
737
737
|
values:
|
|
738
738
|
- value: current
|
|
@@ -741,14 +741,14 @@ enums:
|
|
|
741
741
|
description: An agreed open change states it.
|
|
742
742
|
- value: history
|
|
743
743
|
description: Once published or stated, and no longer current.
|
|
744
|
-
description: Which view a revision belongs to (M-ADR 0230 §4.7
|
|
744
|
+
description: Which view a revision belongs to (M-ADR 0230 §4.7).
|
|
745
745
|
change_effect_element_kind:
|
|
746
746
|
values:
|
|
747
747
|
- value: requirement
|
|
748
748
|
description: A requirement, written by a behaviour delta.
|
|
749
749
|
- value: structure
|
|
750
750
|
description: An element of the structural model, written by a structural delta.
|
|
751
|
-
description: What kind of element a change effect writes (M-ADR 0230 §4.7
|
|
751
|
+
description: What kind of element a change effect writes (M-ADR 0230 §4.7).
|
|
752
752
|
adr_governs:
|
|
753
753
|
values:
|
|
754
754
|
- value: current
|
|
@@ -757,7 +757,7 @@ enums:
|
|
|
757
757
|
description: "The system as the agreed changes will leave it: the decision applies from its change's archive."
|
|
758
758
|
- value: none
|
|
759
759
|
description: "Neither: proposed, rejected, deprecated, superseded by a decision now in force, or its change withdrawn."
|
|
760
|
-
description: Which view a decision governs (M-ADR 0230 §4.2
|
|
760
|
+
description: Which view a decision governs (M-ADR 0230 §4.2).
|
|
761
761
|
contract_revision_state:
|
|
762
762
|
values:
|
|
763
763
|
- value: agreed
|
|
@@ -768,7 +768,22 @@ enums:
|
|
|
768
768
|
description: Applicable, with a sunset.
|
|
769
769
|
- value: retired
|
|
770
770
|
description: "No longer applicable: an archived change's revision the file no longer holds."
|
|
771
|
-
description: A contract revision's applicability in the published specification (M-ADR 0230 §4.2
|
|
771
|
+
description: A contract revision's applicability in the published specification (M-ADR 0230 §4.2).
|
|
772
|
+
ppt_archetype:
|
|
773
|
+
values:
|
|
774
|
+
- value: party
|
|
775
|
+
description: A person or an organisation.
|
|
776
|
+
- value: place
|
|
777
|
+
description: A location where things happen or are kept.
|
|
778
|
+
- value: thing
|
|
779
|
+
description: An object that is tracked individually.
|
|
780
|
+
- value: role
|
|
781
|
+
description: A way a party, place or thing takes part in something.
|
|
782
|
+
- value: moment-interval
|
|
783
|
+
description: Something that happens at a moment or over an interval, and is recorded.
|
|
784
|
+
- value: description
|
|
785
|
+
description: A catalogue-entry description that many things share.
|
|
786
|
+
description: An entity's Party-Place-Thing archetype, as the capability spec template classifies it (schema 7.1.0).
|
|
772
787
|
catalogues:
|
|
773
788
|
domains:
|
|
774
789
|
primary_key: id
|
|
@@ -1220,7 +1235,7 @@ catalogues:
|
|
|
1220
1235
|
- name: effective_change
|
|
1221
1236
|
type: string
|
|
1222
1237
|
nullable: true
|
|
1223
|
-
description: "From the ADR's **Effective:** line (M-ADR 0230 §4.2
|
|
1238
|
+
description: "From the ADR's **Effective:** line (M-ADR 0230 §4.2): the change whose archive makes the decision apply, by its undated name; empty when it applies on acceptance. Added in schema 6.0.0."
|
|
1224
1239
|
- name: governs
|
|
1225
1240
|
type: string
|
|
1226
1241
|
nullable: true
|
|
@@ -1318,7 +1333,7 @@ catalogues:
|
|
|
1318
1333
|
- name: relation
|
|
1319
1334
|
type: string
|
|
1320
1335
|
nullable: false
|
|
1321
|
-
description: alias-of | conflicts-with | references | supersedes (extended in schema 2.12.0 per M-ADR 0158).
|
|
1336
|
+
description: "alias-of | conflicts-with | references | supersedes (extended in schema 2.12.0 per M-ADR 0158). Read as source, relation, target: for supersedes the successor is the source and the superseded term the target."
|
|
1322
1337
|
description: "Edge: cross-domain glossary relationships from glossary-alignment-manifest.yaml. Per M-ADR 0158, four walkers ingest into this catalogue: (a) forward declared pair-form {source, target} entries under aliases/conflicts, (b) reverse-method cross_domain_terms (emits references edges per referencer→owner), (c) reverse-method multi-domain conflicts (pair-wise expansion across domains[]), (d) forward-method supersedes (status: superseded entries with superseded_by back-link)."
|
|
1323
1338
|
natural_key:
|
|
1324
1339
|
- source_id
|
|
@@ -1503,7 +1518,7 @@ catalogues:
|
|
|
1503
1518
|
- name: source
|
|
1504
1519
|
type: string
|
|
1505
1520
|
nullable: false
|
|
1506
|
-
description: "capability-tree | product-map | capability-spec | structural-model. As of schema 6.0.0, source: structural-model is emitted for a capability only the structural model holds (M-ADR 0230 §4.3
|
|
1521
|
+
description: "capability-tree | product-map | capability-spec | structural-model. As of schema 6.0.0, source: structural-model is emitted for a capability only the structural model holds (M-ADR 0230 §4.3)."
|
|
1507
1522
|
source_of_truth: derived
|
|
1508
1523
|
- name: runway_classification
|
|
1509
1524
|
type: string
|
|
@@ -1637,7 +1652,7 @@ catalogues:
|
|
|
1637
1652
|
- name: source
|
|
1638
1653
|
type: string
|
|
1639
1654
|
nullable: false
|
|
1640
|
-
description: "feature-tree | feature-set | capability-spec | feature-spec | feature-grounding | structural-model. As of schema 6.0.0, source: structural-model is emitted for a feature only the structural model holds (M-ADR 0230 §4.3
|
|
1655
|
+
description: "feature-tree | feature-set | capability-spec | feature-spec | feature-grounding | structural-model. As of schema 6.0.0, source: structural-model is emitted for a feature only the structural model holds (M-ADR 0230 §4.3). NOTE: feature-tree-group source removed in 1.3.0 — groups are now first-class in feature_groups.csv. As of schema 2.2.0, source: feature-tree is emitted by the canonical builder for rows derived from feature-tree.md bullets. As of schema 2.5.0, source: feature-grounding is emitted for rows derived from `docs/domains/<dom>/design/<capability>/feature-grounding.md` markdown tables (per M-ADR 0143). As of schema 2.7.0, source: capability-spec is emitted by the canonical builder for rows derived from `## Feature Grounding (code-path trace)` tables in `docs/domains/<dom>/product/<capability>.md` capability spec markdown (per M-ADR 0145). Feature-grounding and capability-spec rows are both capability-owned: product_id is empty; capability_id resolves from the file path."
|
|
1641
1656
|
source_of_truth: derived
|
|
1642
1657
|
- name: runway
|
|
1643
1658
|
type: string
|
|
@@ -1646,7 +1661,7 @@ catalogues:
|
|
|
1646
1661
|
- name: kind
|
|
1647
1662
|
type: string
|
|
1648
1663
|
nullable: false
|
|
1649
|
-
description: "Bound to feature_kind: feature (the default), obligation (M-ADR 0226, Proposed) or journey (M-ADR 0229 §4.2
|
|
1664
|
+
description: "Bound to feature_kind: feature (the default), obligation (M-ADR 0226, Proposed) or journey (M-ADR 0229 §4.2), from the feature spec's **Kind:** line."
|
|
1650
1665
|
- name: source_sdd_id
|
|
1651
1666
|
type: string
|
|
1652
1667
|
nullable: true
|
|
@@ -1760,7 +1775,7 @@ catalogues:
|
|
|
1760
1775
|
- name: id
|
|
1761
1776
|
type: string
|
|
1762
1777
|
nullable: false
|
|
1763
|
-
description: "{domain}-{stem}-{kind} — the kind component joined the id at schema 3.0.0 (M-ADR 0203 §4.4), ending the kindless collision class where one service published as two contract families (openapi + proto) minted one id (measured cost: 1 row + 2 edges permanently unrepresentable, plus legitimate shared-contract citation flagged as divergence). Mechanical migration: new id = old id + '-' + kind; `sdd-cli catalogue migration-map` emits the pairs, and authored alias targets (M-ADR 0147) re-key automatically during the cut window."
|
|
1778
|
+
description: "{domain}-{stem}-{kind} — the kind component joined the id at schema 3.0.0 (M-ADR 0203 §4.4), ending the kindless collision class where one service published as two contract families (openapi + proto) minted one id (measured cost: 1 row + 2 edges permanently unrepresentable, plus legitimate shared-contract citation flagged as divergence). Mechanical migration: new id = old id + '-' + kind; `sdd-cli catalogue migration-map` emits the pairs, and authored alias targets (M-ADR 0147) re-key automatically during the cut window. From schema 7.0.0 the stem is split at its CamelCase boundaries as an entity name is (`OrderPlaced` → `order-placed`), and references to the unsplit form re-key the same way, from id, kind and spec_path."
|
|
1764
1779
|
- name: name
|
|
1765
1780
|
type: string
|
|
1766
1781
|
nullable: false
|
|
@@ -1946,7 +1961,7 @@ catalogues:
|
|
|
1946
1961
|
- name: service_id
|
|
1947
1962
|
type: string
|
|
1948
1963
|
nullable: true
|
|
1949
|
-
description: Matched by container name
|
|
1964
|
+
description: Matched to a service id by the container's diagram id or display-name slug, then by its display name through docs/platform/reference/service-name-aliases.yaml.
|
|
1950
1965
|
- name: type
|
|
1951
1966
|
type: string
|
|
1952
1967
|
nullable: true
|
|
@@ -2211,7 +2226,7 @@ catalogues:
|
|
|
2211
2226
|
- name: id
|
|
2212
2227
|
type: string
|
|
2213
2228
|
nullable: false
|
|
2214
|
-
description: "Slugged PK: {domain_id}-{entitySlug(name)}.
|
|
2229
|
+
description: "Slugged PK: {domain_id}-{entitySlug(name)}. A glossary-manifest entry with domain=platform-wide is the domain entity its slug names when exactly one does, and adds no row of its own; one naming none, or several, is kept as `_platform-wide-{entitySlug(name)}` with a finding. A superseded manifest entry is no entity: glossary_xref holds its supersedes edge."
|
|
2215
2230
|
- name: name
|
|
2216
2231
|
type: string
|
|
2217
2232
|
nullable: false
|
|
@@ -2255,6 +2270,10 @@ catalogues:
|
|
|
2255
2270
|
nullable: true
|
|
2256
2271
|
description: The platform this row belongs to — sdd_id of its platform-root. Stamped by the aggregator per M-ADR 0208 §4.4 (preserve-if-set, fill rule by pull path); left empty by per-SDD builders. Added in 3.7.0.
|
|
2257
2272
|
source_of_truth: execution
|
|
2273
|
+
- name: ppt_archetype
|
|
2274
|
+
type: string
|
|
2275
|
+
nullable: true
|
|
2276
|
+
description: "The entity's Party-Place-Thing archetype (ppt_archetype). A structure fact: where the SDD has a structural model, the model holds it, and a change states it in its deltas. In an SDD without a model it is read from the capability specs' Entity Classification tables: the first archetype a row's archetype cell names, colour labels and qualifiers aside (Moment alone is moment-interval, PartyRole is role); a cell naming none, an entity no table row resolves to, and two tables disagreeing are reported. Added in schema 7.1.0 (M-ADR 0230)."
|
|
2258
2277
|
description: "Data-model entity (a structural realisation of a concept inside a domain's data model). Distinct from glossary_items (vocabulary-level) and from domain_models (file-level). Added in schema 2.13.0 per M-ADR 0159. Forward source: `models[].entities[]` in glossary-alignment-manifest.yaml. Reverse source: `EntityName { ... }` declarations inside `erDiagram` fences in `docs/domains/{domain}/architecture/data-model.mermaid.md`. PK uses an entity-aware slug that normalises across UPPER_SNAKE and PascalCase authoring styles."
|
|
2259
2278
|
foreign_keys:
|
|
2260
2279
|
- column: domain_id
|
|
@@ -2268,6 +2287,7 @@ catalogues:
|
|
|
2268
2287
|
nullable: true
|
|
2269
2288
|
column_enums:
|
|
2270
2289
|
persistence_kind: enums.persistence_kind
|
|
2290
|
+
ppt_archetype: enums.ppt_archetype
|
|
2271
2291
|
repo_capability:
|
|
2272
2292
|
primary_key: id
|
|
2273
2293
|
source_of_truth: derived
|
|
@@ -3933,7 +3953,7 @@ catalogues:
|
|
|
3933
3953
|
- name: input_paths
|
|
3934
3954
|
type: string
|
|
3935
3955
|
nullable: true
|
|
3936
|
-
description: "';'-separated paths of the review's other assessed inputs (its `inputs:` list, M-ADR 0230 §4.9
|
|
3956
|
+
description: "';'-separated paths of the review's other assessed inputs (its `inputs:` list, M-ADR 0230 §4.9), beside target_path: a feature's UX evidence, say. Each is read at corpus_ref, so a later edit to any of them makes the score stale, as an edit to the target does. Added in schema 6.0.0."
|
|
3937
3957
|
- name: staleness_basis
|
|
3938
3958
|
type: string
|
|
3939
3959
|
nullable: false
|
|
@@ -4752,7 +4772,7 @@ catalogues:
|
|
|
4752
4772
|
nullable: true
|
|
4753
4773
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4754
4774
|
source_of_truth: execution
|
|
4755
|
-
description: "Edge: evidence about one revision of a requirement, in one repository that implements it (M-ADR 0230 §4.7
|
|
4775
|
+
description: "Edge: evidence about one revision of a requirement, in one repository that implements it (M-ADR 0230 §4.7; replaces requirement_status in schema 6.0.0). Derived, never authored: observed from a baseline's **Observed in:** line, declared-shipped from an archived change's PASS verdict through its work orders' implements lists, each about the revision that change's delta states. An assertion is current while its revision is, and history once a later change moves the requirement, so evidence about one revision is never read as proof of another. An agreed change is not an assertion: it is an intended revision (requirement_revisions) and a change effect (change_effects). Per (revision, repository), the latest assertion wins."
|
|
4756
4776
|
natural_key:
|
|
4757
4777
|
- revision_id
|
|
4758
4778
|
- repo_id
|
|
@@ -4904,7 +4924,7 @@ catalogues:
|
|
|
4904
4924
|
nullable: true
|
|
4905
4925
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4906
4926
|
source_of_truth: execution
|
|
4907
|
-
description: "Edge: a requirement a user journey cites for one of its steps (M-ADR 0229 §4.4
|
|
4927
|
+
description: "Edge: a requirement a user journey cites for one of its steps (M-ADR 0229 §4.4). 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."
|
|
4908
4928
|
foreign_keys:
|
|
4909
4929
|
- column: journey_id
|
|
4910
4930
|
references: user_journeys.id
|
|
@@ -4955,7 +4975,7 @@ catalogues:
|
|
|
4955
4975
|
nullable: true
|
|
4956
4976
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4957
4977
|
source_of_truth: execution
|
|
4958
|
-
description: "An attribute of a domain-model entity, an element of its own (M-ADR 0230 §4.3
|
|
4978
|
+
description: "An attribute of a domain-model entity, an element of its own (M-ADR 0230 §4.3): adding a field to an entity adds an attribute and does not modify the entity. Built from the domain model: a Mermaid ER diagram's attribute lines, or a TypeQL entity's owned attributes. Authored in the structural model (openspec/structure/) where an SDD has one. Added in schema 5.1.0; built from domain models since 6.0.0."
|
|
4959
4979
|
natural_key:
|
|
4960
4980
|
- entity_id
|
|
4961
4981
|
- name
|
|
@@ -5012,7 +5032,7 @@ catalogues:
|
|
|
5012
5032
|
nullable: true
|
|
5013
5033
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
5014
5034
|
source_of_truth: execution
|
|
5015
|
-
description: "A named relationship from one domain-model entity to another, an element of its own (M-ADR 0230 §4.3
|
|
5035
|
+
description: "A named relationship from one domain-model entity to another, an element of its own (M-ADR 0230 §4.3). Built from the domain model: a Mermaid ER diagram's relationship lines, or a TypeQL binary relation, with each end's cardinality and role. Authored in the structural model (openspec/structure/) where an SDD has one. Added in schema 5.1.0; built from domain models since 6.0.0, when one cardinality became two, one per end."
|
|
5016
5036
|
natural_key:
|
|
5017
5037
|
- from_entity_id
|
|
5018
5038
|
- to_entity_id
|
|
@@ -5065,7 +5085,7 @@ catalogues:
|
|
|
5065
5085
|
nullable: true
|
|
5066
5086
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
5067
5087
|
source_of_truth: execution
|
|
5068
|
-
description: "A domain invariant, a typed element of the domain model (M-ADR 0230 §4.3
|
|
5088
|
+
description: "A domain invariant, a typed element of the domain model (M-ADR 0230 §4.3): a rule the entity's data always satisfies. It is structure, not intent, wherever it is first written. Built from the domain model's keys: a primary key (PK, or a TypeQL @key) or a unique key (UK) is a unique invariant over its attributes, and a TypeQL role's @card is a cardinality invariant on its relationship. Authored in the structural model (openspec/structure/) where an SDD has one, which can state every kind. Added in schema 5.1.0; built from domain models since 6.0.0."
|
|
5069
5089
|
foreign_keys:
|
|
5070
5090
|
- column: entity_id
|
|
5071
5091
|
references: entities.id
|
|
@@ -5111,7 +5131,7 @@ catalogues:
|
|
|
5111
5131
|
nullable: true
|
|
5112
5132
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
5113
5133
|
source_of_truth: execution
|
|
5114
|
-
description: "One row per distinct text of a requirement (M-ADR 0230 §4.7
|
|
5134
|
+
description: "One row per distinct text of a requirement (M-ADR 0230 §4.7; schema 6.0.0). Derived from the behaviour store: the living spec's text is current; an agreed open change's ADDED or MODIFIED block is intended; an archived change's block the living spec no longer holds is history. A revision is identified by its digest, so the same text is one revision wherever it appears."
|
|
5115
5135
|
foreign_keys:
|
|
5116
5136
|
- column: requirement_id
|
|
5117
5137
|
references: requirements.id
|
|
@@ -5160,7 +5180,7 @@ catalogues:
|
|
|
5160
5180
|
type: string
|
|
5161
5181
|
nullable: true
|
|
5162
5182
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
5163
|
-
description: "One row per element a change writes (M-ADR 0230 §4.7
|
|
5183
|
+
description: "One row per element a change writes (M-ADR 0230 §4.7; schema 6.0.0): a requirement from its behaviour deltas, a structural element from its structural deltas (an archived change's from its effect record, structure/applied.yaml). Base and target are the revisions it moves the element between; a requirement's are revision ids (<requirement>@<12 hex digits of its digest>), catalogued in requirement_revisions except the target of a change not yet agreed; a structural element's are sha256 digests."
|
|
5164
5184
|
natural_key:
|
|
5165
5185
|
- change_id
|
|
5166
5186
|
- element_kind
|
|
@@ -5196,7 +5216,7 @@ catalogues:
|
|
|
5196
5216
|
- name: sunset_date
|
|
5197
5217
|
type: string
|
|
5198
5218
|
nullable: true
|
|
5199
|
-
description: "For a deprecated revision, the date it is retired
|
|
5219
|
+
description: "For a deprecated revision, the date it is retired: the sunset an archived change's agreement record states for the version (contracts[].deprecates.sunset, agreement-record schema 1.3.0). Empty for a version deprecated only in BASELINE.md, whose sunset is not read."
|
|
5200
5220
|
- name: change_id
|
|
5201
5221
|
type: string
|
|
5202
5222
|
nullable: true
|
|
@@ -5212,7 +5232,7 @@ catalogues:
|
|
|
5212
5232
|
nullable: true
|
|
5213
5233
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
5214
5234
|
source_of_truth: execution
|
|
5215
|
-
description: A contract version's content revisions, each identified by its content digest (M-ADR 0230 §4.2
|
|
5235
|
+
description: A contract version's content revisions, each identified by its content digest (M-ADR 0230 §4.2; schema 6.0.0). The contract file's current content is active (deprecated where the contract is); a revision an agreed open change pins as the one it agrees (agreement record 1.2.0, contracts[].agrees) is agreed; one an archived change agreed that the file no longer holds is retired. A compatible amendment is a new revision at the same path, so an agreed revision and the active one coexist, both checked. Active is a claim about the published specification, not about deployment.
|
|
5216
5236
|
foreign_keys:
|
|
5217
5237
|
- column: contract_id
|
|
5218
5238
|
references: contracts.id
|
|
@@ -218,7 +218,7 @@ 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
|
|
221
|
+
// Where this estate's journey behaviour is mastered (sdd-manifest-schema 1.13.0, M-ADR 0229 §4.2).
|
|
222
222
|
// `master` is declared at the root, and journey-impact treats a member naming its platform as inheriting it.
|
|
223
223
|
// `test_paths` is never inherited: it is relative to this SDD, so an SDD that holds end-to-end files declares
|
|
224
224
|
// its own block, with the master and its paths. Unknown values fail at load.
|
|
@@ -954,7 +954,7 @@ coverage:
|
|
|
954
954
|
entity_attributes:
|
|
955
955
|
kind: entity
|
|
956
956
|
scoring: scored-at-source
|
|
957
|
-
instrument: an element of the structural model (M-ADR 0230
|
|
957
|
+
instrument: an element of the structural model (M-ADR 0230); scored with its domain model by rubrics/domain-architecture-rubric.md
|
|
958
958
|
relationships:
|
|
959
959
|
entity_id:
|
|
960
960
|
class: membership
|
|
@@ -962,7 +962,7 @@ coverage:
|
|
|
962
962
|
entity_relationships:
|
|
963
963
|
kind: entity
|
|
964
964
|
scoring: scored-at-source
|
|
965
|
-
instrument: an element of the structural model (M-ADR 0230
|
|
965
|
+
instrument: an element of the structural model (M-ADR 0230); scored with its domain model by rubrics/domain-architecture-rubric.md
|
|
966
966
|
relationships:
|
|
967
967
|
from_entity_id:
|
|
968
968
|
class: membership
|
|
@@ -973,7 +973,7 @@ coverage:
|
|
|
973
973
|
domain_invariants:
|
|
974
974
|
kind: entity
|
|
975
975
|
scoring: scored-at-source
|
|
976
|
-
instrument: an element of the structural model (M-ADR 0230
|
|
976
|
+
instrument: an element of the structural model (M-ADR 0230); scored with its domain model by rubrics/domain-architecture-rubric.md
|
|
977
977
|
relationships:
|
|
978
978
|
entity_id:
|
|
979
979
|
class: membership
|
|
@@ -1039,3 +1039,7 @@ coverage:
|
|
|
1039
1039
|
kind: enum
|
|
1040
1040
|
scoring: not-applicable
|
|
1041
1041
|
reason: closed vocabulary, not an artefact
|
|
1042
|
+
ppt_archetype:
|
|
1043
|
+
kind: enum
|
|
1044
|
+
scoring: not-applicable
|
|
1045
|
+
reason: closed vocabulary, not an artefact
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// none — already reported by scores-coverage as unknown currency; not
|
|
41
41
|
// re-reported here.
|
|
42
42
|
//
|
|
43
|
-
// ASSESSED INPUTS (M-ADR 0230 §4.9
|
|
43
|
+
// ASSESSED INPUTS (M-ADR 0230 §4.9): a review that names its other inputs (`input_paths`) is read with
|
|
44
44
|
// each of them, once its target is current. An input that moved stales the score, with the input named. So does an
|
|
45
45
|
// input gone from HEAD, and one the corpus did not hold at the anchor: unlike a missing target, the score can still
|
|
46
46
|
// be re-reviewed against its target, and a review cannot have read an input that was not there, so both prescribe a
|
|
@@ -171,7 +171,7 @@ export async function runStalenessVerdicts(session, options) {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
const asStr = (v) => v === null || v === undefined ? "" : String(v).trim();
|
|
174
|
-
// input_paths (schema 6.0.0, M-ADR 0230 §4.9
|
|
174
|
+
// input_paths (schema 6.0.0, M-ADR 0230 §4.9): a catalogue built before it has no such column, and its
|
|
175
175
|
// scores are read as naming no other input.
|
|
176
176
|
const query = (inputs) => `SELECT rubric_id, target_ref, target_path, corpus_ref, scored_at, staleness_basis, ${inputs}
|
|
177
177
|
FROM "quality_scores"
|
|
@@ -191,7 +191,7 @@ export async function runStalenessVerdicts(session, options) {
|
|
|
191
191
|
const rubricId = asStr(rubricIdRaw);
|
|
192
192
|
const targetRef = asStr(targetRefRaw);
|
|
193
193
|
const basis = asStr(basisRaw);
|
|
194
|
-
// The review's other assessed inputs (M-ADR 0230 §4.9
|
|
194
|
+
// The review's other assessed inputs (M-ADR 0230 §4.9): read with the target, so an edit to any of
|
|
195
195
|
// them makes the score stale as an edit to the target does.
|
|
196
196
|
const inputs = asStr(inputPathsRaw).split(";").map((x) => x.trim()).filter((x) => x !== "");
|
|
197
197
|
// Counted for BOTH bases, before the branch. The first cut of this counter
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// The catalogue from the structural model, and the model a built catalogue implies (M-ADR 0230 §4.3, §5.3
|
|
2
|
-
//
|
|
1
|
+
// The catalogue from the structural model, and the model a built catalogue implies (M-ADR 0230 §4.3, §5.3).
|
|
2
|
+
// Where an SDD has a model, it is the record of structure, so every structure catalogue is built from it whole:
|
|
3
3
|
// a row per element of the current view, with the model's structure columns;
|
|
4
4
|
// its other columns (every class but structure: derived, declaration, evidence, intent, execution) from the
|
|
5
5
|
// source-built row with the same id;
|
|
6
6
|
// for an element only the model holds, the derived columns the model itself settles (MODEL_DERIVED).
|
|
7
7
|
// What the sources hold and the model does not, and a structure column where they disagree, is reported once per
|
|
8
|
-
// catalogue. The model wins: either finding means the sources moved outside a change.
|
|
8
|
+
// catalogue. The model wins: either finding means the sources moved outside a change. A source row that states an
|
|
9
|
+
// element as an agreed open change adds or modifies it is that change's intended element, not a disagreement: it is
|
|
10
|
+
// listed apart, and catalogued once the change archives. Pure: no file is read or written.
|
|
9
11
|
import { stringify } from "yaml";
|
|
10
|
-
import { FORMAT_VERSION } from "./model.js";
|
|
12
|
+
import { elementKey, FORMAT_VERSION } from "./model.js";
|
|
11
13
|
import { STRUCTURE } from "./view.js";
|
|
12
14
|
/** The view each generated file renders, for a derived path column of an element only the model holds. */
|
|
13
15
|
export const VIEW_PATHS = {
|
|
@@ -44,8 +46,10 @@ function sample(ids) {
|
|
|
44
46
|
/**
|
|
45
47
|
* Replace every structure catalogue's rows with the view's. Rows of other catalogues are returned as they are. Source
|
|
46
48
|
* rows sharing an id are merged first, later values filling earlier gaps, as the keyed-table dedupe would.
|
|
49
|
+
* `intended` holds the elements agreed open changes add or modify (intendedWrites); a source row agreeing with one is
|
|
50
|
+
* pushed to `intendedOut` as `<key> (<change>)` instead of being reported.
|
|
47
51
|
*/
|
|
48
|
-
export function overlayModel(schema, view, rows, findings) {
|
|
52
|
+
export function overlayModel(schema, view, rows, findings, intended = new Map(), intendedOut = []) {
|
|
49
53
|
const out = new Map(rows);
|
|
50
54
|
for (const cat of schema.catalogues) {
|
|
51
55
|
if (cat.sourceOfTruth !== STRUCTURE)
|
|
@@ -66,6 +70,20 @@ export function overlayModel(schema, view, rows, findings) {
|
|
|
66
70
|
const elements = view.get(cat.name) ?? new Map();
|
|
67
71
|
const built = [];
|
|
68
72
|
const differ = [];
|
|
73
|
+
const disagree = (from, e) => structural.filter((c) => cell(from[c]) !== "" && cell(from[c]) !== cell(e.columns[c]));
|
|
74
|
+
// The source row states the element as an agreed open change does; one stating it otherwise is reported as such.
|
|
75
|
+
const differIntended = [];
|
|
76
|
+
const asIntended = (id, from) => {
|
|
77
|
+
const w = intended.get(elementKey(cat.name, id));
|
|
78
|
+
if (w === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
const cols = disagree(from, w.element);
|
|
81
|
+
if (cols.length > 0)
|
|
82
|
+
differIntended.push(`${id} (${cols.join(", ")}; ${w.change})`);
|
|
83
|
+
else
|
|
84
|
+
intendedOut.push(`${elementKey(cat.name, id)} (${w.change})`);
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
69
87
|
for (const e of elements.values()) {
|
|
70
88
|
const from = source.get(e.id);
|
|
71
89
|
const row = {};
|
|
@@ -73,8 +91,8 @@ export function overlayModel(schema, view, rows, findings) {
|
|
|
73
91
|
for (const [k, v] of Object.entries(from))
|
|
74
92
|
if (!structural.includes(k))
|
|
75
93
|
row[k] = v;
|
|
76
|
-
const cols =
|
|
77
|
-
if (cols.length > 0)
|
|
94
|
+
const cols = disagree(from, e);
|
|
95
|
+
if (cols.length > 0 && !asIntended(e.id, from))
|
|
78
96
|
differ.push(`${e.id} (${cols.join(", ")})`);
|
|
79
97
|
}
|
|
80
98
|
else {
|
|
@@ -85,12 +103,15 @@ export function overlayModel(schema, view, rows, findings) {
|
|
|
85
103
|
row[c] = e.columns[c];
|
|
86
104
|
built.push(row);
|
|
87
105
|
}
|
|
88
|
-
const missing = [...source.keys()].filter((id) => id !== "" && !elements.has(id)).sort();
|
|
106
|
+
const missing = [...source.keys()].filter((id) => id !== "" && !elements.has(id) && !asIntended(id, source.get(id))).sort();
|
|
89
107
|
if (missing.length > 0) {
|
|
90
|
-
findings.add("divergence", `${cat.name}: the sources hold ${missing.length} row(s) the structural model does not (${sample(missing)}); the model is the record of structure, so they are not catalogued. Add them through a change's structural deltas, or remove them from the sources (M-ADR 0230 §4.3
|
|
108
|
+
findings.add("divergence", `${cat.name}: the sources hold ${missing.length} row(s) the structural model does not (${sample(missing)}); the model is the record of structure, so they are not catalogued. Add them through a change's structural deltas, or remove them from the sources (M-ADR 0230 §4.3)`, "warn", `catalogue:${cat.name}`);
|
|
109
|
+
}
|
|
110
|
+
if (differIntended.length > 0) {
|
|
111
|
+
findings.add("divergence", `${cat.name}: ${differIntended.length} source row(s) state an element an agreed open change adds or modifies, and disagree with it: ${sample(differIntended.sort())}. The change's delta is what archives; bring the source into line with it, or the delta with the source (M-ADR 0230 §4.3, §4.7)`, "warn", `catalogue:${cat.name}`);
|
|
91
112
|
}
|
|
92
113
|
if (differ.length > 0) {
|
|
93
|
-
findings.add("divergence", `${cat.name}: ${differ.length} element(s) where the sources and the structural model disagree, catalogued as the model states them: ${sample(differ.sort())}. A source that is a generated view is rendered again; any other is brought into line through a change (M-ADR 0230 §4.3
|
|
114
|
+
findings.add("divergence", `${cat.name}: ${differ.length} element(s) where the sources and the structural model disagree, catalogued as the model states them: ${sample(differ.sort())}. A source that is a generated view is rendered again; any other is brought into line through a change (M-ADR 0230 §4.3)`, "warn", `catalogue:${cat.name}`);
|
|
94
115
|
}
|
|
95
116
|
out.set(cat.name, built);
|
|
96
117
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Views over the structural model, and how changes compose (M-ADR 0230 §4.5, §4.7
|
|
1
|
+
// Views over the structural model, and how changes compose (M-ADR 0230 §4.5, §4.7).
|
|
2
2
|
// current the model as published
|
|
3
3
|
// at a change current with that change's deltas and those of its dependency closure, dependencies first
|
|
4
4
|
// intended current with every agreed change's deltas
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// What an open change adds, for the references its own content makes (M-ADR 0230 §4.7, §4.11 proof 3).
|
|
2
|
+
// A reference made by an open change resolves against the current view plus that change's additions: the elements its
|
|
3
|
+
// structural deltas ADD, those of the changes in its depends_on closure, and the contracts its agreement record agrees
|
|
4
|
+
// as new files. A reference made by anything else resolves against the current view only, so a current reference that
|
|
5
|
+
// resolves only against an addition is still refused.
|
|
6
|
+
// What ties content to a change is never the reference being resolved:
|
|
7
|
+
// a model element the change's own delta (validateDeltas, viewAt)
|
|
8
|
+
// a source-built row with a change_id that change
|
|
9
|
+
// a contract row the change whose record agrees its file as new
|
|
10
|
+
// a row naming such a contract that change
|
|
11
|
+
// Pure but for reading agreement records.
|
|
12
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { parseDocument } from "yaml";
|
|
15
|
+
import { closure } from "./compose.js";
|
|
16
|
+
import { elementKey, loadChangeStructure } from "./model.js";
|
|
17
|
+
export const CONTRACTS = "contracts";
|
|
18
|
+
const LIFECYCLES = ["proposed", "agreed", "withdrawn", "superseded"];
|
|
19
|
+
/** The change's agreement record (docs/agreements/<change>.yaml): its status, or `unrecorded`, and its new contract files. */
|
|
20
|
+
export async function readAgreement(repo, change) {
|
|
21
|
+
let text;
|
|
22
|
+
try {
|
|
23
|
+
text = await readFile(join(repo, "docs", "agreements", `${change}.yaml`), "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { status: "unrecorded", newContracts: [] };
|
|
27
|
+
}
|
|
28
|
+
let record;
|
|
29
|
+
try {
|
|
30
|
+
record = parseDocument(text).toJS()?.agreement_record;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { status: "unrecorded", newContracts: [] };
|
|
34
|
+
}
|
|
35
|
+
const status = LIFECYCLES.find((s) => s === String(record?.["status"] ?? "")) ?? "unrecorded";
|
|
36
|
+
const newContracts = [];
|
|
37
|
+
const entries = Array.isArray(record?.["contracts"]) ? record["contracts"] : [];
|
|
38
|
+
for (const raw of entries) {
|
|
39
|
+
if (raw === null || typeof raw !== "object")
|
|
40
|
+
continue;
|
|
41
|
+
const e = raw;
|
|
42
|
+
if (e["amended_by_this_change"] !== true)
|
|
43
|
+
continue;
|
|
44
|
+
const files = Array.isArray(e["files"]) ? e["files"].map(String) : e["file"] !== undefined ? [String(e["file"])] : [];
|
|
45
|
+
// agreement-record schema 1.2.0 and later: a file the change relies on carries relies_on, so an agreed file
|
|
46
|
+
// without one is new (1.3.0 states it). An entry with no agrees predates the digests and says nothing either way.
|
|
47
|
+
const digest = (field, f) => {
|
|
48
|
+
const v = e[field];
|
|
49
|
+
return v !== null && typeof v === "object" ? String(v[f] ?? "") : String(v ?? "");
|
|
50
|
+
};
|
|
51
|
+
for (const f of files)
|
|
52
|
+
if (digest("agrees", f) !== "" && digest("relies_on", f) === "")
|
|
53
|
+
newContracts.push(`${String(e["repo"] ?? "")}/${f}`);
|
|
54
|
+
}
|
|
55
|
+
return { status, newContracts };
|
|
56
|
+
}
|
|
57
|
+
/** The open changes (openspec/changes/<change>, not archive/), with their agreement records. */
|
|
58
|
+
export async function loadOpenChanges(repo, problems) {
|
|
59
|
+
let names;
|
|
60
|
+
try {
|
|
61
|
+
names = (await readdir(join(repo, "openspec", "changes"), { withFileTypes: true }))
|
|
62
|
+
.filter((e) => e.isDirectory() && e.name !== "archive")
|
|
63
|
+
.map((e) => e.name)
|
|
64
|
+
.sort();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
names = [];
|
|
68
|
+
}
|
|
69
|
+
const open = new Map();
|
|
70
|
+
const newContracts = new Map();
|
|
71
|
+
for (const name of names) {
|
|
72
|
+
const agreement = await readAgreement(repo, name);
|
|
73
|
+
open.set(name, { structure: await loadChangeStructure(repo, name, problems), status: agreement.status });
|
|
74
|
+
newContracts.set(name, agreement.newContracts);
|
|
75
|
+
}
|
|
76
|
+
return { open, newContracts };
|
|
77
|
+
}
|
|
78
|
+
const cell = (v) => (v === null || v === undefined ? "" : String(v));
|
|
79
|
+
/**
|
|
80
|
+
* Each open change's additions, and the change that adds each. A contract is identified by its built row: the row
|
|
81
|
+
* whose spec_path is `<repo>/<file>` as the record names it. A new contract with no built row adds nothing, and a
|
|
82
|
+
* reference to it stays unresolved.
|
|
83
|
+
*/
|
|
84
|
+
export class Additions {
|
|
85
|
+
open;
|
|
86
|
+
own = new Map();
|
|
87
|
+
adder = new Map();
|
|
88
|
+
constructor(open, newContracts, contractRows) {
|
|
89
|
+
this.open = open;
|
|
90
|
+
const idByPath = new Map();
|
|
91
|
+
for (const r of contractRows)
|
|
92
|
+
if (cell(r["spec_path"]) !== "")
|
|
93
|
+
idByPath.set(cell(r["spec_path"]), cell(r["id"]));
|
|
94
|
+
for (const [name, oc] of open) {
|
|
95
|
+
const keys = new Set();
|
|
96
|
+
for (const d of oc.structure.deltas)
|
|
97
|
+
for (const e of d.added)
|
|
98
|
+
keys.add(elementKey(e.catalogue, e.id));
|
|
99
|
+
for (const p of newContracts.get(name) ?? []) {
|
|
100
|
+
const id = idByPath.get(p);
|
|
101
|
+
if (id !== undefined && id !== "")
|
|
102
|
+
keys.add(elementKey(CONTRACTS, id));
|
|
103
|
+
}
|
|
104
|
+
this.own.set(name, keys);
|
|
105
|
+
for (const k of keys)
|
|
106
|
+
if (!this.adder.has(k))
|
|
107
|
+
this.adder.set(k, name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The open change that adds the element or contract, if one does. */
|
|
111
|
+
addedBy(catalogue, id) {
|
|
112
|
+
return this.adder.get(elementKey(catalogue, id));
|
|
113
|
+
}
|
|
114
|
+
/** Whether the element or contract is the change's own addition or one of its dependency closure's. */
|
|
115
|
+
within(change, catalogue, id) {
|
|
116
|
+
const key = elementKey(catalogue, id);
|
|
117
|
+
return closure(change, this.open).some((c) => this.own.get(c)?.has(key) ?? false);
|
|
118
|
+
}
|
|
119
|
+
/** The contracts the given changes add. */
|
|
120
|
+
contracts(changes) {
|
|
121
|
+
const out = new Set();
|
|
122
|
+
for (const c of changes)
|
|
123
|
+
for (const k of this.own.get(c) ?? [])
|
|
124
|
+
if (k.startsWith(`${CONTRACTS}/`))
|
|
125
|
+
out.add(k.slice(CONTRACTS.length + 1));
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** The open changes a source-built row belongs to: its change_id, or a new contract it is or names. */
|
|
129
|
+
ownersOf(schema, catalogue, row, sddId) {
|
|
130
|
+
const out = new Set();
|
|
131
|
+
const changeId = cell(row["change_id"]);
|
|
132
|
+
if (changeId !== "") {
|
|
133
|
+
const name = changeId.startsWith(`${sddId}::`) ? changeId.slice(sddId.length + 2) : changeId;
|
|
134
|
+
if (this.open.has(name))
|
|
135
|
+
out.add(name);
|
|
136
|
+
}
|
|
137
|
+
const contractOwner = (id) => {
|
|
138
|
+
const by = this.adder.get(elementKey(CONTRACTS, id));
|
|
139
|
+
if (by !== undefined)
|
|
140
|
+
out.add(by);
|
|
141
|
+
};
|
|
142
|
+
const cat = schema.catalogues.find((c) => c.name === catalogue);
|
|
143
|
+
if (catalogue === CONTRACTS && cat)
|
|
144
|
+
contractOwner(cell(row[cat.primaryKey]));
|
|
145
|
+
for (const fk of cat?.foreignKeys ?? [])
|
|
146
|
+
if (fk.references === `${CONTRACTS}.id` && cell(row[fk.column]) !== "")
|
|
147
|
+
contractOwner(cell(row[fk.column]));
|
|
148
|
+
return [...out].sort();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sort a build's dangling references: a row an open change owns, naming that change's addition, resolves in that
|
|
153
|
+
* change's view. Every other stays dangling, including a row nothing ties to the change that adds its target.
|
|
154
|
+
*/
|
|
155
|
+
export function splitDangling(schema, rowsByCatalogue, failures, additions, sddId) {
|
|
156
|
+
const intended = [];
|
|
157
|
+
const dangling = [];
|
|
158
|
+
const index = new Map();
|
|
159
|
+
const rowOf = (catalogue, pk) => {
|
|
160
|
+
let byPk = index.get(catalogue);
|
|
161
|
+
if (!byPk) {
|
|
162
|
+
const key = schema.catalogues.find((c) => c.name === catalogue)?.primaryKey ?? "id";
|
|
163
|
+
byPk = new Map((rowsByCatalogue.get(catalogue) ?? []).map((r) => [cell(r[key]), r]));
|
|
164
|
+
index.set(catalogue, byPk);
|
|
165
|
+
}
|
|
166
|
+
return byPk.get(pk);
|
|
167
|
+
};
|
|
168
|
+
for (const f of failures) {
|
|
169
|
+
const [target, column] = f.target.split(".");
|
|
170
|
+
const targetPk = schema.catalogues.find((c) => c.name === target)?.primaryKey;
|
|
171
|
+
const row = rowOf(f.catalogue, f.referencingRow);
|
|
172
|
+
const owner = f.value === "" || column !== targetPk || row === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: additions.ownersOf(schema, f.catalogue, row, sddId).find((c) => additions.within(c, target, f.value));
|
|
175
|
+
if (owner !== undefined)
|
|
176
|
+
intended.push({ failure: f, change: owner });
|
|
177
|
+
else {
|
|
178
|
+
const by = f.value === "" ? undefined : additions.addedBy(target, f.value);
|
|
179
|
+
dangling.push(by === undefined ? { failure: f } : { failure: f, addedBy: by });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { intended, dangling };
|
|
183
|
+
}
|
|
184
|
+
/** Element key -> the agreed change that adds or modifies it, and the element as that change states it. */
|
|
185
|
+
export function intendedWrites(open, agreed) {
|
|
186
|
+
const out = new Map();
|
|
187
|
+
for (const c of agreed) {
|
|
188
|
+
for (const d of open.get(c)?.structure.deltas ?? [])
|
|
189
|
+
for (const e of [...d.added, ...d.modified])
|
|
190
|
+
out.set(elementKey(e.catalogue, e.id), { change: c, element: e });
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=intended.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The structural model and a change's structural deltas (M-ADR 0230 §4.3, §4.4
|
|
1
|
+
// The structural model and a change's structural deltas (M-ADR 0230 §4.3, §4.4). Pure reads, so the
|
|
2
2
|
// verify-only CLI can carry them. The format is docs/method/methods/guides/structural-model-format.md:
|
|
3
3
|
// openspec/structure/<partition>.yaml the model, one file per partition
|
|
4
4
|
// openspec/changes/<change>/structure/<partition>.yaml a change's delta for that partition
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The generated views of the structural model (M-ADR 0230 §4.3
|
|
1
|
+
// The generated views of the structural model (M-ADR 0230 §4.3): projections, never edited. Each file is
|
|
2
2
|
// rendered from the current view alone, in id order, so the same model renders the same bytes. Its first line names
|
|
3
3
|
// the generator and the digest of the body below it, which tells three states apart:
|
|
4
4
|
// missing the file is absent
|
|
@@ -121,7 +121,12 @@ function naturalKey(cat, e) {
|
|
|
121
121
|
* catalogue of another class, against its built rows), no cycle through a self-reference, natural keys unique, and
|
|
122
122
|
* domain invariants naming their entity's own attributes and relationships.
|
|
123
123
|
*/
|
|
124
|
-
export function validateView(schema, view, built, problems, unresolved) {
|
|
124
|
+
export function validateView(schema, view, built, problems, unresolved, addedBy = () => undefined) {
|
|
125
|
+
// A reference to an open change's addition, from outside that change and its dependants (§4.11 proof 3).
|
|
126
|
+
const outside = (catalogue, id) => {
|
|
127
|
+
const by = addedBy(catalogue, id);
|
|
128
|
+
return by === undefined ? "" : `; open change ${by} adds it, and only that change and those depending on it can name it`;
|
|
129
|
+
};
|
|
125
130
|
for (const [catalogue, byId] of view) {
|
|
126
131
|
const cat = catalogueOf(schema, catalogue);
|
|
127
132
|
for (const e of byId.values()) {
|
|
@@ -133,14 +138,14 @@ export function validateView(schema, view, built, problems, unresolved) {
|
|
|
133
138
|
for (const r of referencesOf(schema, e)) {
|
|
134
139
|
if (r.structural) {
|
|
135
140
|
if (!find(view, r.catalogue, r.targetColumn, r.value))
|
|
136
|
-
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no ${r.catalogue} element in this view (${elementKey(r.catalogue, r.value)})`);
|
|
141
|
+
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no ${r.catalogue} element in this view (${elementKey(r.catalogue, r.value)})${r.targetColumn === "id" ? outside(r.catalogue, r.value) : ""}`);
|
|
137
142
|
}
|
|
138
143
|
else {
|
|
139
144
|
const rows = built(r.catalogue, r.targetColumn);
|
|
140
145
|
if (rows === null)
|
|
141
146
|
unresolved.add(r.catalogue);
|
|
142
147
|
else if (!rows.has(r.value))
|
|
143
|
-
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no row of ${r.catalogue} as built`);
|
|
148
|
+
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no row of ${r.catalogue} as built${outside(r.catalogue, r.value)}`);
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// agreed content the change's proposal, deltas, Gate 0, design, tasks (checkboxes ignored), the test plan's
|
|
5
5
|
// scenario-to-test rows (its Before and After cells ignored) and its structural deltas with their
|
|
6
|
-
// base revisions (M-ADR 0230 §4.4
|
|
6
|
+
// base revisions (M-ADR 0230 §4.4; report only until it is accepted) are what was approved. Each is compared
|
|
7
7
|
// with its state at the record's pinned commit: the latest amendment's commit, else sdd.commit.
|
|
8
8
|
// requirement a requirement's block in a spec: its heading to the next requirement or section heading. The
|
|
9
9
|
// stale check compares the blocks a change targets, not the whole file.
|
|
@@ -16,7 +16,7 @@ export function boundFile(rel) {
|
|
|
16
16
|
return /^(proposal|design|gate-0|tasks|test-plan)\.md$/.test(rel) || /^specs\/.+\/spec\.md$/.test(rel) || structuralFile(rel);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* A change's structural artifact (M-ADR 0230 §4.4
|
|
19
|
+
* A change's structural artifact (M-ADR 0230 §4.4): a partition delta or the no-op claim, both agreed
|
|
20
20
|
* content, so a delta's base revisions are pinned with it. The effect record (structure/applied.yaml) is written by
|
|
21
21
|
* the archive, after agreement, and is not.
|
|
22
22
|
*/
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
// check is vacuous, and says so.
|
|
17
17
|
// agreed content: the change's proposal, deltas, Gate 0, design, tasks (checkboxes ignored), test
|
|
18
18
|
// plan (its execution results ignored) and structural deltas equal their state at that commit
|
|
19
|
-
// (M-ADR 0222 §4.6; a structural delta's change
|
|
19
|
+
// (M-ADR 0222 §4.6; a structural delta's change fails like the rest, a [WARN] only with ENFORCED false).
|
|
20
20
|
// kinds: on a record held to 1.1.0, each test-plan row's Kind (changed or carried) is the one computed
|
|
21
|
-
// from the deltas and the living spec,
|
|
22
|
-
//
|
|
21
|
+
// from the deltas and the living spec, or repaired (declared, on a carried scenario); a row
|
|
22
|
+
// naming no scenario of the deltas fails; and a filled Before, read as whole-word results, is a
|
|
23
|
+
// fail for changed, a pass for carried, and a fail citing the test double's correcting commit
|
|
24
|
+
// for repaired (M-ADR 0223 §4.4).
|
|
23
25
|
// 4. verdicts: every archived change except a baseline has a verdict.md whose verdict line agrees with
|
|
24
26
|
// its record's PASS (M-ADR 0223 §4.6). A record held to 1.1.0 (by its own history, or by the
|
|
25
27
|
// schema the SDD shipped when the change archived) also needs: an agreed record; archived
|
|
@@ -186,6 +188,16 @@ export function parseAgreementRecord(file, text) {
|
|
|
186
188
|
noLongerImplements.push({ repo: e.repo, requirement: e.requirement });
|
|
187
189
|
}
|
|
188
190
|
}
|
|
191
|
+
// Record 1.3.0 (M-ADR 0230 §4.2): a contracts entry may deprecate its version, stating the sunset. A
|
|
192
|
+
// deprecation that names no date states no runway, so it is refused rather than read as a deprecation without one.
|
|
193
|
+
for (const [i, x] of (Array.isArray(r.contracts) ? r.contracts : []).entries()) {
|
|
194
|
+
const d = x?.deprecates;
|
|
195
|
+
if (d === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
const sunset = d?.sunset;
|
|
198
|
+
if (typeof sunset !== "string" || !isCalendarDate(sunset))
|
|
199
|
+
problems.push(`${file}: contracts[${i}].deprecates must be {sunset: YYYY-MM-DD}, a calendar date as a string; found ${JSON.stringify(d)}`);
|
|
200
|
+
}
|
|
189
201
|
if (problems.length > 0)
|
|
190
202
|
return { problems };
|
|
191
203
|
return {
|
|
@@ -205,6 +217,13 @@ export function parseAgreementRecord(file, text) {
|
|
|
205
217
|
problems,
|
|
206
218
|
};
|
|
207
219
|
}
|
|
220
|
+
/** A YYYY-MM-DD string naming a day that exists. */
|
|
221
|
+
function isCalendarDate(s) {
|
|
222
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s))
|
|
223
|
+
return false;
|
|
224
|
+
const t = new Date(`${s}T00:00:00Z`);
|
|
225
|
+
return !Number.isNaN(t.getTime()) && t.toISOString().slice(0, 10) === s;
|
|
226
|
+
}
|
|
208
227
|
/** Every pinned commit in a record: any key named `commit` or ending `_commit`, and each value under verdict.merged and current. */
|
|
209
228
|
function commitFields(r) {
|
|
210
229
|
const out = [];
|
|
@@ -489,7 +508,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
489
508
|
if (a === null || b === null || normaliseBound(f, a) !== normaliseBound(f, b)) {
|
|
490
509
|
const what = `${dir}: AGREED CONTENT CHANGED: ${f} ${a === null ? "was added" : b === null ? "was removed" : "differs"} since ${base}, with no amendment recording a commit and an approver; amend the agreement or renew it (M-ADR 0222 §4.6)`;
|
|
491
510
|
if (structuralFile(f) && !STRUCTURE_ENFORCED)
|
|
492
|
-
warnings.push(`${what}; a structural delta,
|
|
511
|
+
warnings.push(`${what}; a structural delta, reported only because ENFORCED is off`);
|
|
493
512
|
else
|
|
494
513
|
failures.push(what);
|
|
495
514
|
}
|
|
@@ -497,14 +516,14 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
497
516
|
}
|
|
498
517
|
}
|
|
499
518
|
}
|
|
500
|
-
// A test plan's kinds (M-ADR 0223 §4.4 as amended): changed scenarios go red then green, carried ones stay green.
|
|
519
|
+
// A test plan's kinds (M-ADR 0223 §4.4 as amended): changed and repaired scenarios go red then green, carried ones stay green.
|
|
501
520
|
if (versionAtLeast(record?.schemaVersion, "1.1.0") && record?.status === "agreed") {
|
|
502
521
|
const plan = await readText(join(repo, dir, "test-plan.md"));
|
|
503
522
|
if (plan !== null) {
|
|
504
523
|
const header = testPlanHeader(plan);
|
|
505
524
|
const col = (name) => header.indexOf(name);
|
|
506
525
|
if (col("kind") < 0)
|
|
507
|
-
failures.push(`${dir}/test-plan.md: the scenario table has no Kind column (changed | carried)`);
|
|
526
|
+
failures.push(`${dir}/test-plan.md: the scenario table has no Kind column (changed | carried | repaired)`);
|
|
508
527
|
else {
|
|
509
528
|
for (const row of testPlanMapping(plan)) {
|
|
510
529
|
const cell = row[col("requirement")] ?? "";
|
|
@@ -533,13 +552,31 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
533
552
|
continue;
|
|
534
553
|
}
|
|
535
554
|
kindsChecked++;
|
|
536
|
-
|
|
555
|
+
// `repaired` is declared, never computed: a carried scenario whose earlier pass was false-green.
|
|
556
|
+
const repaired = kind === "repaired";
|
|
557
|
+
if (repaired && expected === "changed")
|
|
558
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is changed against the living spec, so it cannot be repaired: repaired is a scenario carried as the living spec states it, whose code did not meet it`);
|
|
559
|
+
else if (!repaired && kind !== expected)
|
|
537
560
|
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is ${expected} against the living spec, and the plan says ${JSON.stringify(kind)}`);
|
|
538
561
|
const before = col("before") >= 0 ? (row[col("before")] ?? "") : "";
|
|
539
|
-
if (before !== ""
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
562
|
+
if (before !== "") {
|
|
563
|
+
// The cell's results, as whole words: "PASS at x; FAIL at y" holds both, and "setup required" or "bypass" holds neither.
|
|
564
|
+
const pass = /\b(?:pass(?:es|ed)?|green)\b/i.test(before);
|
|
565
|
+
const fail = /\b(?:fail(?:s|ed)?|red)\b/i.test(before);
|
|
566
|
+
const said = `the scenario table's Before cell says ${JSON.stringify(before)}`;
|
|
567
|
+
if (repaired) {
|
|
568
|
+
if (!fail)
|
|
569
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is repaired, so its test fails before the code; ${said}`);
|
|
570
|
+
else if (!/\b[0-9a-f]{7,40}\b/i.test(before))
|
|
571
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is repaired, so its Before cell cites the commit that corrects the test double, with its own red run; ${said}`);
|
|
572
|
+
}
|
|
573
|
+
else if (expected === "changed" && (!fail || pass)) {
|
|
574
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is changed, so its test fails before the code${pass && fail ? ", and a cell holding both results is not a result" : ""}; ${said}`);
|
|
575
|
+
}
|
|
576
|
+
else if (expected === "carried" && (!pass || fail)) {
|
|
577
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is carried, so its test passes before the code as regression${fail ? "; if its earlier pass was false-green, declare it repaired and cite the commit that corrects the test double" : ""}; ${said}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
543
580
|
}
|
|
544
581
|
}
|
|
545
582
|
}
|
|
@@ -640,7 +677,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
640
677
|
if (a === null || b === null || normaliseBound(f, a) !== normaliseBound(f, b)) {
|
|
641
678
|
const what = `${dir}: AGREED CONTENT CHANGED: ${f} ${a === null ? "was added" : b === null ? "was removed" : "differs"} from what was agreed at ${base}, with no amendment recording a commit and an approver (M-ADR 0222 §4.6)`;
|
|
642
679
|
if (structuralFile(f) && !STRUCTURE_ENFORCED)
|
|
643
|
-
warnings.push(`${what}; a structural delta,
|
|
680
|
+
warnings.push(`${what}; a structural delta, reported only because ENFORCED is off`);
|
|
644
681
|
else
|
|
645
682
|
failures.push(what);
|
|
646
683
|
}
|
|
@@ -37,13 +37,13 @@ const BEHAVIOUR_STORE_VALIDATORS = [
|
|
|
37
37
|
source: { kind: "native", run: async (ctx) => checkFeatureSpecPointers(ctx.targetRepo) },
|
|
38
38
|
build: () => ({ status: "run", args: [] }),
|
|
39
39
|
},
|
|
40
|
-
// The journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5
|
|
40
|
+
// The journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5). Report only; [N/A] without journeys.
|
|
41
41
|
{
|
|
42
42
|
name: "journey-impact",
|
|
43
43
|
source: { kind: "native", run: async (ctx) => checkJourneyImpact(ctx.targetRepo) },
|
|
44
44
|
build: () => ({ status: "run", args: [] }),
|
|
45
45
|
},
|
|
46
|
-
// The structural model and each open change's structural artifact (M-ADR 0230
|
|
46
|
+
// The structural model and each open change's structural artifact (M-ADR 0230). Report only; [N/A] without a model.
|
|
47
47
|
{
|
|
48
48
|
name: "structural-model",
|
|
49
49
|
source: { kind: "native", run: async (ctx) => checkStructuralModel(ctx.targetRepo) },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// journey-impact: the journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5
|
|
2
|
-
// Report only (M-ADR 0229 §4.5
|
|
1
|
+
// journey-impact: the journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5).
|
|
2
|
+
// Report only (M-ADR 0229 §4.5): every finding is a [WARN] and the check exits 0. Gate 0 carries this report; nothing
|
|
3
3
|
// blocks on it, and the judge does not fail a change for it.
|
|
4
4
|
//
|
|
5
5
|
// For each open change in the behaviour store (openspec/changes/<change>/, not the archive):
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// with no reason is reported too.
|
|
15
15
|
//
|
|
16
16
|
// Also reported: a journey citing a requirement of a feature its journey_feature rows do not name (one record or
|
|
17
|
-
// the other is wrong, M-ADR 0229 §4.4
|
|
18
|
-
// platform it inherits from (an unresolved root, M-ADR 0229 §4.2
|
|
17
|
+
// the other is wrong, M-ADR 0229 §4.4); and an SDD with catalogued journeys whose manifest declares no journey master and that names no
|
|
18
|
+
// platform it inherits from (an unresolved root, M-ADR 0229 §4.2). A member that names its platform inherits the
|
|
19
19
|
// root's declaration, which is checked at the root.
|
|
20
20
|
//
|
|
21
21
|
// Reads the aggregated catalogue (catalogues-aggregated/*.csv) where this SDD has one, since a root's changes can
|
|
@@ -100,7 +100,7 @@ export async function checkJourneyImpact(repo) {
|
|
|
100
100
|
else if (platform)
|
|
101
101
|
lines.push(`journey master: not declared here; inherited from platform ${platform}, where it is checked`);
|
|
102
102
|
else
|
|
103
|
-
warnings.push(`.sdd/sdd-manifest.yaml: ${journeys.length} catalogued journey(s) and no journey master (journeys.master: test-file or store), and no platform to inherit one from: unresolved (M-ADR 0229 §4.2
|
|
103
|
+
warnings.push(`.sdd/sdd-manifest.yaml: ${journeys.length} catalogued journey(s) and no journey master (journeys.master: test-file or store), and no platform to inherit one from: unresolved (M-ADR 0229 §4.2)`);
|
|
104
104
|
const jr = await csv(repo, "journey_requirement", dir);
|
|
105
105
|
const jf = (await csv(repo, "journey_feature", dir)) ?? [];
|
|
106
106
|
const featureKinds = new Map(((await csv(repo, "features", dir)) ?? []).map((f) => [f["id"], f["kind"] ?? ""]));
|
|
@@ -137,7 +137,7 @@ export async function checkJourneyImpact(repo) {
|
|
|
137
137
|
if (touched.get(r["journey_id"])?.has(feature) || mismatched.has(key))
|
|
138
138
|
continue;
|
|
139
139
|
mismatched.add(key);
|
|
140
|
-
warnings.push(`journey ${r["journey_id"]} cites requirements of ${feature}, which its journey_feature rows do not name: the journey's related features or its tags are wrong (M-ADR 0229 §4.4
|
|
140
|
+
warnings.push(`journey ${r["journey_id"]} cites requirements of ${feature}, which its journey_feature rows do not name: the journey's related features or its tags are wrong (M-ADR 0229 §4.4)`);
|
|
141
141
|
}
|
|
142
142
|
const untracedCount = journeys.filter((j) => !traced.has(j["id"])).length;
|
|
143
143
|
lines.push(`journeys: ${journeys.length} catalogued, ${journeys.length - untracedCount} traced to requirements, ${untracedCount} untraced`);
|
|
@@ -203,6 +203,6 @@ export async function checkJourneyImpact(repo) {
|
|
|
203
203
|
const body = lines.map((l) => `${l}\n`).join("");
|
|
204
204
|
if (warnings.length === 0)
|
|
205
205
|
return { exitCode: 0, stdout: `journey-impact:\n${body}[PASS] journey-impact: every listed journey is named\n` };
|
|
206
|
-
return { exitCode: 0, stdout: `journey-impact:\n${body}${warnings.map((w) => `[WARN] ${w}\n`).join("")}[WARN] journey-impact: ${warnings.length} finding(s), report only (M-ADR 0229 §4.5
|
|
206
|
+
return { exitCode: 0, stdout: `journey-impact:\n${body}${warnings.map((w) => `[WARN] ${w}\n`).join("")}[WARN] journey-impact: ${warnings.length} finding(s), report only (M-ADR 0229 §4.5)\n` };
|
|
207
207
|
}
|
|
208
208
|
//# sourceMappingURL=journey-impact.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// structural-model: the structural model and each open change's structural artifact (M-ADR 0230 §4.3, §4.4
|
|
1
|
+
// structural-model: the structural model and each open change's structural artifact (M-ADR 0230 §4.3, §4.4;
|
|
2
2
|
// docs/method/methods/guides/structural-model-format.md).
|
|
3
3
|
//
|
|
4
4
|
// The model (openspec/structure/<partition>.yaml), as the current view:
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// (a platform SDD: its domains and `platform`; an application or integration SDD: its sdd_id)
|
|
7
7
|
// elements a `structure` catalogue, its `structure` columns only, an id held once, required columns,
|
|
8
8
|
// value types and vocabularies (the catalogue metamodel's)
|
|
9
|
-
// references resolved in the view; into a catalogue of another class, against that catalogue as built
|
|
9
|
+
// references resolved in the view; into a catalogue of another class, against that catalogue as built, less the
|
|
10
|
+
// contracts open changes' records agree as new (lib/structure/intended.ts): those are in their change's
|
|
11
|
+
// view, its dependants' and the intended one, so a current reference to one is refused (§4.11 proof 3)
|
|
10
12
|
// hierarchies no cycle through a self-reference; natural keys unique; domain invariants naming their entity's own
|
|
11
13
|
// attributes and relationships
|
|
12
|
-
// views
|
|
13
|
-
// hand (its body no longer matches the digest in its header)
|
|
14
|
+
// views docs/structure/ absent where the model is admitted, and each generated view under it missing, stale
|
|
15
|
+
// (rendered from an earlier model) or edited by hand (its body no longer matches the digest in its header)
|
|
14
16
|
// Each open change (openspec/changes/<change>/structure/):
|
|
15
17
|
// artifact where the model exists, a change has partition deltas or none.yaml, never both
|
|
16
18
|
// deltas well formed; ADDED ids new; MODIFIED and REMOVED ids held by the delta's partition; a base
|
|
@@ -29,20 +31,22 @@
|
|
|
29
31
|
// archived a change archived with structural deltas and no effect record (a raw `openspec archive`): its
|
|
30
32
|
// structure is not applied, so its transition is not shipped
|
|
31
33
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
+
// Enforced (the release that accepts M-ADR 0230): every finding is a [FAIL] and the check
|
|
35
|
+
// exits 1; with ENFORCED false it reports them as [WARN] and exits 0. Without openspec/structure/ and with no change
|
|
36
|
+
// carrying a structure/ directory it reports [N/A], so an SDD without a model is unaffected.
|
|
34
37
|
import { readdir, readFile } from "node:fs/promises";
|
|
35
38
|
import { join } from "node:path";
|
|
36
39
|
import { parseDocument } from "yaml";
|
|
37
40
|
import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
|
|
38
41
|
import { loadCanonicalSchema } from "../catalogue/schema-loader.js";
|
|
39
42
|
import { APPLIED_FILE, elementKey, loadChangeStructure, loadModel, STRUCTURE_DIR } from "../structure/model.js";
|
|
40
|
-
import { agreedOrder, attribute, checkDependencies, checkOverlap, intended, viewAt } from "../structure/compose.js";
|
|
43
|
+
import { agreedOrder, attribute, checkDependencies, checkOverlap, closure, intended, viewAt } from "../structure/compose.js";
|
|
41
44
|
import { describeState, renderViews, VIEW_PATHS, viewState } from "../structure/render.js";
|
|
45
|
+
import { Additions, CONTRACTS, readAgreement } from "../structure/intended.js";
|
|
42
46
|
import { validateDeltas, validateView, viewOf } from "../structure/view.js";
|
|
43
47
|
const VIEW_COUNT = Object.keys(VIEW_PATHS).length;
|
|
44
|
-
/**
|
|
45
|
-
export const ENFORCED =
|
|
48
|
+
/** Findings FAIL (from M-ADR 0230's acceptance); false reports them only, as while M-ADR 0230 was Proposed. Also binds a change's structural deltas as agreed content in the behaviour-store check. */
|
|
49
|
+
export const ENFORCED = true;
|
|
46
50
|
const CHANGES_DIR = join("openspec", "changes");
|
|
47
51
|
async function readText(p) {
|
|
48
52
|
try {
|
|
@@ -60,19 +64,6 @@ async function subdirs(p) {
|
|
|
60
64
|
return [];
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
/** The change's agreement record status (docs/agreements/<change>.yaml), or `unrecorded`. */
|
|
64
|
-
async function agreementStatus(repo, change) {
|
|
65
|
-
const text = await readText(join(repo, "docs", "agreements", `${change}.yaml`));
|
|
66
|
-
if (text === null)
|
|
67
|
-
return "unrecorded";
|
|
68
|
-
try {
|
|
69
|
-
const status = String(parseDocument(text).toJS()?.agreement_record?.status ?? "");
|
|
70
|
-
return ["proposed", "agreed", "withdrawn", "superseded"].find((s) => s === status) ?? "unrecorded";
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return "unrecorded";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
67
|
/** Names of archived changes: openspec/changes/archive/<YYYY-MM-DD>-<name>. */
|
|
77
68
|
async function archivedNames(repo) {
|
|
78
69
|
return new Set((await subdirs(join(repo, CHANGES_DIR, "archive"))).map((d) => d.replace(/^\d{4}-\d{2}-\d{2}-/, "")));
|
|
@@ -91,8 +82,22 @@ function builtLookup(repo) {
|
|
|
91
82
|
const text = await readText(join(repo, "catalogues", `${catalogue}.csv`));
|
|
92
83
|
cache.set(catalogue, text === null ? null : parseCatalogueCsv(text).rows);
|
|
93
84
|
};
|
|
85
|
+
fn.rows = (catalogue) => cache.get(catalogue) ?? [];
|
|
94
86
|
return fn;
|
|
95
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The built catalogues as one view sees them. A contract an open change's record agrees as new is built from the
|
|
90
|
+
* change's own sources, so it is in no view but that change's, its dependants' and, once agreed, the intended one.
|
|
91
|
+
*/
|
|
92
|
+
function builtIn(built, all, mine) {
|
|
93
|
+
return (catalogue, column) => {
|
|
94
|
+
const rows = built(catalogue, column);
|
|
95
|
+
if (rows === null || catalogue !== CONTRACTS || column !== "id")
|
|
96
|
+
return rows;
|
|
97
|
+
const out = new Set([...rows].filter((id) => !all.has(id) || mine.has(id)));
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
96
101
|
export async function checkStructuralModel(repo, options = {}) {
|
|
97
102
|
const enforced = options.enforced ?? ENFORCED;
|
|
98
103
|
const problems = [];
|
|
@@ -123,6 +128,16 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
123
128
|
for (const c of schema.catalogues)
|
|
124
129
|
if (c.sourceOfTruth !== "structure")
|
|
125
130
|
await built.load(c.name);
|
|
131
|
+
const open = new Map();
|
|
132
|
+
const newContracts = new Map();
|
|
133
|
+
for (const s of structures) {
|
|
134
|
+
const agreement = await readAgreement(repo, s.change);
|
|
135
|
+
open.set(s.change, { structure: s, status: agreement.status });
|
|
136
|
+
newContracts.set(s.change, agreement.newContracts);
|
|
137
|
+
}
|
|
138
|
+
const additions = new Additions(open, newContracts, built.rows(CONTRACTS));
|
|
139
|
+
const allNew = additions.contracts(open.keys());
|
|
140
|
+
const addedBy = (catalogue, id) => additions.addedBy(catalogue, id);
|
|
126
141
|
let allowed = null;
|
|
127
142
|
if (profile === "platform-profile") {
|
|
128
143
|
allowed = new Set(["platform", ...(view.get("domains")?.keys() ?? []), ...(built("domains", "id") ?? [])]);
|
|
@@ -143,14 +158,14 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
143
158
|
// The current view.
|
|
144
159
|
const unresolved = new Set();
|
|
145
160
|
const currentProblems = [];
|
|
146
|
-
validateView(schema, view, built, currentProblems, unresolved);
|
|
161
|
+
validateView(schema, view, builtIn(built, allNew, new Set()), currentProblems, unresolved, addedBy);
|
|
147
162
|
problems.push(...currentProblems);
|
|
148
163
|
const elementCount = [...view.values()].reduce((n, m) => n + m.size, 0);
|
|
149
164
|
lines.push(files === null ? `model: none (${STRUCTURE_DIR}/ absent)` : `model: ${files.length} partition file(s), ${elementCount} element(s) in ${view.size} catalogue(s)`);
|
|
150
165
|
if (unresolved.size > 0)
|
|
151
166
|
lines.push(`references into ${[...unresolved].sort().join(", ")}: unresolved (no built catalogue)`);
|
|
152
|
-
// The generated views, against a render of the current view.
|
|
153
|
-
//
|
|
167
|
+
// The generated views, against a render of the current view. Where a model is admitted they are required (M-ADR
|
|
168
|
+
// 0230 §4.3): an SDD reviews an extraction before writing it (§6 step 1), so none is mid-adoption with a model.
|
|
154
169
|
if (files !== null && (await subdirs(join(repo, "docs"))).includes("structure")) {
|
|
155
170
|
let current = 0;
|
|
156
171
|
for (const [path, rendered] of renderViews(view)) {
|
|
@@ -163,14 +178,15 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
163
178
|
lines.push(`generated views: ${current} of ${VIEW_COUNT} current`);
|
|
164
179
|
}
|
|
165
180
|
else if (files !== null) {
|
|
166
|
-
|
|
181
|
+
problems.push("docs/structure/: the generated views are not rendered; where a structural model is admitted they are required. `structure render` writes them (sdd-maintain or sdd-cli)");
|
|
182
|
+
lines.push(`generated views: 0 of ${VIEW_COUNT} (no docs/structure/)`);
|
|
167
183
|
}
|
|
168
184
|
// Open changes.
|
|
169
|
-
const
|
|
185
|
+
const addedByChange = new Map();
|
|
170
186
|
for (const s of structures)
|
|
171
187
|
for (const d of s.deltas)
|
|
172
188
|
for (const e of d.added)
|
|
173
|
-
|
|
189
|
+
addedByChange.set(elementKey(e.catalogue, e.id), s.change);
|
|
174
190
|
const features = new Set(view.get("features")?.keys() ?? []);
|
|
175
191
|
let withDeltas = 0;
|
|
176
192
|
let noOps = 0;
|
|
@@ -190,7 +206,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
190
206
|
}
|
|
191
207
|
if (s.noOp && s.deltas.length > 0)
|
|
192
208
|
problems.push(`${s.noOp.file}: a no-op claim beside ${s.deltas.length} partition delta(s); state one or the other`);
|
|
193
|
-
const elsewhere = new Map([...
|
|
209
|
+
const elsewhere = new Map([...addedByChange].filter(([, c]) => c !== s.change));
|
|
194
210
|
validateDeltas(schema, view, s.deltas, elsewhere, problems);
|
|
195
211
|
// A behaviour delta for a feature the model does not hold needs that feature added here.
|
|
196
212
|
const adds = new Set(s.deltas.flatMap((d) => d.added.filter((e) => e.catalogue === "features").map((e) => e.id)));
|
|
@@ -207,9 +223,6 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
207
223
|
}
|
|
208
224
|
lines.push(`changes: ${changes.length} open, ${withDeltas} with structural deltas, ${noOps} claiming none, ${without} without the artifact`);
|
|
209
225
|
// Composition (M-ADR 0230 §4.5): the three views and the three checks.
|
|
210
|
-
const open = new Map();
|
|
211
|
-
for (const s of structures)
|
|
212
|
-
open.set(s.change, { structure: s, status: await agreementStatus(repo, s.change) });
|
|
213
226
|
const archived = await archivedNames(repo);
|
|
214
227
|
checkOverlap(open, problems);
|
|
215
228
|
checkDependencies(open, archived, problems);
|
|
@@ -219,7 +232,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
219
232
|
if (oc.structure.deltas.length === 0)
|
|
220
233
|
continue;
|
|
221
234
|
const at = [];
|
|
222
|
-
validateView(schema, viewAt(view, name, open), built, at, new Set());
|
|
235
|
+
validateView(schema, viewAt(view, name, open), builtIn(built, allNew, additions.contracts(closure(name, open))), at, new Set(), addedBy);
|
|
223
236
|
for (const p of at) {
|
|
224
237
|
if (known.has(p))
|
|
225
238
|
continue;
|
|
@@ -231,7 +244,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
231
244
|
const agreed = agreedOrder(open);
|
|
232
245
|
if (agreed.length > 0) {
|
|
233
246
|
const inIntended = [];
|
|
234
|
-
validateView(schema, intended(view, open), built, inIntended, new Set());
|
|
247
|
+
validateView(schema, intended(view, open), builtIn(built, allNew, additions.contracts(agreed)), inIntended, new Set(), addedBy);
|
|
235
248
|
for (const p of inIntended) {
|
|
236
249
|
if (known.has(p) || atAgreed.has(p))
|
|
237
250
|
continue;
|
|
@@ -259,7 +272,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
259
272
|
if (problems.length === 0)
|
|
260
273
|
return { exitCode: 0, stdout: `structural-model:\n${body}[PASS] structural-model: ok\n` };
|
|
261
274
|
const tag = enforced ? "FAIL" : "WARN";
|
|
262
|
-
const tail = enforced ? `[FAIL] structural-model: ${problems.length} finding(s)\n` : `[WARN] structural-model: ${problems.length} finding(s), report only (M-ADR 0230
|
|
275
|
+
const tail = enforced ? `[FAIL] structural-model: ${problems.length} finding(s)\n` : `[WARN] structural-model: ${problems.length} finding(s), report only (M-ADR 0230)\n`;
|
|
263
276
|
return { exitCode: enforced ? 1 : 0, stdout: `structural-model:\n${body}${problems.map((p) => `[${tag}] ${p}\n`).join("")}${tail}` };
|
|
264
277
|
}
|
|
265
278
|
//# sourceMappingURL=structural-model.js.map
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/verify-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"sddCliVersion": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
|
+
"sddCliVersion": "5.1.0",
|
|
5
5
|
"description": "Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Ships only the code those verbs reach; it cannot write to the repository it is pointed at. Authoring and maintenance are in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|