@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,402 @@
1
+ // Contracts discoverer — emits `contracts.csv` rows from
2
+ // `contracts/{openapi,proto,avro}/<domain>/<name>.<ext>` (canonical) and
3
+ // `docs/domains/<domain>/contracts/{openapi,proto,avro}/<name>.<ext>`
4
+ // (platform-profile per reverse/workflows/contract.md).
5
+ //
6
+ // Per the canonical schema (catalogue-schema.yaml):
7
+ // - kind enum: rest | graphql | rpc | event
8
+ // - openapi/*.yaml → rest
9
+ // - proto/*.proto → rpc
10
+ // - avro/*.avsc → event
11
+ // - service_id and source_repo_id are nullable; per ADR 0110 the real
12
+ // contract sources may live in sibling contracts-* repos. The
13
+ // canonical builder leaves these empty by default; an OpenAPI
14
+ // `x-source-repo` extension is honoured as a hint when present
15
+ // (resolved against the known repository set).
16
+ // - For OpenAPI files only, `info.title` becomes `description`,
17
+ // `info.version` becomes `version`. Other formats keep these empty.
18
+ import { readdir, readFile } from "node:fs/promises";
19
+ import { extname, join, relative, sep } from "node:path";
20
+ import { parse as parseYaml } from "yaml";
21
+ import { contractIdWithKind } from "../contract-id.js";
22
+ const CONTRACTS_DIR_REL = "contracts";
23
+ const DOCS_DOMAINS_DIR_REL = join("docs", "domains");
24
+ const KIND_SPECS = [
25
+ { subdir: "openapi", extensions: [".yaml", ".yml"], kind: "rest" },
26
+ { subdir: "proto", extensions: [".proto"], kind: "rpc" },
27
+ { subdir: "avro", extensions: [".avsc"], kind: "event" },
28
+ // `authz` — authorization/policy schemas (SpiceDB `.zed` and equivalents).
29
+ //
30
+ // Added because an adopter's contract lock-in gate APPROVED two `.zed` schemas
31
+ // as contracts ("all 11 contracts lock"), while `kind` had no member able to
32
+ // represent them — so the catalogue could not hold an artefact the method's own
33
+ // gate had certified. That gate had already recorded the tension itself, passing
34
+ // SpiceDB "with-waiver on rubric-fit grounds"; this closes the same gap one
35
+ // layer down.
36
+ //
37
+ // Zero-delta when added: no estate holds an in-repo `contracts/spicedb/` tree,
38
+ // so this discovers nothing today and exists so that a declared or migrated
39
+ // authz schema HAS a kind rather than being silently unrepresentable.
40
+ { subdir: "spicedb", extensions: [".zed"], kind: "authz" },
41
+ // `graphql` — GraphQL SDL. Added last of the five, and the reason it was last is
42
+ // the finding: `graphql` had been a member of the `kind` enum since the enum
43
+ // existed, while NO code path could ever produce it. Not discovered here, not
44
+ // mapped from a manifest key, not extracted by the loader, not walked by the
45
+ // validator, and not offered by the template. **A vocabulary member unreachable
46
+ // by construction** — the same class as a rubric anchor no artefact can satisfy,
47
+ // and it stayed invisible for the same reason: an unreachable value is emitted
48
+ // for nothing and so never complains.
49
+ //
50
+ // NOT zero-delta, unlike spicedb: the reverse estate already holds 7 `.graphql`
51
+ // files under the conventional `docs/domains/<domain>/contracts/graphql/` layout
52
+ // (vehicle 5, applications 2), invisible to the catalogue until now. An author
53
+ // whose GraphQL surface could not be keyed documented the wall in their own
54
+ // manifest — naming the schema lines that blocked them — and wrote the only
55
+ // token left in `contracts.status`.
56
+ { subdir: "graphql", extensions: [".graphql", ".graphqls"], kind: "graphql" },
57
+ ];
58
+ export async function buildContracts(inputs) {
59
+ const rows = [];
60
+ const idByPath = new Map();
61
+ const seen = new Set();
62
+ // Contract deprecation source (schema 2.22.0): BASELINE.md deprecation-
63
+ // runway entries per ADR 0073 §4.8 — the same source
64
+ // validate-deprecation-runway.sh enforces. Absent file = no deprecations.
65
+ const deprecatedPaths = await readBaselineDeprecations(inputs.sddPath, inputs.findings);
66
+ const discovered = [];
67
+ // Layout 1: canonical SDD-root layout — contracts/<subdir>/<domain>/<file>.<ext>.
68
+ for (const spec of KIND_SPECS) {
69
+ const dir = join(inputs.sddPath, CONTRACTS_DIR_REL, spec.subdir);
70
+ const files = await collectFiles(dir, spec.extensions);
71
+ for (const path of files) {
72
+ const rel = relative(inputs.sddPath, path);
73
+ discovered.push({
74
+ absPath: path,
75
+ relPath: rel,
76
+ domain: domainSegmentCanonical(rel),
77
+ kind: spec.kind,
78
+ });
79
+ }
80
+ }
81
+ // Layout 2: platform-profile layout — docs/domains/<domain>/contracts/<subdir>/<file>.<ext>.
82
+ // Canonical for platform-profile SDDs per reverse/workflows/contract.md and
83
+ // reverse/workflow.md §3 ("docs/domains/{domain}/contracts/openapi/*.yaml ←
84
+ // refresh API surfaces"). Federated SDDs without docs/domains/ harmlessly
85
+ // contribute zero rows.
86
+ const domainsRoot = join(inputs.sddPath, DOCS_DOMAINS_DIR_REL);
87
+ const domainDirs = await listDirs(domainsRoot);
88
+ for (const domainName of domainDirs) {
89
+ for (const spec of KIND_SPECS) {
90
+ const dir = join(domainsRoot, domainName, CONTRACTS_DIR_REL, spec.subdir);
91
+ const files = await collectFiles(dir, spec.extensions);
92
+ for (const path of files) {
93
+ const rel = relative(inputs.sddPath, path);
94
+ discovered.push({
95
+ absPath: path,
96
+ relPath: rel,
97
+ domain: domainName,
98
+ kind: spec.kind,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ discovered.sort((a, b) => a.relPath.localeCompare(b.relPath));
104
+ for (const entry of discovered) {
105
+ const filename = basenameOf(entry.absPath);
106
+ // Skip schema/config files at any depth.
107
+ if (filename === "schema.yaml" || filename === "schema.yml")
108
+ continue;
109
+ const stem = stemOf(filename);
110
+ // Schema 3.0.0 (M-ADR 0203 §4.4): the id carries its kind component,
111
+ // so one service published as two contract families (openapi + proto)
112
+ // mints two rows instead of colliding — the measured 2.x standing cost
113
+ // this format exists to end. A remaining collision is same-kind and
114
+ // still reported.
115
+ const kindless = entry.domain !== ""
116
+ ? slugify(`${entry.domain}-${stem}`)
117
+ : slugify(stem);
118
+ const slug = contractIdWithKind(kindless, entry.kind);
119
+ if (seen.has(slug)) {
120
+ inputs.findings.add("divergence", `${entry.relPath}: contract id "${slug}" already emitted by an earlier file`);
121
+ continue;
122
+ }
123
+ seen.add(slug);
124
+ let description = "";
125
+ let version = "";
126
+ let sourceRepoId = "";
127
+ let context = "";
128
+ let evidenceClass = "";
129
+ if (entry.kind === "rest") {
130
+ const meta = await tryReadOpenapiInfo(entry.absPath);
131
+ description = meta.description;
132
+ version = meta.version;
133
+ context = meta.context;
134
+ evidenceClass = meta.evidenceClass;
135
+ if (meta.xSourceRepo !== "" && inputs.knownRepositoryIds.has(meta.xSourceRepo)) {
136
+ sourceRepoId = meta.xSourceRepo;
137
+ }
138
+ }
139
+ const normalisedRelPath = entry.relPath.split(sep).join("/");
140
+ rows.push({
141
+ id: slug,
142
+ name: stem,
143
+ service_id: "",
144
+ source_repo_id: sourceRepoId,
145
+ kind: entry.kind,
146
+ version,
147
+ status: deprecatedPaths.has(normalisedRelPath) ? "deprecated" : "active",
148
+ spec_path: entry.relPath,
149
+ visibility: "internal",
150
+ context,
151
+ evidence_class: evidenceClass,
152
+ description,
153
+ discovery: "discovered",
154
+ source_sdd_id: "",
155
+ });
156
+ idByPath.set(entry.relPath, slug);
157
+ }
158
+ // A deprecated BASELINE.md entry that matched no discovered spec is doc
159
+ // drift (path typo, file moved, or already removed) — surface it; the
160
+ // runway gate enforces file existence separately.
161
+ const emittedPaths = new Set(rows.map((r) => String(r["spec_path"]).split(sep).join("/")));
162
+ for (const path of deprecatedPaths) {
163
+ if (!emittedPaths.has(path)) {
164
+ inputs.findings.add("divergence", `contracts: BASELINE.md deprecates "${path}" but no discovered contract spec matches that path`);
165
+ }
166
+ }
167
+ return { rows, idByPath };
168
+ }
169
+ /**
170
+ * Read deprecated contract paths from BASELINE.md at the SDD root.
171
+ *
172
+ * Shape per ADR 0073 §4.8 (the deprecation-runway gate's source):
173
+ * contracts:
174
+ * openapi: [ { path, status, sunset_date? }, ... ]
175
+ * proto: [ ... ]
176
+ * avro: [ ... ]
177
+ *
178
+ * Returns the set of `path` values (forward-slash, SDD-root-relative) whose
179
+ * status is "deprecated". Missing file → empty set (pre-go-live SDDs have no
180
+ * BASELINE.md). Unparseable file → empty set with a divergence finding.
181
+ */
182
+ async function readBaselineDeprecations(sddPath, findings) {
183
+ const baselinePath = join(sddPath, "BASELINE.md");
184
+ let text;
185
+ try {
186
+ text = await readFile(baselinePath, "utf-8");
187
+ }
188
+ catch (err) {
189
+ const code = err.code;
190
+ if (code === "ENOENT")
191
+ return new Set();
192
+ throw err;
193
+ }
194
+ let raw;
195
+ try {
196
+ raw = parseYaml(text);
197
+ }
198
+ catch {
199
+ findings.add("divergence", `contracts: BASELINE.md exists but is not parseable YAML — contract deprecation statuses not wired`);
200
+ return new Set();
201
+ }
202
+ const deprecated = new Set();
203
+ const contracts = raw?.["contracts"];
204
+ if (!contracts || typeof contracts !== "object")
205
+ return deprecated;
206
+ for (const typeKey of ["openapi", "proto", "avro"]) {
207
+ const entries = contracts[typeKey];
208
+ if (!Array.isArray(entries))
209
+ continue;
210
+ for (const entry of entries) {
211
+ if (!entry || typeof entry !== "object")
212
+ continue;
213
+ const e = entry;
214
+ if (String(e["status"] ?? "") !== "deprecated")
215
+ continue;
216
+ const path = String(e["path"] ?? "").trim();
217
+ if (path !== "")
218
+ deprecated.add(path);
219
+ }
220
+ }
221
+ return deprecated;
222
+ }
223
+ async function listDirs(parent) {
224
+ try {
225
+ const entries = await readdir(parent, { withFileTypes: true });
226
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
227
+ }
228
+ catch (err) {
229
+ const code = err.code;
230
+ if (code === "ENOENT")
231
+ return [];
232
+ throw err;
233
+ }
234
+ }
235
+ async function tryReadOpenapiInfo(path) {
236
+ try {
237
+ const text = await readFile(path, "utf-8");
238
+ const raw = parseYaml(text);
239
+ if (!raw || typeof raw !== "object") {
240
+ return { description: "", version: "", xSourceRepo: "", context: "", evidenceClass: "" };
241
+ }
242
+ const info = raw["info"];
243
+ const xSourceRepo = String(raw["x-source-repo"] ?? "").trim();
244
+ // `x-context` may sit on info (preferred) or at the document root.
245
+ const contextRaw = info?.["x-context"] ?? raw["x-context"];
246
+ const context = typeof contextRaw === "string" ? contextRaw.trim() : "";
247
+ // `x-evidence-class` mirrors x-context: info (preferred), document root fallback.
248
+ const evidenceRaw = info?.["x-evidence-class"] ?? raw["x-evidence-class"];
249
+ const evidenceClass = typeof evidenceRaw === "string" ? evidenceRaw.trim() : "";
250
+ const versionRaw = info?.["version"];
251
+ const descRaw = info?.["description"];
252
+ const description = typeof descRaw === "string"
253
+ ? firstSentence(descRaw)
254
+ : "";
255
+ const version = typeof versionRaw === "string"
256
+ ? versionRaw
257
+ : versionRaw !== undefined
258
+ ? String(versionRaw)
259
+ : "";
260
+ return { description, version, xSourceRepo, context, evidenceClass };
261
+ }
262
+ catch {
263
+ // Not all *.yaml files in contracts/openapi/ are full specs (some are
264
+ // shared component fragments). Failing to parse is not an error here.
265
+ return { description: "", version: "", xSourceRepo: "", context: "", evidenceClass: "" };
266
+ }
267
+ }
268
+ function firstSentence(s) {
269
+ // Take up to the first paragraph break, capped at 500 chars.
270
+ const para = s.split(/\r?\n\r?\n/)[0] ?? "";
271
+ return para.replace(/\s+/g, " ").trim().slice(0, 500);
272
+ }
273
+ async function collectFiles(dir, extensions) {
274
+ const out = [];
275
+ let entries;
276
+ try {
277
+ entries = await readdir(dir, { withFileTypes: true });
278
+ }
279
+ catch (err) {
280
+ const code = err.code;
281
+ if (code === "ENOENT")
282
+ return [];
283
+ throw err;
284
+ }
285
+ for (const entry of entries) {
286
+ const full = join(dir, entry.name);
287
+ if (entry.isDirectory()) {
288
+ out.push(...(await collectFiles(full, extensions)));
289
+ }
290
+ else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
291
+ out.push(full);
292
+ }
293
+ }
294
+ return out;
295
+ }
296
+ function basenameOf(absPath) {
297
+ return absPath.split(sep).slice(-1)[0] ?? "";
298
+ }
299
+ function stemOf(filename) {
300
+ return filename.replace(/\.[^.]+$/, "");
301
+ }
302
+ function domainSegmentCanonical(relPath) {
303
+ // Canonical layout: contracts/<subdir>/<domain>/<file>.<ext>
304
+ // → parts = ["contracts", subdir, domain, file]
305
+ const parts = relPath.split(sep);
306
+ if (parts.length < 4)
307
+ return "";
308
+ return parts[2] ?? "";
309
+ }
310
+ function slugify(s) {
311
+ return s
312
+ .toLowerCase()
313
+ .replace(/^_+/, "") // turn _common into common
314
+ .replace(/[^a-z0-9]+/g, "-")
315
+ .replace(/^-+|-+$/g, "");
316
+ }
317
+ const DECLARED_KINDS = {
318
+ openapi: "rest",
319
+ proto: "rpc",
320
+ avro: "event",
321
+ spicedb: "authz",
322
+ graphql: "graphql",
323
+ };
324
+ export function buildDeclaredExternalContracts(inputs) {
325
+ const rows = [];
326
+ const idByPath = new Map();
327
+ const emitted = new Set(inputs.existingIds);
328
+ // Every declared path processed so far — WINNERS AND LOSERS BOTH. A
329
+ // re-declaration of a path already processed is two capabilities citing
330
+ // one contract: a no-op whatever the first outcome was (row emitted, or
331
+ // collision already reported once). The first fix registered only paths
332
+ // that EMITTED, so a path that LOST a slug collision was never recorded
333
+ // and re-collided afresh in every manifest citing it — measured: the one
334
+ // genuine collision reported twice, once per citing manifest. A finding
335
+ // is a fact about the contract, not about each reader of it.
336
+ const seenDeclaredPaths = new Set();
337
+ const localBasenames = new Set([...inputs.discoveredIdByPath.keys()].map((p) => basenameOf(p.split("/").join(sep))));
338
+ for (const m of inputs.manifests) {
339
+ if (!m.contracts)
340
+ continue;
341
+ for (const [blockKey, entries] of Object.entries(m.contracts)) {
342
+ const kind = DECLARED_KINDS[blockKey];
343
+ if (kind === undefined || !Array.isArray(entries))
344
+ continue;
345
+ for (const raw of entries) {
346
+ // Only an OBJECT entry carrying `external: true` qualifies. A bare
347
+ // string is the pre-D13 form and must keep its existing behaviour
348
+ // (resolve locally, or drop with a finding) — otherwise every
349
+ // unresolvable typo in a contracts_root estate becomes a row.
350
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
351
+ continue;
352
+ const entry = raw;
353
+ if (entry["external"] !== true)
354
+ continue;
355
+ const declared = String(entry["path"] ?? "").replace(/^\/+/, "");
356
+ if (declared === "")
357
+ continue;
358
+ const base = basenameOf(declared.split("/").join(sep));
359
+ // If it resolves locally it is not external — the discovered row wins
360
+ // and carries real parsed metadata.
361
+ if (inputs.discoveredIdByPath.has(declared) || localBasenames.has(base))
362
+ continue;
363
+ const stem = stemOf(base);
364
+ const kindless = m.domain !== "" ? slugify(`${m.domain}-${stem}`) : slugify(stem);
365
+ const slug = contractIdWithKind(kindless, kind);
366
+ if (seenDeclaredPaths.has(declared)) {
367
+ // Same path, same contract: legitimate cross-capability sharing.
368
+ // Whatever happened on first sight (row, or collision reported)
369
+ // already happened once; a second citation adds no information.
370
+ continue;
371
+ }
372
+ seenDeclaredPaths.add(declared);
373
+ if (emitted.has(slug)) {
374
+ inputs.findings.add("divergence", `${m.relPath}: declared-external contract id "${slug}" (${declared}) already emitted — `
375
+ + `a same-kind collision (the cross-family collision class ended at schema 3.0.0, `
376
+ + `M-ADR 0203 §4.4); row dropped`);
377
+ continue;
378
+ }
379
+ emitted.add(slug);
380
+ idByPath.set(declared, slug);
381
+ rows.push({
382
+ id: slug,
383
+ name: stem,
384
+ service_id: "",
385
+ source_repo_id: "",
386
+ kind,
387
+ version: "",
388
+ status: "active",
389
+ spec_path: declared,
390
+ visibility: "",
391
+ context: "",
392
+ evidence_class: "",
393
+ description: "",
394
+ discovery: "declared-external",
395
+ source_sdd_id: "",
396
+ });
397
+ }
398
+ }
399
+ }
400
+ return { rows, idByPath };
401
+ }
402
+ //# sourceMappingURL=contracts.js.map