@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.
@@ -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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.4.0",
3
- "built_against_sdd_cli": "1.45.0"
2
+ "version": "0.6.0",
3
+ "built_against_sdd_cli": "2.0.0"
4
4
  }
@@ -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
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.4.0",
3
- "built_against_sdd_cli": "1.45.0"
2
+ "version": "2.0.0",
3
+ "built_against_sdd_cli": "2.0.0"
4
4
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@sdd-method/author-cli",
3
- "version": "0.3.0",
4
- "sddCliVersion": "1.45.0",
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
- "license": "Apache-2.0",
6
+ "license": "UNLICENSED",
7
7
  "type": "module",
8
8
  "bin": {
9
9
  "sdd-author": "dist/author-bin.js"
@@ -23,8 +23,6 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "scripts",
26
- "README.md",
27
- "LICENSE",
28
- "NOTICE"
26
+ "README.md"
29
27
  ]
30
28
  }
@@ -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 | locked | unknown)
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 `locked` dependency declares a contract_path at all
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 6 lock-in gate in the App Exploration Workflow.
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
- # locked the contract is fixed; a contract_path must exist (check 3)
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
@@ -157,8 +157,8 @@ APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
157
157
  #
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
- # further dependencies, 38 of them `locked` — precisely the lock-in population
161
- # a Stage 6 lock-in gate exists to check — none of which this gate could see.
160
+ # further dependencies, 38 of them `locked` — precisely the population
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
- "locked" # contract is fixed; check 3 requires a resolvable contract_path
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 locked."
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 locked dependency declares a contract_path (3a, local) and that path
494
- # resolves to a file (3b, needs the platform). Same split, same reason: "locked
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" == "locked" ]]; then
503
+ if [[ "$required_stability" == "stable" ]]; then
497
504
  if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
498
- log_fail "$dep_label: required_stability is 'locked' but contract_path is empty"
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: locked contract_path '$contract_path' does not exist in platform repo"
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 locked dependency later inherits a dead path.
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" == "locked" ]]; then
567
+ if [[ "$required_stability" == "stable" ]]; then
560
568
  if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
561
- dep_status="${RED}LOCKED UNMET${NC}"
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}LOCKED UNRESOLVED${NC}"
573
+ dep_status="${YELLOW}STABLE UNRESOLVED${NC}"
566
574
  elif [[ -f "$PLATFORM_ROOT/$contract_path" ]]; then
567
- dep_status="${GREEN}LOCKED OK${NC}"
575
+ dep_status="${GREEN}STABLE OK${NC}"
568
576
  else
569
- dep_status="${RED}LOCKED UNMET${NC}"
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 6 lock-in gate NOT met."
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 locked contract_path resolution(s)."
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 6 lock-in gate met."
667
+ log_pass "All dependency checks passed. Stage 9 agreement gate met."
660
668
  fi
661
669
  exit 0
662
670
  }
@@ -341,7 +341,7 @@ check_grounding_doc() {
341
341
 
342
342
  local grounding="$REPO_ROOT/docs/design/integration/feature-grounding.md"
343
343
  if [[ ! -f "$grounding" ]]; then
344
- log_warn "docs/design/integration/feature-grounding.md not found — create before lock-in"
344
+ log_warn "docs/design/integration/feature-grounding.md not found — create before agreement"
345
345
  return
346
346
  fi
347
347