@sdd-method/sdd-cli 5.2.0 → 5.3.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.d.ts +16 -0
- package/dist/lib/behaviour-store/archive.d.ts.map +1 -1
- package/dist/lib/behaviour-store/archive.js +28 -0
- package/dist/lib/behaviour-store/archive.js.map +1 -1
- package/dist/lib/behaviour-store/schema.d.ts +14 -4
- package/dist/lib/behaviour-store/schema.d.ts.map +1 -1
- package/dist/lib/behaviour-store/schema.js +24 -12
- package/dist/lib/behaviour-store/schema.js.map +1 -1
- package/dist/lib/catalogue/build.d.ts.map +1 -1
- package/dist/lib/catalogue/build.js +16 -1
- package/dist/lib/catalogue/build.js.map +1 -1
- package/dist/lib/catalogue/builders/adrs.d.ts +6 -0
- package/dist/lib/catalogue/builders/adrs.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/adrs.js +1 -1
- package/dist/lib/catalogue/builders/adrs.js.map +1 -1
- package/dist/lib/catalogue/builders/contracts.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/contracts.js +5 -0
- package/dist/lib/catalogue/builders/contracts.js.map +1 -1
- package/dist/lib/catalogue/builders/repositories.d.ts +2 -1
- package/dist/lib/catalogue/builders/repositories.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/repositories.js +5 -3
- package/dist/lib/catalogue/builders/repositories.js.map +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +3 -2
- package/dist/lib/catalogue/contract-id.js +1 -1
- package/dist/lib/catalogue/contract-id.js.map +1 -1
- package/dist/lib/catalogue/impact-run.d.ts.map +1 -1
- package/dist/lib/catalogue/impact-run.js +6 -1
- package/dist/lib/catalogue/impact-run.js.map +1 -1
- package/dist/lib/catalogue/traceability-loader.d.ts +3 -1
- package/dist/lib/catalogue/traceability-loader.d.ts.map +1 -1
- package/dist/lib/catalogue/traceability-loader.js +3 -0
- package/dist/lib/catalogue/traceability-loader.js.map +1 -1
- package/dist/lib/contracts-availability/index.d.ts.map +1 -1
- package/dist/lib/contracts-availability/index.js +4 -1
- package/dist/lib/contracts-availability/index.js.map +1 -1
- package/dist/lib/plan/read-sdd.d.ts +1 -0
- package/dist/lib/plan/read-sdd.d.ts.map +1 -1
- package/dist/lib/plan/read-sdd.js +2 -1
- package/dist/lib/plan/read-sdd.js.map +1 -1
- package/dist/lib/scaffold/behaviour-store.d.ts.map +1 -1
- package/dist/lib/scaffold/behaviour-store.js +5 -3
- package/dist/lib/scaffold/behaviour-store.js.map +1 -1
- package/dist/lib/sdd-index/repo-config.d.ts.map +1 -1
- package/dist/lib/sdd-index/repo-config.js +1 -0
- package/dist/lib/sdd-index/repo-config.js.map +1 -1
- package/dist/lib/sdd-index/types.d.ts +1 -1
- package/dist/lib/sdd-index/types.d.ts.map +1 -1
- package/dist/lib/snapshot/advance.d.ts.map +1 -1
- package/dist/lib/snapshot/advance.js +13 -5
- package/dist/lib/snapshot/advance.js.map +1 -1
- package/dist/lib/snapshot/fold.d.ts.map +1 -1
- package/dist/lib/snapshot/fold.js +3 -3
- package/dist/lib/snapshot/fold.js.map +1 -1
- package/dist/lib/snapshot/manifest.d.ts +6 -0
- package/dist/lib/snapshot/manifest.d.ts.map +1 -1
- package/dist/lib/snapshot/manifest.js +18 -2
- package/dist/lib/snapshot/manifest.js.map +1 -1
- package/dist/lib/structure/model.d.ts +3 -0
- package/dist/lib/structure/model.d.ts.map +1 -1
- package/dist/lib/structure/model.js +6 -1
- package/dist/lib/structure/model.js.map +1 -1
- package/dist/lib/structure/removals.d.ts +11 -0
- package/dist/lib/structure/removals.d.ts.map +1 -0
- package/dist/lib/structure/removals.js +121 -0
- package/dist/lib/structure/removals.js.map +1 -0
- package/dist/lib/sync/index.d.ts.map +1 -1
- package/dist/lib/sync/index.js +8 -5
- package/dist/lib/sync/index.js.map +1 -1
- package/dist/lib/validate/behaviour-store.d.ts.map +1 -1
- package/dist/lib/validate/behaviour-store.js +213 -39
- package/dist/lib/validate/behaviour-store.js.map +1 -1
- package/dist/lib/validate/structural-model.d.ts.map +1 -1
- package/dist/lib/validate/structural-model.js +7 -1
- package/dist/lib/validate/structural-model.js.map +1 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +1 -1
- package/scripts/author-version.json +2 -2
- package/scripts/maintain-version.json +2 -2
- package/scripts/verify-version.json +2 -2
|
@@ -34,13 +34,17 @@
|
|
|
34
34
|
// change was withdrawn or replaced (M-ADR 0222 §4.6).
|
|
35
35
|
// 5. surfaces: every archived baseline's evidence lists, under **Surfaces:**, each contract the
|
|
36
36
|
// repositories it was observed in serve, as covered at transport or excluded with a
|
|
37
|
-
// reason, and the list is checked against the catalogue (catalogues/repo_contract.csv
|
|
38
|
-
//
|
|
37
|
+
// reason, and the list is checked against the catalogue (catalogues/repo_contract.csv, less
|
|
38
|
+
// a contract service_contract records the repository's services consuming and none exposing:
|
|
39
|
+
// repo_contract has no direction), never against itself (M-ADR 0222 §4.3, §4.4). A list the catalogue gives nothing to
|
|
39
40
|
// check against (no Observed in line, or a repository with no catalogue contracts) fails:
|
|
40
41
|
// a cross-check of zero contracts is unresolved, not a pass.
|
|
41
42
|
// A baseline whose every requirement has since left the living specs (removed, or its
|
|
42
43
|
// spec retired) is history: its list is not checked, so a retired surface can drop its
|
|
43
|
-
// contract (M-ADR 0222 §4.4).
|
|
44
|
+
// contract (M-ADR 0222 §4.4). A baseline archived where the SDD ships record schema 1.4.0
|
|
45
|
+
// or later records its gate's PASS (**Baseline gate:**), so `change archive` refuses one
|
|
46
|
+
// that has not passed (M-ADR 0222 §4.3). An open baseline is asked these questions, and
|
|
47
|
+
// what it may change and which features key it, as warnings while it can still be fixed.
|
|
44
48
|
// 7. features: every living spec is keyed by a catalogue feature (M-ADR 0221 §4.7). A spec keyed by no
|
|
45
49
|
// feature is dropped from the catalogue by the requirements builder, which reports it only as a
|
|
46
50
|
// build finding; this check makes it a failure, so a shipped requirement cannot vanish from the
|
|
@@ -69,11 +73,11 @@ import { tmpdir } from "node:os";
|
|
|
69
73
|
import { basename, join } from "node:path";
|
|
70
74
|
import { promisify } from "node:util";
|
|
71
75
|
import { parseDocument } from "yaml";
|
|
72
|
-
import { observedIn, orderArchives, parseRequirements, parseSurfaces, renamedFrom, renamePairs, resolveImplements, surfaceMatches } from "../behaviour-store/archive.js";
|
|
76
|
+
import { contractMoves, observedIn, orderArchives, parseRequirements, parseSurfaces, renamedFrom, renamePairs, resolveImplements, surfaceMatches } from "../behaviour-store/archive.js";
|
|
73
77
|
import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
|
|
74
78
|
import { boundFile, conditionProblems, structuralFile, isCommit, normaliseBound, requirementBlock, sameCommit, scenarioKind, taskGroups, testPlanHeader, testPlanMapping, verdictLine, versionAtLeast } from "./agreement-evidence.js";
|
|
75
79
|
import { ENFORCED as STRUCTURE_ENFORCED } from "./structural-model.js";
|
|
76
|
-
import { compareStoreSchema, SCHEMA_NAME, SHIPPED_SCHEMA_DIR, STORE_CONFIG, STORE_SCHEMA_DIR, storeSchemaName } from "../behaviour-store/schema.js";
|
|
80
|
+
import { BASELINE_SCHEMA_NAME, compareStoreSchema, SCHEMA_NAME, SHIPPED_SCHEMA_DIR, shippedSchemaDir, STORE_CONFIG, STORE_SCHEMA_DIR, storeSchemaDir, storeSchemaName } from "../behaviour-store/schema.js";
|
|
77
81
|
const execFileAsync = promisify(execFile);
|
|
78
82
|
/** The OpenSpec release the method depends on (M-ADR 0223 ruling 2). The module installs exactly this. */
|
|
79
83
|
export const OPENSPEC_VERSION = "1.14.3";
|
|
@@ -86,6 +90,8 @@ const COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
|
86
90
|
// OpenSpec counts a task done when its box holds only `x`, either case, any spacing.
|
|
87
91
|
const TASK_DONE_RE = /^\s*-\s*\[\s*[xX]\s*\]/m;
|
|
88
92
|
const TARGET_HEADING_RE = /^##\s+(MODIFIED|REMOVED|RENAMED)\s+Requirements\b/m;
|
|
93
|
+
// A tasks.md group headed by one word, `## <n>. <word>`, is that repository's work order (taskGroups).
|
|
94
|
+
const GROUP_REPO = (repo) => `tasks.md has a work order for ${repo} and the record's work_orders do not; its evidence cannot be checked (a group heading of one word, \`## <n>. ${repo}\`, names the group's repository)`;
|
|
89
95
|
async function exists(p) {
|
|
90
96
|
try {
|
|
91
97
|
await access(p);
|
|
@@ -516,6 +522,14 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
516
522
|
}
|
|
517
523
|
}
|
|
518
524
|
}
|
|
525
|
+
// The archive check reads a task group's repository from its heading; once the record declares work orders, a
|
|
526
|
+
// heading it would read as a repository the record lacks fails here, while the change can still be amended.
|
|
527
|
+
if (versionAtLeast(record?.schemaVersion, "1.1.0") && record?.status === "agreed" && record.workOrders.length > 0 && tasks !== null) {
|
|
528
|
+
const recorded = new Set(record.workOrders.map((w) => w.repo));
|
|
529
|
+
for (const g of taskGroups(tasks))
|
|
530
|
+
if (!recorded.has(g.repo))
|
|
531
|
+
failures.push(`${record.file}: ${GROUP_REPO(g.repo)}`);
|
|
532
|
+
}
|
|
519
533
|
// A test plan's kinds (M-ADR 0223 §4.4 as amended): changed and repaired scenarios go red then green, carried ones stay green.
|
|
520
534
|
if (versionAtLeast(record?.schemaVersion, "1.1.0") && record?.status === "agreed") {
|
|
521
535
|
const plan = await readText(join(repo, dir, "test-plan.md"));
|
|
@@ -717,7 +731,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
717
731
|
const recorded = new Set(record.workOrders.map((w) => w.repo));
|
|
718
732
|
for (const g of groups)
|
|
719
733
|
if (!recorded.has(g.repo))
|
|
720
|
-
failures.push(`${record.file}:
|
|
734
|
+
failures.push(`${record.file}: ${GROUP_REPO(g.repo)}`);
|
|
721
735
|
// Requirement names are unique within a feature, not across a change: a work order names `<feature>::<name>`,
|
|
722
736
|
// or a bare name only one feature in the change has (schema 5.0.0). An ambiguous bare name implements nothing.
|
|
723
737
|
const built = [];
|
|
@@ -818,8 +832,107 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
818
832
|
}
|
|
819
833
|
const orderDecides = (name, key) => [...(peers.get(name) ?? [])].some((p) => p !== name && touches.get(p)?.has(key));
|
|
820
834
|
let scopeUnresolved = 0;
|
|
821
|
-
const
|
|
835
|
+
const openBaselines = [];
|
|
836
|
+
for (const change of open)
|
|
837
|
+
if (await exists(join(repo, STORE_DIR, "changes", change, "baseline-evidence.md")))
|
|
838
|
+
openBaselines.push(change);
|
|
839
|
+
const servedBy = baselines + openBaselines.length > 0 ? await catalogueContracts(repo) : null;
|
|
822
840
|
let surfacesChecked = 0;
|
|
841
|
+
// The contract moves each archived change records, in archive order. A contract's id derives from its path, so a
|
|
842
|
+
// move changes it; an archived baseline's list names the contract as it was, and is resolved through the moves
|
|
843
|
+
// archived after it rather than edited (M-ADR 0222 §4.4). A surface that vanished with no move still fails.
|
|
844
|
+
const movesAt = [];
|
|
845
|
+
for (const name of order) {
|
|
846
|
+
const found = [];
|
|
847
|
+
let names = [];
|
|
848
|
+
try {
|
|
849
|
+
names = (await readdir(join(repo, archiveRel, name, "structure"))).filter((f) => f.endsWith(".yaml")).sort();
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
names = [];
|
|
853
|
+
}
|
|
854
|
+
for (const f of names) {
|
|
855
|
+
try {
|
|
856
|
+
found.push(...contractMoves(parseDocument((await readText(join(repo, archiveRel, name, "structure", f))) ?? "").toJS()?.["moved"]).moves);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// the structural-model check reports an unreadable delta
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
movesAt.push(found);
|
|
863
|
+
}
|
|
864
|
+
// An open change's moves count too, after every archive: the catalogue it is built with already holds the new
|
|
865
|
+
// identity, so the lists that named the old one follow the move while it is open, not only once it archives.
|
|
866
|
+
const openMoves = [];
|
|
867
|
+
for (const change of open) {
|
|
868
|
+
let names = [];
|
|
869
|
+
try {
|
|
870
|
+
names = (await readdir(join(repo, STORE_DIR, "changes", change, "structure"))).filter((f) => f.endsWith(".yaml")).sort();
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
names = [];
|
|
874
|
+
}
|
|
875
|
+
for (const f of names) {
|
|
876
|
+
try {
|
|
877
|
+
openMoves.push(...contractMoves(parseDocument((await readText(join(repo, STORE_DIR, "changes", change, "structure", f))) ?? "").toJS()?.["moved"]).moves);
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
// the structural-model check reports an unreadable delta
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** The identities a contract had before the moves archived after position `after` (-1: every archived move). */
|
|
885
|
+
const formerIdentities = (id, after) => {
|
|
886
|
+
const back = new Map();
|
|
887
|
+
for (const m of [...movesAt.slice(after + 1).flat(), ...openMoves])
|
|
888
|
+
back.set(m.to, [...(back.get(m.to) ?? []), m]);
|
|
889
|
+
const out = [];
|
|
890
|
+
const seen = new Set([id]);
|
|
891
|
+
const queue = [id];
|
|
892
|
+
while (queue.length > 0) {
|
|
893
|
+
for (const m of back.get(queue.shift()) ?? []) {
|
|
894
|
+
if (seen.has(m.from))
|
|
895
|
+
continue;
|
|
896
|
+
seen.add(m.from);
|
|
897
|
+
out.push({ id: m.from, path: m.fromPath ?? "" });
|
|
898
|
+
queue.push(m.from);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
};
|
|
903
|
+
/** What is wrong with a baseline's Surfaces list against the catalogue (M-ADR 0222 §4.3, §4.4). */
|
|
904
|
+
const surfaceProblems = async (dir, evidence, after = -1) => {
|
|
905
|
+
const out = [];
|
|
906
|
+
const parsed = parseSurfaces(evidence);
|
|
907
|
+
if (!parsed.present)
|
|
908
|
+
out.push(`${dir}/baseline-evidence.md: no **Surfaces:** list; name every contract the observed repositories serve, covered at transport or excluded with a reason`);
|
|
909
|
+
for (const p of parsed.problems)
|
|
910
|
+
out.push(`${dir}/baseline-evidence.md: Surfaces: ${p}`);
|
|
911
|
+
if (!parsed.present)
|
|
912
|
+
return out;
|
|
913
|
+
if (servedBy === null) {
|
|
914
|
+
out.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: catalogues/repo_contract.csv or catalogues/contracts.csv is missing; build the catalogue`);
|
|
915
|
+
return out;
|
|
916
|
+
}
|
|
917
|
+
const repos = observedIn(evidence);
|
|
918
|
+
if (repos.length === 0)
|
|
919
|
+
out.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: no **Observed in:** line names a repository`);
|
|
920
|
+
for (const r of repos) {
|
|
921
|
+
const known = servedBy.get(r);
|
|
922
|
+
if ((known?.served ?? []).length === 0) {
|
|
923
|
+
const consumed = known?.consumed ? ` (it consumes ${known.consumed}, which a baseline does not list)` : "";
|
|
924
|
+
out.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: the catalogue records no contract that ${r} serves${consumed}; declare the contracts it serves in its repo-config`);
|
|
925
|
+
}
|
|
926
|
+
for (const c of known?.served ?? []) {
|
|
927
|
+
surfacesChecked++;
|
|
928
|
+
const former = formerIdentities(c.id, after);
|
|
929
|
+
if (!parsed.entries.some((e) => surfaceMatches(e.contract, c.id, c.specPath) || former.some((f) => surfaceMatches(e.contract, f.id, f.path)))) {
|
|
930
|
+
out.push(`${dir}/baseline-evidence.md: Surfaces does not list ${c.specPath || c.id} (${c.id}), which ${r} serves (catalogues/repo_contract.csv, less what service_contract records it only consuming); list it as covered at transport or excluded with a reason`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
};
|
|
823
936
|
// A baseline whose every requirement has since left the living specs (removed, or its spec retired) records a
|
|
824
937
|
// surface that no longer exists; its Surfaces list is history, not checked against today's catalogue, or a
|
|
825
938
|
// retired surface could never drop its contract.
|
|
@@ -831,33 +944,23 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
831
944
|
const evidence = await readText(join(repo, dir, "baseline-evidence.md"));
|
|
832
945
|
const isBaseline = evidence !== null;
|
|
833
946
|
if (isBaseline) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
947
|
+
if (historical.has(name)) {
|
|
948
|
+
// history: see historicalBaselines; its gate and Surfaces list are not checked
|
|
949
|
+
const parsed = parseSurfaces(evidence);
|
|
950
|
+
if (!parsed.present)
|
|
951
|
+
failures.push(`${dir}/baseline-evidence.md: no **Surfaces:** list; name every contract the observed repositories serve, covered at transport or excluded with a reason`);
|
|
952
|
+
for (const p of parsed.problems)
|
|
953
|
+
failures.push(`${dir}/baseline-evidence.md: Surfaces: ${p}`);
|
|
841
954
|
}
|
|
842
|
-
else
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if ((servedBy.get(r) ?? []).length === 0) {
|
|
852
|
-
failures.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: catalogues/repo_contract.csv records no contract for ${r}; declare the contracts it serves in its repo-config`);
|
|
853
|
-
}
|
|
854
|
-
for (const c of servedBy.get(r) ?? []) {
|
|
855
|
-
surfacesChecked++;
|
|
856
|
-
if (!parsed.entries.some((e) => surfaceMatches(e.contract, c.id, c.specPath))) {
|
|
857
|
-
failures.push(`${dir}/baseline-evidence.md: Surfaces does not list ${c.specPath || c.id} (${c.id}), which ${r} serves (catalogues/repo_contract.csv); list it as covered at transport or excluded with a reason`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
955
|
+
else {
|
|
956
|
+
failures.push(...(await surfaceProblems(dir, evidence, order.indexOf(name))));
|
|
957
|
+
// The gate's outcome is recorded where the SDD ships record schema 1.4.0 or later when the baseline archives;
|
|
958
|
+
// a baseline archived earlier carries none and is not held to it.
|
|
959
|
+
const archivedAt = await integratedAt(repo, dir);
|
|
960
|
+
if (versionAtLeast(await shippedRecordSchema(repo, archivedAt), "1.4.0")) {
|
|
961
|
+
const gate = baselineGate(evidence);
|
|
962
|
+
if (gate !== "PASS")
|
|
963
|
+
failures.push(`${dir}/baseline-evidence.md: archived ${gate === null ? "with no **Baseline gate:** line" : `with **Baseline gate:** ${gate}`}; a baseline archives once its gate passes: every requirement cites its evidence, characterization tests exist where the code has none, and every covered surface is evidenced at its transport (M-ADR 0222 §4.3)`);
|
|
861
964
|
}
|
|
862
965
|
}
|
|
863
966
|
}
|
|
@@ -892,8 +995,34 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
892
995
|
lastTouch.delete(`${feature}::${from}`);
|
|
893
996
|
}
|
|
894
997
|
}
|
|
895
|
-
|
|
896
|
-
|
|
998
|
+
// An open baseline is asked the archive's questions while it can still be fixed: its Surfaces list, what it may
|
|
999
|
+
// change given the lineage so far, the features its deltas are keyed by, and its gate. Warnings, since it is in
|
|
1000
|
+
// progress; `change archive` refuses what is still wrong when it archives.
|
|
1001
|
+
const featureIds = await readText(join(repo, "catalogues", "features.csv")).then((t) => (t === null ? null : new Set(parseCatalogueCsv(t).rows.map((r) => r["id"] ?? ""))));
|
|
1002
|
+
for (const change of openBaselines) {
|
|
1003
|
+
const dir = `${STORE_DIR}/changes/${change}`;
|
|
1004
|
+
const evidence = (await readText(join(repo, dir, "baseline-evidence.md"))) ?? "";
|
|
1005
|
+
const found = [...(await surfaceProblems(dir, evidence))];
|
|
1006
|
+
for (const feature of await dirs(join(repo, dir, "specs"))) {
|
|
1007
|
+
if (featureIds !== null && !featureIds.has(feature))
|
|
1008
|
+
found.push(`${dir}/specs/${feature}: no catalogue feature has the id "${feature}"; give the behaviour a feature first (M-ADR 0222 §4.4)`);
|
|
1009
|
+
const text = (await readText(join(repo, dir, "specs", feature, "spec.md"))) ?? "";
|
|
1010
|
+
for (const target of [...parseRequirements(text).filter((r) => r.operation === "MODIFIED" || r.operation === "REMOVED").map((r) => r.name), ...renamedFrom(text)]) {
|
|
1011
|
+
scopeChecked++;
|
|
1012
|
+
const last = lastTouch.get(`${feature}::${target}`);
|
|
1013
|
+
if (last !== "baseline")
|
|
1014
|
+
found.push(`${dir}: a baseline changes "${target}" (${feature}), which ${last === "change" ? "a judged change last shipped" : "no earlier archived change admitted"}; a baseline may remove or modify only what a baseline admitted (M-ADR 0222 §4.4)`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const gate = baselineGate(evidence);
|
|
1018
|
+
if (gate !== "PASS")
|
|
1019
|
+
found.push(`${dir}/baseline-evidence.md: ${gate === null ? "no **Baseline gate:** line yet" : `**Baseline gate:** ${gate}`}; it archives once the gate passes (M-ADR 0222 §4.3)`);
|
|
1020
|
+
warnings.push(...found.map((w) => `open baseline: ${w}`));
|
|
1021
|
+
}
|
|
1022
|
+
if (openBaselines.length > 0)
|
|
1023
|
+
lines.push(`open baselines: ${openBaselines.length}, asked the archive's questions as warnings`);
|
|
1024
|
+
const unseen = servedBy === null ? "" : `; repositories with no catalogue contracts: ${[...new Set(await observedInAll(repo, archiveRel, order))].filter((r) => (servedBy.get(r)?.served ?? []).length === 0).join(", ") || "none"}`;
|
|
1025
|
+
lines.push(baselines === 0 ? "surfaces: vacuous (no baseline)" : `surfaces: ${baselines} baseline(s), ${surfacesChecked} served contract(s) checked against catalogues/repo_contract.csv less what service_contract records only consumed${unseen}${historical.size ? `; ${historical.size} historical baseline(s) not checked (every requirement they baselined has left the living specs)` : ""}`);
|
|
897
1026
|
lines.push(scopeChecked === 0 ? "scope: vacuous (no baseline removes, modifies or renames a requirement)" : `scope: ${scopeChecked} requirement(s) a baseline removes, modifies or renames, checked against their lineage`);
|
|
898
1027
|
if (!history && archived.length > 0)
|
|
899
1028
|
lines.push("order: no git history (not a git work tree, or git log failed), so same-day archives cannot be ordered");
|
|
@@ -944,6 +1073,29 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
944
1073
|
lines.push(drift === 0 ? `schema: ${SCHEMA_NAME}, identical to the shipped copy` : `schema: ${SCHEMA_NAME}, ${drift} file(s) differ from the shipped copy`);
|
|
945
1074
|
}
|
|
946
1075
|
}
|
|
1076
|
+
// The baseline schema: checked as sdd-change is where the bundle ships it (a bundle from before it shipped has none),
|
|
1077
|
+
// and an open baseline selects it rather than sdd-change, whose Gate 0, test plan and agreement a baseline lacks.
|
|
1078
|
+
if (selected === SCHEMA_NAME) {
|
|
1079
|
+
const b = await compareStoreSchema(repo, BASELINE_SCHEMA_NAME);
|
|
1080
|
+
if (b.shipped) {
|
|
1081
|
+
const fix = "sdd-cli sync refreshes it; put adopter guidance in the store's config.yaml rules, not in the schema";
|
|
1082
|
+
for (const f of b.missing)
|
|
1083
|
+
failures.push(`${storeSchemaDir(BASELINE_SCHEMA_NAME)}/${f}: missing from the store's copy of the schema; ${fix}`);
|
|
1084
|
+
for (const f of b.differing)
|
|
1085
|
+
failures.push(`${storeSchemaDir(BASELINE_SCHEMA_NAME)}/${f}: differs from the shipped ${shippedSchemaDir(BASELINE_SCHEMA_NAME)}/${f}; ${fix}`);
|
|
1086
|
+
for (const f of b.extra)
|
|
1087
|
+
failures.push(`${storeSchemaDir(BASELINE_SCHEMA_NAME)}/${f}: not in the shipped schema; ${fix}`);
|
|
1088
|
+
const drift = b.missing.length + b.differing.length + b.extra.length;
|
|
1089
|
+
lines.push(drift === 0 ? `schema: ${BASELINE_SCHEMA_NAME}, identical to the shipped copy` : `schema: ${BASELINE_SCHEMA_NAME}, ${drift} file(s) differ from the shipped copy`);
|
|
1090
|
+
for (const change of openBaselines) {
|
|
1091
|
+
const meta = await readText(join(repo, STORE_DIR, "changes", change, ".openspec.yaml"));
|
|
1092
|
+
const chosen = meta === null ? null : parseDocument(meta).toJS()?.schema;
|
|
1093
|
+
if (chosen !== BASELINE_SCHEMA_NAME) {
|
|
1094
|
+
warnings.push(`open baseline: ${STORE_DIR}/changes/${change}/.openspec.yaml selects ${chosen === null || chosen === undefined ? "no schema" : `"${String(chosen)}"`}; a baseline selects ${BASELINE_SCHEMA_NAME} (schema: ${BASELINE_SCHEMA_NAME}), whose artifacts are a baseline's rather than a change's Gate 0, test plan and agreement`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
947
1099
|
const population = recordFiles.length + open.length + archived.length;
|
|
948
1100
|
const body = lines.map((l) => ` ${l}`).join("\n") + "\n";
|
|
949
1101
|
if (failures.length > 0) {
|
|
@@ -1002,25 +1154,47 @@ async function historicalBaselines(repo, archiveRel, order) {
|
|
|
1002
1154
|
});
|
|
1003
1155
|
return out;
|
|
1004
1156
|
}
|
|
1005
|
-
/**
|
|
1157
|
+
/**
|
|
1158
|
+
* Per repository, the contracts the catalogue says it serves, with how many it only consumes; null when repo_contract or
|
|
1159
|
+
* contracts is missing. repo_contract binds a repository to a contract in either direction, so a row is dropped where
|
|
1160
|
+
* service_contract records the repository's services consuming that contract and none exposing it. Rows
|
|
1161
|
+
* service_contract says nothing about stay: a model that omits a service's exposes rows does not shrink what is checked.
|
|
1162
|
+
*/
|
|
1006
1163
|
async function catalogueContracts(repo) {
|
|
1007
1164
|
const rc = await readText(join(repo, "catalogues", "repo_contract.csv"));
|
|
1008
1165
|
const cs = await readText(join(repo, "catalogues", "contracts.csv"));
|
|
1009
1166
|
if (rc === null || cs === null)
|
|
1010
1167
|
return null;
|
|
1011
1168
|
const spec = new Map(parseCatalogueCsv(cs).rows.map((r) => [r["id"] ?? "", r["spec_path"] ?? ""]));
|
|
1169
|
+
const repoOf = new Map(parseCatalogueCsv((await readText(join(repo, "catalogues", "services.csv"))) ?? "").rows.map((r) => [r["id"] ?? "", r["primary_repo_id"] ?? ""]));
|
|
1170
|
+
const roles = new Map();
|
|
1171
|
+
for (const r of parseCatalogueCsv((await readText(join(repo, "catalogues", "service_contract.csv"))) ?? "").rows) {
|
|
1172
|
+
const key = `${repoOf.get(r["service_id"] ?? "") ?? ""}\u0000${r["contract_id"] ?? ""}`;
|
|
1173
|
+
roles.set(key, (roles.get(key) ?? new Set()).add(r["role"] ?? ""));
|
|
1174
|
+
}
|
|
1012
1175
|
const out = new Map();
|
|
1013
1176
|
for (const r of parseCatalogueCsv(rc).rows) {
|
|
1014
1177
|
const repoId = r["repo_id"] ?? "";
|
|
1015
1178
|
const id = r["contract_id"] ?? "";
|
|
1016
1179
|
if (repoId === "" || id === "")
|
|
1017
1180
|
continue;
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1181
|
+
const entry = out.get(repoId) ?? { served: [], consumed: 0 };
|
|
1182
|
+
const role = roles.get(`${repoId}\u0000${id}`);
|
|
1183
|
+
if (role?.has("consumes") && !role.has("exposes"))
|
|
1184
|
+
entry.consumed++;
|
|
1185
|
+
else
|
|
1186
|
+
entry.served.push({ id, specPath: spec.get(id) ?? "" });
|
|
1187
|
+
out.set(repoId, entry);
|
|
1021
1188
|
}
|
|
1022
1189
|
return out;
|
|
1023
1190
|
}
|
|
1191
|
+
/** A baseline's recorded gate outcome: the last `**Baseline gate:** <outcome>` line's first word, upper-cased; null when there is none. */
|
|
1192
|
+
function baselineGate(evidence) {
|
|
1193
|
+
let out = null;
|
|
1194
|
+
for (const m of evidence.matchAll(/^\s*\*\*Baseline gate:\*\*\s*(\S+)/gm))
|
|
1195
|
+
out = m[1].replace(/[^A-Za-z]/g, "").toUpperCase();
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1024
1198
|
async function observedInAll(repo, archiveRel, names) {
|
|
1025
1199
|
const out = [];
|
|
1026
1200
|
for (const n of names) {
|