@sdd-method/author-cli 0.16.2 → 0.17.1

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.
@@ -136,15 +136,28 @@ export async function orderArchives(sddPath, archiveRel, names) {
136
136
  export function observedNone(evidence) {
137
137
  return /^\s*\*\*Observed in:\*\*\s*none\b/im.test(evidence);
138
138
  }
139
- /** The repositories a baseline was observed in: its evidence file's `**Observed in:** repo[@sha], repo[@sha]` line. */
140
- export function observedIn(evidence) {
139
+ /** One `**Observed in:**` entry: `<repository> @ <commit>`, either side optionally in backticks. */
140
+ const OBSERVED_ENTRY = /^`?([A-Za-z0-9][A-Za-z0-9._/-]*)`?\s*@\s*`?([0-9a-fA-F]{7,40})`?$/;
141
+ function observedEntries(evidence) {
141
142
  const m = /^\s*\*\*Observed in:\*\*\s*(.+)$/m.exec(evidence);
142
143
  if (!m || observedNone(evidence))
143
144
  return [];
144
- return m[1]
145
- .split(",")
146
- .map((s) => s.trim().split(/\s*@\s*/)[0].replace(/`/g, "").trim())
147
- .filter((s) => s !== "");
145
+ return m[1].split(",").map((s) => s.trim()).filter((s) => s !== "");
146
+ }
147
+ /**
148
+ * The repositories a baseline was observed in: its evidence file's `**Observed in:** <repository> @ <commit>[, ...]`
149
+ * line. Only entries of that shape are read; anything else on the line is reported by `observedInProblems` and never
150
+ * taken for a repository.
151
+ */
152
+ export function observedIn(evidence) {
153
+ return observedEntries(evidence).flatMap((e) => {
154
+ const m = OBSERVED_ENTRY.exec(e);
155
+ return m ? [m[1]] : [];
156
+ });
157
+ }
158
+ /** The `**Observed in:**` entries not shaped `<repository> @ <commit>` (detail belongs in the body, not on the line). */
159
+ export function observedInProblems(evidence) {
160
+ return observedEntries(evidence).filter((e) => !OBSERVED_ENTRY.test(e));
148
161
  }
149
162
  /**
150
163
  * A baseline's `**Surfaces:**` list (M-ADR 0222 §4.3, §4.4; agreement-record-schema.yaml, Baselines): one entry per
@@ -165,20 +178,30 @@ export function parseSurfaces(evidence) {
165
178
  // the list is what the file ends up asserting.
166
179
  let at = -1;
167
180
  let none = false;
181
+ let generated = false;
168
182
  lines.forEach((l, i) => {
169
183
  if (/^\s*\*\*Surfaces:\*\*\s*$/.test(l)) {
170
184
  at = i;
171
185
  none = false;
186
+ generated = false;
172
187
  }
173
188
  else if (/^\s*\*\*Surfaces:\*\*\s*none\b/i.test(l)) {
174
189
  at = i;
175
190
  none = true;
191
+ generated = false;
192
+ }
193
+ else if (/^\s*\*\*Surfaces:\*\*\s*generated\b/i.test(l)) {
194
+ at = i;
195
+ none = false;
196
+ generated = true;
176
197
  }
177
198
  });
178
199
  if (at < 0)
179
- return { present: false, none: false, entries: [], problems: [] };
200
+ return { present: false, none: false, generated: false, entries: [], problems: [] };
180
201
  if (none)
181
- return { present: true, none: true, entries: [], problems: [] };
202
+ return { present: true, none: true, generated: false, entries: [], problems: [] };
203
+ if (generated)
204
+ return { present: true, none: false, generated: true, entries: [], problems: [] };
182
205
  const entries = [];
183
206
  const problems = [];
184
207
  for (const line of lines.slice(at + 1)) {
@@ -213,7 +236,37 @@ export function parseSurfaces(evidence) {
213
236
  }
214
237
  if (entries.length + problems.length === 0)
215
238
  problems.push('the **Surfaces:** line lists no contract; a baseline that admits no behaviour writes "**Surfaces:** none"');
216
- return { present: true, none: false, entries, problems };
239
+ return { present: true, none: false, generated: false, entries, problems };
240
+ }
241
+ /**
242
+ * A generator's baseline (agreement-record-schema.yaml, Baselines: generated behaviour) serves no contract; its
243
+ * transport is the generated tree, and the run that generated it is evidence only if another reader can repeat it.
244
+ * The run is recorded on a line of its own:
245
+ * **Generated by:** <repository> @ <commit>; run: `<command and options>`; tree: <hash>[, <hash>]
246
+ * where each tree hash is hex of 12 or more digits, optionally `sha256:`-prefixed and optionally labelled `<kit>=`.
247
+ * Returns what is wrong with the line, or that it is absent.
248
+ */
249
+ export function generatedByProblems(evidence) {
250
+ const line = evidence.split("\n").filter((l) => /^\s*\*\*Generated by:\*\*/.test(l)).pop();
251
+ if (line === undefined)
252
+ return ["no **Generated by:** line"];
253
+ const body = line.replace(/^\s*\*\*Generated by:\*\*\s*/, "");
254
+ const parts = body.split(/;\s*(?=run:|tree:)/);
255
+ const out = [];
256
+ if (!/^`?[A-Za-z0-9][A-Za-z0-9._/-]*`?\s*@\s*`?[0-9a-fA-F]{7,40}`?\s*$/.test(parts[0] ?? ""))
257
+ out.push(`"${(parts[0] ?? "").trim()}" is not "<generator repository> @ <commit>"`);
258
+ const run = parts.find((p) => p.startsWith("run:"));
259
+ if (!run || !/^run:\s*`[^`]+`\s*$/.test(run.trim()))
260
+ out.push("no run: `<command and options>`");
261
+ const tree = parts.find((p) => p.startsWith("tree:"));
262
+ const hashes = tree ? tree.replace(/^tree:\s*/, "").split(",").map((h) => h.trim()).filter((h) => h !== "") : [];
263
+ if (hashes.length === 0)
264
+ out.push("no tree: <content hash of the generated tree>");
265
+ for (const h of hashes) {
266
+ if (!/^(?:[A-Za-z0-9._-]+=)?(?:sha256:)?[0-9a-fA-F]{12,}$/.test(h.replace(/`/g, "")))
267
+ out.push(`tree hash "${h}" is not hex of 12 or more digits`);
268
+ }
269
+ return out;
217
270
  }
218
271
  /**
219
272
  * A structural delta's `moved: {contracts: [{from, to, from_path?}]}` block. A contract's id is derived from its path,
@@ -110,6 +110,7 @@ export async function runCatalogueBuild(inputs, write) {
110
110
  await reportCollapsedFamilies(outDir, join(sddPath, "catalogues"), rowsByCatalogue, findings);
111
111
  // 4. Write CSVs (headers + rows).
112
112
  const rowCounts = new Map();
113
+ const preserved = [];
113
114
  const ownCatalogues = resolve(join(sddPath, "catalogues"));
114
115
  for (const cat of schema.catalogues) {
115
116
  const rows = rowsByCatalogue.get(cat.name) ?? [];
@@ -134,6 +135,7 @@ export async function runCatalogueBuild(inputs, write) {
134
135
  // also silent, and that is why 26 SDDs lost their conformance evidence
135
136
  // while their registries went on citing it.
136
137
  if (result.preserved) {
138
+ preserved.push({ name: cat.name, rows: result.preservedRows ?? result.rowCount });
137
139
  findings.add("convention", `catalogues/${cat.name}.csv: this build produced NO rows for it, and the file on disk ` +
138
140
  `holds ${result.preservedRows}. The existing file was LEFT UNTOUCHED rather than ` +
139
141
  `replaced with a bare header. Either the catalogue has no producer in this CLI — in ` +
@@ -237,6 +239,7 @@ export async function runCatalogueBuild(inputs, write) {
237
239
  findingsCount: findings.count(),
238
240
  pkConflicts: dedupe.conflicts.length,
239
241
  unionedRows: dedupe.unionedRows,
242
+ preserved,
240
243
  outDir,
241
244
  findings: findings.all(),
242
245
  };
@@ -37,12 +37,12 @@ import { readdir, readFile, stat } from "node:fs/promises";
37
37
  import { join } from "node:path";
38
38
  import { parse as parseYaml } from "yaml";
39
39
  import { createHash } from "node:crypto";
40
- import { observedIn, orderArchives, parseRequirements, renamePairs, resolveImplements } from "../../behaviour-store/archive.js";
40
+ import { observedIn, observedInProblems, orderArchives, parseRequirements, renamePairs, resolveImplements } from "../../behaviour-store/archive.js";
41
41
  import { effectsOf, parseAppliedRecord } from "../../structure/effects.js";
42
42
  import { APPLIED_FILE, loadChangeStructure, loadModel } from "../../structure/model.js";
43
43
  import { viewOf } from "../../structure/view.js";
44
44
  import { requirementBlock } from "../../validate/agreement-evidence.js";
45
- export { observedIn, parseRequirements, resolveImplements };
45
+ export { observedIn, observedInProblems, parseRequirements, resolveImplements };
46
46
  /** `sha256:` of a requirement block, its lines trimmed and blank lines dropped (M-ADR 0230 §4.7). */
47
47
  export function requirementDigest(spec, name) {
48
48
  const block = requirementBlock(spec, name);
@@ -276,6 +276,10 @@ export async function buildRequirements(inputs) {
276
276
  structureEffects.push(effectRow(scoped(dir), "structure", e.element, e.operation, e.base ?? "", e.target ?? ""));
277
277
  if (evidence !== null) {
278
278
  const repos = observedIn(evidence);
279
+ const malformed = observedInProblems(evidence);
280
+ if (malformed.length > 0) {
281
+ findings.add("convention", `${rel(sddPath, join(changeDir, "baseline-evidence.md"))}: "**Observed in:**" has ${malformed.length} entr${malformed.length === 1 ? "y" : "ies"} not shaped "<repository> @ <commit>", not read as repositories; detail goes in the body`);
282
+ }
279
283
  if (repos.length === 0 && delta.length > 0) {
280
284
  findings.add("convention", `${rel(sddPath, join(changeDir, "baseline-evidence.md"))}: no "**Observed in:**" line, so its requirements carry no observed status`);
281
285
  }
@@ -65,9 +65,9 @@ async function defaultBehaviourFindings(sddPath, archiveDir) {
65
65
  const r = await checkBehaviourStore(sddPath);
66
66
  return r.stdout.split("\n").filter((l) => l.startsWith("[FAIL]") && l.includes(archiveDir));
67
67
  }
68
- function viewProblems(schema, view) {
68
+ function viewProblems(schema, view, profile) {
69
69
  const out = [];
70
- validateView(schema, view, () => null, out, new Set());
70
+ validateView(schema, view, () => null, out, new Set(), () => undefined, profile);
71
71
  return new Set(out);
72
72
  }
73
73
  /** Does the effect record hold at `commit`? Each added or modified element at its target, each removed one absent. */
@@ -98,6 +98,11 @@ async function verifyAt(root, relSdd, commit, effects) {
98
98
  await rm(scratch, { recursive: true, force: true });
99
99
  }
100
100
  }
101
+ /** The SDD's profile from its manifest, "" when absent or unreadable. */
102
+ async function sddProfile(sddPath) {
103
+ const text = await readFile(join(sddPath, ".sdd", "sdd-manifest.yaml"), "utf8").catch(() => "");
104
+ return /^profile:\s*["']?([A-Za-z0-9-]+)["']?\s*$/m.exec(text)?.[1] ?? "";
105
+ }
101
106
  export async function archiveChange(o) {
102
107
  const say = (s) => o.write(`${s}\n`);
103
108
  const refuse = (s) => {
@@ -241,6 +246,7 @@ async function publish(o, root, relSdd, schema, s, changeRel, mode) {
241
246
  say(`[FAIL] change archive ${o.change}: ${x}`);
242
247
  return 1;
243
248
  };
249
+ const profile = await sddProfile(o.sddPath);
244
250
  const storeRel = join(relSdd, "openspec");
245
251
  const viewsRel = join(relSdd, "docs", "structure");
246
252
  const dirty = await git(root, "status", "--porcelain", "--", storeRel, viewsRel);
@@ -250,9 +256,9 @@ async function publish(o, root, relSdd, schema, s, changeRel, mode) {
250
256
  // §4.5 again, against the model as it now is.
251
257
  const problems = [];
252
258
  const current = viewOf((await loadModel(o.sddPath, problems)) ?? [], problems);
253
- const before = viewProblems(schema, current);
259
+ const before = viewProblems(schema, current, profile);
254
260
  const deltaProblems = [];
255
- validateDeltas(schema, current, s.deltas, new Map(), deltaProblems);
261
+ validateDeltas(schema, current, s.deltas, new Map(), deltaProblems, profile);
256
262
  if (deltaProblems.length > 0)
257
263
  return refuse(`its structural deltas no longer apply to the model as it stands:\n ${deltaProblems.join("\n ")}`);
258
264
  const effects = effectsOf(s, current);
@@ -280,7 +286,7 @@ async function publish(o, root, relSdd, schema, s, changeRel, mode) {
280
286
  // 2. Validate the candidate as a whole.
281
287
  const cp = [];
282
288
  const candidate = viewOf((await loadModel(scratchSdd, cp)) ?? [], cp);
283
- const after = viewProblems(schema, candidate);
289
+ const after = viewProblems(schema, candidate, profile);
284
290
  const worse = [...cp, ...[...after].filter((p) => !before.has(p))];
285
291
  if (worse.length > 0)
286
292
  return refuse(`the model as it would stand fails where it did not:\n ${worse.join("\n ")}`);
@@ -75,6 +75,12 @@ export async function extractStructure(sddPath, build = runCatalogueBuild) {
75
75
  if (built.exitCode !== 0) {
76
76
  return { ...none, refused: `the sources do not build cleanly (sdd-cli catalogue build exits ${built.exitCode}: ${built.internalFkPass ? "" : "dangling references, "}${built.pkConflicts} key conflict(s)); a model extracted from them would carry the same faults. Fix the build first` };
77
77
  }
78
+ // A catalogue the build produced no rows for keeps the SDD's file (catalogue build never replaces rows with a bare
79
+ // header). Those rows came from no source, so a model "extracted from its sources" must not carry them.
80
+ const kept = built.preserved.filter((p) => schema.catalogues.some((c) => c.name === p.name && c.sourceOfTruth === STRUCTURE));
81
+ if (kept.length > 0) {
82
+ return { ...none, refused: `${kept.map((k) => `catalogues/${k.name}.csv holds ${k.rows} row(s)`).join("; ")} that no source gives any more (the build produced none and kept the file); a model extracted now would carry them as if derived. Restore the source, or delete those rows from catalogues/ (keep the header), then extract` };
83
+ }
78
84
  const rows = new Map();
79
85
  for (const c of schema.catalogues) {
80
86
  if (c.sourceOfTruth !== STRUCTURE)
@@ -90,13 +90,15 @@ function find(view, catalogue, column, value) {
90
90
  return undefined;
91
91
  }
92
92
  /** The element's references: each non-external foreign key with a value. */
93
- export function referencesOf(schema, e) {
93
+ export function referencesOf(schema, e, profile = "") {
94
94
  const cat = catalogueOf(schema, e.catalogue);
95
95
  if (!cat)
96
96
  return [];
97
97
  const out = [];
98
98
  for (const fk of cat.foreignKeys) {
99
- if (fk.external)
99
+ // An external key names another SDD's element; so does one external for this SDD's profile (an application or
100
+ // integration model naming a data item the platform model owns), as the FK validator and the graph projection read it.
101
+ if (fk.external || (profile !== "" && (fk.externalForProfiles ?? []).includes(profile)))
100
102
  continue;
101
103
  const v = e.columns[fk.column];
102
104
  if (v === undefined || v === "")
@@ -121,7 +123,7 @@ function naturalKey(cat, e) {
121
123
  * catalogue of another class, against its built rows), no cycle through a self-reference, natural keys unique, and
122
124
  * domain invariants naming their entity's own attributes and relationships.
123
125
  */
124
- export function validateView(schema, view, built, problems, unresolved, addedBy = () => undefined) {
126
+ export function validateView(schema, view, built, problems, unresolved, addedBy = () => undefined, profile = "") {
125
127
  // A reference to an open change's addition, from outside that change and its dependants (§4.11 proof 3).
126
128
  const outside = (catalogue, id) => {
127
129
  const by = addedBy(catalogue, id);
@@ -135,7 +137,7 @@ export function validateView(schema, view, built, problems, unresolved, addedBy
135
137
  problems.push(...shape);
136
138
  if (!cat || cat.sourceOfTruth !== STRUCTURE)
137
139
  continue;
138
- for (const r of referencesOf(schema, e)) {
140
+ for (const r of referencesOf(schema, e, profile)) {
139
141
  if (r.structural) {
140
142
  if (!find(view, r.catalogue, r.targetColumn, r.value))
141
143
  problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no ${r.catalogue} element in this view (${elementKey(r.catalogue, r.value)})${r.targetColumn === "id" ? outside(r.catalogue, r.value) : ""}`);
@@ -218,7 +220,7 @@ export function validateView(schema, view, built, problems, unresolved, addedBy
218
220
  * A base revision that no longer matches the current element is reported as moved: the change is revalidated.
219
221
  * `addedElsewhere` holds the elements other open changes add, for a reference into one of them.
220
222
  */
221
- export function validateDeltas(schema, current, deltas, addedElsewhere, problems) {
223
+ export function validateDeltas(schema, current, deltas, addedElsewhere, problems, profile = "") {
222
224
  const ownAdded = new Set();
223
225
  const dependsOn = new Set();
224
226
  for (const d of deltas) {
@@ -281,7 +283,7 @@ export function validateDeltas(schema, current, deltas, addedElsewhere, problems
281
283
  }
282
284
  // Reads: every element an added or modified element references.
283
285
  for (const e of [...d.added, ...d.modified]) {
284
- for (const r of referencesOf(schema, e)) {
286
+ for (const r of referencesOf(schema, e, profile)) {
285
287
  if (!r.structural)
286
288
  continue;
287
289
  const target = r.targetColumn === "id" ? r.value : find(current, r.catalogue, r.targetColumn, r.value)?.id;
@@ -77,22 +77,29 @@ export async function checkArchetypeCitations(repo) {
77
77
  const lines = [];
78
78
  let problems = 0;
79
79
  let total = 0;
80
+ // A citation file that cannot be read as a whole (it does not parse, names no cached archetype, or lists nothing)
81
+ // has no citations to count, so it is counted apart: "1 of 0" would read as a failure with no population.
82
+ let fileProblems = 0;
80
83
  const bad = (tag, msg) => {
81
84
  lines.push(`[${tag}] ${msg}`);
82
85
  problems++;
83
86
  };
87
+ const badFile = (msg) => {
88
+ lines.push(`[unresolved] ${msg}`);
89
+ fileProblems++;
90
+ };
84
91
  for (const [rel, text] of files) {
85
92
  let doc;
86
93
  try {
87
94
  doc = (parseYaml(text) ?? {});
88
95
  }
89
96
  catch (e) {
90
- bad("unresolved", `${rel}: does not parse: ${e.message.split("\n")[0]}`);
97
+ badFile(`${rel}: does not parse: ${e.message.split("\n")[0]}`);
91
98
  continue;
92
99
  }
93
100
  const archetype = archetypes.find((a) => a.id === doc.archetype && a.bundleName === doc.bundle);
94
101
  if (!archetype) {
95
- bad("unresolved", `${rel}: archetype ${String(doc.archetype)} of bundle ${String(doc.bundle)} is not in the bundle cache (sync the bundle)`);
102
+ badFile(`${rel}: archetype ${String(doc.archetype)} of bundle ${String(doc.bundle)} is not in the bundle cache (sync the bundle)`);
96
103
  continue;
97
104
  }
98
105
  const reqs = await loadArchetypeBehaviour(archetype.bundleCacheDir, archetype.seeds);
@@ -101,7 +108,7 @@ export async function checkArchetypeCitations(repo) {
101
108
  const cites = Array.isArray(doc.citations) ? doc.citations : [];
102
109
  const notAdopted = Array.isArray(doc.not_adopted) ? doc.not_adopted : [];
103
110
  if (cites.length === 0 && notAdopted.length === 0)
104
- bad("unresolved", `${rel}: lists no citations`);
111
+ badFile(`${rel}: lists no citations`);
105
112
  const citedIds = new Set(cites.map((c) => String(c.cites)));
106
113
  for (const n of notAdopted) {
107
114
  total++;
@@ -161,9 +168,11 @@ export async function checkArchetypeCitations(repo) {
161
168
  `(${[...citedIds].filter((id) => byId.has(id)).length} cited, ${notAdopted.length} not adopted)` +
162
169
  (gaps.length ? `; not yet answered: ${gaps.join(", ")}` : ""));
163
170
  }
164
- const verdict = problems
165
- ? `[FAIL] archetype citations: ${problems} of ${total} unresolved or stale`
171
+ const filePart = fileProblems ? `${fileProblems} of ${files.length} citation file(s) unresolved` : "";
172
+ const citePart = problems ? `${problems} of ${total} citation(s) unresolved or stale` : "";
173
+ const verdict = problems || fileProblems
174
+ ? `[FAIL] archetype citations: ${[filePart, citePart].filter(Boolean).join("; ")}${problems ? "" : `; ${total} citation(s) checked resolve`}`
166
175
  : `[PASS] archetype citations: ${total} resolve, none stale`;
167
- return { exitCode: problems ? 1 : 0, stdout: lines.join("\n") + "\n" + verdict + "\n" };
176
+ return { exitCode: problems || fileProblems ? 1 : 0, stdout: lines.join("\n") + "\n" + verdict + "\n" };
168
177
  }
169
178
  //# sourceMappingURL=archetype-citations.js.map
@@ -8,7 +8,8 @@
8
8
  //
9
9
  // 1. records: every agreement record has the shape the checks below read.
10
10
  // 2. agreement: an open change with any completed task has an agreed record and a Gate 0 that
11
- // ends CLEAR (the pre-apply check, M-ADR 0223 §4.3). A record for an open change must
11
+ // ends CLEAR (the pre-apply check, M-ADR 0223 §4.3); an open baseline (baseline-evidence.md)
12
+ // has neither and is not asked for them. A record for an open change must
12
13
  // declare agreement-record schema 1.1.0 or later.
13
14
  // 3. stale: for each open, agreed change, each requirement it MODIFIES, REMOVES or RENAMES is, block by
14
15
  // block, as it was at the agreed commit (the latest amendment's, else sdd.commit; M-ADR 0223
@@ -73,7 +74,7 @@ import { tmpdir } from "node:os";
73
74
  import { basename, join } from "node:path";
74
75
  import { promisify } from "node:util";
75
76
  import { parseDocument } from "yaml";
76
- import { contractMoves, observedIn, orderArchives, parseRequirements, parseSurfaces, renamedFrom, renamePairs, resolveImplements, surfaceMatches } from "../behaviour-store/archive.js";
77
+ import { contractMoves, generatedByProblems, observedIn, observedInProblems, orderArchives, parseRequirements, parseSurfaces, renamedFrom, renamePairs, resolveImplements, surfaceMatches } from "../behaviour-store/archive.js";
77
78
  import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
78
79
  import { boundFile, conditionProblems, structuralFile, isCommit, normaliseBound, requirementBlock, sameCommit, scenarioKind, taskGroups, testPlanHeader, testPlanMapping, verdictLine, versionAtLeast } from "./agreement-evidence.js";
79
80
  import { ENFORCED as STRUCTURE_ENFORCED } from "./structural-model.js";
@@ -443,6 +444,7 @@ export async function checkBehaviourStore(repo, options = {}) {
443
444
  // 2 and 3. open changes
444
445
  const open = (await dirs(join(repo, STORE_DIR, "changes"))).filter((d) => d !== "archive");
445
446
  let started = 0;
447
+ let baselinesTicked = 0;
446
448
  let staleChecked = 0;
447
449
  let contentChecked = 0;
448
450
  let kindsChecked = 0;
@@ -450,7 +452,13 @@ export async function checkBehaviourStore(repo, options = {}) {
450
452
  const dir = `${STORE_DIR}/changes/${change}`;
451
453
  const tasks = await readText(join(repo, dir, "tasks.md"));
452
454
  const record = records.get(change);
453
- if (tasks !== null && TASK_DONE_RE.test(tasks)) {
455
+ // A baseline (its baseline-evidence.md, the signal the archive side reads) has no agreement record and no Gate 0;
456
+ // its tasks are the characterization tests, ticked as each is written (sdd-baseline, M-ADR 0222 §4.3). Its gate is
457
+ // checked when it archives.
458
+ const baseline = await exists(join(repo, dir, "baseline-evidence.md"));
459
+ if (tasks !== null && TASK_DONE_RE.test(tasks) && baseline)
460
+ baselinesTicked++;
461
+ if (tasks !== null && TASK_DONE_RE.test(tasks) && !baseline) {
454
462
  started++;
455
463
  if (!record && recordFiles.some((f) => f.replace(/\.ya?ml$/, "") === change))
456
464
  failures.push(`${dir}: has completed tasks but its agreement record (${AGREEMENTS_DIR}/${change}.yaml) is malformed; see its record failures`);
@@ -596,7 +604,7 @@ export async function checkBehaviourStore(repo, options = {}) {
596
604
  }
597
605
  }
598
606
  }
599
- lines.push(`agreement: ${open.length} open change(s), ${started} with completed tasks`);
607
+ lines.push(`agreement: ${open.length} open change(s), ${started} with completed tasks${baselinesTicked > 0 ? `; ${baselinesTicked} open baseline(s) with ticked characterization tests, which need no agreement or Gate 0` : ""}`);
600
608
  lines.push(staleChecked === 0 ? "stale: vacuous (no open agreed change modifies, removes or renames a requirement)" : `stale: ${staleChecked} targeted requirement(s) checked against their agreed commits`);
601
609
  lines.push(`agreed content: ${contentChecked} open agreed change(s) compared with their pinned commits; ${kindsChecked} test-plan scenario kind(s) checked`);
602
610
  // 8. overlap: two open, agreed changes never name the same requirement of one living spec (M-ADR 0223 §4.10).
@@ -839,6 +847,7 @@ export async function checkBehaviourStore(repo, options = {}) {
839
847
  const servedBy = baselines + openBaselines.length > 0 ? await catalogueContracts(repo) : null;
840
848
  let surfacesChecked = 0;
841
849
  let structureOnly = 0;
850
+ let generatedBaselines = 0;
842
851
  // The contract moves each archived change records, in archive order. A contract's id derives from its path, so a
843
852
  // move changes it; an archived baseline's list names the contract as it was, and is resolved through the moves
844
853
  // archived after it rather than edited (M-ADR 0222 §4.4). A surface that vanished with no move still fails.
@@ -916,9 +925,16 @@ export async function checkBehaviourStore(repo, options = {}) {
916
925
  /** What is wrong with a baseline's Surfaces list against the catalogue (M-ADR 0222 §4.3, §4.4). */
917
926
  const surfaceProblems = async (dir, evidence, after = -1) => {
918
927
  const out = [];
928
+ const malformed = observedInProblems(evidence);
929
+ if (malformed.length > 0) {
930
+ const shown = malformed.slice(0, 3).map((e) => JSON.stringify(e.length > 60 ? `${e.slice(0, 57)}...` : e)).join(", ");
931
+ out.push(`${dir}/baseline-evidence.md: **Observed in:** has ${malformed.length} entr${malformed.length === 1 ? "y" : "ies"} not shaped "<repository> @ <commit>" (${shown}${malformed.length > 3 ? ", ..." : ""}); the line holds only those, comma-separated, and detail goes in the body (agreement-record-schema.yaml, Baselines)`);
932
+ }
919
933
  const parsed = parseSurfaces(evidence);
920
934
  const admits = await admitsBehaviour(dir);
921
935
  if (!admits) {
936
+ if (parsed.generated)
937
+ return [`${dir}/baseline-evidence.md: **Surfaces:** generated, but the baseline admits no behaviour (no requirement in its spec deltas): write "**Surfaces:** none"`];
922
938
  if (parsed.none) {
923
939
  structureOnly++;
924
940
  return out;
@@ -930,6 +946,30 @@ export async function checkBehaviourStore(repo, options = {}) {
930
946
  else if (parsed.none) {
931
947
  return [`${dir}/baseline-evidence.md: **Surfaces:** none, but the baseline admits behaviour (requirements in ${dir}/specs/); name every contract the observed repositories serve, covered at transport or excluded with a reason`];
932
948
  }
949
+ if (parsed.generated) {
950
+ // A generator serves nothing: its transport is the generated tree (agreement-record-schema.yaml, Baselines). The
951
+ // claim holds only where the catalogue confirms that no observed repository serves a contract, and where the run
952
+ // is recorded so another reader can repeat it; anything else fails, so the claim is never a way past the list.
953
+ for (const p of generatedByProblems(evidence)) {
954
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** generated needs the run recorded as "**Generated by:** <generator repository> @ <commit>; run: \`<command and options>\`; tree: <hash>": ${p}`);
955
+ }
956
+ if (servedBy === null) {
957
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** generated cannot be cross-checked: catalogues/repo_contract.csv or catalogues/contracts.csv is missing; build the catalogue`);
958
+ return out;
959
+ }
960
+ const repos = observedIn(evidence);
961
+ if (repos.length === 0)
962
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** generated cannot be cross-checked: no **Observed in:** line names a repository`);
963
+ for (const r of repos) {
964
+ const served = servedBy.get(r)?.served ?? [];
965
+ if (served.length > 0) {
966
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** generated, but ${r} serves ${served.length} contract(s) (catalogues/repo_contract.csv: ${served.slice(0, 3).map((c) => c.id).join(", ")}${served.length > 3 ? ", ..." : ""}); a repository that serves contracts lists each, covered at transport or excluded with a reason`);
967
+ }
968
+ }
969
+ if (out.length === 0)
970
+ generatedBaselines++;
971
+ return out;
972
+ }
933
973
  if (!parsed.present)
934
974
  out.push(`${dir}/baseline-evidence.md: no **Surfaces:** list; name every contract the observed repositories serve, covered at transport or excluded with a reason`);
935
975
  for (const p of parsed.problems)
@@ -1048,7 +1088,7 @@ export async function checkBehaviourStore(repo, options = {}) {
1048
1088
  if (openBaselines.length > 0)
1049
1089
  lines.push(`open baselines: ${openBaselines.length}, asked the archive's questions as warnings`);
1050
1090
  const unseen = servedBy === null ? "" : `; repositories with no catalogue contracts: ${[...new Set(await observedInAll(repo, archiveRel, order))].filter((r) => (servedBy.get(r)?.served ?? []).length === 0).join(", ") || "none"}`;
1051
- lines.push(baselines === 0 ? "surfaces: vacuous (no baseline)" : `surfaces: ${baselines} baseline(s), ${surfacesChecked} served contract(s) checked against catalogues/repo_contract.csv less what service_contract records only consumed${unseen}${structureOnly ? `; ${structureOnly} structure-only baseline(s) admitting no behaviour, Surfaces: none` : ""}${historical.size ? `; ${historical.size} historical baseline(s) not checked (every requirement they baselined has left the living specs)` : ""}`);
1091
+ lines.push(baselines === 0 ? "surfaces: vacuous (no baseline)" : `surfaces: ${baselines} baseline(s), ${surfacesChecked} served contract(s) checked against catalogues/repo_contract.csv less what service_contract records only consumed${unseen}${structureOnly ? `; ${structureOnly} structure-only baseline(s) admitting no behaviour, Surfaces: none` : ""}${generatedBaselines ? `; ${generatedBaselines} generator baseline(s) serving no contract, Surfaces: generated` : ""}${historical.size ? `; ${historical.size} historical baseline(s) not checked (every requirement they baselined has left the living specs)` : ""}`);
1052
1092
  lines.push(scopeChecked === 0 ? "scope: vacuous (no baseline removes, modifies or renames a requirement)" : `scope: ${scopeChecked} requirement(s) a baseline removes, modifies or renames, checked against their lineage`);
1053
1093
  if (!history && archived.length > 0)
1054
1094
  lines.push("order: no git history (not a git work tree, or git log failed), so same-day archives cannot be ordered");
@@ -0,0 +1,228 @@
1
+ // carve-citations: behaviour carved out of one SDD into another keeps one home at a time
2
+ // (docs/method/methods/guides/carving-behaviour-into-a-new-sdd.md; M-ADR 0221, 0222).
3
+ //
4
+ // The new SDD's requirement names what it replaces, on a line of its own:
5
+ // Replaces <sdd-id>:<feature>::<requirement name>.
6
+ // Departs from <sdd-id>:<feature>::<requirement name>: <why>.
7
+ // At cutover the old SDD removes the requirement, its Reason naming the replacement:
8
+ // replaced by <sdd-id>:<feature>::<requirement name>
9
+ //
10
+ // The other SDD is found beside this one: a sibling directory whose .sdd/sdd-manifest.yaml has that sdd_id. Checks:
11
+ // replaces: each Replaces / Departs line parses, and names a requirement the other SDD has: living there (in
12
+ // flight), or removed by an archived change (carved).
13
+ // replaced: each removal's `replaced by` names a requirement living in the other SDD.
14
+ // one home: a requirement removed here as replaced by another SDD is not living here again; and no two living
15
+ // requirements here replace the same requirement.
16
+ // A citation in a living spec or an archived change fails; one in an open change is a [WARN] (it is not agreed yet),
17
+ // as is any citation whose other SDD has no checkout beside this one (unresolved, failed under --strict). Cutover
18
+ // itself is an event of the estate's, not recorded in the store, so a requirement both SDDs hold before the old one is
19
+ // removed is "in flight" and is reported in the inventory, not failed.
20
+ import { readdir, readFile } from "node:fs/promises";
21
+ import { basename, dirname, join } from "node:path";
22
+ const STORE = "openspec";
23
+ const REQ_HEAD = /^###\s+Requirement:\s*(.+?)\s*$/;
24
+ const SECTION = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
25
+ const CITE_START = /^\s*(Replaces|Departs from)\s/;
26
+ const REPLACES = /^\s*Replaces\s+([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)::(.+?)\.\s*$/;
27
+ const DEPARTS = /^\s*Departs from\s+([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)::(.+?):\s+\S.*$/;
28
+ const REPLACED_BY = /replaced by\s+`?([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)::([^`\n]+?)`?\s*(?:[.;,)]\s*)?$/im;
29
+ const key = (r) => `${r.sdd}:${r.feature}::${r.name}`;
30
+ async function read(p) {
31
+ return readFile(p, "utf8").catch(() => null);
32
+ }
33
+ async function dirs(p) {
34
+ try {
35
+ return (await readdir(p, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ /** Requirement blocks of a spec or delta file, each with the section it sits in. */
42
+ function blocks(text, living) {
43
+ const out = [];
44
+ let op = living ? "living" : "";
45
+ let cur = null;
46
+ for (const line of text.split("\n")) {
47
+ const s = SECTION.exec(line);
48
+ if (s) {
49
+ if (cur)
50
+ out.push(cur);
51
+ cur = null;
52
+ op = s[1].toUpperCase();
53
+ continue;
54
+ }
55
+ const h = REQ_HEAD.exec(line);
56
+ if (h) {
57
+ if (cur)
58
+ out.push(cur);
59
+ cur = { name: h[1], op, text: "" };
60
+ continue;
61
+ }
62
+ if (/^##\s/.test(line) && cur) {
63
+ out.push(cur);
64
+ cur = null;
65
+ continue;
66
+ }
67
+ if (cur)
68
+ cur.text += `${line}\n`;
69
+ }
70
+ if (cur)
71
+ out.push(cur);
72
+ return out;
73
+ }
74
+ async function sddId(repo) {
75
+ const m = /^sdd_id:\s*["']?([^"'\n]+?)["']?\s*$/m.exec((await read(join(repo, ".sdd", "sdd-manifest.yaml"))) ?? "");
76
+ return m ? m[1] : "";
77
+ }
78
+ async function view(repo) {
79
+ const living = new Map();
80
+ for (const f of await dirs(join(repo, STORE, "specs"))) {
81
+ const t = await read(join(repo, STORE, "specs", f, "spec.md"));
82
+ if (t !== null)
83
+ living.set(f, new Set(blocks(t, true).map((b) => b.name)));
84
+ }
85
+ const removed = new Map();
86
+ for (const c of await dirs(join(repo, STORE, "changes", "archive"))) {
87
+ for (const f of await dirs(join(repo, STORE, "changes", "archive", c, "specs"))) {
88
+ const t = await read(join(repo, STORE, "changes", "archive", c, "specs", f, "spec.md"));
89
+ for (const b of t === null ? [] : blocks(t, false)) {
90
+ if (b.op === "REMOVED")
91
+ (removed.get(f) ?? removed.set(f, new Set()).get(f)).add(b.name);
92
+ }
93
+ }
94
+ }
95
+ return { living, removed };
96
+ }
97
+ export async function checkCarveCitations(repo) {
98
+ const own = await sddId(repo);
99
+ const failures = [];
100
+ const warnings = [];
101
+ const cites = [];
102
+ const removals = [];
103
+ const collect = (rel, text, living, open, removalsOnly = false) => {
104
+ for (const b of blocks(text, living)) {
105
+ if (removalsOnly && b.op !== "REMOVED")
106
+ continue;
107
+ if (b.op === "REMOVED") {
108
+ const m = REPLACED_BY.exec(b.text);
109
+ if (m)
110
+ removals.push({ where: `${rel}: '${b.name}'`, open, ref: { sdd: m[1], feature: m[2], name: m[3].trim() }, feature: basename(dirname(rel)), name: b.name });
111
+ continue;
112
+ }
113
+ for (const line of b.text.split("\n")) {
114
+ if (!CITE_START.test(line))
115
+ continue;
116
+ const r = REPLACES.exec(line);
117
+ const d = r ? null : DEPARTS.exec(line);
118
+ const m = r ?? d;
119
+ cites.push({
120
+ where: `${rel}: '${b.name}'`,
121
+ open,
122
+ line: line.trim(),
123
+ ref: m ? { sdd: m[1], feature: m[2], name: m[3].trim() } : null,
124
+ kind: /^\s*Departs/.test(line) ? "departs" : "replaces",
125
+ owner: living ? `${basename(dirname(rel))}::${b.name}` : "",
126
+ });
127
+ }
128
+ }
129
+ };
130
+ for (const f of await dirs(join(repo, STORE, "specs"))) {
131
+ const rel = `${STORE}/specs/${f}/spec.md`;
132
+ const t = await read(join(repo, rel));
133
+ if (t !== null)
134
+ collect(rel, t, true, false);
135
+ }
136
+ for (const c of await dirs(join(repo, STORE, "changes"))) {
137
+ const archived = c === "archive";
138
+ for (const ch of archived ? await dirs(join(repo, STORE, "changes", "archive")) : [c]) {
139
+ const base = archived ? `${STORE}/changes/archive/${ch}` : `${STORE}/changes/${ch}`;
140
+ for (const f of await dirs(join(repo, base, "specs"))) {
141
+ const rel = `${base}/specs/${f}/spec.md`;
142
+ const t = await read(join(repo, rel));
143
+ // An archived delta's Replaces lines live on in the living spec it produced; only its removals are read here.
144
+ if (t !== null)
145
+ collect(rel, t, false, !archived, archived);
146
+ }
147
+ }
148
+ }
149
+ if (cites.length === 0 && removals.length === 0) {
150
+ return { exitCode: 0, stdout: "[N/A] no carve citations (Replaces / Departs from lines, or removals replaced by another SDD) in this repository\n" };
151
+ }
152
+ // The other SDDs, found beside this one by their sdd_id.
153
+ const siblings = new Map();
154
+ const parent = dirname(repo);
155
+ for (const d of await dirs(parent)) {
156
+ const p = join(parent, d);
157
+ const id = await sddId(p);
158
+ if (id !== "" && !siblings.has(id))
159
+ siblings.set(id, p);
160
+ }
161
+ const self = await view(repo);
162
+ if (own !== "")
163
+ siblings.set(own, repo);
164
+ const views = new Map();
165
+ const other = async (id) => {
166
+ const p = siblings.get(id);
167
+ if (!p)
168
+ return null;
169
+ if (!views.has(id))
170
+ views.set(id, p === repo ? self : await view(p));
171
+ return views.get(id);
172
+ };
173
+ const report = (open, msg) => (open ? warnings : failures).push(msg);
174
+ let inFlight = 0;
175
+ let carved = 0;
176
+ let unresolved = 0;
177
+ const replacedBy = new Map();
178
+ for (const c of cites) {
179
+ if (c.ref === null) {
180
+ report(c.open, `${c.where}: "${c.line}" is not "Replaces <sdd-id>:<feature>::<requirement name>." or "Departs from <sdd-id>:<feature>::<requirement name>: <why>."`);
181
+ continue;
182
+ }
183
+ const v = await other(c.ref.sdd);
184
+ if (v === null) {
185
+ unresolved++;
186
+ warnings.push(`${c.where}: ${key(c.ref)} unresolved: no checkout beside this SDD has sdd_id "${c.ref.sdd}"; check it out beside this one (failed under --strict)`);
187
+ continue;
188
+ }
189
+ if (v.living.get(c.ref.feature)?.has(c.ref.name))
190
+ inFlight++;
191
+ else if (v.removed.get(c.ref.feature)?.has(c.ref.name))
192
+ carved++;
193
+ else {
194
+ report(c.open, `${c.where}: ${c.kind === "departs" ? "Departs from" : "Replaces"} ${key(c.ref)}, which ${c.ref.sdd} neither has nor removed (feature ${c.ref.feature}${v.living.has(c.ref.feature) ? "" : ", which it has no living spec for"})`);
195
+ continue;
196
+ }
197
+ if (c.owner !== "")
198
+ (replacedBy.get(key(c.ref)) ?? replacedBy.set(key(c.ref), []).get(key(c.ref))).push(c.owner);
199
+ }
200
+ for (const [k, owners] of replacedBy) {
201
+ if (owners.length > 1)
202
+ failures.push(`one home: ${k} is replaced by ${owners.length} living requirements here (${owners.join(", ")}); one requirement carries it`);
203
+ }
204
+ for (const r of removals) {
205
+ const v = await other(r.ref.sdd);
206
+ if (v === null) {
207
+ unresolved++;
208
+ warnings.push(`${r.where}: replaced by ${key(r.ref)} unresolved: no checkout beside this SDD has sdd_id "${r.ref.sdd}" (failed under --strict)`);
209
+ }
210
+ else if (!v.living.get(r.ref.feature)?.has(r.ref.name)) {
211
+ report(r.open, `${r.where}: removed as replaced by ${key(r.ref)}, which is not a living requirement of ${r.ref.sdd}`);
212
+ }
213
+ if (!r.open && self.living.get(r.feature)?.has(r.name)) {
214
+ failures.push(`one home: ${r.where}: removed here as replaced by ${key(r.ref)}, but '${r.name}' is living in ${STORE}/specs/${r.feature}/spec.md again`);
215
+ }
216
+ }
217
+ const lines = [
218
+ ` citations: ${cites.length} Replaces / Departs line(s): ${inFlight} in flight (still living where they came from), ${carved} carved (removed there), ${unresolved} unresolved`,
219
+ ` removals: ${removals.length} replaced by another SDD`,
220
+ ];
221
+ const body = lines.join("\n") + "\n";
222
+ const warn = warnings.map((w) => `[WARN] ${w}\n`).join("");
223
+ if (failures.length > 0) {
224
+ return { exitCode: 1, stdout: body + failures.map((f) => `[FAIL] ${f}\n`).join("") + warn + `[FAIL] carve citations: ${failures.length} problem(s)\n` };
225
+ }
226
+ return { exitCode: 0, stdout: body + warn + `[PASS] carve citations: one home each${warnings.length ? ", with the warnings above" : ""}\n` };
227
+ }
228
+ //# sourceMappingURL=carve-citations.js.map
@@ -1,6 +1,7 @@
1
1
  import { checkArchetypeCitations } from "./archetype-citations.js";
2
2
  import { checkBehaviourStore, checkBehaviourStoreOpenspec } from "./behaviour-store.js";
3
3
  import { checkBundleCoherence } from "./bundle-coherence.js";
4
+ import { checkCarveCitations } from "./carve-citations.js";
4
5
  import { checkFeatureSpecPointers } from "./feature-spec-pointers.js";
5
6
  import { checkIntents } from "./intents.js";
6
7
  import { checkJourneyImpact } from "./journey-impact.js";
@@ -25,6 +26,12 @@ const BEHAVIOUR_STORE_VALIDATORS = [
25
26
  source: { kind: "native", run: async (ctx) => checkArchetypeCitations(ctx.targetRepo) },
26
27
  build: () => ({ status: "run", args: [] }),
27
28
  },
29
+ // Behaviour carved into another SDD keeps one home at a time (the carving guide); without citations it reports [N/A].
30
+ {
31
+ name: "carve-citations",
32
+ source: { kind: "native", run: async (ctx) => checkCarveCitations(ctx.targetRepo) },
33
+ build: () => ({ status: "run", args: [] }),
34
+ },
28
35
  // Intents (M-ADR 0227) can sit in an SDD of any profile; without any the check reports [N/A].
29
36
  {
30
37
  name: "intents",
@@ -162,7 +162,7 @@ export async function checkStructuralModel(repo, options = {}) {
162
162
  // The current view.
163
163
  const unresolved = new Set();
164
164
  const currentProblems = [];
165
- validateView(schema, view, builtIn(built, allNew, new Set()), currentProblems, unresolved, addedBy);
165
+ validateView(schema, view, builtIn(built, allNew, new Set()), currentProblems, unresolved, addedBy, profile);
166
166
  problems.push(...currentProblems);
167
167
  const elementCount = [...view.values()].reduce((n, m) => n + m.size, 0);
168
168
  lines.push(files === null ? `model: none (${STRUCTURE_DIR}/ absent)` : `model: ${files.length} partition file(s), ${elementCount} element(s) in ${view.size} catalogue(s)`);
@@ -211,7 +211,7 @@ export async function checkStructuralModel(repo, options = {}) {
211
211
  if (s.noOp && s.deltas.length > 0)
212
212
  problems.push(`${s.noOp.file}: a no-op claim beside ${s.deltas.length} partition delta(s); state one or the other`);
213
213
  const elsewhere = new Map([...addedByChange].filter(([, c]) => c !== s.change));
214
- validateDeltas(schema, view, s.deltas, elsewhere, problems);
214
+ validateDeltas(schema, view, s.deltas, elsewhere, problems, profile);
215
215
  // A behaviour delta for a feature the model does not hold needs that feature added here.
216
216
  const adds = new Set(s.deltas.flatMap((d) => d.added.filter((e) => e.catalogue === "features").map((e) => e.id)));
217
217
  for (const feature of await subdirs(join(repo, dir, "specs"))) {
@@ -238,7 +238,7 @@ export async function checkStructuralModel(repo, options = {}) {
238
238
  if (oc.structure.deltas.length === 0)
239
239
  continue;
240
240
  const at = [];
241
- validateView(schema, viewAt(view, name, open), builtIn(built, allNew, additions.contracts(closure(name, open))), at, new Set(), addedBy);
241
+ validateView(schema, viewAt(view, name, open), builtIn(built, allNew, additions.contracts(closure(name, open))), at, new Set(), addedBy, profile);
242
242
  for (const p of at) {
243
243
  if (known.has(p))
244
244
  continue;
@@ -250,7 +250,7 @@ export async function checkStructuralModel(repo, options = {}) {
250
250
  const agreed = agreedOrder(open);
251
251
  if (agreed.length > 0) {
252
252
  const inIntended = [];
253
- validateView(schema, intended(view, open), builtIn(built, allNew, additions.contracts(agreed)), inIntended, new Set(), addedBy);
253
+ validateView(schema, intended(view, open), builtIn(built, allNew, additions.contracts(agreed)), inIntended, new Set(), addedBy, profile);
254
254
  for (const p of inIntended) {
255
255
  if (known.has(p) || atAgreed.has(p))
256
256
  continue;
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.17.2",
3
- "built_against_sdd_cli": "5.3.2"
2
+ "version": "0.18.1",
3
+ "built_against_sdd_cli": "5.4.1"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "5.3.2",
3
- "built_against_sdd_cli": "5.3.2"
2
+ "version": "5.4.1",
3
+ "built_against_sdd_cli": "5.4.1"
4
4
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sdd-method/author-cli",
3
- "version": "0.16.2",
4
- "sddCliVersion": "5.3.2",
3
+ "version": "0.17.1",
4
+ "sddCliVersion": "5.4.1",
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",