@sdd-method/author-cli 0.17.0 → 0.17.2
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.
- package/dist/lib/behaviour-store/archive.js +31 -14
- package/dist/lib/catalogue/builders/repositories.js +6 -3
- package/dist/lib/plan/read-sdd.js +1 -0
- package/dist/lib/sdd-index/repo-config.js +1 -0
- package/dist/lib/structure/archive.js +11 -5
- package/dist/lib/structure/view.js +8 -6
- package/dist/lib/validate/behaviour-store.js +28 -1
- package/dist/lib/validate/structural-model.js +4 -4
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/scripts/validate-app-docs.sh +9 -1
|
@@ -172,36 +172,53 @@ 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
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
generated = true;
|
|
198
|
+
claim = "generated";
|
|
197
199
|
}
|
|
198
200
|
});
|
|
201
|
+
const base = { present: true, none: false, generated: false, noneServed: null, entries: [], problems: [] };
|
|
199
202
|
if (at < 0)
|
|
200
|
-
return {
|
|
201
|
-
if (none)
|
|
202
|
-
return {
|
|
203
|
-
if (generated)
|
|
204
|
-
return {
|
|
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 m = /none\s+served\s*\(([^)]*)\)\s*\.?\s*$/i.exec(lines[at]);
|
|
212
|
+
const triggers = m ? m[1].split(",").map((t) => t.trim()).filter((t) => t !== "") : [];
|
|
213
|
+
const problems = [];
|
|
214
|
+
if (!m || triggers.length === 0)
|
|
215
|
+
problems.push(`"none served" names what drives the service: "**Surfaces:** none served (${NONE_SERVED_TRIGGERS.join(" | ")})"`);
|
|
216
|
+
for (const t of triggers) {
|
|
217
|
+
if (!NONE_SERVED_TRIGGERS.includes(t))
|
|
218
|
+
problems.push(`"none served (${t})": "${t}" is not one of ${NONE_SERVED_TRIGGERS.join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
return { ...base, noneServed: triggers, problems };
|
|
221
|
+
}
|
|
205
222
|
const entries = [];
|
|
206
223
|
const problems = [];
|
|
207
224
|
for (const line of lines.slice(at + 1)) {
|
|
@@ -236,7 +253,7 @@ export function parseSurfaces(evidence) {
|
|
|
236
253
|
}
|
|
237
254
|
if (entries.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, entries, problems };
|
|
240
257
|
}
|
|
241
258
|
/**
|
|
242
259
|
* 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
|
|
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
|
|
40
|
-
|
|
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
|
})
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -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
|
|
@@ -1088,7 +1115,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
1088
1115
|
if (openBaselines.length > 0)
|
|
1089
1116
|
lines.push(`open baselines: ${openBaselines.length}, asked the archive's questions as warnings`);
|
|
1090
1117
|
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)` : ""}`);
|
|
1118
|
+
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
1119
|
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
1120
|
if (!history && archived.length > 0)
|
|
1094
1121
|
lines.push("order: no git history (not a git work tree, or git log failed), so same-day archives cannot be ordered");
|
|
@@ -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;
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/author-cli",
|
|
3
|
-
"version": "0.17.
|
|
4
|
-
"sddCliVersion": "5.4.
|
|
3
|
+
"version": "0.17.2",
|
|
4
|
+
"sddCliVersion": "5.4.2",
|
|
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
|
-
|
|
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
|
|