@sdd-method/author-cli 0.0.0-stage → 0.1.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.
Files changed (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +437 -0
  151. package/dist/lib/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,488 @@
1
+ // Capability discoverer — emits `capabilities.csv` rows from
2
+ // traceability YAMLs (authoritative) plus capability spec files
3
+ // (fallback for specs not covered by a traceability).
4
+ //
5
+ // Per ADR 0134 and the catalogue-builder-specification §"Inputs":
6
+ // - Authoritative source: docs/domains/<domain>/product/<cap>.traceability.yaml
7
+ // declares capability.id (canonical), domain, capability_spec, runway.
8
+ // - Fallback: capability spec markdown files at
9
+ // docs/domains/<domain>/product/<cap>.md without an accompanying
10
+ // traceability YAML. Filename stem is used as the capability id.
11
+ // - When a traceability's capability.id differs from the spec file's
12
+ // stem, the canonical id wins; the divergence is logged as a finding.
13
+ import { readFile, stat } from "node:fs/promises";
14
+ import { join, relative, sep } from "node:path";
15
+ import { readdir } from "node:fs/promises";
16
+ const DOMAINS_DIR_REL = join("docs", "domains");
17
+ /**
18
+ * The vocabulary `catalogue-schema.yaml` documents for `capabilities.status`.
19
+ * WIDENED at catalogue schema 3.5.0 from the prior three-member set: the census
20
+ * this constant powers is what produced the evidence, and the ruling went the
21
+ * census's way — `exploration`, `placeholder` and `detailed-pre-lock` each VARY
22
+ * WITHIN a domain, which is the ruled discriminator for a genuine lifecycle
23
+ * phase rather than an authoring convention, so all three were admitted rather
24
+ * than re-authored away.
25
+ *
26
+ * Still held as the census's reference set, NOT as a binding — the column
27
+ * declares no `column_enums`, deliberately and stagedly (binding carries
28
+ * hard-fail teeth, and one out-of-vocabulary row remains at one estate).
29
+ */
30
+ const DOCUMENTED_CAPABILITY_STATUS = new Set([
31
+ "active",
32
+ "planned",
33
+ "exploration",
34
+ "deprecated",
35
+ ]);
36
+ /**
37
+ * M-ADR 0201 SPEC-AUTHORING stages. Not lifecycle phases, and not this column's
38
+ * axis — they belong in the capability spec's own `**Status:**` header, gated
39
+ * there by the spec-lifecycle gate.
40
+ *
41
+ * Held separately so the census can say WHICH AXIS owns the value instead of
42
+ * reporting "unknown word". `placeholder` and `detailed-pre-lock` were briefly
43
+ * admitted into `capabilities.status` at catalogue schema 3.5.0 and retracted at
44
+ * 3.6.0: one column carrying two orthogonal axes cannot be read, because
45
+ * `placeholder` gave no way to tell whether the CAPABILITY was a placeholder or
46
+ * its DOCUMENT was.
47
+ */
48
+ const SPEC_AUTHORING_STAGES = new Set([
49
+ "placeholder",
50
+ "first-draft",
51
+ "detailed-pre-lock",
52
+ "reverse-derived",
53
+ ]);
54
+ /**
55
+ * `traceability.contracts.provenance` — how the capability's contract SET was
56
+ * produced. Added at catalogue schema 3.5.0, reusing the vocabulary the method
57
+ * already ships for `authoring-mode` rather than minting a second one.
58
+ *
59
+ * STRICTLY ORTHOGONAL to `traceability.contracts.status`: a reverse-extracted
60
+ * contract set that is fully mapped declares `status: complete` AND
61
+ * `provenance: reverse-extracted`. Two reverse-estate manifests had written
62
+ * `reverse-extracted` into `contracts.status`, where it is invalid, because
63
+ * there was nowhere to say both true things at once.
64
+ */
65
+ const DOCUMENTED_CONTRACT_PROVENANCE = new Set([
66
+ "hand-authored",
67
+ "reverse-extracted",
68
+ "generated",
69
+ ]);
70
+ export async function buildCapabilities(inputs) {
71
+ const rows = [];
72
+ // Row identity for sliced capabilities is (id, slice_scope); use that as the
73
+ // dedup key so legitimately-sliced capabilities emit one row per slice
74
+ // without colliding on `id`.
75
+ const seen = new Set();
76
+ // The manifest that WON each dedup key, kept so a later duplicate can be
77
+ // DIFFED against it rather than merely counted. See the divergence finding.
78
+ const keptBy = new Map();
79
+ const usedSpecPaths = new Set();
80
+ const sliceKey = (id, sliceScope) => sliceScope === "" ? id : `${id}::${sliceScope}`;
81
+ // Pass 1 — traceability-declared capabilities (authoritative).
82
+ for (const m of inputs.manifests) {
83
+ const cap = m.capability;
84
+ const slice = (cap.slice_scope ?? "").trim();
85
+ const dedupKey = sliceKey(cap.id, slice);
86
+ if (seen.has(dedupKey)) {
87
+ // ⚠ THE DISCARDED COPY'S CONTENT IS THE POINT, not the fact of duplication.
88
+ // This finding used to say only that a duplicate existed. An estate probe
89
+ // found SEVEN capabilities with two canonically-named manifests where the
90
+ // catalogue keeps the first-seen copy and discards the other — and for FOUR
91
+ // of them the score registry's file_path names the DISCARDED file. So a
92
+ // scorer read one file while the catalogue reported another, disagreeing on
93
+ // capability.status, exploration.uncertainty and runway.classification.
94
+ //
95
+ // A duplicate whose copies AGREE is harmless. One that disagrees means a
96
+ // consumer may have read the copy that was thrown away — and a wrong value
97
+ // in a discarded file emits no other finding, ever. So name the fields.
98
+ const kept = keptBy.get(dedupKey);
99
+ const diffs = [];
100
+ if (kept !== undefined) {
101
+ const pairs = [
102
+ ["capability.status", (kept.capability.status ?? "").trim(), (cap.status ?? "").trim()],
103
+ [
104
+ "traceability.runway.classification",
105
+ (kept.traceability.runway?.classification ?? "").trim(),
106
+ (m.traceability.runway?.classification ?? "").trim(),
107
+ ],
108
+ [
109
+ "capability.capability_spec",
110
+ (kept.capability.capability_spec ?? "").trim(),
111
+ (cap.capability_spec ?? "").trim(),
112
+ ],
113
+ [
114
+ "capability.domain",
115
+ (kept.capability.domain ?? "").trim(),
116
+ (cap.domain ?? "").trim(),
117
+ ],
118
+ ];
119
+ for (const [field, keptVal, dropVal] of pairs) {
120
+ if (keptVal !== dropVal) {
121
+ diffs.push(`${field} kept="${keptVal}" discarded="${dropVal}"`);
122
+ }
123
+ }
124
+ }
125
+ const head = slice === ""
126
+ ? `${m.relPath}: capability.id="${cap.id}" already emitted by an earlier manifest`
127
+ : `${m.relPath}: capability.id="${cap.id}" slice="${slice}" already emitted by an earlier manifest`;
128
+ const keptPath = kept === undefined ? "an earlier manifest" : kept.relPath;
129
+ inputs.findings.add("divergence", diffs.length === 0
130
+ ? `${head} (${keptPath}) — THIS FILE IS DISCARDED. The two copies agree on ` +
131
+ `status, runway classification, spec path and domain, so no consumer can be ` +
132
+ `misled by which one was kept; the duplication itself is still worth removing.`
133
+ : `${head} (${keptPath}) — THIS FILE IS DISCARDED AND THE COPIES DISAGREE: ` +
134
+ `${diffs.join("; ")}. Any consumer pointed at the discarded path (a score ` +
135
+ `registry file_path, a review, a link) read values the catalogue does not ` +
136
+ `report. Decide which copy survives per capability; do not bulk-delete, which ` +
137
+ `would publish the discarded values rather than remove them.`);
138
+ continue;
139
+ }
140
+ if (!inputs.knownDomainIds.has(cap.domain)) {
141
+ inputs.findings.add("orphan", `${m.relPath}: capability.domain="${cap.domain}" has no docs/domains/${cap.domain}/`);
142
+ }
143
+ const specRel = cap.capability_spec ?? "";
144
+ let name = titleCase(cap.id);
145
+ let description = "";
146
+ if (specRel) {
147
+ usedSpecPaths.add(specRel);
148
+ const specAbs = join(inputs.sddPath, specRel);
149
+ // F22: an emitted spec_path must exist — a dangling path silently
150
+ // produced an empty name/description while the column pointed at
151
+ // nothing. The row still emits (the capability is declared); the
152
+ // dangle is loud.
153
+ if (!(await fileExists(specAbs))) {
154
+ inputs.findings.add("coverage", `${m.relPath}: capability "${cap.id}" declares capability_spec "${specRel}" which does not exist — name/description left empty, spec_path emitted dangling (F22)`);
155
+ }
156
+ // M-ADR 0211 §4.2 — the manifest is a declaration and its row is correct. The finding
157
+ // fires ONLY on a CONTRADICTION between two declaration surfaces: this manifest
158
+ // catalogues the spec as a capability, and the spec declares a Kind that does not
159
+ // catalogue. A missing template marker is never a finding (measured: six of seven
160
+ // such files in one estate were correct meta-aggregates).
161
+ const declaredKind = (await readSpecDeclaration(specAbs)).kind;
162
+ if (declaredKind !== "" && !CATALOGUING_KINDS.has(declaredKind)) {
163
+ inputs.findings.add(KNOWN_KINDS.has(declaredKind) ? "divergence" : "convention", KNOWN_KINDS.has(declaredKind)
164
+ ? `${m.relPath}: declaration-mismatch — the manifest catalogues "${cap.id}" as a capability but ${specRel} declares Kind: ${declaredKind}, which does not catalogue; the row is emitted from the manifest and the contradiction is the finding (M-ADR 0211 §4.2)`
165
+ : `${specRel}: declares Kind: "${declaredKind}", not in the vocabulary capability | meta-aggregate | policy | synthesis | guide (M-ADR 0211 §4.2)`);
166
+ }
167
+ const headingName = await readSpecH1Name(specAbs);
168
+ if (headingName !== "") {
169
+ name = headingName;
170
+ }
171
+ description = await readSpecOverview(specAbs);
172
+ // Log id ↔ spec stem divergence (informational).
173
+ const stem = stemOf(specRel);
174
+ if (stem !== "" && stem !== cap.id) {
175
+ inputs.findings.add("divergence", `${m.relPath}: capability.id="${cap.id}" differs from spec stem "${stem}"`);
176
+ }
177
+ }
178
+ // WILD-VOCABULARY CENSUS, reporting-only and deliberately NOT a binding.
179
+ //
180
+ // ✅ THE CENSUS WORKED AND ITS QUESTION IS NOW RULED. It measured 44 of the
181
+ // forward estate's rows outside the old three-member vocabulary — exploration
182
+ // 26, placeholder 11, detailed-pre-lock 6, first-draft 1 — and that evidence
183
+ // is what settled the ruling at catalogue schema 3.5.0: all three recurring
184
+ // values were ADMITTED, not re-authored away, because each varies WITHIN a
185
+ // domain (the ruled phase-vs-convention discriminator). Re-authoring them
186
+ // would have rewritten 37 rows that were declaring something true.
187
+ //
188
+ // So this census now reports the RESIDUE, not the population: 1 row at the
189
+ // forward estate (`first-draft`, n=1) and 0 at the reverse estate, measured
190
+ // structurally by key path. It stays reporting-only because the column still
191
+ // binds no enum — `convention` is non-gate-acting under M-ADR 0132 §4.3, so
192
+ // it cannot fail a gate on arrival. Binding is a later deliberate act, gated
193
+ // on that residue reaching zero at both estates.
194
+ //
195
+ // It keeps earning its place: an unenforced field cannot notice a wrong value
196
+ // when one is written, and this is the only thing that notices.
197
+ //
198
+ // Emitted only for an AUTHORED value: Pass 2's hardcoded default is a
199
+ // default, not a declaration, and reporting it would blame an adopter for
200
+ // a value the builder chose.
201
+ const declaredStatus = (cap.status ?? "").trim();
202
+ if (declaredStatus !== "" && !DOCUMENTED_CAPABILITY_STATUS.has(declaredStatus)) {
203
+ inputs.findings.add("convention", SPEC_AUTHORING_STAGES.has(declaredStatus)
204
+ ? `${m.relPath}: capability.status="${declaredStatus}" is a SPEC-AUTHORING stage ` +
205
+ `(M-ADR 0201), not a capability lifecycle phase — this column carries lifecycle ` +
206
+ `only (${[...DOCUMENTED_CAPABILITY_STATUS].join(" | ")}). Authoring stage belongs ` +
207
+ `in the spec's own '**Status:**' header. The value is catalogued as authored and ` +
208
+ `nothing is dropped; re-author to the lifecycle phase TRUE of the capability, ` +
209
+ `which is not a translation of this word`
210
+ : `${m.relPath}: capability.status="${declaredStatus}" is outside the documented ` +
211
+ `vocabulary (${[...DOCUMENTED_CAPABILITY_STATUS].join(" | ")}) — the value is ` +
212
+ `catalogued as authored and nothing is dropped; reported so the population is ` +
213
+ `countable`);
214
+ }
215
+ // contracts.provenance — orthogonal to contracts.status, and read WITHOUT
216
+ // consulting it. A value outside the set is reported and still catalogued,
217
+ // matching the status census's posture rather than dropping the cell.
218
+ const declaredProvenance = (m.traceability.contracts?.provenance ?? "").trim();
219
+ if (declaredProvenance !== "" &&
220
+ !DOCUMENTED_CONTRACT_PROVENANCE.has(declaredProvenance)) {
221
+ inputs.findings.add("convention", `${m.relPath}: traceability.contracts.provenance="${declaredProvenance}" is outside ` +
222
+ `the documented vocabulary (${[...DOCUMENTED_CONTRACT_PROVENANCE].join(" | ")}) — ` +
223
+ `the value is catalogued as authored and nothing is dropped`);
224
+ }
225
+ seen.add(dedupKey);
226
+ keptBy.set(dedupKey, m);
227
+ rows.push({
228
+ id: cap.id,
229
+ name,
230
+ domain_id: cap.domain,
231
+ tree_category: "",
232
+ parent_capability_id: "",
233
+ evidence_class: (cap.evidence_class ?? "").trim(),
234
+ context: (cap.context ?? "").trim(),
235
+ description,
236
+ status: cap.status ?? "active",
237
+ spec_path: specRel,
238
+ contract_provenance: declaredProvenance,
239
+ source: "capability-spec",
240
+ runway_classification: m.traceability.runway?.classification ?? "",
241
+ data_subset: cap.data_subset ?? "",
242
+ slice_scope: slice,
243
+ source_sdd_id: "",
244
+ });
245
+ }
246
+ // Pass 2 — capability spec files not covered by any traceability.
247
+ //
248
+ // M-ADR 0211 §4.2: placement asserts NOTHING. A product/*.md is catalogued only when it
249
+ // DECLARES something — the template H1, a lifecycle Status, or a Kind — and its kind
250
+ // catalogues. A file declaring nothing is silence: named, no row. A file declaring a
251
+ // non-cataloguing kind (policy, synthesis, guide) is respected: no row, no finding.
252
+ // §4.3: files=N rows=M excluded=K, every excluded class named, so the count can DISAGREE.
253
+ const specFiles = await collectCapabilitySpecs(inputs.sddPath);
254
+ const productFiles = new Set(specFiles.map((p) => relative(inputs.sddPath, p)));
255
+ let notACapabilitySpec = 0, nonCataloguingKind = 0, collisions = 0, unknownKind = 0;
256
+ for (const spec of specFiles) {
257
+ const relPath = relative(inputs.sddPath, spec);
258
+ if (usedSpecPaths.has(relPath))
259
+ continue;
260
+ if (rows.some((r) => r["spec_path"] === relPath))
261
+ continue;
262
+ const stem = stemOf(relPath);
263
+ if (seen.has(stem)) {
264
+ // The bare `continue` that dropped platform-tier specs silently for months (M-ADR
265
+ // 0211 §1(c)) — now the same divergence finding the manifest path emits.
266
+ collisions++;
267
+ const kept = rows.find((r) => r["id"] === stem);
268
+ inputs.findings.add("divergence", `capabilities: ${relPath} shares primary key "${stem}" with ${kept ? String(kept["spec_path"]) : "an earlier row"} — the earlier row is kept and THIS file emits no row (M-ADR 0211 §4.3: a stem collision on the spec-only path is now loud; two different things sharing a slug is authoring debt to split, not a key to widen)`);
269
+ continue;
270
+ }
271
+ const decl = await readSpecDeclaration(spec);
272
+ if (!decl.templateH1 && decl.status === "" && decl.kind === "") {
273
+ notACapabilitySpec++;
274
+ inputs.findings.add("coverage", `${relPath}: not-a-capability-spec — the file declares nothing (no template H1, no Status, no Kind), so its directory is not allowed to assert a capability for it; no row (M-ADR 0211 §4.2)`);
275
+ continue;
276
+ }
277
+ const kind = decl.kind === "" ? "capability" : decl.kind;
278
+ if (!KNOWN_KINDS.has(kind)) {
279
+ unknownKind++;
280
+ inputs.findings.add("convention", `${relPath}: declares Kind: "${kind}", not in the vocabulary capability | meta-aggregate | policy | synthesis | guide — catalogued as a capability meanwhile (M-ADR 0211 §4.2)`);
281
+ }
282
+ else if (!CATALOGUING_KINDS.has(kind)) {
283
+ nonCataloguingKind++;
284
+ continue; // respected, silently: the author declared it is not a capability
285
+ }
286
+ const domain = inferDomainFromSpec(relPath);
287
+ if (domain === "") {
288
+ inputs.findings.add("orphan", `${relPath}: cannot infer domain from path`);
289
+ continue;
290
+ }
291
+ if (!inputs.knownDomainIds.has(domain)) {
292
+ inputs.findings.add("orphan", `${relPath}: domain "${domain}" has no docs/domains/${domain}/`);
293
+ }
294
+ const heading = await readSpecH1Name(spec);
295
+ const description = await readSpecOverview(spec);
296
+ seen.add(sliceKey(stem, ""));
297
+ rows.push({
298
+ id: stem,
299
+ name: heading !== "" ? heading : titleCase(stem),
300
+ domain_id: domain,
301
+ tree_category: "",
302
+ parent_capability_id: "",
303
+ evidence_class: "",
304
+ context: "",
305
+ description,
306
+ // Pass-2 specs have no manifest to declare a lifecycle phase; `active` is the STATED
307
+ // default (M-ADR 0211 §4.2), not an assertion made by the directory.
308
+ status: "active",
309
+ spec_path: relPath,
310
+ contract_provenance: "",
311
+ source: "capability-spec",
312
+ runway_classification: "",
313
+ data_subset: "",
314
+ slice_scope: "",
315
+ source_sdd_id: "",
316
+ });
317
+ }
318
+ // M-ADR 0211 §4.3 — files against rows, never distinct keys against themselves.
319
+ const rowFiles = new Set(rows.map((r) => String(r["spec_path"])).filter((p) => productFiles.has(p)));
320
+ const excluded = notACapabilitySpec + nonCataloguingKind + collisions;
321
+ inputs.findings.add("metric", `capabilities: files=${productFiles.size} rows=${rows.length} rows-on-product-files=${rowFiles.size} excluded=${excluded} (not-a-capability-spec=${notACapabilitySpec} non-cataloguing-kind=${nonCataloguingKind} collision=${collisions} unknown-kind=${unknownKind})`);
322
+ if (productFiles.size !== rowFiles.size + excluded) {
323
+ inputs.findings.add("coverage", `capabilities: the population does not balance — ${productFiles.size} product/*.md files ≠ ${rowFiles.size} files with a row + ${excluded} excluded for a named reason. Files and rows are two instruments; they disagree, and the disagreement is the finding (M-ADR 0211 §4.3)`);
324
+ }
325
+ return rows;
326
+ }
327
+ export const CATALOGUING_KINDS = new Set(["capability", "meta-aggregate"]);
328
+ export const KNOWN_KINDS = new Set(["capability", "meta-aggregate", "policy", "synthesis", "guide"]);
329
+ export async function readSpecDeclaration(specAbs) {
330
+ let text;
331
+ try {
332
+ const st = await stat(specAbs);
333
+ if (!st.isFile())
334
+ return { templateH1: false, status: "", kind: "" };
335
+ text = await readFile(specAbs, "utf-8");
336
+ }
337
+ catch {
338
+ return { templateH1: false, status: "", kind: "" };
339
+ }
340
+ const head = text.split(/\r?\n/).slice(0, 60);
341
+ const templateH1 = head.some((l) => /^#\s*(Capability Specification|Spec):/i.test(l.trim()));
342
+ const bold = (field) => {
343
+ for (const l of head) {
344
+ const m = new RegExp(`\\*\\*${field}:\\*\\*\\s*([^|\\n]*)`).exec(l);
345
+ if (m)
346
+ return (m[1] ?? "").trim();
347
+ }
348
+ return "";
349
+ };
350
+ const prose = (field) => {
351
+ for (const l of head) {
352
+ if (!l.startsWith(">"))
353
+ continue;
354
+ const m = new RegExp(`${field}:\\s*([^|\\n]*)`).exec(l);
355
+ if (m)
356
+ return (m[1] ?? "").trim();
357
+ }
358
+ return "";
359
+ };
360
+ return { templateH1, status: bold("Status") || prose("Status"), kind: bold("Kind") || prose("Kind") };
361
+ }
362
+ async function readSpecH1Name(specAbs) {
363
+ try {
364
+ const s = await stat(specAbs);
365
+ if (!s.isFile())
366
+ return "";
367
+ }
368
+ catch {
369
+ return "";
370
+ }
371
+ const text = await readFile(specAbs, "utf-8");
372
+ for (const line of text.split(/\r?\n/)) {
373
+ const t = line.trim();
374
+ if (t === "")
375
+ continue;
376
+ if (!t.startsWith("#"))
377
+ return "";
378
+ const stripped = t
379
+ .replace(/^#+\s*/, "")
380
+ .replace(/^(Capability Specification|Spec):\s*/i, "")
381
+ .trim();
382
+ return stripped;
383
+ }
384
+ return "";
385
+ }
386
+ /**
387
+ * Extract the lead paragraph of a capability spec's `## Overview` section
388
+ * to use as `capabilities.description`.
389
+ *
390
+ * Returns the first non-empty paragraph block after the `## Overview`
391
+ * heading — lines are joined with single spaces, stopping at the next blank
392
+ * line or the next heading. Returns "" when the spec has no Overview section
393
+ * (not all specs require one).
394
+ */
395
+ async function readSpecOverview(specAbs) {
396
+ try {
397
+ const s = await stat(specAbs);
398
+ if (!s.isFile())
399
+ return "";
400
+ }
401
+ catch {
402
+ return "";
403
+ }
404
+ const text = await readFile(specAbs, "utf-8");
405
+ const lines = text.split(/\r?\n/);
406
+ let inOverview = false;
407
+ const paragraph = [];
408
+ for (const line of lines) {
409
+ const t = line.trim();
410
+ if (!inOverview) {
411
+ if (/^##\s+Overview\s*$/i.test(t))
412
+ inOverview = true;
413
+ continue;
414
+ }
415
+ // Stop at the next heading.
416
+ if (t.startsWith("#"))
417
+ break;
418
+ if (t === "") {
419
+ // Blank line ends the lead paragraph once we've captured content.
420
+ if (paragraph.length > 0)
421
+ break;
422
+ continue;
423
+ }
424
+ paragraph.push(t);
425
+ }
426
+ return paragraph.join(" ");
427
+ }
428
+ async function collectCapabilitySpecs(sddPath) {
429
+ const out = [];
430
+ const domainsDir = join(sddPath, DOMAINS_DIR_REL);
431
+ let domains;
432
+ try {
433
+ domains = await readdir(domainsDir, { withFileTypes: true });
434
+ }
435
+ catch {
436
+ return out;
437
+ }
438
+ for (const d of domains) {
439
+ if (!d.isDirectory())
440
+ continue;
441
+ const productDir = join(domainsDir, d.name, "product");
442
+ let entries;
443
+ try {
444
+ entries = await readdir(productDir, { withFileTypes: true });
445
+ }
446
+ catch {
447
+ continue;
448
+ }
449
+ for (const e of entries) {
450
+ if (!e.isFile())
451
+ continue;
452
+ if (!e.name.endsWith(".md"))
453
+ continue;
454
+ const lower = e.name.toLowerCase();
455
+ if (lower === "readme.md" || lower === "index.md")
456
+ continue;
457
+ out.push(join(productDir, e.name));
458
+ }
459
+ }
460
+ return out;
461
+ }
462
+ function inferDomainFromSpec(relPath) {
463
+ const parts = relPath.split(sep);
464
+ const i = parts.indexOf("domains");
465
+ if (i === -1 || i + 1 >= parts.length)
466
+ return "";
467
+ return parts[i + 1] ?? "";
468
+ }
469
+ function stemOf(relPath) {
470
+ const base = relPath.split(sep).slice(-1)[0] ?? "";
471
+ return base.replace(/\.md$/i, "").replace(/\.traceability\.yaml$/i, "");
472
+ }
473
+ function titleCase(slug) {
474
+ return slug
475
+ .split("-")
476
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
477
+ .join(" ");
478
+ }
479
+ async function fileExists(path) {
480
+ try {
481
+ const { stat } = await import("node:fs/promises");
482
+ return (await stat(path)).isFile();
483
+ }
484
+ catch {
485
+ return false;
486
+ }
487
+ }
488
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1,74 @@
1
+ // Edge builder for capability_domain_model — emits `capability_domain_model.csv`.
2
+ //
3
+ // Platform-profile only. Non-platform SDDs return an empty list.
4
+ //
5
+ // Sources:
6
+ // - Forward: for each loaded traceability manifest with
7
+ // `traceability.domain_model.path` set, emit one row linking
8
+ // capability_id → domain_model_id with source="traceability-manifest".
9
+ // - Reverse: for every capability whose domain has a row in
10
+ // domain_models.csv but which did NOT declare via the manifest,
11
+ // emit a row with source="inferred". This captures the
12
+ // "capability lives in domain D, and D has a domain model" inference.
13
+ //
14
+ // Domain model ids follow the `{domain_id}-data-model` convention from
15
+ // the domain-models builder.
16
+ //
17
+ // Added in schema 2.4.0 per ADR 0142.
18
+ export function buildCapabilityDomainModel(inputs) {
19
+ if (inputs.manifest.profile !== "platform-profile") {
20
+ return [];
21
+ }
22
+ const seen = new Set();
23
+ const rows = [];
24
+ // Path 1: Forward — declared in traceability manifests.
25
+ const declaredCapIds = new Set();
26
+ for (const m of inputs.manifests) {
27
+ if (!m.traceability.domain_model)
28
+ continue;
29
+ const capId = m.capability.id;
30
+ if (!inputs.knownCapabilityIds.has(capId))
31
+ continue;
32
+ const domainModelId = `${m.capability.domain}-data-model`;
33
+ if (!inputs.knownDomainModelIds.has(domainModelId))
34
+ continue;
35
+ const id = `${capId}__${domainModelId}`;
36
+ if (seen.has(id))
37
+ continue;
38
+ seen.add(id);
39
+ declaredCapIds.add(capId);
40
+ rows.push({
41
+ id,
42
+ capability_id: capId,
43
+ domain_model_id: domainModelId,
44
+ source: "traceability-manifest",
45
+ });
46
+ }
47
+ // Path 2: Reverse — inferred from capability's domain having a domain model.
48
+ // Only emit for capabilities that didn't declare via manifest.
49
+ for (const cap of inputs.capabilityRows) {
50
+ const capId = String(cap["id"] ?? "");
51
+ if (capId === "" || declaredCapIds.has(capId))
52
+ continue;
53
+ if (!inputs.knownCapabilityIds.has(capId))
54
+ continue;
55
+ const domainId = String(cap["domain_id"] ?? "");
56
+ if (domainId === "")
57
+ continue;
58
+ const domainModelId = `${domainId}-data-model`;
59
+ if (!inputs.knownDomainModelIds.has(domainModelId))
60
+ continue;
61
+ const id = `${capId}__${domainModelId}`;
62
+ if (seen.has(id))
63
+ continue;
64
+ seen.add(id);
65
+ rows.push({
66
+ id,
67
+ capability_id: capId,
68
+ domain_model_id: domainModelId,
69
+ source: "inferred",
70
+ });
71
+ }
72
+ return rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
73
+ }
74
+ //# sourceMappingURL=capability-domain-model.js.map