@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,348 @@
1
+ /**
2
+ * Capability-spec feature extractor per ADR 0145 (schema 2.7.0).
3
+ *
4
+ * Walks `docs/domains/<dom>/product/<capability>.md` (platform-profile),
5
+ * parses the `## Feature Grounding (code-path trace)` markdown table,
6
+ * and emits four catalogue contributions:
7
+ *
8
+ * - `features.csv` rows with `source: capability-spec`.
9
+ * - `capability_feature.csv` rows (1:1 with each feature row).
10
+ * - `feature_repo.csv` rows derived from file-path references in
11
+ * the Implementation column.
12
+ * - `feature_service.csv` rows derived from class names / file
13
+ * basenames / route fragments in the Implementation column,
14
+ * resolved via `services.csv` + the optional per-SDD alias map
15
+ * introduced by ADR 0143.
16
+ *
17
+ * Resolution is best-effort and never breaks FK validation — only
18
+ * confidently-resolved rows are emitted. Unresolved CAPABILITY ids emit
19
+ * findings (convention/orphan). Unresolved SERVICE and REPO tokens are
20
+ * dropped silently, deliberately: the token stream comes from free-text
21
+ * Implementation columns (class names, route fragments, path segments),
22
+ * so most tokens legitimately resolve to nothing and a finding per miss
23
+ * would be noise. The row-level finding — a non-empty Implementation
24
+ * cell yielding ZERO repo resolutions — IS signal and is emitted as a
25
+ * coverage finding per row plus one summary line (M-ADR 0143 §4.4 as
26
+ * amended); it measures resolution coverage, never correctness.
27
+ *
28
+ * Precedence per ADR 0145 §4.7: feature-tree, feature-spec, and
29
+ * feature-grounding rows win on id collision (they are more specific
30
+ * sources); capability-spec rows are skipped with a divergence finding
31
+ * when their id is already taken.
32
+ */
33
+ import { readFile } from "node:fs/promises";
34
+ import { relative } from "node:path";
35
+ import { capabilityIdFromSpecPath, findCapabilitySpecFiles, parseCapabilitySpecTable, } from "./capability-spec-walker.js";
36
+ const DESCRIPTION_MAX = 500;
37
+ /** Columns the service-name resolver scans for class names / route fragments. */
38
+ const SERVICE_RESOLUTION_COLUMNS = ["implementation"];
39
+ /** Columns the repo-name resolver scans for file-path references. */
40
+ const REPO_RESOLUTION_COLUMNS = ["implementation", "grounding_doc"];
41
+ export async function buildCapabilitySpec(inputs) {
42
+ const featureRows = [];
43
+ const capabilityFeatureRows = [];
44
+ const featureRepoRows = [];
45
+ // M-ADR 0143 §4.4 (amended): rows whose Implementation cell is non-empty but resolves to
46
+ // ZERO repos. Counted for the summary finding below; each row also gets its own
47
+ // line. This measures evidence→repo RESOLUTION COVERAGE, not correctness.
48
+ let repoEvidenceRows = 0;
49
+ const repoZeroResolutionFeatureIds = [];
50
+ const featureServiceRows = [];
51
+ const seenFeatureRepo = new Set();
52
+ const seenFeatureService = new Set();
53
+ const seenCapabilityFeature = new Set();
54
+ const seenFeatures = new Set(inputs.existingFeatureIds);
55
+ // Frozen copy — `seenFeatures` grows during this walk, so only this set can
56
+ // answer "did an EARLIER SOURCE emit this id?" at the collision site below.
57
+ const fromEarlierSource = new Set(inputs.existingFeatureIds);
58
+ if (inputs.profile !== "platform-profile") {
59
+ return {
60
+ featureRows,
61
+ capabilityFeatureRows,
62
+ featureRepoRows,
63
+ featureServiceRows,
64
+ repoEvidenceRows,
65
+ repoZeroResolutionFeatureIds,
66
+ };
67
+ }
68
+ const files = await findCapabilitySpecFiles(inputs.sddPath);
69
+ for (const absPath of files) {
70
+ const capabilityId = capabilityIdFromSpecPath(absPath, inputs.sddPath);
71
+ const relPath = relative(inputs.sddPath, absPath);
72
+ if (capabilityId === "") {
73
+ inputs.findings.add("convention", `${relPath}: capability id could not be resolved from file path`);
74
+ continue;
75
+ }
76
+ if (!inputs.knownCapabilityIds.has(capabilityId)) {
77
+ // Common for product README-style files that aren't capability specs.
78
+ // Treat as orphan, not convention — silently skip.
79
+ inputs.findings.add("orphan", `${relPath}: filename stem "${capabilityId}" does not match any capability id in capabilities.csv — skipping`);
80
+ continue;
81
+ }
82
+ const text = await readFile(absPath, "utf-8");
83
+ const rows = parseCapabilitySpecTable(text);
84
+ if (rows.length === 0) {
85
+ inputs.findings.add("coverage", `${relPath}: capability spec found but no "## Feature Grounding (code-path trace)" table recognised`);
86
+ continue;
87
+ }
88
+ const slugsInThisFile = new Set();
89
+ for (const row of rows) {
90
+ if (slugsInThisFile.has(row.slug)) {
91
+ inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: duplicate feature slug "${row.slug}" in same file (first occurrence wins)`);
92
+ continue;
93
+ }
94
+ slugsInThisFile.add(row.slug);
95
+ const featureId = `${capabilityId}-${row.slug}`;
96
+ // Narrow the collision skip to the feature ROW only. When an earlier
97
+ // source already emitted this feature id, feature-tree / feature-spec /
98
+ // feature-grounding win the ROW (they carry runway / group / grounding
99
+ // context). EVERY edge below still emits — including capability_feature:
100
+ // a file-authored feature's row arrives from the feature files with NO
101
+ // capability_feature edge, so for that shape this extractor's edge is
102
+ // the ONLY source of the binding, and suppressing a non-duplicate edge
103
+ // because its sibling row duplicated silently unbinds the feature
104
+ // (measured: four owned-but-untraced pairs from exactly this rule).
105
+ // When the earlier source DID also emit the edge, the ids are identical
106
+ // (`${cap}__${feature}` in every emitter) and keyed-table dedup unions
107
+ // them — dedup rows, keep edges. feature_repo / feature_service remain
108
+ // this source's unique contribution (ADR 0145) as before.
109
+ if (seenFeatures.has(featureId)) {
110
+ // Same one-check-two-meanings split as feature-grounding.ts: a hit on
111
+ // an id seeded from an EARLIER SOURCE is the documented precedence rule
112
+ // firing (correct output, `inferred`, does not gate); a hit on an id
113
+ // this pass emitted itself is a real collision between two capability
114
+ // specs (`divergence`, gates). See the long comment there.
115
+ if (fromEarlierSource.has(featureId)) {
116
+ inputs.findings.add("inferred", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by an earlier source (feature-tree / feature-spec / feature-grounding wins the row; edges still emitted, incl. capability_feature)`);
117
+ }
118
+ else {
119
+ inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by another capability spec in this same pass (first occurrence wins the row; edges still emitted, incl. capability_feature)`);
120
+ }
121
+ }
122
+ else {
123
+ seenFeatures.add(featureId);
124
+ featureRows.push({
125
+ id: featureId,
126
+ name: row.slug,
127
+ product_id: "",
128
+ capability_id: capabilityId,
129
+ parent_group_id: "",
130
+ parent_feature_id: "",
131
+ description: deriveDescription(row),
132
+ maturity: "",
133
+ status: "active",
134
+ source: "capability-spec",
135
+ runway: "",
136
+ source_sdd_id: "",
137
+ });
138
+ }
139
+ // capability_feature edge — emitted on BOTH paths (the binding is not a
140
+ // duplicate just because the row was; PK-identical duplicates union at
141
+ // keyed-table dedup).
142
+ const cfId = `${capabilityId}__${featureId}`;
143
+ if (!seenCapabilityFeature.has(cfId)) {
144
+ seenCapabilityFeature.add(cfId);
145
+ capabilityFeatureRows.push({
146
+ id: cfId,
147
+ capability_id: capabilityId,
148
+ feature_id: featureId,
149
+ });
150
+ }
151
+ // feature_service edges — always emit (unique to capability-spec)
152
+ const services = resolveServices(row, inputs);
153
+ for (const serviceId of services) {
154
+ const edgeId = `${featureId}__${serviceId}`;
155
+ if (seenFeatureService.has(edgeId))
156
+ continue;
157
+ seenFeatureService.add(edgeId);
158
+ featureServiceRows.push({
159
+ id: edgeId,
160
+ feature_id: featureId,
161
+ service_id: serviceId,
162
+ });
163
+ }
164
+ // feature_repo edges — always emit (unique to capability-spec)
165
+ const repos = resolveRepos(row, inputs);
166
+ if ((row.cells.get("implementation") ?? "").trim() !== "") {
167
+ repoEvidenceRows++;
168
+ if (repos.length === 0) {
169
+ repoZeroResolutionFeatureIds.push(featureId);
170
+ inputs.findings.add("coverage", `${relPath}:${row.lineNumber}: feature "${featureId}" — Implementation cell is non-empty but resolved to ZERO repos; name a repo key or path the resolver can match, or map the token in .sdd/feature-grounding-aliases.yaml repo_aliases (M-ADR 0143 §4.4)`);
171
+ }
172
+ }
173
+ for (const repoKey of repos) {
174
+ const edgeId = `${featureId}__${repoKey}`;
175
+ if (seenFeatureRepo.has(edgeId))
176
+ continue;
177
+ seenFeatureRepo.add(edgeId);
178
+ featureRepoRows.push({
179
+ id: edgeId,
180
+ feature_id: featureId,
181
+ repo_id: repoKey,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ // The summary line is emitted by the build (build.ts, after feature_repo is
187
+ // final) so it can split rows fired against features left with NO anchor.
188
+ return {
189
+ featureRows,
190
+ capabilityFeatureRows,
191
+ featureRepoRows,
192
+ featureServiceRows,
193
+ repoEvidenceRows,
194
+ repoZeroResolutionFeatureIds,
195
+ };
196
+ }
197
+ /**
198
+ * Build the description column by joining non-feature cells, capped at
199
+ * 500 chars to match the feature-grounding convention.
200
+ */
201
+ function deriveDescription(row) {
202
+ const parts = [];
203
+ for (const [key, value] of row.cells) {
204
+ if (value === "")
205
+ continue;
206
+ parts.push(`${prettyHeader(key)}: ${value}`);
207
+ }
208
+ const joined = parts.join(" • ");
209
+ return joined.length > DESCRIPTION_MAX
210
+ ? joined.slice(0, DESCRIPTION_MAX)
211
+ : joined;
212
+ }
213
+ function prettyHeader(key) {
214
+ return key
215
+ .split("_")
216
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
217
+ .join(" ");
218
+ }
219
+ /**
220
+ * Resolve service ids referenced in a row's Implementation cell.
221
+ * Same strategy as feature-grounding: tokenize, alias-lookup, normalise,
222
+ * known-id lookup.
223
+ */
224
+ function resolveServices(row, inputs) {
225
+ const out = new Set();
226
+ for (const col of SERVICE_RESOLUTION_COLUMNS) {
227
+ const cell = row.cells.get(col) ?? "";
228
+ if (cell === "")
229
+ continue;
230
+ for (const token of tokenize(cell)) {
231
+ const resolved = resolveServiceToken(token, inputs);
232
+ if (resolved !== null)
233
+ out.add(resolved);
234
+ }
235
+ }
236
+ return [...out].sort();
237
+ }
238
+ function resolveServiceToken(rawToken, inputs) {
239
+ if (inputs.aliases !== null) {
240
+ const aliased = inputs.aliases.serviceAliases.get(rawToken);
241
+ if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
242
+ return aliased;
243
+ }
244
+ }
245
+ const normalised = normaliseToken(rawToken);
246
+ if (normalised === "")
247
+ return null;
248
+ if (inputs.knownServiceIds.has(normalised))
249
+ return normalised;
250
+ if (inputs.aliases !== null) {
251
+ const aliased = inputs.aliases.serviceAliases.get(normalised);
252
+ if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
253
+ return aliased;
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+ function resolveRepos(row, inputs) {
259
+ const out = new Set();
260
+ for (const col of REPO_RESOLUTION_COLUMNS) {
261
+ const cell = row.cells.get(col) ?? "";
262
+ if (cell === "")
263
+ continue;
264
+ const coded = inputs.repoTokenPolicy === "identifier" ? codeSpanTokens(cell) : null;
265
+ for (const token of tokenize(cell)) {
266
+ const resolved = resolveRepoToken(token, inputs);
267
+ if (resolved === null)
268
+ continue;
269
+ if (coded !== null && isPlainWord(token) && !coded.has(token) && inputs.aliases?.repoAliases.get(token) !== resolved)
270
+ continue;
271
+ out.add(resolved);
272
+ }
273
+ }
274
+ return [...out].sort();
275
+ }
276
+ function resolveRepoToken(rawToken, inputs) {
277
+ if (inputs.aliases !== null) {
278
+ const aliased = inputs.aliases.repoAliases.get(rawToken);
279
+ if (aliased !== undefined && inputs.knownRepoKeys.has(aliased)) {
280
+ return aliased;
281
+ }
282
+ }
283
+ if (inputs.knownRepoKeys.has(rawToken))
284
+ return rawToken;
285
+ for (const segment of rawToken.split("/")) {
286
+ const stripped = segment
287
+ .replace(/-master@.*$/, "")
288
+ .replace(/@[^/]+$/, "");
289
+ if (stripped !== "" && inputs.knownRepoKeys.has(stripped)) {
290
+ return stripped;
291
+ }
292
+ const trailingMaster = stripped.replace(/-master$/, "");
293
+ if (trailingMaster !== stripped &&
294
+ inputs.knownRepoKeys.has(trailingMaster)) {
295
+ return trailingMaster;
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ /**
301
+ * Tokenize a markdown cell — mirrors the feature-grounding tokenizer.
302
+ * Matches backticked spans, bare CamelCase / kebab-case identifiers,
303
+ * and path-like tokens. Strips method call parens and array indexers.
304
+ */
305
+ function tokenize(cell) {
306
+ const out = new Set();
307
+ for (const match of cell.matchAll(/`([^`]+)`/g)) {
308
+ const inner = match[1] ?? "";
309
+ addCandidatesFromToken(inner, out);
310
+ }
311
+ const stripped = cell.replace(/`[^`]+`/g, " ");
312
+ for (const match of stripped.matchAll(/[A-Za-z][A-Za-z0-9._@/-]*[A-Za-z0-9]/g)) {
313
+ addCandidatesFromToken(match[0], out);
314
+ }
315
+ return [...out];
316
+ }
317
+ function addCandidatesFromToken(token, out) {
318
+ if (token === "")
319
+ return;
320
+ if (token.includes("/")) {
321
+ out.add(token);
322
+ const first = token.split("/")[0] ?? "";
323
+ if (first !== "" && first !== token)
324
+ out.add(first);
325
+ return;
326
+ }
327
+ const head = token.split(/[(.[]/)[0] ?? "";
328
+ if (head !== "")
329
+ out.add(head);
330
+ out.add(token);
331
+ }
332
+ /** Candidate tokens that came from backticked code spans only. */
333
+ function codeSpanTokens(cell) {
334
+ const out = new Set();
335
+ for (const match of cell.matchAll(/`([^`]+)`/g))
336
+ addCandidatesFromToken(match[1] ?? "", out);
337
+ return out;
338
+ }
339
+ /** A letters-only word — ordinary prose unless it is written as code. */
340
+ const isPlainWord = (token) => /^[A-Za-z]+$/.test(token);
341
+ function normaliseToken(token) {
342
+ return token
343
+ .toLowerCase()
344
+ .replace(/\.(ts|tsx|js|jsx|java|kt|kts|py|go|rs|yaml|yml|json|md)$/i, "")
345
+ .replace(/\(\)$/, "")
346
+ .trim();
347
+ }
348
+ //# sourceMappingURL=capability-spec.js.map
@@ -0,0 +1,183 @@
1
+ // container_dependency member-tier builder — F19 (M-ADR 0189 §4.4/F17
2
+ // follow-on, schema 2.33.1).
3
+ //
4
+ // F17 made the aggregator's adjudication leg the ONLY source of
5
+ // container_dependency rows — populated exactly when an author
6
+ // mis-declared an internal container as `System_Ext`. An author who wrote
7
+ // the C4-correct thing instead — `Rel(container_a, container_b)` between
8
+ // two `Container(...)` nodes — got no row and no finding: the metamodel
9
+ // rewarded the wrong authoring and silently discarded the right one,
10
+ // inverting the incentive F17 was built to create.
11
+ //
12
+ // This builder harvests intra-SDD container→container `Rel(...)` lines:
13
+ // both endpoints must resolve to local containers (via the entry-id map
14
+ // the c4-containers builder fills), neither may be external, and the
15
+ // syndicated platform L1 is skipped for non-root SDDs (F14 — its Rel
16
+ // lines are the platform's statements). The adjudication leg then MERGES
17
+ // into a catalogue that already has content, rather than being its only
18
+ // source; dedup is by id, member rows first.
19
+ import { readFile } from "node:fs/promises";
20
+ import { collectC4DiagramFiles } from "./c4-external-systems.js";
21
+ import { mergeEdgeLabel } from "./edge-label-merge.js";
22
+ import { techContradicts } from "./c4-containers.js";
23
+ const REL_RE = /^(?:Bi)?Rel(?:_[A-Za-z]+)?\s*\(\s*(\w+)\s*,\s*(\w+)\s*(?:,\s*"([^"]*)")?/;
24
+ export async function buildContainerDependency(inputs) {
25
+ if (inputs.containerIdByEntryId.size === 0)
26
+ return [];
27
+ const isRoot = inputs.manifest.category === "platform-root";
28
+ const files = await collectC4DiagramFiles(inputs.sddPath);
29
+ const rows = [];
30
+ const seen = new Set();
31
+ const rowsById = new Map();
32
+ const PERSON_DECL_RE = /Person(?:_Ext)?\s*\(\s*(\w+)\s*,/;
33
+ for (const { abs, syndicated } of files) {
34
+ if (syndicated && !isRoot)
35
+ continue;
36
+ const content = await readFile(abs, "utf-8");
37
+ // F26: per-file Person entries — the endpoint class that has no edge
38
+ // catalogue to a container.
39
+ const personEntries = new Set();
40
+ for (const raw of content.split(/\r?\n/)) {
41
+ const pm = PERSON_DECL_RE.exec(raw.trim());
42
+ if (pm)
43
+ personEntries.add(pm[1] ?? "");
44
+ }
45
+ for (const raw of content.split(/\r?\n/)) {
46
+ const m = REL_RE.exec(raw.trim());
47
+ if (!m)
48
+ continue;
49
+ const [, fromEntry, toEntry, label] = m;
50
+ // F20: resolve the container FIRST. Both maps are SDD-wide, and an
51
+ // entry-id that resolves to a local container IS a container —
52
+ // whatever a sibling diagram in the same SDD called it. The old
53
+ // external-guard-first order silently discarded a true edge whenever
54
+ // the id was also used for a System_Ext elsewhere in the SDD, which
55
+ // is precisely the shape shared load-bearing containers produce
56
+ // (measured: the estate's commonest collision class). The sibling's
57
+ // System_Ext usage is separately harvested and adjudicated; a
58
+ // converted candidate's rewritten edge dedups against the member row
59
+ // by id, so nothing is double-represented.
60
+ const from = inputs.containerIdByEntryId.get(fromEntry ?? "");
61
+ const to = inputs.containerIdByEntryId.get(toEntry ?? "");
62
+ if (from === undefined || to === undefined) {
63
+ // F26: a Rel the builder RESOLVES but cannot catalogue must say so
64
+ // — following F23's rename prescription turned exactly this
65
+ // silence into a two-edge delete. Classification is
66
+ // container-first (F20), then external, then Person (file-local);
67
+ // anything unknown stays silent (no confident classification, no
68
+ // guess). Pairs with a home elsewhere (one-external →
69
+ // system_external_dependency; person→system → persona_c4system)
70
+ // are not reported here.
71
+ const classify = (entry, resolved) => resolved !== undefined
72
+ ? "container"
73
+ : inputs.externalIdByEntryId?.has(entry)
74
+ ? "external"
75
+ : personEntries.has(entry)
76
+ ? "person"
77
+ : "unknown";
78
+ const fromKind = classify(fromEntry ?? "", from);
79
+ const toKind = classify(toEntry ?? "", to);
80
+ const pair = `${fromKind}→${toKind}`;
81
+ // person↔container pairs are catalogued by persona_container as of
82
+ // schema 2.34.0 (or reported there when the persona doesn't
83
+ // resolve) — only the pairs with NO home remain findings here.
84
+ if (pair === "external→external") {
85
+ inputs.findings.add("convention", `container_dependency: Rel(${fromEntry}, ${toEntry}${label ? `, "${label}"` : ""}) resolves as ` +
86
+ `${pair} — no catalogue holds that pair; the relationship is recorded nowhere (F26)`);
87
+ }
88
+ continue;
89
+ }
90
+ if (from === to)
91
+ continue;
92
+ const id = `${from}__${to}`;
93
+ const existing = rowsById.get(id);
94
+ if (existing !== undefined) {
95
+ mergeEdgeLabel(existing, label ?? "", "container_dependency", inputs.findings);
96
+ continue;
97
+ }
98
+ seen.add(id);
99
+ rowsById.set(id, {
100
+ id,
101
+ from_container_id: from,
102
+ to_container_id: to,
103
+ interaction: label ?? "",
104
+ source_sdd_id: inputs.manifest.sddId,
105
+ });
106
+ rows.push(rowsById.get(id));
107
+ }
108
+ }
109
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
110
+ return rows;
111
+ }
112
+ /**
113
+ * F21 referent-collision check — corroborate on an INDEPENDENT evidence
114
+ * channel, never the one under guard. The F20-era version decided referent
115
+ * identity by comparing display names — the same evidence class the guard
116
+ * existed to distrust — so it was circular: the estate sweep that declared
117
+ * different-referent collisions absent USED this predicate and could not
118
+ * have found the counterexample it then declared inexercisable. Measured:
119
+ * nested names (a client layer and the server it proxies, names nesting)
120
+ * are DIFFERENT referents and stayed silent; acronym-vs-expansion is the
121
+ * SAME referent and fired. The predicate was inverted on the two
122
+ * name-shapes the estate actually uses.
123
+ *
124
+ * The independent channel is the technology string: ".NET 6 (in Linton)"
125
+ * against "Java" is a flat contradiction and needs no NLP. A dual-use
126
+ * entry-id (Container + System_Ext in one SDD) whose technologies
127
+ * contradict draws the finding REGARDLESS of what the names say — name
128
+ * agreement is precisely what makes the collision quiet today. Where no
129
+ * technology evidence exists on either side, silence: absence of evidence
130
+ * is not corroboration, and it is not contradiction either.
131
+ */
132
+ export function reportEntryIdReferentCollisions(inputs) {
133
+ const techTokens = (s) => new Set(s
134
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
135
+ .toLowerCase()
136
+ .split(/[^a-z0-9.+#]+/)
137
+ .filter((t) => t !== ""));
138
+ for (const [entryId, containerName] of inputs.containerNameByEntryId) {
139
+ const externalName = inputs.externalNameByEntryId.get(entryId);
140
+ if (externalName === undefined)
141
+ continue;
142
+ const containerTech = inputs.containerTechByEntryId.get(entryId) ?? "";
143
+ const externalTech = inputs.externalTechByEntryId.get(entryId) ?? "";
144
+ if (externalTech !== "" && techContradicts(containerTech, externalTech)) {
145
+ inputs.findings.add("convention", `c4 diagrams: entry-id "${entryId}" is declared Container ("${containerName}", "${containerTech}") and ` +
146
+ `System_Ext ("${externalName}", "${externalTech}") with contradictory technologies — ` +
147
+ `different referents wearing one id; the container interpretation wins for Rel edges — rename one entry-id to split them (F21)`);
148
+ continue;
149
+ }
150
+ if (externalTech !== "")
151
+ continue; // corroborating tech — same referent, silent
152
+ // F23: System_Ext carries no technology argument, so the primary
153
+ // dual-use case has NO independent evidence channel — measured: 0 of
154
+ // 109 external rows carry a technology. Weak corroboration is
155
+ // accepted (the external's description mentioning a container-tech
156
+ // token); otherwise the guard says what it cannot do, loudly. A
157
+ // dual-use entry-id buys nothing post-F20 (the container always wins
158
+ // for Rel edges) and risks a silent different-referent bind — the
159
+ // retirement is a rename, cheap because entry-ids are diagram-local
160
+ // and row identity derives from the display name.
161
+ const externalDesc = inputs.externalDescByEntryId.get(entryId) ?? "";
162
+ if (containerTech !== "" && externalDesc !== "") {
163
+ const ct = techTokens(containerTech);
164
+ const dt = techTokens(externalDesc);
165
+ let overlap = false;
166
+ for (const t of ct)
167
+ if (dt.has(t)) {
168
+ overlap = true;
169
+ break;
170
+ }
171
+ if (overlap)
172
+ continue; // description mentions the container's tech — weak corroboration
173
+ }
174
+ inputs.findings.add("convention", `c4 diagrams: entry-id "${entryId}" is declared Container ("${containerName}") and System_Ext ` +
175
+ `("${externalName}") with no independent evidence to adjudicate referent identity — System_Ext ` +
176
+ `carries no technology field; cannot corroborate, cannot contradict. Rename one entry-id to split ` +
177
+ `the namespaces — but check first whether the dual use is load-bearing: if Rel edges exist only via ` +
178
+ `the Container reading, a rename orphans them (they will surface as F26 findings); in that case ` +
179
+ `annotate the System_Ext description with the technology instead, so weak corroboration can silence ` +
180
+ `this honestly (F23/F26)`);
181
+ }
182
+ }
183
+ //# sourceMappingURL=container-dependency.js.map