@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,115 @@
1
+ /**
2
+ * Coverage mode — `sdd-cli catalogue validate --coverage` (A4 CS-2).
3
+ *
4
+ * Runs the thirteen engine coverage predicates against a catalogues
5
+ * directory and prints EXACTLY the lines the reference shell scripts
6
+ * printed (coverage-render.ts holds the text contract), in a fixed block
7
+ * order: the four catalogue-classics (repo-capability, service-capability,
8
+ * event, contract), then the nine Phase-0 gate checks in the
9
+ * catalogue-integrity-gate's step order. Nothing else is written to
10
+ * stdout — the gate swaps N script invocations for one CLI call and the
11
+ * stream stays parseable by the same baseline machinery.
12
+ *
13
+ * Exit code is 0 in normal operation, exactly as the reference scripts:
14
+ * gating is the findings baseline's job, never this mode's. A classifier
15
+ * probe failure throws (the references exit 2).
16
+ *
17
+ * The one deliberately-unported check is check-repo-config-coverage.sh:
18
+ * it reads the AUTHORING surface, is text-shaped by design (A0's
19
+ * classification), and stays shell.
20
+ */
21
+ import { existsSync } from "node:fs";
22
+ import { join, resolve } from "node:path";
23
+ import { readCatalogueCsv } from "./csv-reader.js";
24
+ import { createEngineSession } from "./engine-validator.js";
25
+ import { loadCanonicalSchema } from "./schema-loader.js";
26
+ import { runContractCoverage, runEventCoverage, runRepoCapabilityCoverage, runServiceCapabilityCoverage, } from "./coverage-predicates.js";
27
+ import { runCapabilityEventCheck, runDataModelEntitiesCheck, runEventCatalogueCheck, runFeatureEventCheck, runFeatureSpecPromotionCheck, runFeatureSurfaceDuplicationCheck, runGlossaryXrefCheck, runPlatformDependenciesCheck, runRelationshipCoherenceCheck, runRepoEdgesCheck, } from "./gate-checks.js";
28
+ import { runScoresCoverage } from "./scores-coverage.js";
29
+ import { renderScaffoldDelta, runScaffoldDelta } from "./scaffold-delta.js";
30
+ import { renderStalenessVerdicts, runStalenessVerdicts } from "./staleness-verdicts.js";
31
+ import { renderCapabilityEventCheck, renderContractCoverage, renderDataModelEntitiesCheck, renderEventCatalogueCheck, renderEventCoverage, renderFeatureEventCheck, renderFeatureSpecPromotionCheck, renderFeatureSurfaceDuplicationCheck, renderGlossaryXrefCheck, renderPlatformDependenciesCheck, renderRelationshipCoherenceCheck, renderRepoCapability, renderRepoEdgesCheck, renderScoresCoverage, renderServiceCapability, } from "./coverage-render.js";
32
+ export async function runCatalogueCoverage(inputs, write) {
33
+ const sddRoot = resolve(inputs.sddPath ?? process.cwd());
34
+ const dir = resolve(sddRoot, inputs.cataloguesDir ?? "catalogues");
35
+ const tier = inputs.tier ?? "member";
36
+ const schema = await loadCanonicalSchema();
37
+ const present = (name) => existsSync(join(dir, `${name}.csv`));
38
+ const rowsByCatalogue = new Map();
39
+ for (const cat of schema.catalogues) {
40
+ if (!present(cat.name))
41
+ continue;
42
+ const parsed = await readCatalogueCsv(join(dir, `${cat.name}.csv`));
43
+ rowsByCatalogue.set(cat.name, parsed.rows);
44
+ }
45
+ const session = await createEngineSession(schema, rowsByCatalogue);
46
+ const lines = [];
47
+ try {
48
+ // The four catalogue-classics, each anchored on its population file
49
+ // exactly as the reference scripts anchor (absent file → silent).
50
+ if (present("repositories")) {
51
+ const r = await runRepoCapabilityCoverage(session);
52
+ lines.push(...renderRepoCapability(r.findings, r.metrics));
53
+ }
54
+ if (present("services")) {
55
+ const r = await runServiceCapabilityCoverage(session, {
56
+ tier,
57
+ repoCapabilityAvailable: present("repo_capability"),
58
+ });
59
+ lines.push(...renderServiceCapability(r.findings, r.metrics, tier));
60
+ }
61
+ if (present("events")) {
62
+ const r = await runEventCoverage(session, { tier });
63
+ lines.push(...renderEventCoverage(r, tier));
64
+ }
65
+ if (present("contracts")) {
66
+ const r = await runContractCoverage(session, { tier, sddRoot });
67
+ lines.push(...renderContractCoverage(r, tier));
68
+ }
69
+ // The nine Phase-0 gate checks, in the gate's step order.
70
+ const eventCatalogue = await runEventCatalogueCheck(session, {
71
+ sddRoot,
72
+ eventsPresent: present("events"),
73
+ });
74
+ lines.push(...renderEventCatalogueCheck(eventCatalogue.gaps));
75
+ lines.push(...renderRepoEdgesCheck(await runRepoEdgesCheck(session, { sddRoot })));
76
+ lines.push(...renderGlossaryXrefCheck((await runGlossaryXrefCheck(session, {
77
+ sddRoot,
78
+ xrefPresent: present("glossary_xref"),
79
+ })).finding));
80
+ lines.push(...renderDataModelEntitiesCheck(await runDataModelEntitiesCheck(session)));
81
+ lines.push(...renderCapabilityEventCheck(await runCapabilityEventCheck(session)));
82
+ lines.push(...renderFeatureEventCheck(await runFeatureEventCheck(session)));
83
+ lines.push(...renderPlatformDependenciesCheck((await runPlatformDependenciesCheck(session, { sddRoot })).finding));
84
+ lines.push(...renderFeatureSpecPromotionCheck(await runFeatureSpecPromotionCheck(session)));
85
+ lines.push(...renderFeatureSurfaceDuplicationCheck(await runFeatureSurfaceDuplicationCheck(session)));
86
+ // The fourteenth check (M-ADR 0205 §4.5, Phase B): every eligible artefact
87
+ // carries a current score. Last in the block order because it is the newest
88
+ // and the baselines hold the stream positionally-agnostic but ordered.
89
+ const available = new Set(schema.catalogues.map((c) => c.name).filter((n) => present(n)));
90
+ lines.push(...renderScoresCoverage(await runScoresCoverage(session, { tier, available, sddRoot }), tier));
91
+ // Staleness VERDICTS — the second half of the fourteenth check's §4.5
92
+ // scope (ledger §3o), member-tier only with the aggregate skip stated.
93
+ // Same coverage class, same reporting-only staging as the rest of the
94
+ // scores family.
95
+ lines.push(...renderStalenessVerdicts(await runStalenessVerdicts(session, { tier, available, sddRoot }), tier));
96
+ // The fifteenth check (M-ADR 0206 §4.5): relationship coherence for the
97
+ // three families where drift or gaps mean something — owned-but-untraced
98
+ // features, publisher-projection drift, supersession drift. Reporting-only
99
+ // at both tiers; newest last in the block order, same as scores was.
100
+ lines.push(...renderRelationshipCoherenceCheck(await runRelationshipCoherenceCheck(session)));
101
+ // The sixteenth check (M-ADR 0207 §4.8): the scaffold declared-vs-derived
102
+ // delta, its resolution self-check, and the §4.6 staleness verdict.
103
+ // Reporting-only at both tiers per §4.9 — nothing here gates until the
104
+ // pilot's stated-in-advance figures land. Newest last in the block order,
105
+ // same as scores and coherence were.
106
+ lines.push(...renderScaffoldDelta(await runScaffoldDelta(session, { tier, available, schema, sddRoot }), tier));
107
+ }
108
+ finally {
109
+ session.dispose();
110
+ }
111
+ for (const line of lines)
112
+ write(`${line}\n`);
113
+ return { exitCode: 0 };
114
+ }
115
+ //# sourceMappingURL=coverage-run.js.map
@@ -0,0 +1,97 @@
1
+ // CSV reader for catalogue files.
2
+ //
3
+ // RFC-4180-aware: handles `"` quoting (including doubled `""` for literal
4
+ // quotes), commas inside quoted cells, and CR/LF/CRLF line breaks. We
5
+ // don't try to be a general CSV library — the canonical writer's output
6
+ // is the input shape we support, plus tolerance for byte-identical
7
+ // catalogues authored by other conforming aggregator implementations.
8
+ //
9
+ // Headers come from the file's first line; column order is preserved
10
+ // for traceability but row access is by column name.
11
+ import { readFile } from "node:fs/promises";
12
+ export async function readCatalogueCsv(path) {
13
+ const text = await readFile(path, "utf-8");
14
+ return parseCatalogueCsv(text);
15
+ }
16
+ export function parseCatalogueCsv(text) {
17
+ const records = parseRecords(text);
18
+ if (records.length === 0)
19
+ return { columns: [], rows: [] };
20
+ const columns = records[0] ?? [];
21
+ const rows = [];
22
+ for (let r = 1; r < records.length; r++) {
23
+ const record = records[r] ?? [];
24
+ if (record.length === 1 && record[0] === "")
25
+ continue; // empty trailing line
26
+ const row = {};
27
+ for (let c = 0; c < columns.length; c++) {
28
+ row[columns[c] ?? ""] = record[c] ?? "";
29
+ }
30
+ rows.push(row);
31
+ }
32
+ return { columns, rows };
33
+ }
34
+ function parseRecords(text) {
35
+ const records = [];
36
+ let current = [];
37
+ let cell = "";
38
+ let i = 0;
39
+ let inQuotes = false;
40
+ while (i < text.length) {
41
+ const ch = text[i] ?? "";
42
+ if (inQuotes) {
43
+ if (ch === '"') {
44
+ if (text[i + 1] === '"') {
45
+ // Escaped double-quote inside a quoted cell.
46
+ cell += '"';
47
+ i += 2;
48
+ continue;
49
+ }
50
+ inQuotes = false;
51
+ i++;
52
+ continue;
53
+ }
54
+ cell += ch;
55
+ i++;
56
+ continue;
57
+ }
58
+ if (ch === '"') {
59
+ inQuotes = true;
60
+ i++;
61
+ continue;
62
+ }
63
+ if (ch === ",") {
64
+ current.push(cell);
65
+ cell = "";
66
+ i++;
67
+ continue;
68
+ }
69
+ if (ch === "\r") {
70
+ if (text[i + 1] === "\n")
71
+ i++;
72
+ current.push(cell);
73
+ records.push(current);
74
+ current = [];
75
+ cell = "";
76
+ i++;
77
+ continue;
78
+ }
79
+ if (ch === "\n") {
80
+ current.push(cell);
81
+ records.push(current);
82
+ current = [];
83
+ cell = "";
84
+ i++;
85
+ continue;
86
+ }
87
+ cell += ch;
88
+ i++;
89
+ }
90
+ // Trailing cell + record.
91
+ if (cell !== "" || current.length > 0) {
92
+ current.push(cell);
93
+ records.push(current);
94
+ }
95
+ return records;
96
+ }
97
+ //# sourceMappingURL=csv-reader.js.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Deterministic CSV writer for catalogues.
3
+ *
4
+ * Emits headers + sorted rows per ADR 0134 builder contract:
5
+ * - column order matches the canonical schema exactly,
6
+ * - rows sorted by primary-key value,
7
+ * - cells with embedded comma/quote/newline are RFC-4180 quoted,
8
+ * - empty trailing newline (POSIX convention).
9
+ *
10
+ * Headers-only emission is supported — pass an empty rows array to write
11
+ * a CSV that downstream consumers can rely on the file existence of.
12
+ *
13
+ * ⚠ BUT A HEADERS-ONLY WRITE NEVER TRUNCATES A CATALOGUE THAT HAS ROWS.
14
+ * `conformance_finding` is written by a DIFFERENT VERB — `sdd-cli conformance
15
+ * --emit-finding` calls writeFindings() into the same `catalogues/` directory —
16
+ * and nothing in the `catalogue build` row-collection path ever sets it. So
17
+ * build emitted a bare header over it, unconditionally, and one verb destroyed
18
+ * another verb's output. Measured across one estate: every SDD that had
19
+ * authored a conformance finding lost it, while the gate registries went on
20
+ * citing those rows as CURRENT evidence for a hard-gated score.
21
+ *
22
+ * ⚠ The first cut of this comment said `c4_system_entity` was a second
23
+ * producerless catalogue. It is not — `build.ts` sets it directly rather than
24
+ * via `builders/`, and the sweep that "found" it only searched `builders/`.
25
+ * An adopter disproved it by counterfactual, installing the defective version
26
+ * and measuring that its rows survived. Stated here because the wrong version
27
+ * shipped in 1.29.0 and a reader deserves the correction beside the claim.
28
+ *
29
+ * The guard is keyed on the CONDITION, not on those two names, so it also
30
+ * catches the case nobody has hit yet: a builder that regresses to zero rows
31
+ * silently deleting a populated catalogue.
32
+ */
33
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
34
+ import { dirname, join } from "node:path";
35
+ /** Data-row count of an existing catalogue file, or 0 when absent/unreadable/header-only. */
36
+ async function existingDataRows(filePath) {
37
+ let text;
38
+ try {
39
+ text = await readFile(filePath, "utf-8");
40
+ }
41
+ catch {
42
+ return 0;
43
+ }
44
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
45
+ return lines.length > 1 ? lines.length - 1 : 0;
46
+ }
47
+ export async function writeCatalogue(opts) {
48
+ await mkdir(opts.outDir, { recursive: true });
49
+ const filePath = join(opts.outDir, `${opts.catalogue.name}.csv`);
50
+ // Refuse to replace rows with a bare header. Emitting one is correct when the
51
+ // file is absent or already empty — that is the "consumers can rely on the
52
+ // file existing" case — and is data loss when it is not.
53
+ if (opts.rows.length === 0) {
54
+ const onDisk = await existingDataRows(filePath);
55
+ if (onDisk > 0) {
56
+ return { rowCount: onDisk, path: filePath, preserved: true, preservedRows: onDisk };
57
+ }
58
+ }
59
+ const text = renderCatalogueCsv(opts.catalogue, opts.rows);
60
+ await mkdir(dirname(filePath), { recursive: true });
61
+ await writeFile(filePath, text, "utf-8");
62
+ return { rowCount: opts.rows.length, path: filePath };
63
+ }
64
+ export function renderCatalogueCsv(catalogue, rows) {
65
+ const columns = catalogue.columns.map((c) => c.name);
66
+ const lines = [];
67
+ lines.push(columns.map(escapeCell).join(","));
68
+ const sorted = [...rows].sort((a, b) => compareCell(a[catalogue.primaryKey], b[catalogue.primaryKey]));
69
+ for (const row of sorted) {
70
+ const cells = columns.map((col) => escapeCell(formatCell(row[col])));
71
+ lines.push(cells.join(","));
72
+ }
73
+ // Trailing newline per POSIX convention.
74
+ return `${lines.join("\n")}\n`;
75
+ }
76
+ function formatCell(value) {
77
+ if (value === null || value === undefined)
78
+ return "";
79
+ if (typeof value === "boolean")
80
+ return value ? "true" : "false";
81
+ return String(value);
82
+ }
83
+ function escapeCell(value) {
84
+ if (value === "")
85
+ return "";
86
+ if (/[",\n\r]/.test(value)) {
87
+ return `"${value.replace(/"/g, '""')}"`;
88
+ }
89
+ return value;
90
+ }
91
+ function compareCell(a, b) {
92
+ const sa = a === undefined || a === null ? "" : String(a);
93
+ const sb = b === undefined || b === null ? "" : String(b);
94
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
95
+ }
96
+ //# sourceMappingURL=csv-writer.js.map
@@ -0,0 +1,77 @@
1
+ // data_plane ↔ product_platform_dependencies coherence cross-check
2
+ // (M-ADR 0193 §4.2, as amended 2026-07-19).
3
+ //
4
+ // The two surfaces are complementary views, not mutually derived: a data_plane
5
+ // binding carries pattern / fine locality / runtime that platform-dependencies
6
+ // lack, and a platform-dependency carries capability / contract / stability that
7
+ // a binding lacks. The method CROSS-CHECKS them for coherence rather than
8
+ // deriving one from the other.
9
+ //
10
+ // The check is deliberately COARSE, and per-target correspondence is NOT
11
+ // asserted. Under the gateway convention (M-ADR 0189: consumers reach a
12
+ // capability through a BFF/gateway), a `retrieve@internal-domain` binding names
13
+ // the DOMAIN service it logically reads (its finer-grained `target`), while the
14
+ // matching `product_platform_dependencies` row names the GATEWAY through which
15
+ // access physically flows. The two targets legitimately differ — that
16
+ // difference is the locality/granularity signal the axis exists to capture, not
17
+ // drift. Matching binding.target against platform-dep.target_service_id would
18
+ // flag every such binding falsely, and adopters would "fix" the false finding by
19
+ // coarsening `target` to the gateway id — destroying the axis's value. So the
20
+ // coherence check asserts only the coarse invariant: a product that declares
21
+ // platform-targeting data-plane bindings should also declare *some* platform
22
+ // dependency. A product with internal bindings and zero platform-deps has one
23
+ // surface without the other — a genuine coherence gap worth surfacing.
24
+ import { isOutputSidePattern } from "./manifest-loader.js";
25
+ const INTERNAL_LOCALITIES = new Set([
26
+ "internal-platform",
27
+ "internal-domain",
28
+ ]);
29
+ /**
30
+ * Coarse coherence check: if a product declares platform-targeting INBOUND
31
+ * data-plane bindings (internal locality) but no `product_platform_dependencies`
32
+ * rows at all, the two complementary surfaces are out of step. Returns 1 if
33
+ * flagged, else 0. Per-target correspondence is intentionally NOT checked, and
34
+ * output-side bindings are excluded entirely (see header).
35
+ */
36
+ // @rule data-plane-platform-dep-coherence
37
+ export function checkDataPlanePlatformDepCoherence(inputs) {
38
+ const { productId, bindings, platformDeps, findings } = inputs;
39
+ const platformTargeting = bindings.filter((b) => INTERNAL_LOCALITIES.has(String(b["locality"] ?? "")) &&
40
+ !isOutputSidePattern(String(b["pattern"] ?? "")));
41
+ if (platformTargeting.length === 0)
42
+ return 0; // no platform-targeting bindings
43
+ if (platformDeps.length > 0)
44
+ return 0; // both surfaces present — coherent
45
+ findings.add("convention", `data_plane coherence: ${productId} declares ${platformTargeting.length} platform-targeting ` +
46
+ `inbound data_plane binding(s) (internal locality) but no product_platform_dependencies rows — the two ` +
47
+ `complementary surfaces are out of step; declare the platform dependencies in ` +
48
+ `app-platform-dependencies.yaml (M-ADR 0193 §4.2). Per-target correspondence is not asserted ` +
49
+ `(gateway indirection, M-ADR 0189).`);
50
+ return 1;
51
+ }
52
+ /**
53
+ * Coherence cross-check between the `surface: api` trait and a `publish`
54
+ * data_plane binding. Returns 1 if a drift finding was raised, else 0.
55
+ *
56
+ * One-directional: only a headless (`surface: api`) product missing its
57
+ * `publish` binding is flagged. A `publish` binding on a `ui` surface is
58
+ * legitimate and never flagged (see header). Runs only over a DECLARED
59
+ * data-plane (bindings present) — same gating as the platform-dep check; a
60
+ * product that has not authored a data_plane is not nagged.
61
+ */
62
+ // @rule publish-surface-coherence
63
+ export function checkPublishSurfaceCoherence(inputs) {
64
+ const { productId, surface, bindings, findings } = inputs;
65
+ const effectiveSurface = surface ?? "ui";
66
+ if (effectiveSurface !== "api")
67
+ return 0; // publish is orthogonal to a ui surface
68
+ const hasPublish = bindings.some((b) => String(b["pattern"] ?? "") === "publish");
69
+ if (hasPublish)
70
+ return 0; // both views present — coherent
71
+ findings.add("convention", `data_plane/surface coherence: ${productId} declares surface: api (headless, M-ADR 0167) but its ` +
72
+ `data_plane has no publish binding — a headless product's entire surface is programmatic, so the ` +
73
+ `contract that constitutes it is missing from the declared composition. Add a publish binding, or ` +
74
+ `reconsider the surface classification (M-ADR 0193 §4.7). Complementary views, not a derivation.`);
75
+ return 1;
76
+ }
77
+ //# sourceMappingURL=data-plane-coherence.js.map
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Schema→DDL generator — M-ADR 0205 §4.3 (engine-evolution Phase A1).
3
+ *
4
+ * Emits, from the loaded canonical schema:
5
+ * (a) CREATE TABLE DDL for every declared catalogue, typed per the
6
+ * schema's column types; and
7
+ * (b) a deterministic list of constraint-violation queries (fk | pk |
8
+ * enum) whose row semantics mirror fk-validator.ts and
9
+ * enum-validator.ts EXACTLY — the Phase A2 dual-run comparison
10
+ * (old path vs engine path, count AND list) depends on that
11
+ * equivalence, so any semantic change here must change there too:
12
+ * - fk: a required (non-nullable, non-external) FK fails when
13
+ * empty; a non-empty value must resolve against the target
14
+ * catalogue's primary keys unless the FK is effective-external
15
+ * (declared `external`, or external for the build's profile via
16
+ * `externalForProfiles`) — mirroring FkValidationOptions.
17
+ * - pk: duplicate non-empty primary-key values.
18
+ * - enum: a non-empty (trimmed) bound-column value outside the
19
+ * bound enum's declared set. Emptiness is nullability's concern,
20
+ * not the enum's, exactly as in enum-validator.ts.
21
+ *
22
+ * Determinism per M-ADR 0205 §4.2: emission order is the schema's own
23
+ * declaration order; every query carries a total ORDER BY.
24
+ *
25
+ * Digest coupling per §4.3 is inherited, not re-implemented: this module
26
+ * reads the schema through loadCanonicalSchema, and the vendored copy it
27
+ * loads is guarded by canonical-schema-drift.test.ts.
28
+ *
29
+ * There is NO hand-maintained DDL anywhere: an unknown column type or a
30
+ * binding to an unknown enum throws at generation time — loud, never a
31
+ * silent fallback (the known*Ids.has() lesson).
32
+ */
33
+ import { PLATFORM_STAMP_COLUMN, isPlatformScopedFk } from "./fk-validator.js";
34
+ /** Closed map from schema column types to engine column types. */
35
+ const TYPE_MAP = {
36
+ string: "VARCHAR",
37
+ int: "INTEGER",
38
+ integer: "INTEGER",
39
+ number: "DOUBLE",
40
+ date: "DATE",
41
+ boolean: "BOOLEAN",
42
+ };
43
+ /** Engine type for a schema column type; throws on an unmapped type. */
44
+ export function sqlType(catalogue, column, type) {
45
+ const mapped = TYPE_MAP[type];
46
+ if (!mapped) {
47
+ throw new Error(`ddl-generator: ${catalogue}.${column} declares unmapped column type "${type}" — extend TYPE_MAP deliberately, never default`);
48
+ }
49
+ return mapped;
50
+ }
51
+ /**
52
+ * The one definition of effective-external, shared with the engine
53
+ * validator so the three consumers (fk-validator.ts, the query generator,
54
+ * engine-validator.ts) cannot drift: declared `external`, or external for
55
+ * the build's profile via `externalForProfiles`.
56
+ */
57
+ export function isEffectiveExternal(fk, options) {
58
+ return (fk.external ||
59
+ (options.profile !== undefined &&
60
+ (fk.externalForProfiles ?? []).includes(options.profile)));
61
+ }
62
+ function quote(identifier) {
63
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
64
+ throw new Error(`ddl-generator: identifier "${identifier}" is outside the schema's naming form — refusing to interpolate it into SQL`);
65
+ }
66
+ return `"${identifier}"`;
67
+ }
68
+ function sqlStringLiteral(value) {
69
+ return `'${value.replace(/'/g, "''")}'`;
70
+ }
71
+ /**
72
+ * One CREATE TABLE statement per catalogue, in schema declaration order.
73
+ * Plain typed columns only: enforcement of PK/FK/enum semantics lives in
74
+ * the violation queries, whose findings-shaped output (never a load
75
+ * abort) is what the validators' exit contracts need.
76
+ */
77
+ export function generateTableDdl(schema) {
78
+ return schema.catalogues.map((cat) => {
79
+ const cols = cat.columns
80
+ .map((c) => ` ${quote(c.name)} ${sqlType(cat.name, c.name, c.type)}`)
81
+ .join(",\n");
82
+ return `CREATE TABLE ${quote(cat.name)} (\n${cols}\n);`;
83
+ });
84
+ }
85
+ /**
86
+ * The violation-query set for a schema, mirroring the in-memory
87
+ * validators' semantics under the same options they take.
88
+ */
89
+ export function generateConstraintQueries(schema, options = {}) {
90
+ const catalogueNames = new Set(schema.catalogues.map((c) => c.name));
91
+ const queries = [];
92
+ const catByName = new Map(schema.catalogues.map((c) => [c.name, c]));
93
+ for (const cat of schema.catalogues) {
94
+ queries.push(pkQuery(cat, options));
95
+ for (const fk of cat.foreignKeys) {
96
+ const [targetCat, targetCol] = fk.references.split(".");
97
+ if (!targetCat || !targetCol) {
98
+ throw new Error(`ddl-generator: malformed FK reference "${fk.references}" on ${cat.name}.${fk.column}`);
99
+ }
100
+ if (!catalogueNames.has(targetCat)) {
101
+ throw new Error(`ddl-generator: FK ${cat.name}.${fk.column} references unknown catalogue "${targetCat}"`);
102
+ }
103
+ if (!cat.columns.some((c) => c.name === fk.column)) {
104
+ throw new Error(`ddl-generator: FK column ${cat.name}.${fk.column} is not a declared column — the schema 3.1.3 vacuous-binding class`);
105
+ }
106
+ const effectiveExternal = isEffectiveExternal(fk, options);
107
+ // Mirrors fk-validator.ts exactly: the required-empty clause is keyed
108
+ // on the DECLARED external flag; the dangling clause is keyed on
109
+ // effective-external (unless the aggregator asks for internal
110
+ // treatment).
111
+ const checkEmpty = !fk.nullable && !fk.external;
112
+ const checkDangling = !effectiveExternal || options.treatExternalAsInternal === true;
113
+ if (!checkEmpty && !checkDangling)
114
+ continue;
115
+ const t = quote(cat.name);
116
+ const col = quote(fk.column);
117
+ const pk = quote(cat.primaryKey);
118
+ const clauses = [];
119
+ if (checkEmpty) {
120
+ clauses.push(`(t.${col} IS NULL OR t.${col} = '')`);
121
+ }
122
+ if (checkDangling) {
123
+ // M-ADR 0208 §4.4 (F-D): a platform-scoped target resolves on
124
+ // (platform_sdd_id, pk) — equal stamps, or either side unstamped.
125
+ // Mirrors fk-validator.ts's `resolved` predicate exactly.
126
+ const stamp = quote(PLATFORM_STAMP_COLUMN);
127
+ const scope = isPlatformScopedFk(cat, catByName.get(targetCat), options)
128
+ ? ` AND (COALESCE(t.${stamp}, '') = '' OR COALESCE(r.${stamp}, '') = '' OR r.${stamp} = t.${stamp})`
129
+ : "";
130
+ clauses.push(`(t.${col} IS NOT NULL AND t.${col} <> '' AND NOT EXISTS (` +
131
+ `SELECT 1 FROM ${quote(targetCat)} r WHERE r.${quote(targetCol)} = t.${col}${scope}))`);
132
+ }
133
+ queries.push({
134
+ catalogue: cat.name,
135
+ kind: "fk",
136
+ column: fk.column,
137
+ target: fk.references,
138
+ sql: `SELECT t.${pk} AS row_pk, COALESCE(t.${col}, '') AS value FROM ${t} t ` +
139
+ `WHERE ${clauses.join(" OR ")} ORDER BY row_pk, value;`,
140
+ });
141
+ }
142
+ for (const [column, enumRef] of Object.entries(cat.columnEnums)) {
143
+ const enumName = enumRef.replace(/^enums\./, "");
144
+ const enumDef = schema.enums[enumName];
145
+ if (!enumDef) {
146
+ throw new Error(`ddl-generator: ${cat.name}.${column} binds unknown enum "${enumRef}" — a binding that names no enum must not validate as vacuously true`);
147
+ }
148
+ if (!cat.columns.some((c) => c.name === column)) {
149
+ throw new Error(`ddl-generator: ${cat.name}.${column} binds a column the catalogue does not declare — the schema 3.1.3 vacuous-binding class`);
150
+ }
151
+ const allowed = enumDef.values
152
+ .map((v) => sqlStringLiteral(v.value))
153
+ .join(", ");
154
+ const t = quote(cat.name);
155
+ const col = quote(column);
156
+ const pk = quote(cat.primaryKey);
157
+ queries.push({
158
+ catalogue: cat.name,
159
+ kind: "enum",
160
+ column,
161
+ target: enumName,
162
+ sql:
163
+ // TRIM in the projection too: enum-validator.ts records the
164
+ // TRIMMED value in its violation objects, and A2's count-and-list
165
+ // parity compares those objects field-for-field.
166
+ `SELECT t.${pk} AS row_pk, TRIM(t.${col}) AS value FROM ${t} t ` +
167
+ `WHERE t.${col} IS NOT NULL AND TRIM(t.${col}) <> '' ` +
168
+ `AND TRIM(t.${col}) NOT IN (${allowed}) ORDER BY row_pk, value;`,
169
+ });
170
+ }
171
+ }
172
+ return queries;
173
+ }
174
+ /**
175
+ * Dangling-value queries for the FKs the main set SKIPS as
176
+ * effective-external — one query per skipped FK, returning the rows the
177
+ * in-memory path logs as `[finding] external` (unresolved non-empty
178
+ * values; resolution happens at aggregation). Empty when
179
+ * treatExternalAsInternal is set, exactly as the main set then skips
180
+ * nothing. Kept separate from generateConstraintQueries because these
181
+ * rows are FINDINGS, not failures — the exit contracts differ.
182
+ */
183
+ export function generateExternalFindingQueries(schema, options = {}) {
184
+ if (options.treatExternalAsInternal === true)
185
+ return [];
186
+ const queries = [];
187
+ for (const cat of schema.catalogues) {
188
+ for (const fk of cat.foreignKeys) {
189
+ if (!isEffectiveExternal(fk, options))
190
+ continue;
191
+ const [targetCat, targetCol] = fk.references.split(".");
192
+ if (!targetCat || !targetCol) {
193
+ throw new Error(`ddl-generator: malformed FK reference "${fk.references}" on ${cat.name}.${fk.column}`);
194
+ }
195
+ const t = quote(cat.name);
196
+ const col = quote(fk.column);
197
+ const pk = quote(cat.primaryKey);
198
+ queries.push({
199
+ catalogue: cat.name,
200
+ kind: "fk",
201
+ column: fk.column,
202
+ target: fk.references,
203
+ sql: `SELECT t.${pk} AS row_pk, t.${col} AS value FROM ${t} t ` +
204
+ `WHERE t.${col} IS NOT NULL AND t.${col} <> '' AND NOT EXISTS (` +
205
+ `SELECT 1 FROM ${quote(targetCat)} r WHERE r.${quote(targetCol)} = t.${col}) ` +
206
+ `ORDER BY row_pk, value;`,
207
+ });
208
+ }
209
+ }
210
+ return queries;
211
+ }
212
+ function pkQuery(cat, options = {}) {
213
+ const t = quote(cat.name);
214
+ const pk = quote(cat.primaryKey);
215
+ // M-ADR 0208 §4.4 (F-D): for a platform-scoped catalogue the key is
216
+ // (platform_sdd_id, pk). n rows sharing a pk are distinct iff every one
217
+ // carries a stamp and the stamps are all different — an EMPTY stamp is
218
+ // unresolved, never a key, so it collides with everything. Mirrors
219
+ // aggregate.ts findDuplicatePrimaryKeys.
220
+ const scoped = isPlatformScopedFk(cat, cat, options);
221
+ const having = scoped
222
+ ? `HAVING COUNT(*) > COUNT(DISTINCT NULLIF(${quote(PLATFORM_STAMP_COLUMN)}, ''))`
223
+ : `HAVING COUNT(*) > 1`;
224
+ return {
225
+ catalogue: cat.name,
226
+ kind: "pk",
227
+ column: cat.primaryKey,
228
+ sql: `SELECT ${pk} AS row_pk, COUNT(*) AS n FROM ${t} ` +
229
+ `WHERE ${pk} IS NOT NULL AND ${pk} <> '' ` +
230
+ `GROUP BY ${pk} ${having} ORDER BY row_pk;`,
231
+ };
232
+ }
233
+ //# sourceMappingURL=ddl-generator.js.map