@sdd-method/verify-cli 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  # Emitted by sdd-cli's YAML emitter with no line folding; the three header scalars are quoted for the
8
8
  # textual readers downstream. The one deliberate difference from the authored form is schema_status:
9
9
  # the authored form says canonical; a projection never does.
10
- schema_version: "7.3.0"
10
+ schema_version: "7.4.0"
11
11
  schema_owner: "sdd-method"
12
12
  schema_status: "generated"
13
13
  enums:
@@ -785,6 +785,11 @@ enums:
785
785
  - value: description
786
786
  description: A catalogue-entry description that many things share.
787
787
  description: An entity's Party-Place-Thing archetype, as the capability spec template classifies it (schema 7.1.0).
788
+ repository_status:
789
+ values:
790
+ - value: empty
791
+ description: "Registered, and observed empty on the date its repo-config gives: nothing to snapshot, scan or attribute yet."
792
+ description: Whether a registered repository holds anything yet (7.4.0).
788
793
  catalogues:
789
794
  domains:
790
795
  primary_key: id
@@ -1599,6 +1604,10 @@ catalogues:
1599
1604
  nullable: true
1600
1605
  description: The platform this row belongs to — sdd_id of its platform-root. Stamped by the aggregator per M-ADR 0208 §4.4 (preserve-if-set, fill rule by pull path); left empty by per-SDD builders. Added in 3.7.0.
1601
1606
  source_of_truth: execution
1607
+ - name: variant
1608
+ type: string
1609
+ nullable: true
1610
+ description: "The variant a feature group belongs to, from a [variant: <id>] marker on its heading (or inherited from the heading above it), as features.variant. Added in 7.4.0."
1602
1611
  description: |
1603
1612
  Hierarchical grouping for features. First-class entity — replaces the previous reverse-only synthetic `feature-tree-group` rows in features.csv. Both forward and reverse can use feature_groups: forward application-product SDDs whose `feature-tree.md` has H2/H3 structure, reverse SDDs whose extracted feature trees are inherently hierarchical, and any SDD that promotes groups to first-class with authored metadata. Groups can nest via `parent_group_id`.
1604
1613
  foreign_keys:
@@ -1673,6 +1682,10 @@ catalogues:
1673
1682
  nullable: true
1674
1683
  description: The platform this row belongs to — sdd_id of its platform-root. Stamped by the aggregator per M-ADR 0208 §4.4 (preserve-if-set, fill rule by pull path); left empty by per-SDD builders. Added in 3.7.0.
1675
1684
  source_of_truth: execution
1685
+ - name: variant
1686
+ type: string
1687
+ nullable: true
1688
+ description: "The customer-specific or segment variant the feature belongs to, from a [variant: <id>] marker in feature-tree.md on its bullet or on a group heading above it (inherited by everything beneath a marked group). The id is one the SDD declares in feature-variant-vocabulary.yaml at its root; an undeclared id is a convention finding and is not recorded. Empty for a feature of the product itself. Added in 7.4.0."
1676
1689
  foreign_keys:
1677
1690
  - column: product_id
1678
1691
  references: products.id
@@ -2160,6 +2173,10 @@ catalogues:
2160
2173
  type: string
2161
2174
  nullable: true
2162
2175
  description: "Whether the repository is inside the scope of what THIS SDD specifies, from its reverse-metadata.yaml (M-ADR 0213 §4.2): component (part of what the SDD specifies) or support (supports a component without being one). Empty for a repository with no reverse metadata. Its kind is component_type; the SDD's profile is products/applications.profile."
2176
+ - name: status
2177
+ type: string
2178
+ nullable: true
2179
+ description: "Whether the repository holds anything yet, from its repo-config: empty for a repository registered while it is empty (status: empty, with the date it was observed so in observed_empty_at, required), which coverage checks report as not applicable rather than as gaps; otherwise empty. A delta-reverse re-checks it: a repository declared empty that is exported with content is a finding. Added in 7.4.0."
2163
2180
  foreign_keys:
2164
2181
  - column: domain_id
2165
2182
  references: domains.id
@@ -2170,6 +2187,7 @@ catalogues:
2170
2187
  column_enums:
2171
2188
  component_type: enums.service_type
2172
2189
  repo_scope: enums.repo_scope
2190
+ status: enums.repository_status
2173
2191
  domain_models:
2174
2192
  primary_key: id
2175
2193
  source_of_truth: structure
@@ -62,12 +62,15 @@ SELECT
62
62
  WHERE s."primary_repo_id" = r."id"
63
63
  AND s."id" IS NOT NULL AND s."id" <> ''
64
64
  AND NOT EXISTS (SELECT 1 FROM "capability_service" cs
65
- WHERE cs."service_id" = s."id")) AS unclaimed_ids
65
+ WHERE cs."service_id" = s."id")) AS unclaimed_ids,
66
+ __IS_EMPTY__ AS is_empty
66
67
  FROM "repositories" r
67
68
  WHERE r."id" IS NOT NULL AND r."id" <> ''
68
69
  ORDER BY repo_id;`;
69
70
  export async function runRepoCapabilityCoverage(session) {
70
- const reader = await session.conn.runAndReadAll(REPO_FLAGS_SQL);
71
+ // repositories.status arrived in schema 7.4.0; a catalogue set built earlier has no such column.
72
+ const hasStatus = (await session.conn.runAndReadAll(`SELECT 1 FROM information_schema.columns WHERE table_name = 'repositories' AND column_name = 'status'`)).getRows().length > 0;
73
+ const reader = await session.conn.runAndReadAll(REPO_FLAGS_SQL.replace("__IS_EMPTY__", hasStatus ? `COALESCE(r."status", '') = 'empty'` : "FALSE"));
71
74
  const rows = reader.getRows().map((r) => ({
72
75
  repoId: String(r[0]),
73
76
  hasCap: r[1] === true,
@@ -78,6 +81,7 @@ export async function runRepoCapabilityCoverage(session) {
78
81
  unclaimedIds: r[6] === null || r[6] === undefined
79
82
  ? []
80
83
  : String(r[6]).split("\x1f"),
84
+ empty: r[7] === true,
81
85
  }));
82
86
  const findings = [];
83
87
  let byCapability = 0;
@@ -86,7 +90,19 @@ export async function runRepoCapabilityCoverage(session) {
86
90
  let unresolved = 0;
87
91
  let shapeA = 0;
88
92
  let shapeB = 0;
93
+ let registeredEmpty = 0;
89
94
  for (const row of rows) {
95
+ // A repository registered while empty has nothing to resolve to a capability yet (sdm-py85); one that claims a
96
+ // capability while declaring itself empty contradicts itself, and is reported.
97
+ if (row.empty && !row.hasCap) {
98
+ registeredEmpty++;
99
+ continue;
100
+ }
101
+ if (row.empty && row.hasCap) {
102
+ conflicting++;
103
+ findings.push({ predicate: "repo_capability", kind: "divergence", subjectId: row.repoId, classification: "empty_with_capability" });
104
+ continue;
105
+ }
90
106
  if (row.hasCap && row.hasExempt) {
91
107
  conflicting++;
92
108
  findings.push({
@@ -146,6 +162,7 @@ export async function runRepoCapabilityCoverage(session) {
146
162
  shapeA,
147
163
  shapeB,
148
164
  other: unresolved - shapeA - shapeB,
165
+ ...(registeredEmpty > 0 ? { registeredEmpty } : {}),
149
166
  },
150
167
  };
151
168
  }
@@ -47,6 +47,14 @@ export function renderRepoCapability(findings, metrics) {
47
47
  `must not claim a capability (per M-ADR 0188 §4.1)`,
48
48
  }));
49
49
  }
50
+ else if (f.classification === "empty_with_capability") {
51
+ lines.push(renderFindingLine({
52
+ category: "divergence",
53
+ subject,
54
+ summary: `repo ${f.subjectId} is registered empty (repo-config status: empty) but has a repo_capability edge — ` +
55
+ `an empty repository implements nothing yet; drop the status once it has content`,
56
+ }));
57
+ }
50
58
  else if (f.classification === "shape_a") {
51
59
  const names = (f.unclaimedServices ?? []).join(", ") +
52
60
  ((f.unclaimedCount ?? 0) > 3 ? " …" : "");
@@ -82,10 +90,11 @@ export function renderRepoCapability(findings, metrics) {
82
90
  lines.push(`[metric] repo_capability_resolution: n/a — 0 repository rows, nothing to resolve`);
83
91
  }
84
92
  if (metrics.total > 0) {
85
- const resolved = metrics.byCapability + metrics.byExemption;
93
+ const resolved = metrics.byCapability + metrics.byExemption + (metrics.registeredEmpty ?? 0);
86
94
  const pct = fmt1f((100.0 * resolved) / metrics.total);
87
95
  lines.push(`[metric] repo_capability_resolution: ${resolved}/${metrics.total} (${pct}%) ` +
88
96
  `— ${metrics.byCapability} by capability, ${metrics.byExemption} by exemption, ` +
97
+ `${metrics.registeredEmpty ? `${metrics.registeredEmpty} registered empty (not applicable), ` : ""}` +
89
98
  `${metrics.conflicting} conflicting, ${metrics.unresolved} unresolved`);
90
99
  if (metrics.unresolved > 0) {
91
100
  lines.push(`[metric] repo_capability_orphan_shape: ${metrics.shapeA} with no local ` +
@@ -82,11 +82,11 @@ export function buildImpactPropagation(edges, semantics) {
82
82
  continue;
83
83
  }
84
84
  if (flow === "toward-source" || flow === "both") {
85
- out.push({ from: e.target, to: e.source, type: e.type });
85
+ out.push({ from: e.target, to: e.source, type: e.type, flow });
86
86
  notePair(e.target, e.source, e.type);
87
87
  }
88
88
  if (flow === "toward-target" || flow === "both") {
89
- out.push({ from: e.source, to: e.target, type: e.type });
89
+ out.push({ from: e.source, to: e.target, type: e.type, flow });
90
90
  notePair(e.source, e.target, e.type);
91
91
  }
92
92
  }
@@ -159,6 +159,43 @@ export async function computeImpactClosure(propagation, seed) {
159
159
  conn.disconnectSync();
160
160
  }
161
161
  }
162
+ /**
163
+ * Consumers of `seed` (sdm-6npk): who depends on it, directly or through a chain of dependencies. Only dependency-
164
+ * shaped steps (declared flow toward-source: a change at the target reaches the source that depends on it) are
165
+ * followed; membership (toward-target) and realisation (both) are not, so a shared hub's parts and wholes do not flood
166
+ * the answer. Breadth-first, so each consumer is reported with a shortest dependency path from the seed.
167
+ */
168
+ export function consumersOf(propagation, seed) {
169
+ const next = new Map();
170
+ for (const e of propagation) {
171
+ if (e.flow !== "toward-source")
172
+ continue;
173
+ (next.get(e.from) ?? next.set(e.from, []).get(e.from)).push(e);
174
+ }
175
+ const prev = new Map();
176
+ const seen = new Set([seed]);
177
+ const queue = [seed];
178
+ const order = [];
179
+ while (queue.length > 0) {
180
+ const cur = queue.shift();
181
+ for (const e of (next.get(cur) ?? []).slice().sort((a, b) => a.to.localeCompare(b.to))) {
182
+ if (seen.has(e.to))
183
+ continue;
184
+ seen.add(e.to);
185
+ prev.set(e.to, cur);
186
+ order.push(e.to);
187
+ queue.push(e.to);
188
+ }
189
+ }
190
+ return order
191
+ .map((id) => {
192
+ const path = [id];
193
+ for (let p = prev.get(id); p !== undefined; p = prev.get(p))
194
+ path.unshift(p);
195
+ return { id, path };
196
+ })
197
+ .sort((a, b) => a.path.length - b.path.length || a.id.localeCompare(b.id));
198
+ }
162
199
  export async function runCatalogueImpact(inputs, write) {
163
200
  const sddRoot = resolve(inputs.sddPath ?? process.cwd());
164
201
  const tier = inputs.tier ?? "member";
@@ -200,6 +237,21 @@ export async function runCatalogueImpact(inputs, write) {
200
237
  return { exitCode: 2 };
201
238
  }
202
239
  const prop = buildImpactPropagation(edges, semantics);
240
+ if (inputs.consumers === true) {
241
+ const found = consumersOf(prop.edges, inputs.of);
242
+ const byCat = new Map();
243
+ for (const c of found)
244
+ byCat.set(c.id.slice(0, c.id.indexOf("/")), (byCat.get(c.id.slice(0, c.id.indexOf("/"))) ?? 0) + 1);
245
+ write(`consumers of ${inputs.of} (${tier} tier, schema ${schema.schemaVersion})\n`);
246
+ write(` semantics: who depends on it, directly or through dependencies: only dependency-shaped relationships\n` +
247
+ ` (declared flow toward-source) are followed, never membership or realisation; each consumer with a shortest path\n`);
248
+ write(` consumers: ${found.length}\n`);
249
+ for (const [cat, n] of [...byCat.entries()].sort())
250
+ write(` ${cat}: ${n}\n`);
251
+ for (const c of found)
252
+ write(` ${c.id} (${c.path.join(" <- ")})\n`);
253
+ return { exitCode: 0, result: { seed: inputs.of, closure: found.map((c) => c.id), byCatalogue: byCat } };
254
+ }
203
255
  const closure = await computeImpactClosure(prop.edges, inputs.of);
204
256
  const byCatalogue = new Map();
205
257
  for (const id of closure) {
@@ -1043,3 +1043,7 @@ coverage:
1043
1043
  kind: enum
1044
1044
  scoring: not-applicable
1045
1045
  reason: closed vocabulary, not an artefact
1046
+ repository_status:
1047
+ kind: enum
1048
+ scoring: not-applicable
1049
+ reason: closed vocabulary, not an artefact