@sdd-method/author-cli 0.17.0 → 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.
@@ -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 ")}`);
@@ -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;
@@ -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.18.0",
3
- "built_against_sdd_cli": "5.4.0"
2
+ "version": "0.18.1",
3
+ "built_against_sdd_cli": "5.4.1"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "5.4.0",
3
- "built_against_sdd_cli": "5.4.0"
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.17.0",
4
- "sddCliVersion": "5.4.0",
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",