@sdd-method/author-cli 0.4.0 → 0.5.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/author-program.js +3 -0
- package/dist/lib/catalogue/build.js +13 -0
- package/dist/lib/catalogue/builders/capabilities.js +4 -1
- package/dist/lib/catalogue/builders/product-edges.js +3 -1
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +16 -1
- package/dist/lib/catalogue/builders/requirements.js +240 -0
- package/dist/lib/catalogue/canonical-schema.yaml +160 -7
- package/dist/lib/catalogue/rubric-coverage-map.yaml +36 -0
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/sdd-index/repo-config.js +7 -2
- package/dist/lib/vocabulary/migrate.js +237 -0
- package/dist/lib/vocabulary/renames.js +40 -0
- package/dist/lib/vocabulary/vocabulary-renames.yaml +76 -0
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/migrate.js +32 -0
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/scripts/validate-app-dependencies.sh +29 -21
package/dist/author-program.js
CHANGED
|
@@ -22,6 +22,7 @@ import { registerConformance } from "./verbs/conformance.js";
|
|
|
22
22
|
import { registerContext } from "./verbs/context.js";
|
|
23
23
|
import { registerContracts } from "./verbs/contracts.js";
|
|
24
24
|
import { registerGenerateBriefing } from "./verbs/generate-briefing.js";
|
|
25
|
+
import { registerMigrate } from "./verbs/migrate.js";
|
|
25
26
|
import { registerPlan } from "./verbs/plan.js";
|
|
26
27
|
import { registerRender } from "./verbs/render.js";
|
|
27
28
|
import { registerScaffold } from "./verbs/scaffold.js";
|
|
@@ -44,6 +45,7 @@ export const AUTHOR_ONLY_SURFACE = [
|
|
|
44
45
|
"generate-briefing",
|
|
45
46
|
"context pull",
|
|
46
47
|
"catalogue check",
|
|
48
|
+
"migrate vocabulary",
|
|
47
49
|
];
|
|
48
50
|
/** The full surface: verify's, plus the author's own. */
|
|
49
51
|
export const AUTHOR_SURFACE = [...VERIFY_SURFACE, ...AUTHOR_ONLY_SURFACE];
|
|
@@ -83,6 +85,7 @@ export function buildAuthorProgram() {
|
|
|
83
85
|
// The verify surface first, from verify's own registration: verify ⊂ author is structural.
|
|
84
86
|
registerVerifySurface(program);
|
|
85
87
|
registerScaffold(program);
|
|
88
|
+
registerMigrate(program);
|
|
86
89
|
registerPlan(program);
|
|
87
90
|
registerContracts(program);
|
|
88
91
|
registerConformance(program);
|
|
@@ -34,6 +34,7 @@ import { readCatalogueCsv } from "./csv-reader.js";
|
|
|
34
34
|
import { buildDataItems, buildFeatureDemand, buildIntegrationSupply } from "./builders/data-items.js";
|
|
35
35
|
import { buildEvidenceTargetRepos } from "./builders/evidence-target-repo.js";
|
|
36
36
|
import { buildRepoDependencies } from "./builders/repo-dependency.js";
|
|
37
|
+
import { buildRequirements } from "./builders/requirements.js";
|
|
37
38
|
import { buildDomains, buildProducts, buildApplicationDataPlaneBindings, buildApplicationDataPlaneDeviations, buildApplicationConformanceCoverage, buildImplementations, buildRepositories, buildServices, buildCapabilities, buildCapabilityService, buildCapabilityAdr, buildCapabilityFeature, buildCapabilityContract, buildAdrs, buildAdrSupersedes, buildFeatures, buildFeatureGroups, buildFeatureGrounding, buildCapabilitySpec, buildContracts, buildDeclaredExternalContracts, buildRepoEdges, deriveFeatureRepoFromServices, buildRepoCapabilityExemption, buildServiceCapabilityExemption, buildEvents, buildServiceEvent, buildCapabilityEvent, buildFeatureEvent, buildServiceContract, buildServiceContractConsumes, buildFeatureContract, buildDomainModels, buildCapabilityDomainModel, buildFeatureDomainModel, buildFeatureSubscriptionChannels, buildProductPlatformDependencies, buildGlossaryItems, backfillGlossaryRelations, buildGlossaryXref, buildC4Systems, buildC4Containers, buildC4Components, buildC4ExternalSystems, buildExternalSystemExemption, buildContainerDependency, reportEntryIdReferentCollisions, buildSystemExternalDependency, buildPersonaC4System, buildPersonaContainer, buildPersonas, buildQualityScores, buildScaffolds, buildScreens, buildUserJourneys, buildPersonaJourney, buildPersonaFeature, buildJourneyFeature, buildProductEdges, buildIntegrations, entitySlug, buildEntitiesFromManifest, buildEntitiesFromMermaid, buildServiceEntityFromTables, enrichEntityPersistenceKind, buildC4ContainerEntity, buildC4SystemEntity, } from "./builders/index.js";
|
|
38
39
|
import { buildIntegrationComponents } from "./builders/integration-components.js";
|
|
39
40
|
import { loadReverseProductQuality } from "./reverse-product-quality.js";
|
|
@@ -1151,6 +1152,18 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1151
1152
|
// M-ADR 0217: build-time and generation-time dependencies between repositories,
|
|
1152
1153
|
// declared (repo-config depends_on) and derived (per-component sharedLibraries).
|
|
1153
1154
|
out.set("repo_dependency", await buildRepoDependencies({ sddPath, configs: reposResult.configs, knownRepoIds: knownRepoIdsFinal, findings }));
|
|
1155
|
+
// M-ADR 0221 §4.5-4.7: the behaviour store projected into requirements, their lineage and their status.
|
|
1156
|
+
{
|
|
1157
|
+
const r = await buildRequirements({
|
|
1158
|
+
sddPath,
|
|
1159
|
+
findings,
|
|
1160
|
+
knownFeatureIds: new Set((out.get("features") ?? []).map((row) => String(row["id"]))),
|
|
1161
|
+
knownRepoIds: knownRepoIdsFinal,
|
|
1162
|
+
});
|
|
1163
|
+
out.set("requirements", r.requirements);
|
|
1164
|
+
out.set("requirement_lineage", r.lineage);
|
|
1165
|
+
out.set("requirement_status", r.status);
|
|
1166
|
+
}
|
|
1154
1167
|
// 15. C4 systems + containers + components (L1 + L2 + L3). The L3 walker
|
|
1155
1168
|
// is the forward-method authoring path reading
|
|
1156
1169
|
// `c4-l3-*.mermaid.md` files alongside the L2 layout. The earlier Path-B
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { readFile, stat } from "node:fs/promises";
|
|
14
14
|
import { join, relative, sep } from "node:path";
|
|
15
15
|
import { readdir } from "node:fs/promises";
|
|
16
|
+
import { rename } from "../../vocabulary/renames.js";
|
|
16
17
|
const DOMAINS_DIR_REL = join("docs", "domains");
|
|
17
18
|
/**
|
|
18
19
|
* The vocabulary `catalogue-schema.yaml` documents for `capabilities.status`.
|
|
@@ -48,7 +49,9 @@ const DOCUMENTED_CAPABILITY_STATUS = new Set([
|
|
|
48
49
|
const SPEC_AUTHORING_STAGES = new Set([
|
|
49
50
|
"placeholder",
|
|
50
51
|
"first-draft",
|
|
51
|
-
"
|
|
52
|
+
rename("authoring-status-detailed").new,
|
|
53
|
+
// The stage's former name (M-ADR 0222 §4.1) is the same stage on the same wrong axis.
|
|
54
|
+
rename("authoring-status-detailed").old,
|
|
52
55
|
"reverse-derived",
|
|
53
56
|
]);
|
|
54
57
|
/**
|
|
@@ -15,6 +15,7 @@ import { readFile } from "node:fs/promises";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { parse as parseYaml } from "yaml";
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { canonicalValue } from "../../vocabulary/renames.js";
|
|
18
19
|
const REL_PATH = "app-platform-dependencies.yaml";
|
|
19
20
|
const DependencyEntrySchema = z.object({
|
|
20
21
|
capability_id: z.string().optional(),
|
|
@@ -70,7 +71,8 @@ export async function buildProductEdges(inputs) {
|
|
|
70
71
|
continue;
|
|
71
72
|
const capabilityId = (dep.capability_id ?? "").trim();
|
|
72
73
|
const serviceId = (dep.service_id ?? "").trim();
|
|
73
|
-
|
|
74
|
+
// Normalised silently here: product-platform-dependencies reads the same file and reports the former name.
|
|
75
|
+
const requiredStability = canonicalValue("stability-stable", (dep.required_stability ?? "").trim());
|
|
74
76
|
const notes = (dep.notes ?? "").trim();
|
|
75
77
|
if (capabilityId !== "") {
|
|
76
78
|
const id = `${inputs.productId}__${capabilityId}`;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// service_id: <target-service-slug> # platform-side service
|
|
22
22
|
// domain: <domain-hint> # optional (reverse shape)
|
|
23
23
|
// contract_type: rest | graphql | openapi | grpc | event | other
|
|
24
|
-
// required_stability:
|
|
24
|
+
// required_stability: stable | provisional | unknown
|
|
25
25
|
// contract_path: <relative-path-in-platform-sdd> # optional (reverse)
|
|
26
26
|
// consumed_by: # optional (reverse)
|
|
27
27
|
// - <service-name-within-this-product>
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
58
58
|
import { join, relative, sep } from "node:path";
|
|
59
59
|
import { parse as parseYaml } from "yaml";
|
|
60
|
+
import { canonicalValue, isFormerValue } from "../../vocabulary/renames.js";
|
|
60
61
|
const FORWARD_REL_PATH = "app-platform-dependencies.yaml";
|
|
61
62
|
/**
|
|
62
63
|
* Contract types that denote an ASYNC shared-stream dependency (M-ADR 0192).
|
|
@@ -260,6 +261,20 @@ export async function buildProductPlatformDependencies(inputs) {
|
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
263
|
}
|
|
264
|
+
// The stability class `locked` is `stable` from catalogue schema 4.0.0 (M-ADR 0222 §4.1). A source
|
|
265
|
+
// still carrying the former name is read as the new one for one major release, so no cell carries
|
|
266
|
+
// the former word, and reported once per SDD so the adopter can run the migration.
|
|
267
|
+
let former = 0;
|
|
268
|
+
for (let i = 0; i < rows.length; i++) {
|
|
269
|
+
const v = String(rows[i]["required_stability"] ?? "");
|
|
270
|
+
if (isFormerValue("stability-stable", v)) {
|
|
271
|
+
rows[i] = { ...rows[i], required_stability: canonicalValue("stability-stable", v) };
|
|
272
|
+
former++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (former > 0) {
|
|
276
|
+
inputs.findings.add("convention", `product_platform_dependencies: ${former} dependenc${former === 1 ? "y declares" : "ies declare"} the former required_stability 'locked', read as 'stable' (M-ADR 0222 §4.1); run 'sdd-cli migrate vocabulary'`);
|
|
277
|
+
}
|
|
263
278
|
return rows;
|
|
264
279
|
}
|
|
265
280
|
/**
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Behaviour store -> requirements, requirement_lineage, requirement_status (M-ADR 0221 §4.5-4.7, M-ADR 0223 §4.7).
|
|
2
|
+
//
|
|
3
|
+
// The store is an OpenSpec project inside the SDD (`openspec/`). Everything here is a projection: the living specs
|
|
4
|
+
// and changes are the record, and the catalogue reads them at the SDD's own commit. A sibling-repository store is
|
|
5
|
+
// read in that repository, not here.
|
|
6
|
+
//
|
|
7
|
+
// requirements one row per `### Requirement:` in a living spec (stage living), keyed by the spec's
|
|
8
|
+
// directory name, which is the catalogue feature id; plus one per requirement an open,
|
|
9
|
+
// agreed change ADDS (stage agreed).
|
|
10
|
+
// requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED or RENAMED a
|
|
11
|
+
// requirement that is current now. A change carrying baseline-evidence.md is a baseline.
|
|
12
|
+
// requirement_status per (requirement, repository): observed from a baseline's `**Observed in:**` line;
|
|
13
|
+
// declared-shipped from an archived change whose agreement record's verdict is PASS, through
|
|
14
|
+
// its work orders' `implements` lists; agreed from an open change whose record is agreed.
|
|
15
|
+
// The latest event wins, archived changes in archive order, then open changes.
|
|
16
|
+
//
|
|
17
|
+
// A feature or repository the catalogue does not hold is a finding and produces no row, so no foreign key dangles.
|
|
18
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
const STORE = "openspec";
|
|
22
|
+
const AGREEMENTS = join("docs", "agreements");
|
|
23
|
+
async function isDir(p) {
|
|
24
|
+
try {
|
|
25
|
+
return (await stat(p)).isDirectory();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function dirs(p) {
|
|
32
|
+
try {
|
|
33
|
+
return (await readdir(p, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function readText(p) {
|
|
40
|
+
try {
|
|
41
|
+
return await readFile(p, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Requirement names and scenario counts under each `## <OP> Requirements` section (or the whole file for a living spec). */
|
|
48
|
+
export function parseRequirements(text) {
|
|
49
|
+
const out = [];
|
|
50
|
+
let op = null;
|
|
51
|
+
let current = null;
|
|
52
|
+
for (const line of text.split("\n")) {
|
|
53
|
+
const section = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\b/.exec(line);
|
|
54
|
+
if (section) {
|
|
55
|
+
op = section[1];
|
|
56
|
+
current = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (/^##\s/.test(line) && !/^###/.test(line)) {
|
|
60
|
+
current = null;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// RENAMED: "- TO: `### Requirement: New name`"
|
|
64
|
+
const renamedTo = /^\s*-\s*TO:\s*`?###\s+Requirement:\s*(.+?)`?\s*$/.exec(line);
|
|
65
|
+
if (op === "RENAMED" && renamedTo) {
|
|
66
|
+
current = { name: renamedTo[1].trim(), operation: op, scenarios: 0 };
|
|
67
|
+
out.push(current);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const req = /^###\s+Requirement:\s*(.+?)\s*$/.exec(line);
|
|
71
|
+
if (req) {
|
|
72
|
+
current = { name: req[1], operation: op, scenarios: 0 };
|
|
73
|
+
out.push(current);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (current && /^####\s+Scenario:/.test(line))
|
|
77
|
+
current.scenarios++;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
async function deltaOf(changeDir) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const feature of await dirs(join(changeDir, "specs"))) {
|
|
84
|
+
const file = join(changeDir, "specs", feature, "spec.md");
|
|
85
|
+
const text = await readText(file);
|
|
86
|
+
if (text === null)
|
|
87
|
+
continue;
|
|
88
|
+
for (const r of parseRequirements(text)) {
|
|
89
|
+
if (r.operation)
|
|
90
|
+
out.push({ feature, name: r.name, operation: r.operation, file, scenarios: r.scenarios });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
async function agreementRecord(sddPath, change) {
|
|
96
|
+
const text = await readText(join(sddPath, AGREEMENTS, `${change}.yaml`));
|
|
97
|
+
if (text === null)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const doc = parseYaml(text);
|
|
101
|
+
return doc?.agreement_record ?? null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The repositories a baseline was observed in: its evidence file's `**Observed in:** repo[@sha], repo[@sha]` line. */
|
|
108
|
+
export function observedIn(evidence) {
|
|
109
|
+
const m = /^\s*\*\*Observed in:\*\*\s*(.+)$/m.exec(evidence);
|
|
110
|
+
if (!m)
|
|
111
|
+
return [];
|
|
112
|
+
return m[1]
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((s) => s.trim().split(/\s*@\s*/)[0].replace(/`/g, "").trim())
|
|
115
|
+
.filter((s) => s !== "");
|
|
116
|
+
}
|
|
117
|
+
const rel = (sddPath, p) => p.slice(sddPath.length + 1).split("\\").join("/");
|
|
118
|
+
export async function buildRequirements(inputs) {
|
|
119
|
+
const { sddPath, findings } = inputs;
|
|
120
|
+
const empty = { requirements: [], lineage: [], status: [] };
|
|
121
|
+
const store = join(sddPath, STORE);
|
|
122
|
+
if (!(await isDir(store)))
|
|
123
|
+
return empty;
|
|
124
|
+
const requirements = new Map();
|
|
125
|
+
const unknownFeatures = new Set();
|
|
126
|
+
const known = (feature) => {
|
|
127
|
+
if (inputs.knownFeatureIds.has(feature))
|
|
128
|
+
return true;
|
|
129
|
+
unknownFeatures.add(feature);
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
// 1. Living specs.
|
|
133
|
+
for (const feature of await dirs(join(store, "specs"))) {
|
|
134
|
+
const file = join(store, "specs", feature, "spec.md");
|
|
135
|
+
const text = await readText(file);
|
|
136
|
+
if (text === null || !known(feature))
|
|
137
|
+
continue;
|
|
138
|
+
for (const r of parseRequirements(text)) {
|
|
139
|
+
const id = `${feature}::${r.name}`;
|
|
140
|
+
if (requirements.has(id)) {
|
|
141
|
+
findings.add("convention", `${rel(sddPath, file)}: requirement "${r.name}" is named twice; names must be unique within a spec (M-ADR 0221 §4.7)`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
requirements.set(id, { id, feature_id: feature, name: r.name, stage: "living", spec_path: rel(sddPath, file), scenario_count: r.scenarios, source_sdd_id: "", platform_sdd_id: "" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const events = [];
|
|
148
|
+
const lineage = [];
|
|
149
|
+
const unknownRepos = new Set();
|
|
150
|
+
const pushEvent = (e) => {
|
|
151
|
+
if (!inputs.knownRepoIds.has(e.repo)) {
|
|
152
|
+
unknownRepos.add(e.repo);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (requirements.has(e.requirementId))
|
|
156
|
+
events.push(e);
|
|
157
|
+
};
|
|
158
|
+
// 2. Archived changes, in archive order.
|
|
159
|
+
for (const dir of await dirs(join(store, "changes", "archive"))) {
|
|
160
|
+
const changeDir = join(store, "changes", "archive", dir);
|
|
161
|
+
const change = dir.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
162
|
+
const archivedOn = /^(\d{4}-\d{2}-\d{2})-/.exec(dir)?.[1] ?? "";
|
|
163
|
+
const evidence = await readText(join(changeDir, "baseline-evidence.md"));
|
|
164
|
+
const kind = evidence !== null ? "baseline" : "change";
|
|
165
|
+
const delta = await deltaOf(changeDir);
|
|
166
|
+
for (const d of delta) {
|
|
167
|
+
if (d.operation === "REMOVED")
|
|
168
|
+
continue;
|
|
169
|
+
const requirementId = `${d.feature}::${d.name}`;
|
|
170
|
+
if (!requirements.has(requirementId))
|
|
171
|
+
continue;
|
|
172
|
+
lineage.push({ id: `${requirementId}__${dir}`, requirement_id: requirementId, change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" });
|
|
173
|
+
}
|
|
174
|
+
if (evidence !== null) {
|
|
175
|
+
const repos = observedIn(evidence);
|
|
176
|
+
if (repos.length === 0) {
|
|
177
|
+
findings.add("convention", `${rel(sddPath, join(changeDir, "baseline-evidence.md"))}: no "**Observed in:**" line, so its requirements carry no observed status`);
|
|
178
|
+
}
|
|
179
|
+
for (const repo of repos)
|
|
180
|
+
for (const d of delta)
|
|
181
|
+
if (d.operation !== "REMOVED")
|
|
182
|
+
pushEvent({ requirementId: `${d.feature}::${d.name}`, repo, status: "observed", basis: dir });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const record = await agreementRecord(sddPath, change);
|
|
186
|
+
if (record?.verdict?.result !== "PASS")
|
|
187
|
+
continue; // the behaviour-store validator reports an archived change without a PASS verdict
|
|
188
|
+
for (const wo of record.work_orders ?? []) {
|
|
189
|
+
if (!wo.repo)
|
|
190
|
+
continue;
|
|
191
|
+
for (const name of wo.implements ?? []) {
|
|
192
|
+
const match = delta.filter((d) => d.name === name && d.operation !== "REMOVED");
|
|
193
|
+
if (match.length === 0) {
|
|
194
|
+
findings.add("convention", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", which the change's delta does not name`);
|
|
195
|
+
}
|
|
196
|
+
for (const d of match)
|
|
197
|
+
pushEvent({ requirementId: `${d.feature}::${d.name}`, repo: wo.repo, status: "declared-shipped", basis: dir });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 3. Open changes whose agreement record is agreed.
|
|
202
|
+
for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
|
|
203
|
+
const record = await agreementRecord(sddPath, dir);
|
|
204
|
+
if (record?.status !== "agreed")
|
|
205
|
+
continue;
|
|
206
|
+
const delta = await deltaOf(join(store, "changes", dir));
|
|
207
|
+
for (const d of delta) {
|
|
208
|
+
const id = `${d.feature}::${d.name}`;
|
|
209
|
+
if (d.operation === "ADDED" && !requirements.has(id) && known(d.feature)) {
|
|
210
|
+
requirements.set(id, { id, feature_id: d.feature, name: d.name, stage: "agreed", spec_path: rel(sddPath, d.file), scenario_count: d.scenarios, source_sdd_id: "", platform_sdd_id: "" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const wo of record.work_orders ?? []) {
|
|
214
|
+
if (!wo.repo)
|
|
215
|
+
continue;
|
|
216
|
+
for (const name of wo.implements ?? []) {
|
|
217
|
+
for (const d of delta.filter((x) => x.name === name && x.operation !== "REMOVED")) {
|
|
218
|
+
pushEvent({ requirementId: `${d.feature}::${d.name}`, repo: wo.repo, status: "agreed", basis: dir });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const f of unknownFeatures)
|
|
224
|
+
findings.add("convention", `${STORE}/: a spec keyed "${f}" names no catalogue feature, so its requirements are not catalogued (M-ADR 0221 §4.7)`);
|
|
225
|
+
for (const r of unknownRepos)
|
|
226
|
+
findings.add("convention", `${STORE}/: a status names repository "${r}", which the catalogue does not hold; no status row`);
|
|
227
|
+
// Latest event per (requirement, repository) wins.
|
|
228
|
+
const status = new Map();
|
|
229
|
+
for (const e of events) {
|
|
230
|
+
const id = `${e.requirementId}__${e.repo}`;
|
|
231
|
+
status.set(id, { id, requirement_id: e.requirementId, repo_id: e.repo, status: e.status, basis: e.basis, source_sdd_id: "", platform_sdd_id: "" });
|
|
232
|
+
}
|
|
233
|
+
const byId = (a, b) => String(a["id"]).localeCompare(String(b["id"]));
|
|
234
|
+
return {
|
|
235
|
+
requirements: [...requirements.values()].sort(byId),
|
|
236
|
+
lineage,
|
|
237
|
+
status: [...status.values()].sort(byId),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=requirements.js.map
|
|
@@ -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: "4.0.0"
|
|
11
11
|
schema_owner: "sdd-method"
|
|
12
12
|
schema_status: "generated"
|
|
13
13
|
enums:
|
|
@@ -389,7 +389,7 @@ enums:
|
|
|
389
389
|
description: M-ADR 0159 entity persistence kind. Bound at 3.13.0 from the column descriptions; measured against both reference estates before binding (M-ADR 0205 §4.4).
|
|
390
390
|
required_stability:
|
|
391
391
|
values:
|
|
392
|
-
- value:
|
|
392
|
+
- value: stable
|
|
393
393
|
- value: provisional
|
|
394
394
|
- value: unknown
|
|
395
395
|
description: "M-ADR 0204: the stability a consumer requires of a target; external | legacy live in target_locus, not here. Bound at 3.13.0 from the column descriptions; measured against both reference estates before binding (M-ADR 0205 §4.4)."
|
|
@@ -613,6 +613,35 @@ enums:
|
|
|
613
613
|
- value: generated-from
|
|
614
614
|
description: Its content was produced from the other repository's templates.
|
|
615
615
|
description: How one repository depends on another's code (M-ADR 0217 §4.1).
|
|
616
|
+
requirement_stage:
|
|
617
|
+
values:
|
|
618
|
+
- value: living
|
|
619
|
+
description: "In the living spec: shipped behaviour."
|
|
620
|
+
- value: agreed
|
|
621
|
+
description: Added by an open change whose agreement record is agreed; not yet shipped.
|
|
622
|
+
description: Where a catalogued requirement stands (M-ADR 0221 §4.4).
|
|
623
|
+
requirement_change_kind:
|
|
624
|
+
values:
|
|
625
|
+
- value: baseline
|
|
626
|
+
description: Recorded existing behaviour through the baseline gate.
|
|
627
|
+
- value: change
|
|
628
|
+
description: A delta agreed, built and judged through the change gate.
|
|
629
|
+
description: What kind of archived change a lineage row names (M-ADR 0222 §4.3).
|
|
630
|
+
requirement_operation:
|
|
631
|
+
values:
|
|
632
|
+
- value: ADDED
|
|
633
|
+
- value: MODIFIED
|
|
634
|
+
- value: RENAMED
|
|
635
|
+
description: How a change's delta treated a requirement that is current now (a removed requirement has no row).
|
|
636
|
+
requirement_status_value:
|
|
637
|
+
values:
|
|
638
|
+
- value: agreed
|
|
639
|
+
description: In an agreed, open change.
|
|
640
|
+
- value: declared-shipped
|
|
641
|
+
description: Its change archived on a passing judge, with this repository's task group complete.
|
|
642
|
+
- value: observed
|
|
643
|
+
description: Independent evidence from this repository's code matches (a baseline's characterization tests or a reverse snapshot).
|
|
644
|
+
description: A requirement's status in one implementing repository (M-ADR 0221 §4.5).
|
|
616
645
|
catalogues:
|
|
617
646
|
domains:
|
|
618
647
|
primary_key: id
|
|
@@ -1276,7 +1305,7 @@ catalogues:
|
|
|
1276
1305
|
- name: contract_provenance
|
|
1277
1306
|
type: string
|
|
1278
1307
|
nullable: true
|
|
1279
|
-
description: "How this capability's declared contract SET was produced, from `traceability.contracts.provenance`. hand-authored | reverse-extracted | generated — the vocabulary the method already ships for `authoring-mode`, reused deliberately so provenance has one vocabulary and not two. Empty means undeclared, and undeclared reads as `hand-authored` by convention. Added at 3.5.0. ⚠ STRICTLY ORTHOGONAL to the contract-set maturity ladder (`traceability.contracts.status`: planned | provisional | partial | complete |
|
|
1308
|
+
description: "How this capability's declared contract SET was produced, from `traceability.contracts.provenance`. hand-authored | reverse-extracted | generated — the vocabulary the method already ships for `authoring-mode`, reused deliberately so provenance has one vocabulary and not two. Empty means undeclared, and undeclared reads as `hand-authored` by convention. Added at 3.5.0. ⚠ STRICTLY ORTHOGONAL to the contract-set maturity ladder (`traceability.contracts.status`: planned | provisional | partial | complete | stable | none): provenance never constrains status and status never constrains provenance, and `reverse-extracted` + `complete` is the legitimate pair that motivated the field. Evidence: 2 reverse-estate manifests had written `reverse-extracted` INTO contracts.status, where it is invalid, because a complete reverse-derived contract surface had no way to say both true things at once and the author picked the provenance one. A rule here that constrained maturity would re-create exactly that defect. ⚠ Note the asymmetry this column exposes and does not fix: the maturity ladder it is orthogonal to has NO catalogue column at all — `traceability.contracts.status` is validator-enforced but uncatalogued, so it is queryable by no consumer. That is the authored-but-uncatalogued class (a block with no column has no automated reader, so nothing can notice when it is absent or wrong); this column is given one precisely so the provenance half does not join it. Giving the ladder a column is a candidate follow-on, not done here."
|
|
1280
1309
|
- name: source
|
|
1281
1310
|
type: string
|
|
1282
1311
|
nullable: false
|
|
@@ -1523,7 +1552,7 @@ catalogues:
|
|
|
1523
1552
|
- name: kind
|
|
1524
1553
|
type: string
|
|
1525
1554
|
nullable: false
|
|
1526
|
-
description: rest | graphql | rpc | event | authz. rpc covers Proto/gRPC; event covers Avro schemas; authz covers authorization/policy schemas (SpiceDB .zed and equivalents), added because an adopter contract
|
|
1555
|
+
description: rest | graphql | rpc | event | authz. rpc covers Proto/gRPC; event covers Avro schemas; authz covers authorization/policy schemas (SpiceDB .zed and equivalents), added because an adopter contract agreement gate certified .zed schemas as contracts while no kind could represent them.
|
|
1527
1556
|
- name: version
|
|
1528
1557
|
type: string
|
|
1529
1558
|
nullable: true
|
|
@@ -1888,7 +1917,7 @@ catalogues:
|
|
|
1888
1917
|
- name: version
|
|
1889
1918
|
type: string
|
|
1890
1919
|
nullable: true
|
|
1891
|
-
description: Optional explicit version; null until the domain model is formally versioned (typically
|
|
1920
|
+
description: Optional explicit version; null until the domain model is formally versioned (typically when a contract is first pinned).
|
|
1892
1921
|
- name: evidence_class
|
|
1893
1922
|
type: string
|
|
1894
1923
|
nullable: true
|
|
@@ -2142,7 +2171,7 @@ catalogues:
|
|
|
2142
2171
|
- name: required_stability
|
|
2143
2172
|
type: string
|
|
2144
2173
|
nullable: true
|
|
2145
|
-
description: "provisional |
|
|
2174
|
+
description: "provisional | stable | unknown. The consuming SDD's expectation of contract stability, carried verbatim from app-platform-dependencies.yaml. Not a bound enum: an out-of-vocabulary value is carried, not rejected, and is reported by validate-app-dependencies.sh."
|
|
2146
2175
|
description: "Edge: product depends on / consumes service. Forward sources from app-platform-dependencies.yaml. service_id is external because the consuming product (an application-product SDD) references a service owned by the platform SDD."
|
|
2147
2176
|
foreign_keys:
|
|
2148
2177
|
- column: product_id
|
|
@@ -2930,7 +2959,7 @@ catalogues:
|
|
|
2930
2959
|
- name: required_stability
|
|
2931
2960
|
type: string
|
|
2932
2961
|
nullable: true
|
|
2933
|
-
description: "provisional |
|
|
2962
|
+
description: "provisional | stable | unknown. Optional; the consuming SDD's expectation of contract stability — and only that: where the target lives and what lifecycle its implementation is in are separate concerns that do not belong in this field. Not a bound enum: an out-of-vocabulary value is carried, not rejected, and is reported by validate-app-dependencies.sh."
|
|
2934
2963
|
- name: contract_path
|
|
2935
2964
|
type: string
|
|
2936
2965
|
nullable: true
|
|
@@ -4115,3 +4144,127 @@ catalogues:
|
|
|
4115
4144
|
external: true
|
|
4116
4145
|
column_enums:
|
|
4117
4146
|
kind: enums.repo_dependency_kind
|
|
4147
|
+
requirements:
|
|
4148
|
+
primary_key: id
|
|
4149
|
+
columns:
|
|
4150
|
+
- name: id
|
|
4151
|
+
type: string
|
|
4152
|
+
nullable: false
|
|
4153
|
+
description: <feature_id>::<requirement name>. The name is unique within its spec and stable across changes (M-ADR 0221 §4.7).
|
|
4154
|
+
- name: feature_id
|
|
4155
|
+
type: string
|
|
4156
|
+
nullable: false
|
|
4157
|
+
description: "The catalogue feature whose living spec holds the requirement: the spec's directory name under openspec/specs/."
|
|
4158
|
+
- name: name
|
|
4159
|
+
type: string
|
|
4160
|
+
nullable: false
|
|
4161
|
+
description: The requirement's name as the spec heads it; automated tests carry it.
|
|
4162
|
+
- name: stage
|
|
4163
|
+
type: string
|
|
4164
|
+
nullable: false
|
|
4165
|
+
description: "Bound to requirement_stage: living (in the living spec) or agreed (added by an open change whose agreement record is agreed)."
|
|
4166
|
+
- name: spec_path
|
|
4167
|
+
type: string
|
|
4168
|
+
nullable: false
|
|
4169
|
+
description: "The file that holds the requirement: the living spec, or the open change's delta."
|
|
4170
|
+
- name: scenario_count
|
|
4171
|
+
type: integer
|
|
4172
|
+
nullable: false
|
|
4173
|
+
description: Scenarios under the requirement in that file.
|
|
4174
|
+
- name: source_sdd_id
|
|
4175
|
+
type: string
|
|
4176
|
+
nullable: true
|
|
4177
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4178
|
+
- name: platform_sdd_id
|
|
4179
|
+
type: string
|
|
4180
|
+
nullable: true
|
|
4181
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4182
|
+
description: "A requirement in the SDD's behaviour store (M-ADR 0221): one row per requirement of a living spec, keyed by the catalogue feature the spec describes, and per requirement an open agreed change adds. Projected from openspec/ at the SDD's commit; never authored here."
|
|
4183
|
+
foreign_keys:
|
|
4184
|
+
- column: feature_id
|
|
4185
|
+
references: features.id
|
|
4186
|
+
nullable: false
|
|
4187
|
+
column_enums:
|
|
4188
|
+
stage: enums.requirement_stage
|
|
4189
|
+
requirement_lineage:
|
|
4190
|
+
primary_key: id
|
|
4191
|
+
columns:
|
|
4192
|
+
- name: id
|
|
4193
|
+
type: string
|
|
4194
|
+
nullable: false
|
|
4195
|
+
description: <requirement_id>__<change_id>.
|
|
4196
|
+
- name: requirement_id
|
|
4197
|
+
type: string
|
|
4198
|
+
nullable: false
|
|
4199
|
+
- name: change_id
|
|
4200
|
+
type: string
|
|
4201
|
+
nullable: false
|
|
4202
|
+
description: The archived change's directory name (its archive date prefix included).
|
|
4203
|
+
- name: change_kind
|
|
4204
|
+
type: string
|
|
4205
|
+
nullable: false
|
|
4206
|
+
description: "Bound to requirement_change_kind: baseline or change."
|
|
4207
|
+
- name: operation
|
|
4208
|
+
type: string
|
|
4209
|
+
nullable: false
|
|
4210
|
+
description: "Bound to requirement_operation: how the change's delta treated the requirement."
|
|
4211
|
+
- name: archived_on
|
|
4212
|
+
type: string
|
|
4213
|
+
nullable: false
|
|
4214
|
+
description: The archive date (YYYY-MM-DD) from the change's directory name.
|
|
4215
|
+
- name: source_sdd_id
|
|
4216
|
+
type: string
|
|
4217
|
+
nullable: true
|
|
4218
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4219
|
+
- name: platform_sdd_id
|
|
4220
|
+
type: string
|
|
4221
|
+
nullable: true
|
|
4222
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4223
|
+
description: "Annotation: an archived change that made a current requirement what it is (M-ADR 0221 §4.6), in archive order. A baseline change (one carrying baseline-evidence.md) records existing behaviour; any other change carries a verdict."
|
|
4224
|
+
foreign_keys:
|
|
4225
|
+
- column: requirement_id
|
|
4226
|
+
references: requirements.id
|
|
4227
|
+
nullable: false
|
|
4228
|
+
column_enums:
|
|
4229
|
+
change_kind: enums.requirement_change_kind
|
|
4230
|
+
operation: enums.requirement_operation
|
|
4231
|
+
requirement_status:
|
|
4232
|
+
primary_key: id
|
|
4233
|
+
columns:
|
|
4234
|
+
- name: id
|
|
4235
|
+
type: string
|
|
4236
|
+
nullable: false
|
|
4237
|
+
description: <requirement_id>__<repo_id>.
|
|
4238
|
+
- name: requirement_id
|
|
4239
|
+
type: string
|
|
4240
|
+
nullable: false
|
|
4241
|
+
- name: repo_id
|
|
4242
|
+
type: string
|
|
4243
|
+
nullable: false
|
|
4244
|
+
description: "The implementing repository: a work order's repository, or a repository a baseline was observed in."
|
|
4245
|
+
- name: status
|
|
4246
|
+
type: string
|
|
4247
|
+
nullable: false
|
|
4248
|
+
description: Bound to requirement_status_value.
|
|
4249
|
+
- name: basis
|
|
4250
|
+
type: string
|
|
4251
|
+
nullable: false
|
|
4252
|
+
description: "The change that produced the status: the open change's or the archived change's directory name."
|
|
4253
|
+
- name: source_sdd_id
|
|
4254
|
+
type: string
|
|
4255
|
+
nullable: true
|
|
4256
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4257
|
+
- name: platform_sdd_id
|
|
4258
|
+
type: string
|
|
4259
|
+
nullable: true
|
|
4260
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4261
|
+
description: "Edge: a requirement's status in a repository that implements it (M-ADR 0221 §4.5; M-ADR 0223 §4.7). Derived, never authored: agreed from an open agreed change's work orders, declared-shipped from an archived change's PASS verdict and its work orders, observed from a baseline's evidence. The latest event in archive order wins."
|
|
4262
|
+
foreign_keys:
|
|
4263
|
+
- column: requirement_id
|
|
4264
|
+
references: requirements.id
|
|
4265
|
+
nullable: false
|
|
4266
|
+
- column: repo_id
|
|
4267
|
+
references: repositories.id
|
|
4268
|
+
nullable: false
|
|
4269
|
+
column_enums:
|
|
4270
|
+
status: enums.requirement_status_value
|
|
@@ -869,3 +869,39 @@ coverage:
|
|
|
869
869
|
kind: enum
|
|
870
870
|
scoring: not-applicable
|
|
871
871
|
reason: closed vocabulary, not an artefact
|
|
872
|
+
requirements:
|
|
873
|
+
kind: entity
|
|
874
|
+
scoring: not-applicable
|
|
875
|
+
reason: projected from the behaviour store, whose baseline and change gates hold its quality (M-ADR 0222)
|
|
876
|
+
relationships:
|
|
877
|
+
feature_id:
|
|
878
|
+
class: membership
|
|
879
|
+
impact_flow: toward-target
|
|
880
|
+
requirement_lineage:
|
|
881
|
+
kind: annotation
|
|
882
|
+
scoring: derived-edge
|
|
883
|
+
class: lineage
|
|
884
|
+
impact_flow: none
|
|
885
|
+
reason: derived from the archived changes scored by their own judge
|
|
886
|
+
requirement_status:
|
|
887
|
+
kind: edge
|
|
888
|
+
scoring: derived-edge
|
|
889
|
+
class: realisation
|
|
890
|
+
impact_flow: both
|
|
891
|
+
reason: derived from agreement records, verdicts and baseline evidence
|
|
892
|
+
requirement_stage:
|
|
893
|
+
kind: enum
|
|
894
|
+
scoring: not-applicable
|
|
895
|
+
reason: closed vocabulary, not an artefact
|
|
896
|
+
requirement_change_kind:
|
|
897
|
+
kind: enum
|
|
898
|
+
scoring: not-applicable
|
|
899
|
+
reason: closed vocabulary, not an artefact
|
|
900
|
+
requirement_operation:
|
|
901
|
+
kind: enum
|
|
902
|
+
scoring: not-applicable
|
|
903
|
+
reason: closed vocabulary, not an artefact
|
|
904
|
+
requirement_status_value:
|
|
905
|
+
kind: enum
|
|
906
|
+
scoring: not-applicable
|
|
907
|
+
reason: closed vocabulary, not an artefact
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { parse as parseYaml } from "yaml";
|
|
11
|
+
import { canonicalValue } from "../vocabulary/renames.js";
|
|
11
12
|
import { walkFiles } from "../sdd-reader.js";
|
|
12
13
|
export const REPO_CONFIG_DIR = "orchestration/repo-config";
|
|
13
14
|
const CONTRACT_KINDS = [
|
|
@@ -229,8 +230,12 @@ function parseContractEntry(entry, kind, sourceRel, warnings) {
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
function parseStability(v) {
|
|
232
|
-
if (v ===
|
|
233
|
-
return
|
|
233
|
+
if (v === undefined)
|
|
234
|
+
return undefined;
|
|
235
|
+
// The fixed-contract class was `locked` (M-ADR 0222 §4.1); read as `stable` for one major release.
|
|
236
|
+
const c = canonicalValue("stability-stable", v);
|
|
237
|
+
if (c === "provisional" || c === "stable")
|
|
238
|
+
return c;
|
|
234
239
|
return undefined;
|
|
235
240
|
}
|
|
236
241
|
function stringField(record, key) {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate vocabulary` (M-ADR 0222 §4.1; docs/method/foundations/vocabulary-renames.yaml): rewrite an SDD's
|
|
3
|
+
* OWN files from the former tool-read values to the new ones. Authored content, so the verb is the author's
|
|
4
|
+
* and the full CLI's, never maintain's (M-ADR 0218), and it is deterministic: a second run changes nothing.
|
|
5
|
+
*
|
|
6
|
+
* Scope: tracked files only, minus every lock-managed path (method and pack payload, which sync and pack
|
|
7
|
+
* cycles own), `.sdd/` and `catalogues/` (rebuilt by catalogue build). A value is rewritten only in a known
|
|
8
|
+
* position, never as a free-text word:
|
|
9
|
+
* - a spec `**Status:**` header (bold or the `> … Status:` prose form) detailed-pre-lock -> detailed
|
|
10
|
+
* - the spec-lifecycle ratchet baseline's `<status> :: <path>` keys detailed-pre-lock -> detailed
|
|
11
|
+
* - a YAML value of `required_stability`, `stability`, `template-stability`,
|
|
12
|
+
* `traceability.contracts.status`, and the same keys in Markdown examples locked -> stable
|
|
13
|
+
* - an agreement tag anywhere (`release-vMAJOR.MINOR-locked`) -> release-vMAJOR.MINOR-agreed
|
|
14
|
+
* - the key `lock_in_tag` -> agreement_tag
|
|
15
|
+
* - gate-registry `next_gate: lock-in` and the key `remediation_before_lock_in` -> agreement, remediation_before_agreement
|
|
16
|
+
* Reported, never rewritten: an adopter-named `x-<adopter>-stability` extension (the adopter's to rename, M-ADR 0222 §4.1),
|
|
17
|
+
* and `detailed-pre-lock` in a manifest's `capability.status` (a spec-authoring stage on the wrong axis, which
|
|
18
|
+
* the traceability check already reports; it is not translated, because the true lifecycle value is not a
|
|
19
|
+
* translation of the word).
|
|
20
|
+
*/
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { promisify } from "node:util";
|
|
25
|
+
import { isPair, isScalar, parseDocument, visit } from "yaml";
|
|
26
|
+
import { rename } from "./renames.js";
|
|
27
|
+
const execFileAsync = promisify(execFile);
|
|
28
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
29
|
+
const STABILITY_KEYS = new Set(["required_stability", "stability", "template-stability"]);
|
|
30
|
+
// MAJOR.MINOR as the method writes it, and any dotted version an adopter wrote (release-v1.0.1-locked).
|
|
31
|
+
const TAG_RE = /\brelease-v(?:\d+|X|MAJOR)(?:\.(?:\d+|X|MINOR|PATCH))+-locked\b/g;
|
|
32
|
+
function lineOf(text, offset) {
|
|
33
|
+
let n = 1;
|
|
34
|
+
for (let i = 0; i < offset && i < text.length; i++)
|
|
35
|
+
if (text.charCodeAt(i) === 10)
|
|
36
|
+
n++;
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
async function trackedFiles(sddPath) {
|
|
40
|
+
const { stdout } = await execFileAsync("git", ["-C", sddPath, "ls-files", "-z"], { maxBuffer: 64 * 1024 * 1024 });
|
|
41
|
+
return stdout.split("\0").filter((p) => p !== "");
|
|
42
|
+
}
|
|
43
|
+
async function managedPaths(sddPath) {
|
|
44
|
+
const out = new Set();
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = await readFile(join(sddPath, "method-baseline.lock"), "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
for (const line of text.split("\n")) {
|
|
53
|
+
const m = /^\s+-\s+[0-9a-f]{64}\s+(.+)$/.exec(line);
|
|
54
|
+
if (m)
|
|
55
|
+
out.add(m[1].trim());
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** Edits for text forms that are values wherever they appear: the agreement tag and the lock_in_tag key. */
|
|
60
|
+
function tokenEdits(text) {
|
|
61
|
+
const edits = [];
|
|
62
|
+
const tag = rename("agreement-tag");
|
|
63
|
+
for (const m of text.matchAll(TAG_RE)) {
|
|
64
|
+
const to = m[0].replace(/-locked$/, "-agreed");
|
|
65
|
+
edits.push({ start: m.index, end: m.index + m[0].length, text: to, rewrite: { from: m[0], to, rule: tag.id } });
|
|
66
|
+
}
|
|
67
|
+
const key = rename("agreement-tag-key");
|
|
68
|
+
for (const m of text.matchAll(new RegExp(`\\b${key.old}\\b`, "g"))) {
|
|
69
|
+
edits.push({ start: m.index, end: m.index + key.old.length, text: key.new, rewrite: { from: key.old, to: key.new, rule: key.id } });
|
|
70
|
+
}
|
|
71
|
+
return edits;
|
|
72
|
+
}
|
|
73
|
+
function markdownEdits(text) {
|
|
74
|
+
const edits = tokenEdits(text);
|
|
75
|
+
const status = rename("authoring-status-detailed");
|
|
76
|
+
const stab = rename("stability-stable");
|
|
77
|
+
const lines = text.split("\n");
|
|
78
|
+
let offset = 0;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const header = /^(\s*(?:[-*]\s+)?\*\*Status:\*\*\s*)(\S+)/.exec(line) ?? /^(>.*\bStatus:\s*)([^\s|]+)/.exec(line);
|
|
81
|
+
if (header && header[2] === status.old) {
|
|
82
|
+
const start = offset + header[1].length;
|
|
83
|
+
edits.push({ start, end: start + status.old.length, text: status.new, rewrite: { from: status.old, to: status.new, rule: status.id } });
|
|
84
|
+
}
|
|
85
|
+
// Stability values in Markdown examples (YAML blocks or prose quoting a key: value pair).
|
|
86
|
+
// Not after a hyphen or word character: `x-<adopter>-stability: locked` is the adopter's (M-ADR 0222 §4.1).
|
|
87
|
+
const stabRe = /(?<![\w-])(required_stability|stability|template-stability)(\s*:\s*["']?)locked\b/g;
|
|
88
|
+
for (const m of line.matchAll(stabRe)) {
|
|
89
|
+
const start = offset + m.index + m[1].length + m[2].length;
|
|
90
|
+
edits.push({ start, end: start + stab.old.length, text: stab.new, rewrite: { from: stab.old, to: stab.new, rule: stab.id } });
|
|
91
|
+
}
|
|
92
|
+
offset += line.length + 1;
|
|
93
|
+
}
|
|
94
|
+
return edits;
|
|
95
|
+
}
|
|
96
|
+
function baselineEdits(text) {
|
|
97
|
+
const status = rename("authoring-status-detailed");
|
|
98
|
+
const edits = [];
|
|
99
|
+
let offset = 0;
|
|
100
|
+
for (const line of text.split("\n")) {
|
|
101
|
+
if (line.startsWith(`${status.old} :: `)) {
|
|
102
|
+
edits.push({ start: offset, end: offset + status.old.length, text: status.new, rewrite: { from: status.old, to: status.new, rule: status.id } });
|
|
103
|
+
}
|
|
104
|
+
offset += line.length + 1;
|
|
105
|
+
}
|
|
106
|
+
return edits;
|
|
107
|
+
}
|
|
108
|
+
function pathKeys(path) {
|
|
109
|
+
const keys = [];
|
|
110
|
+
for (const n of path)
|
|
111
|
+
if (isPair(n) && isScalar(n.key))
|
|
112
|
+
keys.push(String(n.key.value));
|
|
113
|
+
return keys;
|
|
114
|
+
}
|
|
115
|
+
/** The scalar's own source text, so a quoted value keeps its quotes. */
|
|
116
|
+
function scalarEdit(text, node, from, to, rule) {
|
|
117
|
+
const r = node.range;
|
|
118
|
+
if (!r)
|
|
119
|
+
return undefined;
|
|
120
|
+
const src = text.slice(r[0], r[1]);
|
|
121
|
+
const at = src.indexOf(from);
|
|
122
|
+
if (at < 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
return { start: r[0] + at, end: r[0] + at + from.length, text: to, rewrite: { from, to, rule } };
|
|
125
|
+
}
|
|
126
|
+
function yamlEdits(text, file, reported) {
|
|
127
|
+
const edits = tokenEdits(text);
|
|
128
|
+
let doc;
|
|
129
|
+
try {
|
|
130
|
+
doc = parseDocument(text);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return edits;
|
|
134
|
+
}
|
|
135
|
+
if (doc.errors.length > 0)
|
|
136
|
+
return edits;
|
|
137
|
+
const stab = rename("stability-stable");
|
|
138
|
+
const gate = rename("gate-registry-next-gate");
|
|
139
|
+
const remed = rename("gate-registry-remediation-key");
|
|
140
|
+
const status = rename("authoring-status-detailed");
|
|
141
|
+
visit(doc, {
|
|
142
|
+
Pair(_, pair, path) {
|
|
143
|
+
if (!isScalar(pair.key))
|
|
144
|
+
return;
|
|
145
|
+
const key = String(pair.key.value);
|
|
146
|
+
const val = isScalar(pair.value) ? pair.value : undefined;
|
|
147
|
+
const v = val ? String(val.value) : undefined;
|
|
148
|
+
const ancestors = pathKeys(path);
|
|
149
|
+
if (/^x-.+-stability$/.test(key)) {
|
|
150
|
+
reported.push({ file, line: lineOf(text, pair.key.range?.[0] ?? 0), text: `${key}: ${v ?? "…"}`, reason: "an adopter-named extension: the adopter's to rename; the method's value is `stable`" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (val && v === stab.old && (STABILITY_KEYS.has(key) || (key === "status" && ancestors.slice(-2).join(".") === "traceability.contracts"))) {
|
|
154
|
+
const e = scalarEdit(text, val, stab.old, stab.new, stab.id);
|
|
155
|
+
if (e)
|
|
156
|
+
edits.push(e);
|
|
157
|
+
}
|
|
158
|
+
if (val && key === "next_gate" && v === gate.old) {
|
|
159
|
+
const e = scalarEdit(text, val, gate.old, gate.new, gate.id);
|
|
160
|
+
if (e)
|
|
161
|
+
edits.push(e);
|
|
162
|
+
}
|
|
163
|
+
if (key === remed.old) {
|
|
164
|
+
const e = scalarEdit(text, pair.key, remed.old, remed.new, remed.id);
|
|
165
|
+
if (e)
|
|
166
|
+
edits.push(e);
|
|
167
|
+
}
|
|
168
|
+
if (val && key === "status" && v === status.old && ancestors.slice(-1)[0] === "capability") {
|
|
169
|
+
reported.push({ file, line: lineOf(text, val.range?.[0] ?? 0), text: `capability.status: ${v}`, reason: "a spec-authoring stage on the lifecycle axis: re-author to the capability's true lifecycle phase; not translated" });
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return edits;
|
|
175
|
+
}
|
|
176
|
+
function applyEdits(text, edits) {
|
|
177
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start);
|
|
178
|
+
let out = text;
|
|
179
|
+
let lastStart = Number.POSITIVE_INFINITY;
|
|
180
|
+
for (const e of sorted) {
|
|
181
|
+
if (e.end > lastStart)
|
|
182
|
+
continue; // overlapping edits: keep the later one only
|
|
183
|
+
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
184
|
+
lastStart = e.start;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
export async function migrateVocabulary(sddPath, opts = {}) {
|
|
189
|
+
const managed = await managedPaths(sddPath);
|
|
190
|
+
const files = (await trackedFiles(sddPath)).filter((f) => !managed.has(f) && !f.startsWith(".sdd/") && !f.startsWith("catalogues/") && !f.includes("node_modules/"));
|
|
191
|
+
const rewrites = [];
|
|
192
|
+
const reported = [];
|
|
193
|
+
let changed = 0;
|
|
194
|
+
for (const rel of files) {
|
|
195
|
+
const abs = join(sddPath, rel);
|
|
196
|
+
let st;
|
|
197
|
+
try {
|
|
198
|
+
st = await stat(abs);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!st.isFile() || st.size > MAX_BYTES)
|
|
204
|
+
continue;
|
|
205
|
+
const buf = await readFile(abs);
|
|
206
|
+
if (buf.includes(0))
|
|
207
|
+
continue;
|
|
208
|
+
const text = buf.toString("utf8");
|
|
209
|
+
let edits;
|
|
210
|
+
if (/\.ya?ml$/.test(rel))
|
|
211
|
+
edits = yamlEdits(text, rel, reported);
|
|
212
|
+
else if (rel.endsWith("spec-lifecycle-baseline.txt"))
|
|
213
|
+
edits = baselineEdits(text);
|
|
214
|
+
else if (/\.(md|hbs)$/.test(rel))
|
|
215
|
+
edits = markdownEdits(text);
|
|
216
|
+
else
|
|
217
|
+
edits = tokenEdits(text);
|
|
218
|
+
if (edits.length === 0)
|
|
219
|
+
continue;
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
for (const e of edits.sort((a, b) => a.start - b.start)) {
|
|
222
|
+
const k = `${e.start}:${e.end}`;
|
|
223
|
+
if (seen.has(k))
|
|
224
|
+
continue;
|
|
225
|
+
seen.add(k);
|
|
226
|
+
rewrites.push({ file: rel, line: lineOf(text, e.start), ...e.rewrite });
|
|
227
|
+
}
|
|
228
|
+
const next = applyEdits(text, edits);
|
|
229
|
+
if (next !== text) {
|
|
230
|
+
changed++;
|
|
231
|
+
if (!opts.dryRun)
|
|
232
|
+
await writeFile(abs, next);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { rewrites, reported, filesScanned: files.length, filesChanged: changed };
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The method's vocabulary renames (M-ADR 0222 §4.1), loaded from the vendored copy of
|
|
3
|
+
* sdd-method's docs/method/foundations/vocabulary-renames.yaml (one-way, sdd-method ->
|
|
4
|
+
* vendored; scripts/sync-vendored-script.sh, drift-tested in tests/parity).
|
|
5
|
+
*
|
|
6
|
+
* For one major release every reader accepts a former value and reads it as the new one;
|
|
7
|
+
* from `removed_in` the former value is an error. Readers ask this module rather than
|
|
8
|
+
* keeping their own copy, so a rename has one definition here as it has one in the bundles.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { parse } from "yaml";
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
export const VENDORED_RENAMES_PATH = join(__dirname, "vocabulary-renames.yaml");
|
|
16
|
+
let cached;
|
|
17
|
+
export function vocabularyRenames() {
|
|
18
|
+
if (cached === undefined) {
|
|
19
|
+
const doc = parse(readFileSync(VENDORED_RENAMES_PATH, "utf8"));
|
|
20
|
+
cached = { introduced_in: String(doc.introduced_in), removed_in: String(doc.removed_in), renames: doc.renames };
|
|
21
|
+
}
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
24
|
+
/** The rename with this id; throws on an id the file does not hold, so a reader cannot silently lose its alias. */
|
|
25
|
+
export function rename(id) {
|
|
26
|
+
const r = vocabularyRenames().renames.find((x) => x.id === id);
|
|
27
|
+
if (r === undefined)
|
|
28
|
+
throw new Error(`vocabulary-renames.yaml has no rename "${id}"`);
|
|
29
|
+
return r;
|
|
30
|
+
}
|
|
31
|
+
/** The value as a reader should treat it: the new name for the former one, anything else unchanged. */
|
|
32
|
+
export function canonicalValue(id, value) {
|
|
33
|
+
const r = rename(id);
|
|
34
|
+
return value === r.old ? r.new : value;
|
|
35
|
+
}
|
|
36
|
+
/** Whether a value is the former name under this rename (for the reader's finding). */
|
|
37
|
+
export function isFormerValue(id, value) {
|
|
38
|
+
return value === rename(id).old;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=renames.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Vocabulary renames (M-ADR 0222 §4.1)
|
|
2
|
+
#
|
|
3
|
+
# The tool-read values the method renamed when "lock" left its vocabulary, as data:
|
|
4
|
+
# every reader that accepts a former value, and the migrate verb, follows this file.
|
|
5
|
+
# For one major release a reader accepts the former value, treats it as the new one
|
|
6
|
+
# and reports it; from removed_in the former value is an error.
|
|
7
|
+
#
|
|
8
|
+
# Each reader that aliases a former value marks the place with a comment line
|
|
9
|
+
# vocabulary-alias: <old> -> <new>
|
|
10
|
+
# and the correspondence registry holds the readers and this file to each other in
|
|
11
|
+
# both directions (orchestration/correspondence-registry.yaml, vocabulary-renames).
|
|
12
|
+
#
|
|
13
|
+
# Consumed by:
|
|
14
|
+
# - validate-spec-lifecycle.sh, validate-traceability.sh, validate-app-dependencies.sh,
|
|
15
|
+
# dispatch-release.sh (the bundled readers)
|
|
16
|
+
# - sdd-cli's catalogue build and repo-config reader
|
|
17
|
+
# - `sdd-cli migrate vocabulary` (rewrites an SDD's own files)
|
|
18
|
+
#
|
|
19
|
+
# Distribution: ships in every method baseline via the seed-file lists.
|
|
20
|
+
|
|
21
|
+
schema_version: "1.0.0"
|
|
22
|
+
introduced_in: "2.0.0" # the sdd-cli major that reads both names
|
|
23
|
+
removed_in: "3.0.0" # the sdd-cli major that rejects the former names
|
|
24
|
+
|
|
25
|
+
renames:
|
|
26
|
+
- id: authoring-status-detailed
|
|
27
|
+
old: detailed-pre-lock
|
|
28
|
+
new: detailed
|
|
29
|
+
positions:
|
|
30
|
+
- "spec `**Status:**` header (M-ADR 0201's authoring ladder)"
|
|
31
|
+
- "work-order status"
|
|
32
|
+
- "capability manifest `capability.status` (a spec-authoring stage on the wrong axis; reported, never translated)"
|
|
33
|
+
|
|
34
|
+
- id: stability-stable
|
|
35
|
+
old: locked
|
|
36
|
+
new: stable
|
|
37
|
+
positions:
|
|
38
|
+
- "`required_stability` (app and integration platform dependencies)"
|
|
39
|
+
- "`traceability.contracts.status` (the contract-set maturity ladder)"
|
|
40
|
+
- "repository config `stability` (a stability class)"
|
|
41
|
+
- "runtime-artefact `stability` and `template-stability`"
|
|
42
|
+
note: "Only where `locked` is a stability CLASS. A pinned version is a different thing (M-ADR 0222 §4.1)."
|
|
43
|
+
|
|
44
|
+
- id: agreement-tag
|
|
45
|
+
old: release-vMAJOR.MINOR-locked
|
|
46
|
+
new: release-vMAJOR.MINOR-agreed
|
|
47
|
+
positions:
|
|
48
|
+
- "the agreement tag on a release branch (M-ADRs 0072-0074)"
|
|
49
|
+
|
|
50
|
+
- id: agreement-tag-key
|
|
51
|
+
old: lock_in_tag
|
|
52
|
+
new: agreement_tag
|
|
53
|
+
positions:
|
|
54
|
+
- "a platform BASELINE.md record key (M-ADR 0073)"
|
|
55
|
+
|
|
56
|
+
- id: gate-registry-next-gate
|
|
57
|
+
old: lock-in
|
|
58
|
+
new: agreement
|
|
59
|
+
positions:
|
|
60
|
+
- "gate registry `next_gate` value (domain-architecture rubric)"
|
|
61
|
+
|
|
62
|
+
- id: gate-registry-remediation-key
|
|
63
|
+
old: remediation_before_lock_in
|
|
64
|
+
new: remediation_before_agreement
|
|
65
|
+
positions:
|
|
66
|
+
- "gate registry key (domain-architecture rubric)"
|
|
67
|
+
|
|
68
|
+
- id: journey-status-accepted
|
|
69
|
+
old: accepted-pre-lock
|
|
70
|
+
new: accepted
|
|
71
|
+
positions:
|
|
72
|
+
- "user-journey status (templates only; no reader)"
|
|
73
|
+
|
|
74
|
+
# An extension an adopter names itself (an `x-<adopter>-stability` contract extension,
|
|
75
|
+
# say) is the adopter's to rename; its values follow the method's (M-ADR 0222 §4.1). The migrate verb
|
|
76
|
+
# reports such extensions and never rewrites them.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { migrateVocabulary } from "../lib/vocabulary/migrate.js";
|
|
3
|
+
/**
|
|
4
|
+
* `migrate vocabulary` (M-ADR 0222 §4.1): rewrite the SDD's own files from the former tool-read values to the
|
|
5
|
+
* new ones, in known positions only, and report what it must not rewrite. Authored content, so the verb is the
|
|
6
|
+
* author's and the full CLI's (M-ADR 0218). Exit 0 when nothing is left to migrate, 1 when it rewrote values
|
|
7
|
+
* (or, with --dry-run, found values it would rewrite), so a wave can gate on "migrated".
|
|
8
|
+
*/
|
|
9
|
+
export function registerMigrate(program) {
|
|
10
|
+
const migrate = program.command("migrate").description("Migrate an SDD's own files to the method's current vocabulary");
|
|
11
|
+
migrate
|
|
12
|
+
.command("vocabulary")
|
|
13
|
+
.description("Rewrite the SDD's own files from the former tool-read values (detailed-pre-lock, stability locked, release-vX.X-locked, lock_in_tag, next_gate lock-in, remediation_before_lock_in) to the new ones, in known positions only; method- and pack-managed files, .sdd/ and catalogues/ are untouched (exit 1 when anything was or would be rewritten)")
|
|
14
|
+
.option("--sdd-path <path>", "Path to the SDD repo root (defaults to cwd)")
|
|
15
|
+
.option("--dry-run", "List what would be rewritten; write nothing")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const root = resolve(opts.sddPath ?? process.cwd());
|
|
18
|
+
const r = await migrateVocabulary(root, { dryRun: opts.dryRun === true });
|
|
19
|
+
const w = (s) => process.stdout.write(s);
|
|
20
|
+
for (const x of r.rewrites)
|
|
21
|
+
w(` ${opts.dryRun ? "would rewrite" : "rewrote"} ${x.file}:${x.line} ${x.from} -> ${x.to} (${x.rule})\n`);
|
|
22
|
+
for (const x of r.reported)
|
|
23
|
+
w(` reported ${x.file}:${x.line} ${x.text} — ${x.reason}\n`);
|
|
24
|
+
const verb = opts.dryRun ? "would rewrite" : "rewrote";
|
|
25
|
+
w(r.rewrites.length === 0
|
|
26
|
+
? `[PASS] migrate vocabulary: nothing to migrate in ${r.filesScanned} file(s) the SDD owns${r.reported.length ? `; ${r.reported.length} reported for the adopter` : ""}\n`
|
|
27
|
+
: `[INFO] migrate vocabulary: ${verb} ${r.rewrites.length} value(s) in ${r.filesChanged} file(s) of ${r.filesScanned}${r.reported.length ? `; ${r.reported.length} reported for the adopter` : ""}\n`);
|
|
28
|
+
if (r.rewrites.length > 0)
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=migrate.js.map
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/author-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"sddCliVersion": "2.0.0",
|
|
5
5
|
"description": "Re-author one SDD's content — scaffold, plan, contracts, conformance, api-surface, snapshots, briefings, catalogue check — plus every read-only verify verb. Ships only the code those verbs reach; it cannot sync the baseline or rebuild the catalogues. Maintenance is in @sdd-method/maintain-cli, administration in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Checks (for each entry in app-platform-dependencies.yaml), split by whether they
|
|
13
13
|
# need a platform surface at all — LOCAL checks run wherever the app repo is:
|
|
14
|
-
# 1. (local) required_stability is a valid value (provisional |
|
|
14
|
+
# 1. (local) required_stability is a valid value (provisional | stable | unknown)
|
|
15
15
|
# 2a. (local) service_id is declared and non-empty
|
|
16
16
|
# 2b. (platform) service_id resolves against the platform source of truth:
|
|
17
17
|
# catalogues/services.csv (exact id match) when the catalogue exists,
|
|
18
18
|
# else docs/platform/service-architecture.md (word-anchored match)
|
|
19
|
-
# 3a. (local) a `
|
|
19
|
+
# 3a. (local) a `stable` dependency declares a contract_path at all
|
|
20
20
|
# 3b. (platform) that contract_path resolves to a file under the platform root
|
|
21
21
|
#
|
|
22
22
|
# Outputs a dependency status table.
|
|
23
|
-
# Used as the Stage
|
|
23
|
+
# Used as the Stage 9 agreement gate in the App Exploration Workflow.
|
|
24
24
|
#
|
|
25
25
|
# THE STABILITY VOCABULARY, AND WHY IT IS WHAT IT IS
|
|
26
26
|
# `required_stability` states the CONSUMING app's expectation of contract
|
|
27
27
|
# stability. Three values express that:
|
|
28
28
|
#
|
|
29
29
|
# provisional the contract may still change; the app tolerates it
|
|
30
|
-
#
|
|
30
|
+
# stable the contract is fixed; a contract_path must exist (check 3)
|
|
31
31
|
# unknown the expectation is not determined
|
|
32
32
|
#
|
|
33
33
|
# `unknown` is accepted because the canonical schema's
|
|
@@ -158,7 +158,7 @@ APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
|
|
|
158
158
|
# This gate read only the forward file, so an entire equal-status source went
|
|
159
159
|
# ungated. Measured in one reverse estate: 15 evidence files declaring 65
|
|
160
160
|
# further dependencies, 38 of them `locked` — precisely the population
|
|
161
|
-
# a Stage
|
|
161
|
+
# a Stage 9 agreement gate exists to check — none of which this gate could see.
|
|
162
162
|
#
|
|
163
163
|
# A source that the catalogue reads and the gate does not is a coverage gap by
|
|
164
164
|
# construction, so the gate now reads both.
|
|
@@ -180,7 +180,7 @@ PLATFORM_SERVICES_CSV="$PLATFORM_ROOT/catalogues/services.csv"
|
|
|
180
180
|
# offer (instances app-dependency-stability-vocabulary-forward and -reverse).
|
|
181
181
|
VALID_STABILITY_VALUES=(
|
|
182
182
|
"provisional" # contract may still change; the app tolerates it
|
|
183
|
-
"
|
|
183
|
+
"stable" # contract is fixed; check 3 requires a resolvable contract_path
|
|
184
184
|
"unknown" # expectation not determined; no contract_path required
|
|
185
185
|
)
|
|
186
186
|
|
|
@@ -353,6 +353,13 @@ validate_dependency() {
|
|
|
353
353
|
service_id="$(yq e ".platform_dependencies[$index].service_id // \"\"" "$source_file")"
|
|
354
354
|
contract_type="$(yq e ".platform_dependencies[$index].contract_type // \"\"" "$source_file")"
|
|
355
355
|
required_stability="$(yq e ".platform_dependencies[$index].required_stability // \"\"" "$source_file")"
|
|
356
|
+
# vocabulary-alias: locked -> stable
|
|
357
|
+
# The fixed-contract stability class was `locked` (M-ADR 0222 §4.1; docs/method/foundations/vocabulary-renames.yaml).
|
|
358
|
+
# Read as `stable` for one major release and reported; from sdd-cli 3.0.0 it is invalid.
|
|
359
|
+
if [[ "$required_stability" == "locked" ]]; then
|
|
360
|
+
log_warn "$source_file: platform_dependencies[$index] uses the former required_stability 'locked', read as 'stable' (M-ADR 0222 §4.1). Run 'sdd-cli migrate vocabulary'; from sdd-cli 3.0.0 the former value is invalid."
|
|
361
|
+
required_stability="stable"
|
|
362
|
+
fi
|
|
356
363
|
contract_path="$(yq e ".platform_dependencies[$index].contract_path // \"\"" "$source_file")"
|
|
357
364
|
|
|
358
365
|
local dep_label="${service_id:-dep[$index]}"
|
|
@@ -403,7 +410,7 @@ validate_dependency() {
|
|
|
403
410
|
done
|
|
404
411
|
|
|
405
412
|
if [[ "$stability_deprecated" -eq 1 ]]; then
|
|
406
|
-
log_fail "$dep_label: required_stability '$required_stability' is not a stability value (M-ADR 0204 §4.4). Declare target_locus: '$required_stability' on the entry and give required_stability a real value (${VALID_STABILITY_VALUES[*]}) — a third-party contract can be
|
|
413
|
+
log_fail "$dep_label: required_stability '$required_stability' is not a stability value (M-ADR 0204 §4.4). Declare target_locus: '$required_stability' on the entry and give required_stability a real value (${VALID_STABILITY_VALUES[*]}) — a third-party contract can be stable."
|
|
407
414
|
((dep_errors++))
|
|
408
415
|
else
|
|
409
416
|
log_fail "$dep_label: invalid required_stability '$required_stability' (allowed: ${VALID_STABILITY_VALUES[*]})"
|
|
@@ -490,26 +497,26 @@ validate_dependency() {
|
|
|
490
497
|
((WARNINGS++))
|
|
491
498
|
fi
|
|
492
499
|
|
|
493
|
-
# Check 3: a
|
|
494
|
-
# resolves to a file (3b, needs the platform). Same split, same reason: "
|
|
500
|
+
# Check 3: a stable dependency declares a contract_path (3a, local) and that path
|
|
501
|
+
# resolves to a file (3b, needs the platform). Same split, same reason: "stable
|
|
495
502
|
# with no path at all" is an app-repo defect that needs nothing external to catch.
|
|
496
|
-
if [[ "$required_stability" == "
|
|
503
|
+
if [[ "$required_stability" == "stable" ]]; then
|
|
497
504
|
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
498
|
-
log_fail "$dep_label: required_stability is '
|
|
505
|
+
log_fail "$dep_label: required_stability is 'stable' but contract_path is empty"
|
|
499
506
|
((dep_errors++))
|
|
500
507
|
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
501
508
|
((UNRESOLVED_CONTRACT_PATHS++))
|
|
502
509
|
else
|
|
503
510
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
504
511
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
505
|
-
log_fail "$dep_label:
|
|
512
|
+
log_fail "$dep_label: stable contract_path '$contract_path' does not exist in platform repo"
|
|
506
513
|
((dep_errors++))
|
|
507
514
|
fi
|
|
508
515
|
fi
|
|
509
516
|
elif [[ "$required_stability" == "provisional" || "$required_stability" == "unknown" ]]; then
|
|
510
517
|
# Neither requires a contract_path. If one is declared it is still worth
|
|
511
518
|
# reporting when it does not resolve — silently accepting a path that
|
|
512
|
-
# points at nothing is how a
|
|
519
|
+
# points at nothing is how a stable dependency later inherits a dead path.
|
|
513
520
|
if [[ -n "$contract_path" && "$contract_path" != "null" && "$PLATFORM_AVAILABLE" -eq 1 ]]; then
|
|
514
521
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
515
522
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
@@ -552,21 +559,22 @@ print_status_table() {
|
|
|
552
559
|
local service_id required_stability contract_type contract_path dep_status
|
|
553
560
|
service_id="$(yq e ".platform_dependencies[$i].service_id // \"(missing)\"" "$source_file")"
|
|
554
561
|
required_stability="$(yq e ".platform_dependencies[$i].required_stability // \"(missing)\"" "$source_file")"
|
|
562
|
+
if [[ "$required_stability" == "locked" ]]; then required_stability="stable"; fi # the former name, reported above
|
|
555
563
|
contract_type="$(yq e ".platform_dependencies[$i].contract_type // \"(missing)\"" "$source_file")"
|
|
556
564
|
contract_path="$(yq e ".platform_dependencies[$i].contract_path // \"\"" "$source_file")"
|
|
557
565
|
|
|
558
566
|
# Determine display status
|
|
559
|
-
if [[ "$required_stability" == "
|
|
567
|
+
if [[ "$required_stability" == "stable" ]]; then
|
|
560
568
|
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
561
|
-
dep_status="${RED}
|
|
569
|
+
dep_status="${RED}STABLE UNMET${NC}"
|
|
562
570
|
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
563
571
|
# A declared path that was never resolved is not "OK" and not "UNMET".
|
|
564
572
|
# Reporting either would be a claim the run cannot support.
|
|
565
|
-
dep_status="${YELLOW}
|
|
573
|
+
dep_status="${YELLOW}STABLE UNRESOLVED${NC}"
|
|
566
574
|
elif [[ -f "$PLATFORM_ROOT/$contract_path" ]]; then
|
|
567
|
-
dep_status="${GREEN}
|
|
575
|
+
dep_status="${GREEN}STABLE OK${NC}"
|
|
568
576
|
else
|
|
569
|
-
dep_status="${RED}
|
|
577
|
+
dep_status="${RED}STABLE UNMET${NC}"
|
|
570
578
|
fi
|
|
571
579
|
elif [[ "$required_stability" == "provisional" ]]; then
|
|
572
580
|
dep_status="${YELLOW}PROVISIONAL${NC}"
|
|
@@ -637,7 +645,7 @@ main() {
|
|
|
637
645
|
fi
|
|
638
646
|
|
|
639
647
|
if [[ "$FAILURES" -gt 0 ]]; then
|
|
640
|
-
log_fail "$FAILURES dependency check(s) failed. Stage
|
|
648
|
+
log_fail "$FAILURES dependency check(s) failed. Stage 9 agreement gate NOT met."
|
|
641
649
|
exit 1
|
|
642
650
|
fi
|
|
643
651
|
|
|
@@ -648,7 +656,7 @@ main() {
|
|
|
648
656
|
# while never executing (M-ADR 0200 §4.3).
|
|
649
657
|
if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
650
658
|
log_warn "Ran check 1 (stability vocabulary) and the local halves of checks 2-3 over $TOTAL_DEPS dependency(ies): $FAILURES failure(s), $WARNINGS warning(s)."
|
|
651
|
-
log_warn "NOT RUN, for want of --platform-root: $UNRESOLVED_SERVICE_IDS service_id resolution(s) and $UNRESOLVED_CONTRACT_PATHS
|
|
659
|
+
log_warn "NOT RUN, for want of --platform-root: $UNRESOLVED_SERVICE_IDS service_id resolution(s) and $UNRESOLVED_CONTRACT_PATHS stable contract_path resolution(s)."
|
|
652
660
|
log_info "This is a partial run, not a met gate — pass --platform-root to complete it."
|
|
653
661
|
exit 0
|
|
654
662
|
fi
|
|
@@ -656,7 +664,7 @@ main() {
|
|
|
656
664
|
if [[ "$WARNINGS" -gt 0 ]]; then
|
|
657
665
|
log_warn "All required checks passed with $WARNINGS warning(s)."
|
|
658
666
|
else
|
|
659
|
-
log_pass "All dependency checks passed. Stage
|
|
667
|
+
log_pass "All dependency checks passed. Stage 9 agreement gate met."
|
|
660
668
|
fi
|
|
661
669
|
exit 0
|
|
662
670
|
}
|