@sdd-method/author-cli 0.17.1 → 0.17.3

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.
@@ -172,37 +172,48 @@ export function observedInProblems(evidence) {
172
172
  * `**Surfaces:** none` on one line is the typed claim that the baseline lists no surface, which only a baseline that
173
173
  * admits no behaviour may make (the caller decides; `none` is true and the list is empty).
174
174
  */
175
+ /** What drives a service that serves no contract (sdm-xfln): the triggers `**Surfaces:** none served (...)` may name. */
176
+ export const NONE_SERVED_TRIGGERS = ["scheduled", "queue-triggered", "stream-triggered"];
175
177
  export function parseSurfaces(evidence) {
176
178
  const lines = evidence.split("\n");
177
179
  // The LAST standalone line: an evidence file may carry an earlier prose section under the same label, and
178
180
  // the list is what the file ends up asserting.
179
181
  let at = -1;
180
- let none = false;
181
- let generated = false;
182
+ let claim = "list";
182
183
  lines.forEach((l, i) => {
183
184
  if (/^\s*\*\*Surfaces:\*\*\s*$/.test(l)) {
184
185
  at = i;
185
- none = false;
186
- generated = false;
186
+ claim = "list";
187
+ }
188
+ else if (/^\s*\*\*Surfaces:\*\*\s*none\s+served\b/i.test(l)) {
189
+ at = i;
190
+ claim = "none served";
187
191
  }
188
192
  else if (/^\s*\*\*Surfaces:\*\*\s*none\b/i.test(l)) {
189
193
  at = i;
190
- none = true;
191
- generated = false;
194
+ claim = "none";
192
195
  }
193
196
  else if (/^\s*\*\*Surfaces:\*\*\s*generated\b/i.test(l)) {
194
197
  at = i;
195
- none = false;
196
- generated = true;
198
+ claim = "generated";
197
199
  }
198
200
  });
201
+ const base = { present: true, none: false, generated: false, noneServed: null, repoClaims: [], entries: [], problems: [] };
199
202
  if (at < 0)
200
- return { present: false, none: false, generated: false, entries: [], problems: [] };
201
- if (none)
202
- return { present: true, none: true, generated: false, entries: [], problems: [] };
203
- if (generated)
204
- return { present: true, none: false, generated: true, entries: [], problems: [] };
203
+ return { ...base, present: false };
204
+ if (claim === "none")
205
+ return { ...base, none: true };
206
+ if (claim === "generated")
207
+ return { ...base, generated: true };
208
+ if (claim === "none served") {
209
+ // `**Surfaces:** none served (scheduled)`: the service serves no contract; what drives it is named, from a
210
+ // closed vocabulary, so the claim says why and cannot be left vague.
211
+ const problems = [];
212
+ const triggers = noneServedTriggers(lines[at], "**Surfaces:** none served", problems);
213
+ return { ...base, noneServed: triggers, problems };
214
+ }
205
215
  const entries = [];
216
+ const repoClaims = [];
206
217
  const problems = [];
207
218
  for (const line of lines.slice(at + 1)) {
208
219
  if (line.trim() === "" && entries.length + problems.length === 0)
@@ -217,6 +228,12 @@ export function parseSurfaces(evidence) {
217
228
  }
218
229
  const contract = m[1].trim();
219
230
  const status = m[2].split(";")[0].trim();
231
+ if (/^none\s+served\b/i.test(status)) {
232
+ // `- <repo>: none served (<trigger>)`: that observed repository serves no contract (sdm-wz6a); the list's other
233
+ // items still account for the contracts the other observed repositories serve.
234
+ repoClaims.push({ repo: contract, triggers: noneServedTriggers(status, `- ${contract}: none served`, problems) });
235
+ continue;
236
+ }
220
237
  const excluded = /^excluded\s*,\s*(.+)$/i.exec(status);
221
238
  if (/^covered at transport$/i.test(status)) {
222
239
  if (contract.includes("*"))
@@ -234,9 +251,21 @@ export function parseSurfaces(evidence) {
234
251
  problems.push(`"${contract}: ${status}" is neither "covered at transport" nor "excluded, <reason>"`);
235
252
  }
236
253
  }
237
- if (entries.length + problems.length === 0)
254
+ if (entries.length + repoClaims.length + problems.length === 0)
238
255
  problems.push('the **Surfaces:** line lists no contract; a baseline that admits no behaviour writes "**Surfaces:** none"');
239
- return { present: true, none: false, generated: false, entries, problems };
256
+ return { present: true, none: false, generated: false, noneServed: null, repoClaims, entries, problems };
257
+ }
258
+ /** The triggers of a `none served (<trigger>[, <trigger>])` claim, reporting a missing or unknown one. */
259
+ function noneServedTriggers(text, form, problems) {
260
+ const m = /none\s+served\s*\(([^)]*)\)\s*\.?\s*$/i.exec(text);
261
+ const triggers = m ? m[1].split(",").map((t) => t.trim()).filter((t) => t !== "") : [];
262
+ if (!m || triggers.length === 0)
263
+ problems.push(`"none served" names what drives the service: "${form} (${NONE_SERVED_TRIGGERS.join(" | ")})"`);
264
+ for (const t of triggers) {
265
+ if (!NONE_SERVED_TRIGGERS.includes(t))
266
+ problems.push(`"none served (${t})": "${t}" is not one of ${NONE_SERVED_TRIGGERS.join(", ")}`);
267
+ }
268
+ return triggers;
240
269
  }
241
270
  /**
242
271
  * A generator's baseline (agreement-record-schema.yaml, Baselines: generated behaviour) serves no contract; its
@@ -33,17 +33,20 @@ const LOC_REPORT_REL = join("docs", "platform", "reference", "loc-report.csv");
33
33
  const REPOS_DIR_REL = "repos";
34
34
  const REVERSE_METADATA_BASENAME = "reverse-metadata.yaml";
35
35
  // The contract kinds a repo-config `contracts:` block keys, under `contracts/<kind>/`. The sdd-index reader
36
- // accepts the same six; `spicedb` (an `authz` contract) was once missing here, and zod strips an unknown key,
36
+ // accepts the same seven; `spicedb` (an `authz` contract) was once missing here, and zod strips an unknown key,
37
37
  // so a declared `.zed` schema vanished without a finding. `cli` (schema 4.1.0) declares the command manifest a
38
38
  // command-line repository serves, and `ui` (schema 7.2.0) the manifest of an embeddable UI surface a UI-only
39
- // repository serves. GraphQL is not keyed in repo-config: that is its own question, on this schema.
40
- export const REPO_CONFIG_CONTRACT_KINDS = ["openapi", "proto", "avro", "spicedb", "cli", "ui"];
39
+ // repository serves. `graphql` declares a GraphQL SDL a repository serves: the contracts builder discovered it,
40
+ // and the kind enum held it, while repo-config could not key it, so a repository serving only GraphQL had no
41
+ // served contract and its baselines' Surfaces could never be cross-checked (sdm-pvy8).
42
+ export const REPO_CONFIG_CONTRACT_KINDS = ["openapi", "proto", "avro", "spicedb", "graphql", "cli", "ui"];
41
43
  const ContractsBlockSchema = z
42
44
  .object({
43
45
  openapi: z.array(z.string()).optional(),
44
46
  proto: z.array(z.string()).optional(),
45
47
  avro: z.array(z.string()).optional(),
46
48
  spicedb: z.array(z.string()).optional(),
49
+ graphql: z.array(z.string()).optional(),
47
50
  cli: z.array(z.string()).optional(),
48
51
  ui: z.array(z.string()).optional(),
49
52
  })
@@ -62,6 +62,7 @@ function legacyShape(node, sourcePath) {
62
62
  proto: [],
63
63
  avro: [],
64
64
  spicedb: [],
65
+ graphql: [],
65
66
  cli: [],
66
67
  ui: [],
67
68
  };
@@ -16,6 +16,7 @@ const CONTRACT_KINDS = [
16
16
  "proto",
17
17
  "avro",
18
18
  "spicedb",
19
+ "graphql",
19
20
  "cli",
20
21
  "ui",
21
22
  ];
@@ -848,6 +848,7 @@ export async function checkBehaviourStore(repo, options = {}) {
848
848
  let surfacesChecked = 0;
849
849
  let structureOnly = 0;
850
850
  let generatedBaselines = 0;
851
+ let noneServedBaselines = 0;
851
852
  // The contract moves each archived change records, in archive order. A contract's id derives from its path, so a
852
853
  // move changes it; an archived baseline's list names the contract as it was, and is resolved through the moves
853
854
  // archived after it rather than edited (M-ADR 0222 §4.4). A surface that vanished with no move still fails.
@@ -935,6 +936,8 @@ export async function checkBehaviourStore(repo, options = {}) {
935
936
  if (!admits) {
936
937
  if (parsed.generated)
937
938
  return [`${dir}/baseline-evidence.md: **Surfaces:** generated, but the baseline admits no behaviour (no requirement in its spec deltas): write "**Surfaces:** none"`];
939
+ if (parsed.noneServed !== null)
940
+ return [`${dir}/baseline-evidence.md: **Surfaces:** none served, but the baseline admits no behaviour (no requirement in its spec deltas): write "**Surfaces:** none"`];
938
941
  if (parsed.none) {
939
942
  structureOnly++;
940
943
  return out;
@@ -946,6 +949,30 @@ export async function checkBehaviourStore(repo, options = {}) {
946
949
  else if (parsed.none) {
947
950
  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`];
948
951
  }
952
+ if (parsed.noneServed !== null) {
953
+ // A service driven only by a schedule, a queue or a stream serves no contract: the Surfaces rule names what the
954
+ // observed repositories SERVE, and a contract a repository only consumes is not a surface. The claim holds only
955
+ // where the catalogue confirms that no observed repository serves a contract (sdm-xfln); anything else fails, so
956
+ // it is never a way past the list.
957
+ for (const p of parsed.problems)
958
+ out.push(`${dir}/baseline-evidence.md: Surfaces: ${p}`);
959
+ if (servedBy === null) {
960
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** none served cannot be cross-checked: catalogues/repo_contract.csv or catalogues/contracts.csv is missing; build the catalogue`);
961
+ return out;
962
+ }
963
+ const repos = observedIn(evidence);
964
+ if (repos.length === 0)
965
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** none served cannot be cross-checked: no **Observed in:** line names a repository`);
966
+ for (const r of repos) {
967
+ const served = servedBy.get(r)?.served ?? [];
968
+ if (served.length > 0) {
969
+ out.push(`${dir}/baseline-evidence.md: **Surfaces:** none served, 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`);
970
+ }
971
+ }
972
+ if (out.length === 0)
973
+ noneServedBaselines++;
974
+ return out;
975
+ }
949
976
  if (parsed.generated) {
950
977
  // A generator serves nothing: its transport is the generated tree (agreement-record-schema.yaml, Baselines). The
951
978
  // claim holds only where the catalogue confirms that no observed repository serves a contract, and where the run
@@ -983,8 +1010,25 @@ export async function checkBehaviourStore(repo, options = {}) {
983
1010
  const repos = observedIn(evidence);
984
1011
  if (repos.length === 0)
985
1012
  out.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: no **Observed in:** line names a repository`);
1013
+ // `- <repo>: none served (<trigger>)` items (sdm-wz6a): each names an observed repository the catalogue records
1014
+ // serving no contract; one serving a contract lists its contracts instead.
1015
+ const claimed = new Set();
1016
+ for (const c of parsed.repoClaims) {
1017
+ if (!repos.includes(c.repo)) {
1018
+ out.push(`${dir}/baseline-evidence.md: Surfaces: "- ${c.repo}: none served" names a repository **Observed in:** does not (${repos.join(", ") || "none"})`);
1019
+ continue;
1020
+ }
1021
+ const served = servedBy.get(c.repo)?.served ?? [];
1022
+ if (served.length > 0) {
1023
+ out.push(`${dir}/baseline-evidence.md: Surfaces: "- ${c.repo}: none served", but ${c.repo} serves ${served.length} contract(s) (catalogues/repo_contract.csv: ${served.slice(0, 3).map((x) => x.id).join(", ")}${served.length > 3 ? ", ..." : ""}); list each, covered at transport or excluded with a reason`);
1024
+ continue;
1025
+ }
1026
+ claimed.add(c.repo);
1027
+ }
986
1028
  for (const r of repos) {
987
1029
  const known = servedBy.get(r);
1030
+ if (claimed.has(r))
1031
+ continue;
988
1032
  if ((known?.served ?? []).length === 0) {
989
1033
  const consumed = known?.consumed ? ` (it consumes ${known.consumed}, which a baseline does not list)` : "";
990
1034
  out.push(`${dir}/baseline-evidence.md: Surfaces cannot be cross-checked: the catalogue records no contract that ${r} serves${consumed}; declare the contracts it serves in its repo-config`);
@@ -1088,7 +1132,7 @@ export async function checkBehaviourStore(repo, options = {}) {
1088
1132
  if (openBaselines.length > 0)
1089
1133
  lines.push(`open baselines: ${openBaselines.length}, asked the archive's questions as warnings`);
1090
1134
  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"}`;
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)` : ""}`);
1135
+ 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` : ""}${noneServedBaselines ? `; ${noneServedBaselines} baseline(s) of services serving no contract, Surfaces: none served` : ""}${historical.size ? `; ${historical.size} historical baseline(s) not checked (every requirement they baselined has left the living specs)` : ""}`);
1092
1136
  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`);
1093
1137
  if (!history && archived.length > 0)
1094
1138
  lines.push("order: no git history (not a git work tree, or git log failed), so same-day archives cannot be ordered");
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.18.1",
3
- "built_against_sdd_cli": "5.4.1"
2
+ "version": "0.18.3",
3
+ "built_against_sdd_cli": "5.4.3"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "5.4.1",
3
- "built_against_sdd_cli": "5.4.1"
2
+ "version": "5.4.3",
3
+ "built_against_sdd_cli": "5.4.3"
4
4
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sdd-method/author-cli",
3
- "version": "0.17.1",
4
- "sddCliVersion": "5.4.1",
3
+ "version": "0.17.3",
4
+ "sddCliVersion": "5.4.3",
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",
@@ -149,6 +149,10 @@ check_feature_specs() {
149
149
  local specs_failed=0
150
150
  local specs_passed=0
151
151
  local specs_flat=0
152
+ local sdd_id=""
153
+ if [[ -f "$APP_ROOT/.sdd/sdd-manifest.yaml" ]]; then
154
+ sdd_id="$(sed -nE 's/^sdd_id:[[:space:]]*["'"'"']?([^"'"'"' ]+)["'"'"']?[[:space:]]*$/\1/p' "$APP_ROOT/.sdd/sdd-manifest.yaml" | head -1)"
155
+ fi
152
156
 
153
157
  # Discover feature specs under features/*/
154
158
  local feature_spec
@@ -166,7 +170,11 @@ check_feature_specs() {
166
170
 
167
171
  if [[ "$(dirname "$feature_spec")" == "$APP_ROOT/features" ]]; then
168
172
  local name; name="$(basename "$feature_spec" .md)"
169
- log_warn "$rel: stored flat under features/; a feature spec's path is features/${name}/${name}.md, one directory per feature with its story files beside it (app-feature-spec skill). Advisory: reported, not failed"
173
+ # The directory and file take the feature's slug, the one feature-tree.md uses: a stem repeating the
174
+ # product id (the manifest's sdd_id) is not the slug, and the catalogue build refuses it.
175
+ local slug="$name"
176
+ if [[ -n "$sdd_id" && "$name" == "${sdd_id}-"* ]]; then slug="${name#"${sdd_id}-"}"; fi
177
+ log_warn "$rel: stored flat under features/; a feature spec's path is features/${slug}/${slug}.md, one directory per feature named by its feature-tree slug (app-feature-spec skill). Advisory: reported, not failed"
170
178
  specs_flat=$((specs_flat + 1))
171
179
  fi
172
180