@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,458 @@
1
+ // Repositories discoverer — emits `repositories.csv` rows from
2
+ // `orchestration/repo-config/<domain>/<repo>.yaml` (recursive).
3
+ //
4
+ // Per ADR 0134 + Q7.1 resolution: `service_type` is constrained to the
5
+ // canonical `enums.service_type` set (api_service, worker, gateway,
6
+ // frontend, mobile, library, infrastructure, documentation). Unknown
7
+ // values log a `[finding] convention` and are kept verbatim — the FK
8
+ // validator doesn't enforce enum membership today, but a coverage
9
+ // report can flag drift.
10
+ //
11
+ // Each repo's primary_key (`id`) is the bare repo slug (after the org
12
+ // prefix if present). `name` keeps the full `org/slug` form when the
13
+ // config declares it.
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { basename, join, relative } from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import { z } from "zod";
18
+ import { readCatalogueCsv } from "../csv-reader.js";
19
+ import { runtimeServiceTypes, } from "../schema-loader.js";
20
+ const REPO_CONFIG_DIR_REL = join("orchestration", "repo-config");
21
+ // LOC metrics report, produced out-of-band by the `measure-loc` skill.
22
+ // Keyed by repo slug (`repo_name`), which equals repositories.id. Optional:
23
+ // only the platform-profile SDD carries it; absence leaves the LOC columns
24
+ // empty (logged once as a coverage finding).
25
+ const LOC_REPORT_REL = join("docs", "platform", "reference", "loc-report.csv");
26
+ // Per-repo reverse metadata, written by the reverse tooling at
27
+ // `repos/<slug>/reverse-metadata.yaml`. Carries that repo's OWN `profile`,
28
+ // which is not the SDD's: an integration SDD routinely mirrors a
29
+ // `support-repo` (a shared library, a vendor proto, device firmware) next to
30
+ // its `integration-product` components. Optional — absent for SDDs whose
31
+ // components are generated rather than cloned, and for forward SDDs with no
32
+ // mirrored source, where the manifest profile remains the honest answer.
33
+ const REPOS_DIR_REL = "repos";
34
+ const REVERSE_METADATA_BASENAME = "reverse-metadata.yaml";
35
+ const ContractsBlockSchema = z
36
+ .object({
37
+ openapi: z.array(z.string()).optional(),
38
+ proto: z.array(z.string()).optional(),
39
+ avro: z.array(z.string()).optional(),
40
+ })
41
+ .optional();
42
+ const EventEntrySchema = z.object({
43
+ topic: z.string().optional(),
44
+ schema: z.string().optional(),
45
+ });
46
+ const EventsBlockSchema = z
47
+ .object({
48
+ produces: z.array(EventEntrySchema).optional(),
49
+ consumes: z.array(EventEntrySchema).optional(),
50
+ })
51
+ .optional();
52
+ // `capability_exemption` (M-ADR 0188 §4.2) is typed loosely ON PURPOSE.
53
+ //
54
+ // A schema violation here does not reject the block — it rejects the whole
55
+ // repo-config, and `buildRepositories` then `continue`s past it, so the
56
+ // repository never reaches `repositories.csv` at all. A repo that vanishes
57
+ // from the catalogue also vanishes from the coverage invariant, which would
58
+ // make a malformed exemption the most effective silencer in the method.
59
+ //
60
+ // So: parse permissively, validate in the exemption builder, and drop only
61
+ // the exemption row. The repository survives, resolves to nothing, and is
62
+ // reported as an orphan. Failing open on the row and closed on the
63
+ // repository is the only ordering that cannot be abused.
64
+ const CapabilityExemptionSchema = z
65
+ .object({
66
+ reason: z.unknown().optional(),
67
+ note: z.unknown().optional(),
68
+ })
69
+ .optional();
70
+ // `service_capability_exemption` (M-ADR 0133 §4.2b) — same shape and same
71
+ // permissive-parse reasoning as `capability_exemption` above, one tier down.
72
+ const ServiceCapabilityExemptionSchema = z
73
+ .object({
74
+ reason: z.unknown().optional(),
75
+ note: z.unknown().optional(),
76
+ })
77
+ .optional();
78
+ // `services` (M-ADR 0197) is typed loosely ON PURPOSE, same reasoning as
79
+ // `capability_exemption` above: a schema violation here must not reject the
80
+ // whole repo-config. Shape and per-entry validation happen in the services
81
+ // builder, which drops only the malformed block or entry and reports an
82
+ // orphan finding — the repository always survives.
83
+ const ServicesBlockSchema = z.unknown().optional();
84
+ // `additional_context` accepts strings OR loosely-parsed key:value
85
+ // objects (which arise when adopters leave colons unquoted in their
86
+ // YAML — e.g. `- docs/.../ADR 0017: Use WorkOS for Auth.md`). The
87
+ // canonical builder reconstructs the original string downstream.
88
+ const RepoConfigSchema = z.object({
89
+ repo: z.string(),
90
+ // Explicit repositories.id. Without it the id is the last segment of
91
+ // `repo:`, so an existing repository and a generated replacement that shares
92
+ // its short name (M-ADR 0214 §4.2 qualified member keys) would collide.
93
+ id: z.string().optional(),
94
+ domain: z.string().optional(),
95
+ service_type: z.string().optional(),
96
+ description: z.string().optional(),
97
+ tier: z.string().optional(),
98
+ group: z.string().optional(),
99
+ category: z.string().optional(),
100
+ additional_context: z.array(z.unknown()).optional(),
101
+ // Forward-declared capability edges (M-ADR 0188 §6.1). Capability spec
102
+ // paths, relative to the SDD root. Unlike `additional_context`, this block
103
+ // is single-purpose, so an entry that resolves to no capability is an
104
+ // error worth reporting rather than a non-match to skip silently.
105
+ capabilities: z.array(z.string()).optional(),
106
+ capability_exemption: CapabilityExemptionSchema,
107
+ service_capability_exemption: ServiceCapabilityExemptionSchema,
108
+ contracts: ContractsBlockSchema,
109
+ events: EventsBlockSchema,
110
+ services: ServicesBlockSchema,
111
+ // Build-time and generation-time dependencies on other repositories (M-ADR 0217 §4.2).
112
+ // Shape-checked loosely here; buildRepoDependencies validates each entry and reports
113
+ // a bad one, so one malformed entry does not drop the whole repo-config.
114
+ depends_on: z.array(z.unknown()).optional(),
115
+ });
116
+ /**
117
+ * Reconstruct a string from a possibly-mis-parsed YAML list entry.
118
+ *
119
+ * Adopters sometimes write list entries with unquoted colons like:
120
+ * - docs/.../ADR 0017: Use WorkOS for Auth.md
121
+ *
122
+ * The YAML parser reads each such entry as a single-key object whose key
123
+ * is the part before the first colon and whose value is the part after.
124
+ * This helper rejoins them so the consuming code sees a plain string.
125
+ *
126
+ * Returns null if the entry isn't a string and can't be rejoined cleanly.
127
+ */
128
+ export function normaliseAdditionalContextEntry(entry) {
129
+ if (typeof entry === "string")
130
+ return entry;
131
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
132
+ const keys = Object.keys(entry);
133
+ if (keys.length === 1) {
134
+ const k = keys[0];
135
+ const v = entry[k];
136
+ if (typeof v === "string")
137
+ return `${k}: ${v}`;
138
+ if (v === null)
139
+ return k;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ export async function buildRepositories(inputs) {
145
+ const configs = await loadRepoConfigs(inputs.sddPath, inputs.findings);
146
+ const validServiceTypes = canonicalServiceTypeValues(inputs.schema);
147
+ // Only attempt the LOC join when there are repos to annotate — avoids a
148
+ // spurious second coverage finding for SDDs with no repo-config at all.
149
+ const locByRepo = configs.length > 0
150
+ ? await loadLocReport(inputs.sddPath, inputs.findings)
151
+ : new Map();
152
+ const repoMeta = configs.length > 0
153
+ ? await loadRepoProfiles(inputs.sddPath, inputs.findings, inputs.manifest.profile)
154
+ : new Map();
155
+ const rows = [];
156
+ for (const cfg of configs) {
157
+ const repoFull = cfg.data.repo;
158
+ const repoId = cfg.repoId;
159
+ const serviceType = cfg.data.service_type ?? "";
160
+ const domain = cfg.data.domain ?? "";
161
+ if (serviceType !== "" && !validServiceTypes.has(serviceType)) {
162
+ inputs.findings.add("convention", `${cfg.relPath}: service_type "${serviceType}" not in canonical enum (${[...validServiceTypes].join(", ")})`);
163
+ }
164
+ let resolvedDomain = domain;
165
+ if (domain === "") {
166
+ inputs.findings.add("convention", `${cfg.relPath}: missing required \`domain\` field`);
167
+ }
168
+ else if (!inputs.knownDomainIds.has(domain)) {
169
+ // Per ADR 0136: leave domain_id empty when the declared domain
170
+ // doesn't resolve locally (typical for app/integration SDDs whose
171
+ // repo-config declares "applications"/"integrations").
172
+ //
173
+ // NOT `orphan`, and the argument is about TIER rather than a relabelling
174
+ // of the same claim. A locally-unresolvable domain is two different
175
+ // things and THIS TIER CANNOT TELL THEM APART: ADR 0136 externality (the
176
+ // domain is owned by a sibling SDD — correct, expected, nothing to fix)
177
+ // or a genuinely dangling domain name (a typo). The discriminator lives
178
+ // at estate scope, not here. Measured across two estates: 137
179
+ // locally-unresolvable declarations, of which 137 resolve in a sibling
180
+ // and 0 resolve nowhere — and none in a platform-profile SDD.
181
+ //
182
+ // So `orphan` — which the gate acts on per M-ADR 0132 §4.3 — asserted a
183
+ // defect at a tier that cannot establish one, the failure M-ADR 0199
184
+ // §4.2 names. `inferred` records the builder's documented resolution
185
+ // (empty the column, keep the repository) without claiming a defect this
186
+ // build cannot see.
187
+ inputs.findings.add("inferred", `${cfg.relPath}: domain "${domain}" has no docs/domains/${domain}/ directory — domain_id left empty per ADR 0136. Whether that is external ownership or a dangling name is decidable only at estate scope`);
188
+ resolvedDomain = "";
189
+ }
190
+ const loc = locByRepo.get(repoId);
191
+ rows.push({
192
+ id: repoId,
193
+ name: repoFull,
194
+ domain_id: resolvedDomain,
195
+ product_id: inputs.productId,
196
+ // The repo's own profile where the reverse tooling recorded one;
197
+ // the SDD's only as a fallback. Stamping the SDD's profile onto every
198
+ // row typed a mirrored support-repo as an `integration-product`
199
+ // component — a wrong value in a populated column, which reads as
200
+ // authoritative and makes a filter on it return the same set as no
201
+ // filter at all.
202
+ profile: repoMeta.get(repoId)?.profile ?? inputs.manifest.profile,
203
+ // M-ADR 0213: whether the repository is inside this SDD's scope; empty when it has no reverse
204
+ // metadata (a forward SDD's generated components) or cannot be decided.
205
+ repo_scope: repoMeta.get(repoId)?.scope ?? "",
206
+ group: cfg.data.group ?? "",
207
+ category: cfg.data.category ?? "",
208
+ tier: cfg.data.tier ?? "",
209
+ component_type: serviceType,
210
+ files: loc?.files ?? "",
211
+ code: loc?.code ?? "",
212
+ comment: loc?.comment ?? "",
213
+ blank: loc?.blank ?? "",
214
+ total_lines: loc?.total_lines ?? "",
215
+ languages: loc?.languages ?? "",
216
+ doc_path: cfg.relPath,
217
+ source_sdd_id: "",
218
+ });
219
+ }
220
+ return { rows, configs };
221
+ }
222
+ async function loadRepoConfigs(sddPath, findings) {
223
+ const baseDir = join(sddPath, REPO_CONFIG_DIR_REL);
224
+ let yamlPaths;
225
+ try {
226
+ yamlPaths = await collectYamlPaths(baseDir);
227
+ }
228
+ catch (err) {
229
+ const code = err.code;
230
+ if (code === "ENOENT") {
231
+ findings.add("coverage", `${REPO_CONFIG_DIR_REL}/ not found — emitting empty repositories.csv`);
232
+ return [];
233
+ }
234
+ throw err;
235
+ }
236
+ const configs = [];
237
+ for (const path of yamlPaths) {
238
+ if (path.endsWith("schema.yaml") || path.endsWith("schema.yml")) {
239
+ // schema files are tooling, not repo config
240
+ continue;
241
+ }
242
+ const text = await readFile(path, "utf-8");
243
+ let raw;
244
+ try {
245
+ raw = parseYaml(text);
246
+ }
247
+ catch (err) {
248
+ findings.add("convention", `failed to parse YAML at ${relative(sddPath, path)}: ${err.message}`);
249
+ continue;
250
+ }
251
+ if (!raw || typeof raw !== "object") {
252
+ findings.add("convention", `${relative(sddPath, path)}: not an object — skipped`);
253
+ continue;
254
+ }
255
+ const parsed = RepoConfigSchema.safeParse(raw);
256
+ if (!parsed.success) {
257
+ const issues = parsed.error.issues
258
+ .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
259
+ .join("; ");
260
+ findings.add("convention", `${relative(sddPath, path)}: schema violation — ${issues}`);
261
+ continue;
262
+ }
263
+ const repoFull = parsed.data.repo;
264
+ const explicitId = parsed.data.id?.trim() ?? "";
265
+ const repoId = explicitId !== ""
266
+ ? explicitId
267
+ : repoFull.includes("/")
268
+ ? (repoFull.split("/").slice(-1)[0] ?? repoFull)
269
+ : repoFull;
270
+ configs.push({
271
+ path,
272
+ relPath: relative(sddPath, path),
273
+ data: parsed.data,
274
+ repoId,
275
+ });
276
+ }
277
+ // Deterministic order by relative path (for sorted repo IDs).
278
+ configs.sort((a, b) => a.relPath.localeCompare(b.relPath));
279
+ // Two configs deriving one id would silently lose a row downstream; keep the
280
+ // first and say which one went.
281
+ const byId = new Map();
282
+ const kept = [];
283
+ for (const cfg of configs) {
284
+ const prior = byId.get(cfg.repoId);
285
+ if (prior !== undefined) {
286
+ findings.add("convention", `${cfg.relPath}: repository id '${cfg.repoId}' is already derived by ${prior} — the first is kept; set an explicit \`id:\` on one of them`);
287
+ continue;
288
+ }
289
+ byId.set(cfg.repoId, cfg.relPath);
290
+ kept.push(cfg);
291
+ }
292
+ return kept;
293
+ }
294
+ const REPO_SCOPES = ["component", "support"];
295
+ /**
296
+ * The legacy per-repo `profile` recorded only a binary (M-ADR 0213 §1: 662 of 663
297
+ * measured repositories): the SDD's own profile means a component, `support-repo`
298
+ * means support. Any other value is not mapped — it needs a human (§4.5 step 3).
299
+ */
300
+ export function scopeFromLegacyProfile(profile, sddProfile) {
301
+ if (profile === "support-repo")
302
+ return "support";
303
+ if (profile === sddProfile)
304
+ return "component";
305
+ return undefined;
306
+ }
307
+ /**
308
+ * Every folder under `repos/` that holds a reverse-metadata.yaml, relative to `repos/`,
309
+ * sorted. A docs-corpus SDD groups its mirrors (`repos/<group>/<id>/`), and reading only
310
+ * `repos/<id>/` left all of one estate's 221 grouped files unread — their repo_scope
311
+ * silently empty in the catalogue. A folder that holds the file is a repository's own
312
+ * doc set, so the walk does not descend below it; dot-folders are skipped.
313
+ */
314
+ async function metadataDirs(root, rel = "") {
315
+ let entries;
316
+ try {
317
+ entries = await readdir(join(root, rel), { withFileTypes: true });
318
+ }
319
+ catch {
320
+ return [];
321
+ }
322
+ const out = [];
323
+ for (const e of entries.filter((x) => x.isDirectory() && !x.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name))) {
324
+ const child = rel === "" ? e.name : join(rel, e.name);
325
+ try {
326
+ await readFile(join(root, child, REVERSE_METADATA_BASENAME), "utf8");
327
+ out.push(child);
328
+ }
329
+ catch {
330
+ out.push(...(await metadataDirs(root, child)));
331
+ }
332
+ }
333
+ return out;
334
+ }
335
+ async function loadRepoProfiles(sddPath, findings, sddProfile) {
336
+ const out = new Map();
337
+ const root = join(sddPath, REPOS_DIR_REL);
338
+ const keyedFrom = new Map();
339
+ for (const dirRel of await metadataDirs(root)) {
340
+ let raw;
341
+ try {
342
+ raw = await readFile(join(root, dirRel, REVERSE_METADATA_BASENAME), "utf8");
343
+ }
344
+ catch {
345
+ continue;
346
+ }
347
+ let parsed;
348
+ try {
349
+ parsed = parseYaml(raw);
350
+ }
351
+ catch (err) {
352
+ // A file that EXISTS and cannot be parsed is not the same fact as a
353
+ // file that is absent, and the fallback treats them identically — the
354
+ // row silently keeps the SDD's profile and looks exactly like a repo
355
+ // that was never reverse-analysed. Say so, or the next reader debugs
356
+ // the builder instead of the one malformed line.
357
+ findings.add("convention", `${join(REPOS_DIR_REL, dirRel, REVERSE_METADATA_BASENAME)}: not parseable as YAML (${err.code ?? "parse error"}) — profile falls back to the SDD's, which may mistype this repo`);
358
+ continue;
359
+ }
360
+ const file = join(REPOS_DIR_REL, dirRel, REVERSE_METADATA_BASENAME);
361
+ // The repository this file describes: its own canonical_repo_key, else its folder
362
+ // name. Measured across both estates: no file carries a key that disagrees with
363
+ // its folder, so every file read before keys exactly as before.
364
+ const rawKey = parsed?.canonical_repo_key;
365
+ const repoKey = typeof rawKey === "string" && rawKey.trim() !== "" ? rawKey.trim() : basename(dirRel);
366
+ const prior = keyedFrom.get(repoKey);
367
+ if (prior !== undefined) {
368
+ // Report-only for now (convention, not divergence): reading grouped metadata surfaces
369
+ // duplicates that were always there (2 in the one estate that groups), and a new
370
+ // gate-acting finding would turn a regression baseline red on old facts. It gates
371
+ // once both estates read zero, as the recorded-path check does.
372
+ findings.add("convention", `${file}: describes repository '${repoKey}', already described by ${prior} — the first is kept; one of the two is misfiled`);
373
+ continue;
374
+ }
375
+ keyedFrom.set(repoKey, file);
376
+ const rawRole = parsed?.repo_scope;
377
+ const rawProfile = parsed?.profile;
378
+ const profile = typeof rawProfile === "string" && rawProfile.trim() !== "" ? rawProfile.trim() : undefined;
379
+ let scope;
380
+ if (typeof rawRole === "string" && rawRole.trim() !== "") {
381
+ if (REPO_SCOPES.includes(rawRole.trim())) {
382
+ scope = rawRole.trim();
383
+ }
384
+ else {
385
+ findings.add("convention", `${file}: repo_scope '${rawRole.trim()}' is not one of ${REPO_SCOPES.join(", ")} (M-ADR 0213 §4.2)`);
386
+ }
387
+ }
388
+ else if (profile !== undefined) {
389
+ scope = scopeFromLegacyProfile(profile, sddProfile);
390
+ if (scope === undefined) {
391
+ findings.add("convention", `${file}: legacy profile '${profile}' is neither this SDD's profile nor support-repo, so it maps to no repo_scope — decide component or support by hand (M-ADR 0213 §4.5)`);
392
+ }
393
+ }
394
+ if (profile === undefined && scope === undefined) {
395
+ findings.add("convention", `${file}: no usable \`repo_scope\` or legacy \`profile\` — profile falls back to the SDD's, which may mistype this repo`);
396
+ continue;
397
+ }
398
+ // A repository migrated to repo_scope keeps the deprecated column consistent
399
+ // until its removal: support reads support-repo, component the SDD's profile.
400
+ out.set(repoKey, {
401
+ profile: profile ?? (scope === "support" ? "support-repo" : sddProfile),
402
+ ...(scope ? { scope } : {}),
403
+ });
404
+ }
405
+ return out;
406
+ }
407
+ async function loadLocReport(sddPath, findings) {
408
+ const out = new Map();
409
+ const path = join(sddPath, LOC_REPORT_REL);
410
+ let parsed;
411
+ try {
412
+ parsed = await readCatalogueCsv(path);
413
+ }
414
+ catch (err) {
415
+ if (err.code === "ENOENT") {
416
+ findings.add("coverage", `${LOC_REPORT_REL} not found — LOC columns left empty (run the measure-loc skill to populate)`);
417
+ return out;
418
+ }
419
+ throw err;
420
+ }
421
+ for (const row of parsed.rows) {
422
+ const repo = (row["repo_name"] ?? "").trim();
423
+ if (repo === "")
424
+ continue;
425
+ out.set(repo, {
426
+ files: row["files"] ?? "",
427
+ code: row["code"] ?? "",
428
+ comment: row["comment"] ?? "",
429
+ blank: row["blank"] ?? "",
430
+ total_lines: row["total_lines"] ?? "",
431
+ languages: row["languages"] ?? "",
432
+ });
433
+ }
434
+ return out;
435
+ }
436
+ async function collectYamlPaths(dir) {
437
+ const out = [];
438
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
439
+ const full = join(dir, entry.name);
440
+ if (entry.isDirectory()) {
441
+ out.push(...(await collectYamlPaths(full)));
442
+ }
443
+ else if (entry.isFile() &&
444
+ (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
445
+ out.push(full);
446
+ }
447
+ }
448
+ return out;
449
+ }
450
+ function canonicalServiceTypeValues(schema) {
451
+ const enumDef = schema.enums["service_type"];
452
+ if (!enumDef) {
453
+ // Fall back to runtime types if the canonical enum is missing
454
+ return new Set(runtimeServiceTypes(schema));
455
+ }
456
+ return new Set(enumDef.values.map((v) => v.value));
457
+ }
458
+ //# sourceMappingURL=repositories.js.map
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Scaffold declarations walker (M-ADR 0207 §4.1, schema 3.4.0).
3
+ *
4
+ * Reads `orchestration/scaffolds/*.yaml` — the authored surface for forward
5
+ * scaffold declarations — and derives `scaffolds` + `scaffold_items` rows.
6
+ * The manifest declares intent; this builder's RESOLUTION PASS derives each
7
+ * item's `state` against the same build's catalogue rows, which is why it
8
+ * runs after every target-bearing catalogue (the quality-scores ordering
9
+ * constraint, for the same reason).
10
+ *
11
+ * The conditional-FK rule (a `declared` item's target must not resolve, a
12
+ * `realised` item's must) holds by construction: resolution is what computes
13
+ * `state`, so conforming output cannot disagree with it. An authored `state:`
14
+ * key is therefore a convention finding — the manifest has no business
15
+ * asserting a verdict the builder derives.
16
+ *
17
+ * Absent or empty `orchestration/scaffolds/` emits NOTHING and SAYS nothing:
18
+ * zero adoption is the designed state (M-ADR 0207 §4.4), not a coverage gap.
19
+ * This walker deliberately differs from the repo-config family, whose absent
20
+ * surface is a coverage finding.
21
+ */
22
+ import { readdir, readFile } from "node:fs/promises";
23
+ import { join } from "node:path";
24
+ import { parse as parseYaml } from "yaml";
25
+ import { z } from "zod";
26
+ const SCAFFOLDS_DIR_REL = join("orchestration", "scaffolds");
27
+ /**
28
+ * Top-level manifest shape. Loose (`passthrough`) so an unknown key is
29
+ * detectable rather than a rejection; per-item validation is manual because
30
+ * each defect must produce a finding naming file, item and value — a zod
31
+ * issue path would not.
32
+ */
33
+ const ScaffoldManifestSchema = z
34
+ .object({
35
+ scaffold_schema: z.string(),
36
+ id: z.string(),
37
+ name: z.string(),
38
+ status: z.string(),
39
+ corpus_ref: z.string(),
40
+ items: z.array(z.unknown()).optional(),
41
+ })
42
+ .passthrough();
43
+ function enumValues(schema, enumName) {
44
+ const def = schema.enums[enumName];
45
+ return def ? def.values.map((v) => v.value) : [];
46
+ }
47
+ function catalogueByName(schema, name) {
48
+ return schema.catalogues.find((c) => c.name === name);
49
+ }
50
+ /**
51
+ * Resolve one declared item against the build's rows. Returns null when the
52
+ * declaration itself is defective (a finding has already been logged).
53
+ */
54
+ function resolveItem(args) {
55
+ const { raw, relPath, index, schema, rowsByCatalogue, findings } = args;
56
+ const where = `${relPath} item ${index + 1}`;
57
+ const itemKind = typeof raw["kind"] === "string" ? raw["kind"] : "";
58
+ const validKinds = enumValues(schema, "scaffold_item_kind");
59
+ if (!validKinds.includes(itemKind)) {
60
+ findings.add("convention", `${where}: unknown item kind '${itemKind || "(missing)"}' — expected one of ${validKinds.join(" | ")}; item dropped`);
61
+ return null;
62
+ }
63
+ if ("state" in raw) {
64
+ findings.add("convention", `${where}: authored 'state' ignored — state is derived by the resolution pass, never authored (M-ADR 0207 §4.1)`);
65
+ }
66
+ // contract items default target_kind to `contracts` and must not name
67
+ // anything else — the spec-path vocabulary belongs to that one catalogue.
68
+ const targetKind = typeof raw["target_kind"] === "string" && raw["target_kind"] !== ""
69
+ ? raw["target_kind"]
70
+ : itemKind === "contract"
71
+ ? "contracts"
72
+ : "";
73
+ if (itemKind === "contract" && targetKind !== "contracts") {
74
+ findings.add("convention", `${where}: contract items resolve against 'contracts' (spec_path vocabulary), not '${targetKind}'; item dropped`);
75
+ return null;
76
+ }
77
+ const targetCat = catalogueByName(schema, targetKind);
78
+ if (!targetCat) {
79
+ findings.add("convention", `${where}: unknown target_kind '${targetKind || "(missing)"}' — not a catalogue in schema ${schema.schemaVersion}; item dropped`);
80
+ return null;
81
+ }
82
+ const rows = rowsByCatalogue.get(targetKind) ?? [];
83
+ if (itemKind === "edge") {
84
+ const sourceRef = typeof raw["edge_source_ref"] === "string" ? raw["edge_source_ref"] : "";
85
+ const targetRefEnd = typeof raw["edge_target_ref"] === "string" ? raw["edge_target_ref"] : "";
86
+ if (sourceRef === "" || targetRefEnd === "") {
87
+ findings.add("convention", `${where}: edge items declare edge_source_ref and edge_target_ref; item dropped`);
88
+ return null;
89
+ }
90
+ const [srcFk, dstFk] = [targetCat.foreignKeys[0], targetCat.foreignKeys[1]];
91
+ if (!srcFk || !dstFk) {
92
+ findings.add("convention", `${where}: '${targetKind}' declares ${targetCat.foreignKeys.length} FK column(s) — endpoint mapping needs two (schema declaration order); item dropped`);
93
+ return null;
94
+ }
95
+ const [srcCol, dstCol] = [srcFk.column, dstFk.column];
96
+ const realised = rows.some((r) => r[srcCol] === sourceRef && r[dstCol] === targetRefEnd);
97
+ return {
98
+ itemKind,
99
+ targetKind,
100
+ targetRef: `${sourceRef}__${targetRefEnd}`,
101
+ edgeSourceRef: sourceRef,
102
+ edgeTargetRef: targetRefEnd,
103
+ realised,
104
+ };
105
+ }
106
+ const targetRef = typeof raw["target_ref"] === "string" ? raw["target_ref"] : "";
107
+ if (targetRef === "") {
108
+ findings.add("convention", `${where}: missing target_ref; item dropped`);
109
+ return null;
110
+ }
111
+ const matchCol = itemKind === "contract" ? "spec_path" : targetCat.primaryKey;
112
+ const realised = rows.some((r) => r[matchCol] === targetRef);
113
+ return { itemKind, targetKind, targetRef, edgeSourceRef: null, edgeTargetRef: null, realised };
114
+ }
115
+ export async function buildScaffolds(inputs) {
116
+ const { sddPath, findings, sddId, schema, rowsByCatalogue } = inputs;
117
+ const dir = join(sddPath, SCAFFOLDS_DIR_REL);
118
+ let files;
119
+ try {
120
+ files = (await readdir(dir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
121
+ }
122
+ catch (err) {
123
+ const code = err.code;
124
+ if (code === "ENOENT") {
125
+ // Zero adoption is the designed state — headers-only, no finding.
126
+ return { scaffolds: [], scaffoldItems: [] };
127
+ }
128
+ throw err;
129
+ }
130
+ files.sort();
131
+ const validStatuses = enumValues(schema, "scaffold_status");
132
+ const scaffolds = [];
133
+ const scaffoldItems = [];
134
+ for (const file of files) {
135
+ const relPath = `${SCAFFOLDS_DIR_REL}/${file}`;
136
+ let parsed;
137
+ try {
138
+ parsed = parseYaml(await readFile(join(dir, file), "utf8"));
139
+ }
140
+ catch (err) {
141
+ findings.add("convention", `${relPath}: YAML parse failure (${err.message}); manifest dropped`);
142
+ continue;
143
+ }
144
+ const result = ScaffoldManifestSchema.safeParse(parsed);
145
+ if (!result.success) {
146
+ for (const issue of result.error.issues) {
147
+ findings.add("convention", `${relPath}: ${issue.path.join(".") || "(root)"}: ${issue.message}; manifest dropped`);
148
+ }
149
+ continue;
150
+ }
151
+ const manifest = result.data;
152
+ if (!validStatuses.includes(manifest.status)) {
153
+ findings.add("convention", `${relPath}: status '${manifest.status}' not in scaffold_status (${validStatuses.join(" | ")}); manifest dropped`);
154
+ continue;
155
+ }
156
+ scaffolds.push({
157
+ id: manifest.id,
158
+ name: manifest.name,
159
+ status: manifest.status,
160
+ source_path: relPath,
161
+ corpus_ref: manifest.corpus_ref,
162
+ manifest_schema: manifest.scaffold_schema,
163
+ sdd_id: sddId,
164
+ source_sdd_id: "",
165
+ });
166
+ const rawItems = manifest.items ?? [];
167
+ rawItems.forEach((rawItem, index) => {
168
+ if (typeof rawItem !== "object" || rawItem === null || Array.isArray(rawItem)) {
169
+ findings.add("convention", `${relPath} item ${index + 1}: not a mapping; item dropped`);
170
+ return;
171
+ }
172
+ const resolved = resolveItem({
173
+ raw: rawItem,
174
+ relPath,
175
+ index,
176
+ schema,
177
+ rowsByCatalogue,
178
+ findings,
179
+ });
180
+ if (!resolved)
181
+ return;
182
+ scaffoldItems.push({
183
+ id: `${manifest.id}__${resolved.itemKind}__${resolved.targetKind}__${resolved.targetRef}`,
184
+ scaffold_id: manifest.id,
185
+ item_kind: resolved.itemKind,
186
+ target_kind: resolved.targetKind,
187
+ target_ref: resolved.targetRef,
188
+ edge_source_ref: resolved.edgeSourceRef ?? "",
189
+ edge_target_ref: resolved.edgeTargetRef ?? "",
190
+ state: resolved.realised ? "realised" : "declared",
191
+ source_sdd_id: "",
192
+ });
193
+ });
194
+ }
195
+ return { scaffolds, scaffoldItems };
196
+ }
197
+ //# sourceMappingURL=scaffolds.js.map