@sdd-method/author-cli 0.3.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.
@@ -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
@@ -10,7 +10,7 @@
10
10
  // c4_container_entity ⋈ c4_containers.system_id
11
11
  // → aggregate (system_id, entity_id) with container_count.
12
12
  //
13
- // Both handle empty inputs gracefully (an adopter's pre-lock state has zero
13
+ // Both handle empty inputs gracefully (an adopter's state before agreement has zero
14
14
  // service_entity edges → both roll-up outputs are empty; no errors).
15
15
  export function buildC4ContainerEntity(inputs) {
16
16
  // Index containers by service_id.
@@ -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
- "detailed-pre-lock",
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
  /**
@@ -27,7 +27,7 @@ const KIND_SPECS = [
27
27
  { subdir: "avro", extensions: [".avsc"], kind: "event" },
28
28
  // `authz` — authorization/policy schemas (SpiceDB `.zed` and equivalents).
29
29
  //
30
- // Added because a contract lock-in gate can approve `.zed` schemas as
30
+ // Added because a contract agreement gate can approve `.zed` schemas as
31
31
  // contracts while `kind` had no member able to represent them — so the
32
32
  // catalogue could not hold an artefact the method's own gate had certified.
33
33
  // Such a gate records the tension itself, passing the schema with a waiver on
@@ -169,7 +169,7 @@ export function buildCapabilityEvent(inputs) {
169
169
  if (eid === "")
170
170
  continue;
171
171
  if (!inputs.knownEventIds.has(eid)) {
172
- inputs.findings.add("orphan", `${m.relPath}: ${key} entry "${eventName}" doesn't resolve to a known event id "${eid}" (likely a pre-lock sketch — events.csv has no matching row)`);
172
+ inputs.findings.add("orphan", `${m.relPath}: ${key} entry "${eventName}" doesn't resolve to a known event id "${eid}" (likely a sketch from before agreement — events.csv has no matching row)`);
173
173
  continue;
174
174
  }
175
175
  tryEmitAuthored(authored, capId, eid, role, m.relPath, inputs.findings);
@@ -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
- const requiredStability = (dep.required_stability ?? "").trim();
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: locked | provisional | unknown
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: "3.20.0"
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: locked
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 | locked | 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."
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 lock-in gate certified .zed schemas as contracts while no kind could represent them.
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 at first contract lock-in).
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 | locked | 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."
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 | locked | 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."
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