@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,193 @@
1
+ // Shared parser for per-service `repos/<id>/docs/service-architecture.md`.
2
+ //
3
+ // Extracted from builders/services.ts so two callers can share it:
4
+ // - the per-SDD services builder (buildServices), and
5
+ // - the aggregator's federated reverse-metadata enrichment pass
6
+ // (ADR 0163 P2), which reaches member SDDs' arch docs that the
7
+ // platform-root's own build never sees.
8
+ //
9
+ // The metadata is read faithfully — not inferred — from three blocks:
10
+ // - `## Runtime / Build Shape` table `Primary runtime` → runtime_type
11
+ // (+ a derived language token).
12
+ // - `## Operational Status` table `Classification` → status,
13
+ // `Confidence` → confidence (emitted out-of-band by an adopter's
14
+ // derive-operational-evidence script).
15
+ // - `## Purpose` lead paragraph → description.
16
+ import { readdir, readFile } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ const REPOS_DIR_REL = "repos";
19
+ const SERVICE_ARCHITECTURE_REL = join("docs", "service-architecture.md");
20
+ export const EMPTY_METADATA = {
21
+ runtime_type: "",
22
+ language: "",
23
+ status: "",
24
+ confidence: "",
25
+ description: "",
26
+ };
27
+ /**
28
+ * Walk `<sddPath>/repos/` and index every service-architecture.md by its
29
+ * repo slug — the directory name immediately above `docs/`. Handles both
30
+ * the flat (`repos/<repo>/docs/`) and grouped (`repos/<group>/<repo>/docs/`)
31
+ * layouts. First match wins (deterministic via readdir + walk order).
32
+ */
33
+ export async function indexServiceArchitectures(sddPath) {
34
+ const index = new Map();
35
+ const reposDir = join(sddPath, REPOS_DIR_REL);
36
+ async function walk(dir) {
37
+ let entries;
38
+ try {
39
+ entries = await readdir(dir, { withFileTypes: true });
40
+ }
41
+ catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (!entry.isDirectory())
46
+ continue;
47
+ const child = join(dir, entry.name);
48
+ const archPath = join(child, SERVICE_ARCHITECTURE_REL);
49
+ try {
50
+ const docs = await readdir(join(child, "docs"));
51
+ if (docs.includes("service-architecture.md")) {
52
+ if (!index.has(entry.name))
53
+ index.set(entry.name, archPath);
54
+ }
55
+ }
56
+ catch {
57
+ // no docs/ here — keep descending for the grouped layout.
58
+ }
59
+ await walk(child);
60
+ }
61
+ }
62
+ await walk(reposDir);
63
+ return index;
64
+ }
65
+ /** Read and parse a service-architecture.md from disk. */
66
+ export async function readServiceMetadata(archPath) {
67
+ let text;
68
+ try {
69
+ text = await readFile(archPath, "utf-8");
70
+ }
71
+ catch (err) {
72
+ if (err.code === "ENOENT")
73
+ return EMPTY_METADATA;
74
+ throw err;
75
+ }
76
+ return parseServiceArchitecture(text);
77
+ }
78
+ /** Parse the three metadata blocks out of service-architecture.md text. */
79
+ export function parseServiceArchitecture(text) {
80
+ const buildShape = parseKeyValueTable(text, /^##\s+Runtime\s*\/\s*Build Shape\b/i);
81
+ const opsStatus = parseKeyValueTable(text, /^##\s+Operational Status\b/i);
82
+ const primaryRuntime = buildShape.get("primary runtime") ?? "";
83
+ return {
84
+ runtime_type: primaryRuntime,
85
+ language: deriveLanguage(primaryRuntime),
86
+ // Strip markdown bold from the classification cell (e.g. **library**).
87
+ status: stripEmphasis(opsStatus.get("classification") ?? ""),
88
+ confidence: stripEmphasis(opsStatus.get("confidence") ?? ""),
89
+ description: parseLeadParagraph(text, /^##\s+Purpose\b/i),
90
+ };
91
+ }
92
+ /**
93
+ * Parse a two-column markdown table (`| key | value |`) that follows the
94
+ * first heading matching `headingRe`, returning a lowercased-key → value
95
+ * map. Header and separator rows are skipped. Stops at the next heading or
96
+ * the end of the table block.
97
+ */
98
+ function parseKeyValueTable(text, headingRe) {
99
+ const out = new Map();
100
+ const lines = text.split(/\r?\n/);
101
+ let inSection = false;
102
+ let seenTable = false;
103
+ for (const line of lines) {
104
+ const t = line.trim();
105
+ if (!inSection) {
106
+ if (headingRe.test(t))
107
+ inSection = true;
108
+ continue;
109
+ }
110
+ if (t.startsWith("#"))
111
+ break; // next heading ends the section
112
+ const isRow = t.startsWith("|") && t.endsWith("|");
113
+ if (!isRow) {
114
+ if (seenTable)
115
+ break; // table block ended
116
+ continue;
117
+ }
118
+ seenTable = true;
119
+ const cells = t
120
+ .slice(1, -1)
121
+ .split("|")
122
+ .map((c) => c.trim());
123
+ if (cells.length < 2)
124
+ continue;
125
+ const key = cells[0].toLowerCase();
126
+ // Skip header row ("aspect"/"field") and separator row (---).
127
+ if (key === "" || /^:?-+:?$/.test(key) || key === "aspect" || key === "field") {
128
+ continue;
129
+ }
130
+ out.set(key, cells[1]);
131
+ }
132
+ return out;
133
+ }
134
+ /**
135
+ * First non-empty paragraph block after the heading matching `headingRe`,
136
+ * lines joined with single spaces. "" when the heading is absent.
137
+ */
138
+ function parseLeadParagraph(text, headingRe) {
139
+ const lines = text.split(/\r?\n/);
140
+ let inSection = false;
141
+ const paragraph = [];
142
+ for (const line of lines) {
143
+ const t = line.trim();
144
+ if (!inSection) {
145
+ if (headingRe.test(t))
146
+ inSection = true;
147
+ continue;
148
+ }
149
+ if (t.startsWith("#"))
150
+ break;
151
+ if (t === "") {
152
+ if (paragraph.length > 0)
153
+ break;
154
+ continue;
155
+ }
156
+ paragraph.push(t);
157
+ }
158
+ return paragraph.join(" ");
159
+ }
160
+ function stripEmphasis(s) {
161
+ return s.replace(/\*\*/g, "").replace(/^_+|_+$/g, "").trim();
162
+ }
163
+ /**
164
+ * Derive a language token from a "Primary runtime" value such as
165
+ * "Java 8", "Node.js 18 / AWS Lambda", "Terraform ~> 1.3",
166
+ * "Vue 3 / TypeScript". Returns "" when nothing recognisable matches.
167
+ */
168
+ function deriveLanguage(primaryRuntime) {
169
+ const s = primaryRuntime.toLowerCase();
170
+ if (s === "")
171
+ return "";
172
+ // Order matters: more specific signals first.
173
+ if (/\btypescript\b/.test(s))
174
+ return "TypeScript";
175
+ if (/\bvue\b/.test(s))
176
+ return "TypeScript";
177
+ if (/\bnode(\.js)?\b/.test(s))
178
+ return "JavaScript";
179
+ if (/\bjava\b/.test(s))
180
+ return "Java";
181
+ if (/\bkotlin\b/.test(s))
182
+ return "Kotlin";
183
+ if (/\bterraform\b/.test(s))
184
+ return "HCL";
185
+ if (/\bpython\b|\bsynapse\b/.test(s))
186
+ return "Python";
187
+ if (/\bgo(lang)?\b/.test(s))
188
+ return "Go";
189
+ if (/\.net\b|\bc#\b|\bdotnet\b/.test(s))
190
+ return "C#";
191
+ return "";
192
+ }
193
+ //# sourceMappingURL=service-architecture-parser.js.map
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Loader for the optional `.sdd/service-contract-aliases.yaml` config.
3
+ *
4
+ * Adopters use this map to translate outbound interface display text
5
+ * (as it appears in service-architecture.md External Interfaces tables)
6
+ * into canonical contract names that resolve against contracts.csv.
7
+ *
8
+ * File shape:
9
+ *
10
+ * interface_aliases:
11
+ * # Case-insensitive exact match on the Interface column text.
12
+ * Example service: example-oauth-api
13
+ * Gateway: example-gateway-api
14
+ * Legacy gateway: example-gateway-api
15
+ *
16
+ * interface_contains:
17
+ * # Case-insensitive substring match. The longest key wins on
18
+ * # ties; ordering in the file otherwise matters only when keys
19
+ * # overlap. Use when adopter interface text has many qualified
20
+ * # variants ("Example API", "Example `/token`", etc.).
21
+ * Example: example-oauth-api
22
+ * Legacy gateway: example-gateway-api
23
+ *
24
+ * skip_callers:
25
+ * # Repo names to skip entirely (postman collections, smoke tests,
26
+ * # legacy folders inside a federated SDD that aren't runtime
27
+ * # services). Use when the federated SDD lacks a services.csv to
28
+ * # gate emission and consumes rows would otherwise dangle at the
29
+ * # aggregator with unresolved service_id.
30
+ * - postman-tacho
31
+ * - smoke-tests
32
+ *
33
+ * same_product_inference:
34
+ * # Maps generic interface text (case-insensitive exact) to a
35
+ * # contract-name template. `{prefix}` is substituted with the
36
+ * # caller's service_id minus a known suffix (-api, -bff, -ui,
37
+ * # -consumer, -web). The builder then looks up the resolved name
38
+ * # in contracts.csv.
39
+ * Private API: "{prefix}-private-api"
40
+ * Private API Gateway: "{prefix}-private-api"
41
+ *
42
+ * Either block is optional. Empty / missing file = no aliases.
43
+ */
44
+ import { readFile } from "node:fs/promises";
45
+ import { join } from "node:path";
46
+ import { parse as parseYaml } from "yaml";
47
+ const ALIASES_FILENAME = join(".sdd", "service-contract-aliases.yaml");
48
+ export async function loadServiceContractAliases(sddPath) {
49
+ const full = join(sddPath, ALIASES_FILENAME);
50
+ let text;
51
+ try {
52
+ text = await readFile(full, "utf-8");
53
+ }
54
+ catch (err) {
55
+ const code = err.code;
56
+ if (code === "ENOENT")
57
+ return null;
58
+ throw err;
59
+ }
60
+ const raw = parseYaml(text);
61
+ if (raw === null || typeof raw !== "object") {
62
+ throw new Error(`${ALIASES_FILENAME}: expected a mapping at the document root`);
63
+ }
64
+ return {
65
+ interfaceAliases: toLowerKeyMap(raw.interface_aliases),
66
+ interfaceContains: toContainsList(raw.interface_contains),
67
+ sameProductTemplates: toLowerKeyMap(raw.same_product_inference),
68
+ skipCallers: toStringSet(raw.skip_callers),
69
+ relPath: ALIASES_FILENAME,
70
+ };
71
+ }
72
+ function toStringSet(raw) {
73
+ const out = new Set();
74
+ if (Array.isArray(raw)) {
75
+ for (const item of raw) {
76
+ if (typeof item === "string" && item !== "")
77
+ out.add(item);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ function toContainsList(raw) {
83
+ const out = [];
84
+ if (raw === null || raw === undefined)
85
+ return out;
86
+ if (typeof raw !== "object" || Array.isArray(raw))
87
+ return out;
88
+ for (const [k, v] of Object.entries(raw)) {
89
+ if (typeof v === "string" && v !== "") {
90
+ out.push([k.toLowerCase().trim(), v]);
91
+ }
92
+ }
93
+ // Sort longest-key-first so the most specific substring wins.
94
+ out.sort((a, b) => b[0].length - a[0].length);
95
+ return out;
96
+ }
97
+ function toLowerKeyMap(raw) {
98
+ const out = new Map();
99
+ if (raw === null || raw === undefined)
100
+ return out;
101
+ if (typeof raw !== "object" || Array.isArray(raw))
102
+ return out;
103
+ for (const [k, v] of Object.entries(raw)) {
104
+ if (typeof v === "string" && v !== "")
105
+ out.set(k.toLowerCase().trim(), v);
106
+ }
107
+ return out;
108
+ }
109
+ //# sourceMappingURL=service-contract-aliases-loader.js.map
@@ -0,0 +1,345 @@
1
+ // Staleness VERDICTS — the second half of M-ADR 0205 §4.5's staleness scope
2
+ // (ledger §3o): "staleness meaning corpus_ref older than the target's last
3
+ // content change". The scores-coverage predicate computes each row's BASIS;
4
+ // this module computes the VERDICT the basis supports. A stale score is worse
5
+ // than a missing one, because it reads as coverage — the predicate otherwise
6
+ // counts a score written against a corpus that has since moved as fully
7
+ // covering its target.
8
+ //
9
+ // Verdict rules, per basis:
10
+ // corpus_ref + target_path — PRECISE, and a CONTENT test: the target is stale
11
+ // iff its blob at HEAD differs from its blob at the anchor. An anchor the
12
+ // repo does not know is UNKNOWN-ANCHOR (never guessed); an anchor with
13
+ // no target_path cannot support a per-target verdict and is counted as
14
+ // such rather than compared against repo HEAD (which would mark a row
15
+ // stale whenever ANYTHING in the repo moved — alarm, not measurement).
16
+ //
17
+ // WHY CONTENT AND NOT `git log`: the previous test asked whether any
18
+ // commit touched the path since the anchor, which fires on a PURE RENAME
19
+ // — so a portability rename restarts the staleness treadmill for every
20
+ // anchored score in the corpus, and the estate is told to re-review
21
+ // hundreds of artefacts whose text never changed. Measured instance: one
22
+ // score reported stale-by-hours was byte-IDENTICAL to its assessed state,
23
+ // caught only by comparing blobs by hand. A test that cannot tell "moved"
24
+ // from "changed" produces re-review debt out of nothing.
25
+ //
26
+ // Renames are exempt BY CONSTRUCTION, not by a special case: where the
27
+ // path does not exist at the anchor, git's own rename detection over
28
+ // <anchor>..HEAD supplies the pre-rename name and the blob is compared
29
+ // across it. Where that also fails the row is PATH-ABSENT-AT-ANCHOR —
30
+ // reported as its own outcome rather than guessed either way.
31
+ //
32
+ // TARGET-MISSING (any basis): the target_path does not exist at HEAD. A
33
+ // deleted artefact cannot be re-reviewed, so folding it into `stale`
34
+ // prescribes a remedy that cannot be performed (M-ADR 0199 §4.4). It is
35
+ // counted and reported on its own, and its remedy is to withdraw or
36
+ // re-point the score.
37
+ // scored_at + target_path — COARSE: last content change of the path
38
+ // (committer date) after scored_at ⇒ stale. A path git does not track
39
+ // is UNKNOWN-ANCHOR.
40
+ // none — already reported by scores-coverage as unknown currency; not
41
+ // re-reported here.
42
+ //
43
+ // MEMBER TIER ONLY, stated not silent: the aggregate tier reads sibling
44
+ // catalogues whose target files and git histories are not local, so a verdict
45
+ // there would be a guess. Reporting-only (`coverage` class) like the rest of
46
+ // the scores family — promotion to gate-acting is a later, deliberate act.
47
+ import { execFile as execFileCb } from "node:child_process";
48
+ import { access } from "node:fs/promises";
49
+ import { join } from "node:path";
50
+ import { promisify } from "node:util";
51
+ const execFile = promisify(execFileCb);
52
+ const NO_RUN = {
53
+ assessed: 0,
54
+ stale: [],
55
+ current: 0,
56
+ unknownAnchor: 0,
57
+ anchorWithoutPath: 0,
58
+ targetMissing: [],
59
+ pathAbsentAtAnchor: 0,
60
+ directoryTargets: 0,
61
+ };
62
+ async function git(sddRoot, args) {
63
+ try {
64
+ const { stdout } = await execFile("git", [...args], {
65
+ cwd: sddRoot,
66
+ encoding: "utf8",
67
+ maxBuffer: 4 * 1024 * 1024,
68
+ });
69
+ return { ok: true, stdout: stdout.trim() };
70
+ }
71
+ catch {
72
+ return { ok: false, stdout: "" };
73
+ }
74
+ }
75
+ /**
76
+ * Is the target genuinely GONE, or merely not committed?
77
+ *
78
+ * `git rev-parse HEAD:<path>` fails for both a deleted file and an untracked
79
+ * one that is sitting on disk. Treating the second as deleted would tell an
80
+ * author to withdraw a score for an artefact they can see in front of them —
81
+ * so absence from the HEAD tree is confirmed against the working tree before
82
+ * it is called missing. (Caught by this module's own untracked-path control,
83
+ * which is what it was there for.)
84
+ */
85
+ async function existsOnDisk(sddRoot, path) {
86
+ try {
87
+ await access(join(sddRoot, path));
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * DIRECTORY TARGETS are a real and different case, raised by the reverse estate
96
+ * from its own pre-cut data: 18 of its 84 stale rows name a whole-domain
97
+ * directory rather than a file.
98
+ *
99
+ * `git rev-parse <rev>:<path>` returns a TREE oid for a directory and a BLOB oid
100
+ * for a file, so the comparison below works for both — but it does not mean the
101
+ * same thing. A file is stale when its own bytes changed; a directory is stale
102
+ * when ANYTHING beneath it changed, which for a whole-domain tree is close to
103
+ * unconditional. That is a wider test, and it is defensible (a score over a tree
104
+ * really is invalidated by movement inside it) — but it must be SAID, because a
105
+ * number that is near-unconditional by construction reads exactly like a number
106
+ * that is measuring something.
107
+ *
108
+ * Git's rename detection also does not apply: a tree has no rename pair. A moved
109
+ * directory therefore lands in `pathAbsentAtAnchor` rather than being exempted.
110
+ */
111
+ async function objectTypeAt(sddRoot, rev, path) {
112
+ const oid = await git(sddRoot, ["rev-parse", `${rev}:${path}`]);
113
+ if (!oid.ok || oid.stdout === "")
114
+ return null;
115
+ const t = await git(sddRoot, ["cat-file", "-t", oid.stdout]);
116
+ if (!t.ok)
117
+ return null;
118
+ return t.stdout === "tree" ? "tree" : "blob";
119
+ }
120
+ /** Blob (or tree — see `objectTypeAt`) id of `<rev>:<path>`, else null. */
121
+ async function blobAt(sddRoot, rev, path) {
122
+ const r = await git(sddRoot, ["rev-parse", `${rev}:${path}`]);
123
+ return r.ok && r.stdout !== "" ? r.stdout : null;
124
+ }
125
+ /**
126
+ * new-path -> pre-rename path, from git's OWN rename detection over
127
+ * `<anchor>..HEAD`. One call per distinct anchor, not per row: anchors are few
128
+ * and this walk is the expensive part.
129
+ *
130
+ * Using git's detector rather than a heuristic matters — a basename match would
131
+ * pair unrelated files that happen to share a name, which is precisely the
132
+ * shared-token failure this corpus keeps producing.
133
+ */
134
+ async function renameMap(sddRoot, anchor) {
135
+ const map = new Map();
136
+ const r = await git(sddRoot, ["diff", "--name-status", "-M", anchor, "HEAD"]);
137
+ if (!r.ok)
138
+ return map;
139
+ for (const line of r.stdout.split("\n")) {
140
+ if (!line.startsWith("R"))
141
+ continue;
142
+ const parts = line.split("\t");
143
+ if (parts.length < 3)
144
+ continue;
145
+ map.set(parts[2], parts[1]);
146
+ }
147
+ return map;
148
+ }
149
+ export async function runStalenessVerdicts(session, options) {
150
+ if (!options.available.has("quality_scores"))
151
+ return { ...NO_RUN };
152
+ if (options.tier !== "member") {
153
+ return {
154
+ ...NO_RUN,
155
+ skipped: "aggregate tier cannot reach member repos' files or git histories — staleness verdicts are member-tier",
156
+ };
157
+ }
158
+ const inRepo = await git(options.sddRoot, ["rev-parse", "--git-dir"]);
159
+ if (!inRepo.ok) {
160
+ return {
161
+ ...NO_RUN,
162
+ skipped: `${options.sddRoot} is not a git repository — no anchor can be compared, verdicts UNATTEMPTED (not fresh)`,
163
+ };
164
+ }
165
+ const asStr = (v) => v === null || v === undefined ? "" : String(v).trim();
166
+ const reader = await session.conn.runAndReadAll(`SELECT rubric_id, target_ref, target_path, corpus_ref, scored_at, staleness_basis
167
+ FROM "quality_scores"
168
+ WHERE staleness_basis IN ('corpus_ref', 'scored_at');`);
169
+ const rows = reader.getRows();
170
+ const stale = [];
171
+ let current = 0;
172
+ let unknownAnchor = 0;
173
+ let anchorWithoutPath = 0;
174
+ let pathAbsentAtAnchor = 0;
175
+ let directoryTargets = 0;
176
+ const targetMissing = [];
177
+ const anchorKnown = new Map();
178
+ const renamesByAnchor = new Map();
179
+ for (const [rubricIdRaw, targetRefRaw, targetPathRaw, corpusRefRaw, scoredAtRaw, basisRaw] of rows) {
180
+ const rubricId = asStr(rubricIdRaw);
181
+ const targetRef = asStr(targetRefRaw);
182
+ const basis = asStr(basisRaw);
183
+ // Counted for BOTH bases, before the branch. The first cut of this counter
184
+ // sat inside the corpus_ref branch only, so it was blind to every
185
+ // scored_at row — and the estate that ASKED for this count has all 18 of
186
+ // its directory targets on the scored_at basis, so the metric it requested
187
+ // read zero for it. The controls passed because they were written with
188
+ // corpus_ref fixtures: a control that exercises one branch says nothing
189
+ // about the other.
190
+ {
191
+ const p = asStr(targetPathRaw);
192
+ if (p !== "" && (await objectTypeAt(options.sddRoot, "HEAD", p)) === "tree") {
193
+ directoryTargets += 1;
194
+ }
195
+ }
196
+ if (basis === "corpus_ref") {
197
+ const anchor = asStr(corpusRefRaw);
198
+ const path = asStr(targetPathRaw);
199
+ if (path === "") {
200
+ anchorWithoutPath += 1;
201
+ continue;
202
+ }
203
+ let known = anchorKnown.get(anchor);
204
+ if (known === undefined) {
205
+ known = (await git(options.sddRoot, ["cat-file", "-e", `${anchor}^{commit}`])).ok;
206
+ anchorKnown.set(anchor, known);
207
+ }
208
+ if (!known) {
209
+ unknownAnchor += 1;
210
+ continue;
211
+ }
212
+ const headBlob = await blobAt(options.sddRoot, "HEAD", path);
213
+ if (headBlob === null) {
214
+ if (await existsOnDisk(options.sddRoot, path)) {
215
+ // Present but uncommitted: no blob to compare against, and nothing
216
+ // deleted. Unknown, which is not fresh.
217
+ unknownAnchor += 1;
218
+ }
219
+ else {
220
+ targetMissing.push({ rubricId, targetRef, targetPath: path });
221
+ }
222
+ continue;
223
+ }
224
+ let anchorBlob = await blobAt(options.sddRoot, anchor, path);
225
+ if (anchorBlob === null) {
226
+ // The path did not exist at the anchor under this name. Ask git whether
227
+ // it was renamed there rather than concluding anything.
228
+ let renames = renamesByAnchor.get(anchor);
229
+ if (renames === undefined) {
230
+ renames = await renameMap(options.sddRoot, anchor);
231
+ renamesByAnchor.set(anchor, renames);
232
+ }
233
+ const oldPath = renames.get(path);
234
+ if (oldPath !== undefined) {
235
+ anchorBlob = await blobAt(options.sddRoot, anchor, oldPath);
236
+ }
237
+ }
238
+ if (anchorBlob === null) {
239
+ pathAbsentAtAnchor += 1;
240
+ continue;
241
+ }
242
+ if (anchorBlob === headBlob) {
243
+ // Byte-identical to its assessed state — including across a rename,
244
+ // which is the whole point of testing content instead of touches.
245
+ current += 1;
246
+ }
247
+ else {
248
+ stale.push({
249
+ rubricId,
250
+ targetRef,
251
+ basis: "corpus_ref",
252
+ detail: "target CONTENT differs from its state at the anchor",
253
+ });
254
+ }
255
+ }
256
+ else {
257
+ const path = asStr(targetPathRaw);
258
+ const at = asStr(scoredAtRaw);
259
+ if (path === "" || at === "") {
260
+ // basis says scored_at, but the row cannot actually support it —
261
+ // count as unknown rather than trusting the label over the data.
262
+ unknownAnchor += 1;
263
+ continue;
264
+ }
265
+ if ((await blobAt(options.sddRoot, "HEAD", path)) === null &&
266
+ !(await existsOnDisk(options.sddRoot, path))) {
267
+ targetMissing.push({ rubricId, targetRef, targetPath: path });
268
+ continue;
269
+ }
270
+ const last = await git(options.sddRoot, ["log", "-1", "--format=%cI", "--", path]);
271
+ if (!last.ok || last.stdout === "") {
272
+ unknownAnchor += 1;
273
+ continue;
274
+ }
275
+ const reviewedMs = Date.parse(at);
276
+ if (Number.isNaN(reviewedMs)) {
277
+ unknownAnchor += 1;
278
+ continue;
279
+ }
280
+ if (Date.parse(last.stdout) > reviewedMs) {
281
+ stale.push({
282
+ rubricId,
283
+ targetRef,
284
+ basis: "scored_at",
285
+ detail: `target last changed ${last.stdout.slice(0, 10)}, after the review of ${at.slice(0, 10)}`,
286
+ });
287
+ }
288
+ else {
289
+ current += 1;
290
+ }
291
+ }
292
+ }
293
+ return {
294
+ assessed: rows.length,
295
+ stale,
296
+ current,
297
+ unknownAnchor,
298
+ anchorWithoutPath,
299
+ targetMissing,
300
+ pathAbsentAtAnchor,
301
+ directoryTargets,
302
+ };
303
+ }
304
+ export function renderStalenessVerdicts(res, tier) {
305
+ const out = [];
306
+ if (res.skipped !== undefined) {
307
+ out.push(`[metric] scores_staleness (${tier}): n/a — ${res.skipped}`);
308
+ return out;
309
+ }
310
+ if (res.assessed === 0)
311
+ return out;
312
+ for (const s of res.stale) {
313
+ out.push(`[finding] coverage: ${s.rubricId} score for '${s.targetRef}' is STALE — ` +
314
+ `${s.detail}; the assessment predates the artefact it reads as covering — ` +
315
+ `re-review it or record why the change does not invalidate the score`);
316
+ }
317
+ if (res.pathAbsentAtAnchor > 0) {
318
+ // Counted AND said. A bucket that only ever appears as a number in a metric
319
+ // line is a state nobody acts on, and this one means an assessment cannot
320
+ // be verified at all.
321
+ out.push(`[finding] coverage: ${res.pathAbsentAtAnchor} score(s) have a target that DID NOT EXIST ` +
322
+ `AT THE ANCHOR under any name git can pair — the assessment cannot be verified ` +
323
+ `against its own corpus_ref; re-anchor the score, or re-review and re-anchor together`);
324
+ }
325
+ for (const t of res.targetMissing) {
326
+ // Remedy deliberately differs from STALE's. M-ADR 0199 §4.4 binds a remedy
327
+ // to be performable at the tier that emitted it, and "re-review it" cannot
328
+ // be performed on an artefact that no longer exists.
329
+ out.push(`[finding] coverage: ${t.rubricId} score for '${t.targetRef}' has a TARGET THAT NO LONGER ` +
330
+ `EXISTS — '${t.targetPath}' is absent at HEAD; the assessment cannot be re-reviewed, ` +
331
+ `so withdraw the score or re-point it at the successor artefact`);
332
+ }
333
+ out.push(`[metric] scores_staleness (${tier}): ${res.assessed} verdict-capable score(s) — ` +
334
+ `${res.stale.length} stale / ${res.current} current / ` +
335
+ `${res.targetMissing.length} target-missing / ` +
336
+ `${res.unknownAnchor} unknown-anchor / ${res.anchorWithoutPath} anchor-without-path / ` +
337
+ `${res.pathAbsentAtAnchor} path-absent-at-anchor` +
338
+ (res.directoryTargets > 0
339
+ ? ` (of which ${res.directoryTargets} target a DIRECTORY — stale there means anything beneath it changed, a wider test than a file's and near-unconditional over a large tree)`
340
+ : "") +
341
+ ` (staleness is a CONTENT test — a rename is not a change; unknown is not fresh;` +
342
+ ` reporting-only per M-ADR 0205 §4.5)`);
343
+ return out;
344
+ }
345
+ //# sourceMappingURL=staleness-verdicts.js.map