@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,391 @@
1
+ /**
2
+ * Feature-grounding extractor per ADR 0143 (schema 2.5.0).
3
+ *
4
+ * Walks `docs/domains/<dom>/design/<capability>/feature-grounding.md`
5
+ * (platform-profile) and `docs/design/integration/feature-grounding.md`
6
+ * (integration-product), parses the canonical markdown table, and
7
+ * emits four catalogue contributions:
8
+ *
9
+ * - `features.csv` rows with `source: feature-grounding`.
10
+ * - `capability_feature.csv` rows (1:1 with each feature row).
11
+ * - `feature_repo.csv` rows derived from file-path references in
12
+ * the Evidence column.
13
+ * - `feature_service.csv` rows derived from class names / file
14
+ * basenames / route fragments in API Routes / Backend Handlers
15
+ * / Evidence columns, resolved via `services.csv` + the optional
16
+ * per-SDD alias map.
17
+ *
18
+ * Resolution failures (unresolved capability, service, repo) emit
19
+ * findings but never break FK validation — only confidently-resolved
20
+ * rows are emitted.
21
+ *
22
+ * Precedence per ADR 0143 §4.4: feature-tree and feature-spec rows
23
+ * win on id collision; grounding rows are skipped with a divergence
24
+ * finding.
25
+ */
26
+ import { readFile } from "node:fs/promises";
27
+ import { relative } from "node:path";
28
+ import { capabilityIdFromPath, findIntegrationGroundingFile, findPlatformGroundingFiles, parseGroundingTable, } from "./feature-grounding-walker.js";
29
+ const DESCRIPTION_MAX = 500;
30
+ /** Columns the service-name resolver scans for class names / route fragments. */
31
+ const SERVICE_RESOLUTION_COLUMNS = [
32
+ "ui_components",
33
+ "api_routes",
34
+ "backend_handlers",
35
+ "evidence",
36
+ ];
37
+ /** Columns the repo-name resolver scans for file-path references. */
38
+ const REPO_RESOLUTION_COLUMNS = ["evidence", "ui_components"];
39
+ export async function buildFeatureGrounding(inputs) {
40
+ const featureRows = [];
41
+ const capabilityFeatureRows = [];
42
+ const featureRepoRows = [];
43
+ // M-ADR 0143 §4.4 (amended): rows whose Evidence cell is non-empty but resolves to
44
+ // ZERO repos. Counted for the summary finding below; each row also gets its own
45
+ // line. This measures evidence→repo RESOLUTION COVERAGE, not correctness.
46
+ let repoEvidenceRows = 0;
47
+ const repoZeroResolutionFeatureIds = [];
48
+ const featureServiceRows = [];
49
+ const seenFeatureRepo = new Set();
50
+ const seenFeatureService = new Set();
51
+ const seenFeatures = new Set(inputs.existingFeatureIds);
52
+ // Kept separate from `seenFeatures`, which grows during this walk. The two
53
+ // sets are what distinguish a BY-DESIGN cross-source collision from a REAL
54
+ // within-source one — see the split at the collision site below.
55
+ const fromEarlierSource = new Set(inputs.existingFeatureIds);
56
+ const files = [];
57
+ if (inputs.profile === "platform-profile") {
58
+ for (const absPath of await findPlatformGroundingFiles(inputs.sddPath)) {
59
+ const capId = capabilityIdFromPath(absPath, inputs.sddPath);
60
+ files.push({ absPath, capabilityId: capId });
61
+ }
62
+ }
63
+ else if (inputs.profile === "integration-product") {
64
+ const absPath = await findIntegrationGroundingFile(inputs.sddPath);
65
+ if (absPath !== null) {
66
+ // For integration-product, the integration itself is the
67
+ // capability surface; use the SDD's product id.
68
+ files.push({ absPath, capabilityId: inputs.productId });
69
+ }
70
+ }
71
+ for (const { absPath, capabilityId: pathCapabilityId } of files) {
72
+ const relPath = relative(inputs.sddPath, absPath);
73
+ if (pathCapabilityId === "") {
74
+ inputs.findings.add("convention", `${relPath}: capability id could not be resolved from file path`);
75
+ continue;
76
+ }
77
+ // Resolve a design-dir slug that is not itself a capability id via the
78
+ // slug → id alias map (spec / feature-directory basename → declared id).
79
+ const capabilityId = inputs.knownCapabilityIds.has(pathCapabilityId)
80
+ ? pathCapabilityId
81
+ : (inputs.capabilitySlugAliases?.get(pathCapabilityId) ?? pathCapabilityId);
82
+ if (!inputs.knownCapabilityIds.has(capabilityId)) {
83
+ inputs.findings.add("convention", `${relPath}: capability "${pathCapabilityId}" from file path does not resolve against capabilities.csv`);
84
+ continue;
85
+ }
86
+ const text = await readFile(absPath, "utf-8");
87
+ const rows = parseGroundingTable(text);
88
+ if (rows.length === 0) {
89
+ inputs.findings.add("coverage", `${relPath}: feature-grounding.md found but no recognisable Feature-to-Code Mapping table`);
90
+ continue;
91
+ }
92
+ const slugsInThisFile = new Set();
93
+ for (const row of rows) {
94
+ if (slugsInThisFile.has(row.slug)) {
95
+ inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: duplicate feature slug "${row.slug}" in same file (first occurrence wins)`);
96
+ continue;
97
+ }
98
+ slugsInThisFile.add(row.slug);
99
+ const featureId = `${capabilityId}-${row.slug}`;
100
+ const featureRowCollides = seenFeatures.has(featureId);
101
+ // Feature row + capability_feature edge: emit only on first sight.
102
+ // feature-tree / feature-spec win the row identity when they declared
103
+ // it first — they carry runway / group / parent context that
104
+ // grounding doesn't, plus capability-spec.ts already emitted the
105
+ // capability_feature row on the spec source path.
106
+ //
107
+ // feature_repo and feature_service edges: emit unconditionally per
108
+ // ADR 0143 §2 (these are the source's unique contribution; nothing
109
+ // else carries per-row code-mapping). Pre-0.30.2 the collision skip
110
+ // dropped these edges too — bug fixed per
111
+ // `coordination/feature-grounding-collision-edges-coordination.md`.
112
+ if (featureRowCollides) {
113
+ // ONE CHECK, TWO MEANINGS — split them, because they are not the same
114
+ // event and only one of them is a defect.
115
+ //
116
+ // `seenFeatures` is seeded from earlier sources AND grows during this
117
+ // walk, so a hit means either:
118
+ // - an earlier SOURCE emitted this id. That is ADR 0143 §2's
119
+ // documented precedence rule working exactly as specified: the
120
+ // grounding pass re-declares ids that feature-tree/feature-spec
121
+ // already own. It happens for essentially every grounded feature,
122
+ // measured at 1,545 standing instances across two estates, and it
123
+ // is correct output. Reported as `inferred` (the category
124
+ // feature-contract.ts already uses for the identical
125
+ // both-paths-produced-this dedup shape, and one the gate does not
126
+ // act on).
127
+ // - an earlier FILE in THIS source pass emitted it — two
128
+ // feature-grounding files under one capability declaring the same
129
+ // slug. That is a real authoring collision and stays `divergence`,
130
+ // matching the within-one-file duplicate check above.
131
+ //
132
+ // Before this split the gate's contract ("fail on any new divergence
133
+ // line") could not work here: a real collision arrived inside a crowd
134
+ // of 1,545 accepted ones, so the whole category was unusable as a
135
+ // signal and unbaselineable as a backlog.
136
+ if (fromEarlierSource.has(featureId)) {
137
+ inputs.findings.add("inferred", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by an earlier source (feature-tree / feature-spec wins for the row; feature_repo + feature_service edges still emitted per ADR 0143 §2)`);
138
+ }
139
+ else {
140
+ inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by another file in this same feature-grounding pass — two files under one capability declare the same slug (first occurrence wins)`);
141
+ }
142
+ }
143
+ else {
144
+ seenFeatures.add(featureId);
145
+ featureRows.push({
146
+ id: featureId,
147
+ name: row.slug,
148
+ product_id: "",
149
+ capability_id: capabilityId,
150
+ parent_group_id: "",
151
+ parent_feature_id: "",
152
+ description: deriveDescription(row),
153
+ maturity: "",
154
+ status: "active",
155
+ source: "feature-grounding",
156
+ runway: "",
157
+ source_sdd_id: "",
158
+ });
159
+ const cfId = `${capabilityId}__${featureId}`;
160
+ capabilityFeatureRows.push({
161
+ id: cfId,
162
+ capability_id: capabilityId,
163
+ feature_id: featureId,
164
+ });
165
+ }
166
+ // feature_service edges — emit regardless of row collision.
167
+ const services = resolveServices(row, inputs);
168
+ for (const serviceId of services) {
169
+ const edgeId = `${featureId}__${serviceId}`;
170
+ if (seenFeatureService.has(edgeId))
171
+ continue;
172
+ seenFeatureService.add(edgeId);
173
+ featureServiceRows.push({
174
+ id: edgeId,
175
+ feature_id: featureId,
176
+ service_id: serviceId,
177
+ });
178
+ }
179
+ // feature_repo edges — emit regardless of row collision.
180
+ const repos = resolveRepos(row, inputs);
181
+ if ((row.cells.get("evidence") ?? "").trim() !== "") {
182
+ repoEvidenceRows++;
183
+ if (repos.length === 0) {
184
+ repoZeroResolutionFeatureIds.push(featureId);
185
+ inputs.findings.add("coverage", `${relPath}:${row.lineNumber}: feature "${featureId}" — Evidence 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)`);
186
+ }
187
+ }
188
+ for (const repoKey of repos) {
189
+ const edgeId = `${featureId}__${repoKey}`;
190
+ if (seenFeatureRepo.has(edgeId))
191
+ continue;
192
+ seenFeatureRepo.add(edgeId);
193
+ featureRepoRows.push({
194
+ id: edgeId,
195
+ feature_id: featureId,
196
+ repo_id: repoKey,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ // Summary emitted by the build after feature_repo is final (split against
202
+ // features left with NO anchor from any surface).
203
+ return {
204
+ featureRows,
205
+ capabilityFeatureRows,
206
+ featureRepoRows,
207
+ featureServiceRows,
208
+ repoEvidenceRows,
209
+ repoZeroResolutionFeatureIds,
210
+ };
211
+ }
212
+ /**
213
+ * Build the description column by joining non-feature cells, capped at
214
+ * 500 chars to match the feature-tree bullet convention.
215
+ */
216
+ function deriveDescription(row) {
217
+ const parts = [];
218
+ for (const [key, value] of row.cells) {
219
+ if (value === "")
220
+ continue;
221
+ parts.push(`${prettyHeader(key)}: ${value}`);
222
+ }
223
+ const joined = parts.join(" • ");
224
+ return joined.length > DESCRIPTION_MAX
225
+ ? joined.slice(0, DESCRIPTION_MAX)
226
+ : joined;
227
+ }
228
+ function prettyHeader(key) {
229
+ return key
230
+ .split("_")
231
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
232
+ .join(" ");
233
+ }
234
+ /**
235
+ * Resolve service ids referenced in a row's cells. Strategy:
236
+ * - Tokenize backtick-quoted identifiers + CamelCase / kebab-case tokens.
237
+ * - Strip extensions (.ts/.java/.yaml/.kt/.py) and trailing parens.
238
+ * - Try exact match against services.id (lowercased).
239
+ * - Try alias map (case-sensitive — display names often carry case).
240
+ */
241
+ function resolveServices(row, inputs) {
242
+ const out = new Set();
243
+ for (const col of SERVICE_RESOLUTION_COLUMNS) {
244
+ const cell = row.cells.get(col) ?? "";
245
+ if (cell === "")
246
+ continue;
247
+ for (const token of tokenize(cell)) {
248
+ const resolved = resolveServiceToken(token, inputs);
249
+ if (resolved !== null)
250
+ out.add(resolved);
251
+ }
252
+ }
253
+ return [...out].sort();
254
+ }
255
+ function resolveServiceToken(rawToken, inputs) {
256
+ // Try alias first (case-sensitive).
257
+ if (inputs.aliases !== null) {
258
+ const aliased = inputs.aliases.serviceAliases.get(rawToken);
259
+ if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
260
+ return aliased;
261
+ }
262
+ }
263
+ // Normalise + try exact match.
264
+ const normalised = normaliseToken(rawToken);
265
+ if (normalised === "")
266
+ return null;
267
+ if (inputs.knownServiceIds.has(normalised))
268
+ return normalised;
269
+ // Try alias on normalised form too.
270
+ if (inputs.aliases !== null) {
271
+ const aliased = inputs.aliases.serviceAliases.get(normalised);
272
+ if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
273
+ return aliased;
274
+ }
275
+ }
276
+ return null;
277
+ }
278
+ /**
279
+ * Resolve repo ids from file-path references. Strategy:
280
+ * - Tokenize path-like spans (contain `/` or end in known extensions).
281
+ * - For each token: try longest-prefix match against
282
+ * `canonical_repo_key`s; e.g. `example-service-api-master@abc/src/...`
283
+ * → `example-service-api` (after stripping branch/sha suffix).
284
+ * - Also accept the alias map for display-name → canonical-key.
285
+ */
286
+ function resolveRepos(row, inputs) {
287
+ const out = new Set();
288
+ for (const col of REPO_RESOLUTION_COLUMNS) {
289
+ const cell = row.cells.get(col) ?? "";
290
+ if (cell === "")
291
+ continue;
292
+ const coded = inputs.repoTokenPolicy === "identifier" ? codeSpanTokens(cell) : null;
293
+ for (const token of tokenize(cell)) {
294
+ const resolved = resolveRepoToken(token, inputs);
295
+ if (resolved === null)
296
+ continue;
297
+ if (coded !== null && isPlainWord(token) && !coded.has(token) && inputs.aliases?.repoAliases.get(token) !== resolved)
298
+ continue;
299
+ out.add(resolved);
300
+ }
301
+ }
302
+ return [...out].sort();
303
+ }
304
+ function resolveRepoToken(rawToken, inputs) {
305
+ // Try alias first.
306
+ if (inputs.aliases !== null) {
307
+ const aliased = inputs.aliases.repoAliases.get(rawToken);
308
+ if (aliased !== undefined && inputs.knownRepoKeys.has(aliased)) {
309
+ return aliased;
310
+ }
311
+ }
312
+ // Direct match on the full token (rare).
313
+ if (inputs.knownRepoKeys.has(rawToken))
314
+ return rawToken;
315
+ // Walk every path segment and try to recover a repo key. Each
316
+ // segment may carry a `-master@<sha>` or `@<sha>` suffix; strip
317
+ // those before matching.
318
+ for (const segment of rawToken.split("/")) {
319
+ const stripped = segment
320
+ .replace(/-master@.*$/, "")
321
+ .replace(/@[^/]+$/, "");
322
+ if (stripped !== "" && inputs.knownRepoKeys.has(stripped)) {
323
+ return stripped;
324
+ }
325
+ // Also try the segment with the `-master` literal removed (no @sha).
326
+ const trailingMaster = stripped.replace(/-master$/, "");
327
+ if (trailingMaster !== stripped &&
328
+ inputs.knownRepoKeys.has(trailingMaster)) {
329
+ return trailingMaster;
330
+ }
331
+ }
332
+ return null;
333
+ }
334
+ /**
335
+ * Extract candidate identifier tokens from a markdown cell. Matches:
336
+ * - Backticked spans: `` `AuthHandler.handleLoginRequest()` ``
337
+ * - Bare identifiers: `ExampleOauthService`
338
+ * - kebab-case identifiers: `example-service-api`
339
+ * - Path-like tokens with at least one `/`.
340
+ * Strips leading method names (e.g. `AuthHandler.foo()` → `AuthHandler`).
341
+ */
342
+ function tokenize(cell) {
343
+ const out = new Set();
344
+ // Backticked spans.
345
+ for (const match of cell.matchAll(/`([^`]+)`/g)) {
346
+ const inner = match[1] ?? "";
347
+ addCandidatesFromToken(inner, out);
348
+ }
349
+ // Bare identifiers outside backticks — strip backtick spans first.
350
+ const stripped = cell.replace(/`[^`]+`/g, " ");
351
+ for (const match of stripped.matchAll(/[A-Za-z][A-Za-z0-9._@/-]*[A-Za-z0-9]/g)) {
352
+ addCandidatesFromToken(match[0], out);
353
+ }
354
+ return [...out];
355
+ }
356
+ function addCandidatesFromToken(token, out) {
357
+ if (token === "")
358
+ return;
359
+ // Path token: take first segment as a candidate AND keep the full path.
360
+ if (token.includes("/")) {
361
+ out.add(token);
362
+ const first = token.split("/")[0] ?? "";
363
+ if (first !== "" && first !== token)
364
+ out.add(first);
365
+ return;
366
+ }
367
+ // Method/class form: split on `.`, contribute each segment except (), [].
368
+ const head = token.split(/[(.[]/)[0] ?? "";
369
+ if (head !== "")
370
+ out.add(head);
371
+ // Also keep the full token for alias-map matching on exact display
372
+ // strings like `AuthHandler.handleLoginRequest`.
373
+ out.add(token);
374
+ }
375
+ /** Candidate tokens that came from backticked code spans only. */
376
+ function codeSpanTokens(cell) {
377
+ const out = new Set();
378
+ for (const match of cell.matchAll(/`([^`]+)`/g))
379
+ addCandidatesFromToken(match[1] ?? "", out);
380
+ return out;
381
+ }
382
+ /** A letters-only word — ordinary prose unless it is written as code. */
383
+ const isPlainWord = (token) => /^[A-Za-z]+$/.test(token);
384
+ function normaliseToken(token) {
385
+ return token
386
+ .toLowerCase()
387
+ .replace(/\.(ts|tsx|js|jsx|java|kt|kts|py|go|rs|yaml|yml|json|md)$/i, "")
388
+ .replace(/\(\)$/, "")
389
+ .trim();
390
+ }
391
+ //# sourceMappingURL=feature-grounding.js.map
@@ -0,0 +1,63 @@
1
+ // FeatureGroups discoverer — emits `feature_groups.csv` rows from
2
+ // hierarchical `feature-tree.md` files.
3
+ //
4
+ // Per Q7.2 resolution (adopt-canonical-catalogue-builder.md §7.2),
5
+ // FeatureGroups are first-class entities. This builder consumes the
6
+ // shared `feature-tree-walker` and emits one row per H2/H3 heading
7
+ // outside fenced code blocks.
8
+ //
9
+ // ASCII-tree representations inside fenced code blocks are NOT parsed
10
+ // — those are an adopter authoring style that doesn't map cleanly to
11
+ // catalogue entities. Adopters wanting first-class groups format their
12
+ // feature trees with proper H2/H3 headings.
13
+ //
14
+ // Source paths: see `findFeatureTreeFile` in feature-tree-walker.ts.
15
+ import { readFile } from "node:fs/promises";
16
+ import { relative } from "node:path";
17
+ import { featureGroupId, findFeatureTreeFile, walkFeatureTree, } from "./feature-tree-walker.js";
18
+ export async function buildFeatureGroups(inputs) {
19
+ const featureTreePath = await findFeatureTreeFile(inputs.sddPath);
20
+ if (!featureTreePath) {
21
+ return [];
22
+ }
23
+ const text = await readFile(featureTreePath, "utf-8");
24
+ const events = walkFeatureTree(text);
25
+ const rows = [];
26
+ for (const ev of events) {
27
+ if (ev.kind === "h2") {
28
+ rows.push({
29
+ id: featureGroupId(inputs.productId, ev.slug),
30
+ name: ev.text,
31
+ product_id: inputs.productId,
32
+ capability_id: "",
33
+ parent_group_id: "",
34
+ description: "",
35
+ source: "feature-tree-h2",
36
+ source_sdd_id: "",
37
+ });
38
+ }
39
+ else if (ev.kind === "h3") {
40
+ if (ev.parentH2Slug === "") {
41
+ // H3 with no enclosing H2 — orphan; surface as finding but emit anyway.
42
+ inputs.findings.add("convention", `${relative(inputs.sddPath, featureTreePath)}: H3 "${ev.text}" has no enclosing H2`);
43
+ }
44
+ const id = ev.parentH2Slug !== ""
45
+ ? featureGroupId(inputs.productId, ev.parentH2Slug, ev.slug)
46
+ : featureGroupId(inputs.productId, ev.slug);
47
+ rows.push({
48
+ id,
49
+ name: ev.text,
50
+ product_id: inputs.productId,
51
+ capability_id: "",
52
+ parent_group_id: ev.parentH2Slug !== ""
53
+ ? featureGroupId(inputs.productId, ev.parentH2Slug)
54
+ : "",
55
+ description: "",
56
+ source: "feature-tree-h3",
57
+ source_sdd_id: "",
58
+ });
59
+ }
60
+ }
61
+ return rows;
62
+ }
63
+ //# sourceMappingURL=feature-groups.js.map
@@ -0,0 +1,228 @@
1
+ // feature_subscription_channels builder — emits one row per
2
+ // `subscription_channels:` entry in any `reverse-evidence/feature-grounding-evidence.yaml`
3
+ // file in the SDD. Added in schema 2.9.0.
4
+ //
5
+ // Per resolved decision §1 of `content/plans/v0-9-0-cohort-sweep-derived-bundle.md`
6
+ // (see also `docs/method/methods/reverse/rubrics/reverse-application-evidence-quality.md`
7
+ // per-feature slot vocabulary): `subscription_channels:` is a peer of
8
+ // `graphql_operations:` in the per-feature evidence shape — `graphql_operations:`
9
+ // carries the operation names declared in the GraphQL schema; this builder
10
+ // captures the channel/topic layer (AppSync WebSocket, Pekko WS, MQTT, SSE,
11
+ // etc.) that physically delivers push events.
12
+ //
13
+ // Input shape (per the rubric):
14
+ //
15
+ // features:
16
+ // - feature_slug: <slug>
17
+ // subscription_channels:
18
+ // - name: <channel-identifier>
19
+ // transport: appsync-websocket | pekko-ws | mqtt | sse | webhook-push | other
20
+ // evidence: <sub-repo>/<path>:<line>
21
+ // payload_ref: <event-name> # optional
22
+ //
23
+ // Feature-id resolution: the canonical builder emits feature rows with ids
24
+ // shaped `{product_id|capability_id}-{feature_slug}`. Adopters' evidence
25
+ // yamls carry only the bare `feature_slug:`. This builder accepts a set of
26
+ // known feature ids (`existingFeatureIds`) and probes each candidate
27
+ // resolution (`{productId}-{slug}` and `{slug}` and `<capability>-{slug}`
28
+ // for any capability id ending in `-{slug}`). When no candidate resolves,
29
+ // the row is dropped and an `orphan` finding is emitted so reviewers can
30
+ // reconcile.
31
+ import { readFile, readdir } from "node:fs/promises";
32
+ import { join, relative, sep } from "node:path";
33
+ import { parse as parseYaml } from "yaml";
34
+ const VALID_TRANSPORTS = new Set([
35
+ "appsync-websocket",
36
+ "pekko-ws",
37
+ "mqtt",
38
+ "sse",
39
+ "webhook-push",
40
+ "other",
41
+ ]);
42
+ export async function buildFeatureSubscriptionChannels(inputs) {
43
+ const rows = [];
44
+ const seenIds = new Set();
45
+ const evidenceFiles = await collectEvidenceFiles(inputs.sddPath);
46
+ for (const file of evidenceFiles) {
47
+ const relPath = relative(inputs.sddPath, file);
48
+ // The per-design-dir `feature-grounding-evidence.yaml` is this builder's
49
+ // dedicated contract — read/parse failures are its findings. The
50
+ // per-domain `<domain>-feature-grounding.yaml` files (ADR 0164) are
51
+ // primarily authored for other tooling and only opportunistically scanned
52
+ // for a `feature_subscription_channels:` key; they may not be strict YAML,
53
+ // so their read/parse failures are skipped silently (not this builder's
54
+ // authority to validate).
55
+ const isCanonical = file.substring(file.lastIndexOf(sep) + 1) === "feature-grounding-evidence.yaml";
56
+ let text;
57
+ try {
58
+ text = await readFile(file, "utf-8");
59
+ }
60
+ catch {
61
+ if (isCanonical) {
62
+ inputs.findings.add("convention", `${relPath}: cannot read evidence file`);
63
+ }
64
+ continue;
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = parseYaml(text);
69
+ }
70
+ catch (err) {
71
+ if (isCanonical) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ inputs.findings.add("convention", `${relPath}: invalid YAML — ${msg}`);
74
+ }
75
+ continue;
76
+ }
77
+ if (!parsed || typeof parsed !== "object")
78
+ continue;
79
+ // Per-feature subscription_channels live under `features:` (per-design-dir
80
+ // layout) or a top-level `feature_subscription_channels:` list (per-domain
81
+ // layout, ADR 0164). Both are lists of { feature_slug, subscription_channels }.
82
+ const obj = parsed;
83
+ const featureEntries = [];
84
+ if (Array.isArray(obj["features"]))
85
+ featureEntries.push(...obj["features"]);
86
+ if (Array.isArray(obj["feature_subscription_channels"])) {
87
+ featureEntries.push(...obj["feature_subscription_channels"]);
88
+ }
89
+ if (featureEntries.length === 0)
90
+ continue;
91
+ for (const f of featureEntries) {
92
+ if (!f || typeof f !== "object")
93
+ continue;
94
+ const feat = f;
95
+ const slug = String(feat["feature_slug"] ?? "").trim();
96
+ if (slug === "")
97
+ continue;
98
+ const channels = feat["subscription_channels"];
99
+ if (!Array.isArray(channels) || channels.length === 0)
100
+ continue;
101
+ const featureId = resolveFeatureId(slug, inputs.productId, inputs.existingFeatureIds);
102
+ if (featureId === "") {
103
+ inputs.findings.add("orphan", `${relPath}: feature_slug="${slug}" carries subscription_channels but does not resolve to any catalogue feature id`);
104
+ continue;
105
+ }
106
+ for (const ch of channels) {
107
+ if (!ch || typeof ch !== "object")
108
+ continue;
109
+ const channelObj = ch;
110
+ const channelName = String(channelObj["name"] ?? "").trim();
111
+ if (channelName === "") {
112
+ inputs.findings.add("convention", `${relPath}: subscription_channels entry under feature_slug="${slug}" missing required "name" field`);
113
+ continue;
114
+ }
115
+ const transport = String(channelObj["transport"] ?? "").trim();
116
+ if (transport === "") {
117
+ inputs.findings.add("convention", `${relPath}: subscription_channels entry "${channelName}" under feature_slug="${slug}" missing required "transport" field`);
118
+ continue;
119
+ }
120
+ if (!VALID_TRANSPORTS.has(transport)) {
121
+ inputs.findings.add("convention", `${relPath}: subscription_channels entry "${channelName}" under feature_slug="${slug}" has unknown transport="${transport}" (expected one of ${[...VALID_TRANSPORTS].join(", ")})`);
122
+ // Don't drop — emit the row with the literal transport. The
123
+ // schema accepts any string; this finding lets reviewers
124
+ // notice the deviation.
125
+ }
126
+ const evidence = typeof channelObj["evidence"] === "string"
127
+ ? channelObj["evidence"].trim()
128
+ : "";
129
+ const payloadRef = typeof channelObj["payload_ref"] === "string"
130
+ ? channelObj["payload_ref"].trim()
131
+ : "";
132
+ const rowId = `${featureId}__${channelName}`;
133
+ if (seenIds.has(rowId)) {
134
+ inputs.findings.add("divergence", `${relPath}: duplicate subscription_channels row id="${rowId}" — same (feature_id, channel_name) already emitted`);
135
+ continue;
136
+ }
137
+ seenIds.add(rowId);
138
+ rows.push({
139
+ id: rowId,
140
+ feature_id: featureId,
141
+ channel_name: channelName,
142
+ transport,
143
+ evidence,
144
+ payload_ref: payloadRef,
145
+ source_sdd_id: "",
146
+ });
147
+ }
148
+ }
149
+ }
150
+ return rows;
151
+ }
152
+ /**
153
+ * Resolve a bare `feature_slug` from evidence yaml to a canonical feature id
154
+ * present in `existingFeatureIds`. Candidates probed in order:
155
+ * 1. `{productId}-{slug}` — typical for application-product / integration-product features.
156
+ * 2. The slug as-is — when the feature id is just the slug (legacy / capability-spec source).
157
+ * 3. Any existing id ending in `-{slug}` — covers `{capability-id}-{slug}` for capability-spec sourced features.
158
+ *
159
+ * Returns `""` when no candidate resolves.
160
+ */
161
+ function resolveFeatureId(slug, productId, existing) {
162
+ if (productId !== "") {
163
+ const candidate = `${productId}-${slug}`;
164
+ if (existing.has(candidate))
165
+ return candidate;
166
+ }
167
+ if (existing.has(slug))
168
+ return slug;
169
+ for (const id of existing) {
170
+ if (id.endsWith(`-${slug}`))
171
+ return id;
172
+ }
173
+ return "";
174
+ }
175
+ async function collectEvidenceFiles(sddPath) {
176
+ const out = [];
177
+ await walk(sddPath, sddPath, out);
178
+ return out;
179
+ }
180
+ async function walk(root, current, out) {
181
+ let entries;
182
+ try {
183
+ entries = await readdir(current, { withFileTypes: true });
184
+ }
185
+ catch {
186
+ return;
187
+ }
188
+ for (const e of entries) {
189
+ if (e.name === "node_modules" || e.name === ".git" || e.name === "source" || e.name === "catalogues" || e.name === "dist") {
190
+ continue;
191
+ }
192
+ const abs = join(current, e.name);
193
+ if (e.isDirectory()) {
194
+ // Check whether this is the reverse-evidence dir; if so, look for the file directly.
195
+ if (e.name === "reverse-evidence") {
196
+ // Two reverse-evidence layouts are read (ADR 0164):
197
+ // - per-design-dir: `reverse-evidence/feature-grounding-evidence.yaml`
198
+ // (features: → per-feature subscription_channels:).
199
+ // - per-domain: `reverse-evidence/<domain>-feature-grounding.yaml`
200
+ // at the SDD root (top-level feature_subscription_channels: list).
201
+ let reEntries;
202
+ try {
203
+ reEntries = await readdir(abs, { withFileTypes: true });
204
+ }
205
+ catch {
206
+ continue;
207
+ }
208
+ for (const re of reEntries) {
209
+ if (!re.isFile())
210
+ continue;
211
+ if (re.name === "feature-grounding-evidence.yaml" ||
212
+ re.name.endsWith("-feature-grounding.yaml")) {
213
+ out.push(join(abs, re.name));
214
+ }
215
+ }
216
+ continue;
217
+ }
218
+ // Bound recursion depth via a heuristic on path component count from root
219
+ // to avoid pathological deep trees; reverse-evidence lives at most a few
220
+ // levels deep in adopters we've seen.
221
+ const rel = relative(root, abs);
222
+ if (rel.split(sep).length > 8)
223
+ continue;
224
+ await walk(root, abs, out);
225
+ }
226
+ }
227
+ }
228
+ //# sourceMappingURL=feature-subscription-channels.js.map