@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,136 @@
1
+ /**
2
+ * FK validator per ADR 0134 builder-spec §"Run internal FK validation
3
+ * before exiting" + §"External FK handling".
4
+ *
5
+ * For each catalogue's foreign keys:
6
+ * - Internal FKs (external=false): every non-empty value MUST resolve
7
+ * to a primary-key in the referenced catalogue's emitted rows. Missing
8
+ * resolution is a hard failure.
9
+ * - External FKs (external=true, or external for the build's profile via
10
+ * `externalForProfiles` — schema 2.23.0): every non-empty value is logged
11
+ * as a `[finding] external` line and skipped from validation. The
12
+ * aggregator resolves these post-merge per ADR 0134 §4.5. A required
13
+ * (non-nullable) external FK still fails when EMPTY — the value must be
14
+ * named even if it resolves cross-SDD.
15
+ *
16
+ * Returns a structured result; the caller decides exit code + reporting.
17
+ *
18
+ * POST-CUTOVER ROLE (M-ADR 0205 §4.4, A4): no production path calls this
19
+ * module — validate/build/aggregate run the engine path. It remains as the
20
+ * reference side of the standing differential (parity-harness.ts and the
21
+ * estate sweep driver), so implementation drift between the two gets
22
+ * caught by the suite instead of lurking.
23
+ */
24
+ /** The stamp column M-ADR 0208 §4.4 keys on. */
25
+ export const PLATFORM_STAMP_COLUMN = "platform_sdd_id";
26
+ /**
27
+ * Whether the FK `cat.fk → targetCat` resolves under the platform-scoped
28
+ * key (see FkValidationOptions.platformScopedTargets). One definition,
29
+ * shared with the query generator so the two paths cannot drift.
30
+ */
31
+ export function isPlatformScopedFk(cat, targetCat, options) {
32
+ if (!(options.platformScopedTargets ?? []).includes(targetCat.name))
33
+ return false;
34
+ const has = (c) => c.columns.some((col) => col.name === PLATFORM_STAMP_COLUMN);
35
+ return has(cat) && has(targetCat);
36
+ }
37
+ /**
38
+ * Build an in-memory map of every catalogue's primary-key set, then walk
39
+ * every row's FK columns and check resolution.
40
+ */
41
+ export function validateForeignKeys(schema, rowsByCatalogue, findings, options = {}) {
42
+ // Index primary keys per catalogue, and — for platform-scoped targets —
43
+ // the set of stamps each pk is present under ('' for an unstamped row).
44
+ const pkIndex = new Map();
45
+ const stampIndex = new Map();
46
+ const catByName = new Map(schema.catalogues.map((c) => [c.name, c]));
47
+ for (const cat of schema.catalogues) {
48
+ const rows = rowsByCatalogue.get(cat.name) ?? [];
49
+ const pks = new Set();
50
+ const stamps = new Map();
51
+ for (const row of rows) {
52
+ const pk = row[cat.primaryKey];
53
+ if (pk !== undefined && pk !== null && pk !== "") {
54
+ pks.add(String(pk));
55
+ const stamp = String(row[PLATFORM_STAMP_COLUMN] ?? "");
56
+ const set = stamps.get(String(pk)) ?? new Set();
57
+ set.add(stamp);
58
+ stamps.set(String(pk), set);
59
+ }
60
+ }
61
+ pkIndex.set(cat.name, pks);
62
+ stampIndex.set(cat.name, stamps);
63
+ }
64
+ const failures = [];
65
+ let externalCount = 0;
66
+ for (const cat of schema.catalogues) {
67
+ const rows = rowsByCatalogue.get(cat.name) ?? [];
68
+ for (const fk of cat.foreignKeys) {
69
+ // Effective-external (2.23.0): declared external OR external for the
70
+ // profile under build. Governs the resolution skip only — the
71
+ // required-empty check below stays keyed on the DECLARED `external` so
72
+ // a profile-external FK still requires a (cross-SDD) value to be named.
73
+ const effectiveExternal = fk.external ||
74
+ (options.profile !== undefined &&
75
+ (fk.externalForProfiles ?? []).includes(options.profile));
76
+ const [targetCat, targetCol] = fk.references.split(".");
77
+ if (!targetCat || !targetCol) {
78
+ throw new Error(`malformed FK reference "${fk.references}" on ${cat.name}.${fk.column}`);
79
+ }
80
+ const targetPks = pkIndex.get(targetCat);
81
+ const targetDef = catByName.get(targetCat);
82
+ if (!targetPks || !targetDef) {
83
+ throw new Error(`FK ${cat.name}.${fk.column} references unknown catalogue "${targetCat}"`);
84
+ }
85
+ const scoped = isPlatformScopedFk(cat, targetDef, options);
86
+ const targetStamps = stampIndex.get(targetCat);
87
+ for (const row of rows) {
88
+ const raw = row[fk.column];
89
+ if (raw === undefined || raw === null || raw === "") {
90
+ if (!fk.nullable && !fk.external) {
91
+ // Required FK with no value — internal violation regardless of external flag.
92
+ failures.push({
93
+ catalogue: cat.name,
94
+ column: fk.column,
95
+ referencingRow: String(row[cat.primaryKey] ?? ""),
96
+ value: "",
97
+ target: fk.references,
98
+ });
99
+ }
100
+ continue;
101
+ }
102
+ const value = String(raw);
103
+ let resolved = targetPks.has(value);
104
+ if (resolved && scoped) {
105
+ const mine = String(row[PLATFORM_STAMP_COLUMN] ?? "");
106
+ const theirs = targetStamps.get(value) ?? new Set();
107
+ resolved = mine === "" || theirs.has("") || theirs.has(mine);
108
+ }
109
+ if (effectiveExternal && !options.treatExternalAsInternal) {
110
+ if (!resolved) {
111
+ // Per spec: optimistically resolve against own rows; only log when unresolved.
112
+ externalCount++;
113
+ findings.add("external", `${cat.name}.${fk.column} → ${fk.references}=${value} ` +
114
+ `(referencing row pk=${row[cat.primaryKey] ?? "?"}; resolves at aggregation)`);
115
+ }
116
+ continue;
117
+ }
118
+ if (!resolved) {
119
+ failures.push({
120
+ catalogue: cat.name,
121
+ column: fk.column,
122
+ referencingRow: String(row[cat.primaryKey] ?? ""),
123
+ value,
124
+ target: fk.references,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return {
131
+ internalPass: failures.length === 0,
132
+ internalFailures: failures,
133
+ externalFindings: externalCount,
134
+ };
135
+ }
136
+ //# sourceMappingURL=fk-validator.js.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `catalogue build --check`: are the committed catalogues what the committed specs build to?
3
+ * (M-ADR 0218 §5, bead sdm-jts.6.)
4
+ *
5
+ * Builds into a temporary directory and compares, so it writes nothing into the SDD. Only what
6
+ * the build PRODUCES is compared: a hand-written file that happens to sit in catalogues/ (a
7
+ * README, say) is not a build output and is ignored. Stale means either a produced file whose
8
+ * bytes differ from the committed one, or a produced file the SDD has not committed, or a
9
+ * committed catalogue the build no longer produces.
10
+ *
11
+ * Measured before building it: an isolated clone (no siblings) of an application, an
12
+ * integration, a platform root and a packaged SDD rebuilt byte-identically to what each had
13
+ * committed, so a fresh SDD passes and the check flags only real drift. Catalogue content is a
14
+ * function of committed content, not of the commit: nothing stamps HEAD's sha.
15
+ */
16
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
17
+ import { existsSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { runCatalogueBuild } from "./build.js";
21
+ /** Stale files: produced but differing or uncommitted, and committed .csv the build no longer produces. Pure over two directories. */
22
+ export async function compareCatalogueDirs(producedDir, committedDir) {
23
+ const produced = (await readdir(producedDir)).sort();
24
+ const committed = existsSync(committedDir) ? (await readdir(committedDir)).sort() : [];
25
+ const stale = [];
26
+ for (const f of produced) {
27
+ if (!committed.includes(f)) {
28
+ stale.push(`${f} (produced, not committed)`);
29
+ continue;
30
+ }
31
+ const [a, b] = await Promise.all([readFile(join(producedDir, f)), readFile(join(committedDir, f))]);
32
+ if (!a.equals(b))
33
+ stale.push(`${f} (differs)`);
34
+ }
35
+ for (const f of committed) {
36
+ if (f.endsWith(".csv") && !produced.includes(f))
37
+ stale.push(`${f} (committed, no longer produced)`);
38
+ }
39
+ return stale;
40
+ }
41
+ export async function checkCataloguesFresh(sddPath, opts, out) {
42
+ const label = opts.label ?? "catalogue build --check";
43
+ const tmp = await mkdtemp(join(tmpdir(), "sdd-catalogue-check-"));
44
+ try {
45
+ const built = await runCatalogueBuild({
46
+ sddPath,
47
+ outDir: tmp,
48
+ ...(opts.strict !== undefined ? { strict: opts.strict } : {}),
49
+ ...(opts.snapshotStore !== undefined ? { snapshotStore: opts.snapshotStore } : {}),
50
+ }, () => { });
51
+ if (built.exitCode !== 0) {
52
+ out(`[FAIL] ${label}: the build itself failed (exit ${built.exitCode}); run catalogue build to see why.\n`);
53
+ return { exitCode: built.exitCode, stale: [] };
54
+ }
55
+ const produced = (await readdir(tmp)).sort();
56
+ const stale = await compareCatalogueDirs(tmp, join(sddPath, "catalogues"));
57
+ if (stale.length === 0) {
58
+ out(`[PASS] ${label}: ${produced.length} catalogue file(s) reproduce byte-identically from the committed specs.\n`);
59
+ return { exitCode: 0, stale };
60
+ }
61
+ out(`[FAIL] ${label}: ${stale.length} catalogue file(s) are stale against the committed specs — catalogue build (sdd-maintain or sdd-cli) rebuilds them:\n`);
62
+ for (const s of stale)
63
+ out(` ${s}\n`);
64
+ return { exitCode: 1, stale };
65
+ }
66
+ finally {
67
+ await rm(tmp, { recursive: true, force: true });
68
+ }
69
+ }
70
+ //# sourceMappingURL=freshness.js.map
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Phase-0 gate checks as SQL — M-ADR 0205 §4.4 (engine-evolution Phase A3,
3
+ * final tranche). The nine small catalogue-integrity-gate steps
4
+ * re-expressed on the engine: emptiness-vs-populated-inputs signals
5
+ * (repo-edges 0157, data-model-entities 0159, capability-event 0160,
6
+ * feature-event 0161, platform-dependencies 0162, glossary-xref 0158),
7
+ * the per-domain event-catalogue count compare (0156), and the two
8
+ * features.csv Phase-0 checks (0177 F1 promotion, F2 near-duplicate
9
+ * surfaces — the classified borderline: engine for access, the token
10
+ * Jaccard stays host-language).
11
+ *
12
+ * The shell scripts remain the REFERENCE implementations until A4. Known,
13
+ * deliberate divergence class: several references read CSV columns
14
+ * POSITIONALLY with a naive comma split (`awk -F, $5`) and count PHYSICAL
15
+ * lines rather than CSV records — this port reads by column name over
16
+ * parsed records. Where real data makes those disagree, the dual-run
17
+ * driver surfaces it and the adjudication is a defect in the shell side
18
+ * (the exact matcher-error class M-ADR 0205 §1 exists to kill), never a
19
+ * smoothing of this side.
20
+ *
21
+ * File and YAML ingestion stay host-side, mirroring each reference's
22
+ * refusal semantics (a missing key, an unparseable document, an absent
23
+ * tool each behave exactly as the reference behaves).
24
+ */
25
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { parse as parseYaml } from "yaml";
28
+ async function scalar(session, sql) {
29
+ const reader = await session.conn.runAndReadAll(sql);
30
+ return Number(reader.getRows()[0][0]);
31
+ }
32
+ const count = (table, where = "") => `SELECT COUNT(*) FROM "${table}"${where ? ` WHERE ${where}` : ""};`;
33
+ // @rule gate-event-catalogue
34
+ export async function runEventCatalogueCheck(session, options) {
35
+ if (!options.eventsPresent)
36
+ return { skipped: true, gaps: [] };
37
+ const domainsDir = join(options.sddRoot, "docs", "domains");
38
+ if (!existsSync(domainsDir))
39
+ return { skipped: false, gaps: [] };
40
+ const gaps = [];
41
+ for (const domain of readdirSync(domainsDir).sort()) {
42
+ const catalogPath = join(domainsDir, domain, "contracts", "events", "event-catalog.yaml");
43
+ if (!existsSync(catalogPath))
44
+ continue;
45
+ // Mirror of `yq '.events | length' || echo 0`: missing key or null → 0,
46
+ // an array → its length, a mapping → its key count, unparseable → 0.
47
+ let declared = 0;
48
+ try {
49
+ const doc = parseYaml(readFileSync(catalogPath, "utf-8"));
50
+ const events = doc?.["events"];
51
+ if (Array.isArray(events))
52
+ declared = events.length;
53
+ else if (events !== null && events !== undefined && typeof events === "object")
54
+ declared = Object.keys(events).length;
55
+ }
56
+ catch {
57
+ declared = 0;
58
+ }
59
+ if (declared <= 0)
60
+ continue;
61
+ const emitted = await scalar(session, count("events", `COALESCE("domain_id", '') = '${domain.replace(/'/g, "''")}'`));
62
+ if (emitted < declared)
63
+ gaps.push({ domain, declared, emitted });
64
+ }
65
+ return { skipped: false, gaps };
66
+ }
67
+ // @rule gate-repo-edges
68
+ export async function runRepoEdgesCheck(session, options) {
69
+ const repoCaps = await scalar(session, count("repo_capability"));
70
+ const capSvcs = await scalar(session, count("capability_service"));
71
+ const svcsWithRepo = await scalar(session, count("services", `COALESCE("primary_repo_id", '') <> ''`));
72
+ const repoContracts = await scalar(session, count("repo_contract"));
73
+ const contracts = await scalar(session, count("contracts"));
74
+ const svcContracts = await scalar(session, count("service_contract"));
75
+ // Mirror of the reference's two glob patterns (bash without globstar:
76
+ // `**` collapses to one level) — direct files AND one directory deep,
77
+ // schema.yaml deliberately NOT excluded because the reference scans it.
78
+ let repoConfigsWithContracts = 0;
79
+ const cfgDir = join(options.sddRoot, "orchestration", "repo-config");
80
+ if (existsSync(cfgDir)) {
81
+ const candidates = [];
82
+ for (const entry of readdirSync(cfgDir, { withFileTypes: true })) {
83
+ const p = join(cfgDir, entry.name);
84
+ if (entry.isFile() && entry.name.endsWith(".yaml"))
85
+ candidates.push(p);
86
+ else if (entry.isDirectory()) {
87
+ for (const f of readdirSync(p)) {
88
+ if (f.endsWith(".yaml"))
89
+ candidates.push(join(p, f));
90
+ }
91
+ }
92
+ }
93
+ for (const p of candidates) {
94
+ if (/^contracts:/m.test(readFileSync(p, "utf-8")))
95
+ repoConfigsWithContracts++;
96
+ }
97
+ }
98
+ return {
99
+ ...(repoCaps === 0 && capSvcs > 0 && svcsWithRepo > 0
100
+ ? { repoCapabilityGap: { capabilityService: capSvcs, servicesWithRepo: svcsWithRepo } }
101
+ : {}),
102
+ ...(repoContracts === 0 && contracts > 0 && repoConfigsWithContracts > 0
103
+ ? { repoContractForwardGap: { contracts, repoConfigsWithContracts } }
104
+ : {}),
105
+ ...(repoContracts === 0 && svcContracts > 0 && svcsWithRepo > 0
106
+ ? { repoContractPathXGap: { serviceContracts: svcContracts, servicesWithRepo: svcsWithRepo } }
107
+ : {}),
108
+ ...(repoContracts === 0 &&
109
+ contracts > 0 &&
110
+ repoConfigsWithContracts === 0 &&
111
+ svcContracts === 0
112
+ ? { repoContractReverseGap: { contracts } }
113
+ : {}),
114
+ };
115
+ }
116
+ // ─── 0158: glossary-xref ingestion gap ───────────────────────────────────
117
+ // @rule gate-glossary-xref
118
+ export async function runGlossaryXrefCheck(session, options) {
119
+ const manifest = join(options.sddRoot, "docs", "platform", "reference", "glossary-alignment-manifest.yaml");
120
+ if (!existsSync(manifest) || !options.xrefPresent)
121
+ return { finding: false };
122
+ const rows = await scalar(session, count("glossary_xref"));
123
+ return { finding: rows === 0 };
124
+ }
125
+ // @rule gate-data-model-entities
126
+ export async function runDataModelEntitiesCheck(session) {
127
+ const domainModels = await scalar(session, count("domain_models"));
128
+ const entities = await scalar(session, count("entities"));
129
+ const services = await scalar(session, count("services"));
130
+ const serviceEntity = await scalar(session, count("service_entity"));
131
+ const containerEntity = await scalar(session, count("c4_container_entity"));
132
+ const containersWithService = await scalar(session, count("c4_containers", `COALESCE("service_id", '') <> ''`));
133
+ return {
134
+ ...(domainModels > 0 && entities === 0 ? { entitiesGap: { domainModels } } : {}),
135
+ ...(entities > 0 && services > 0 && serviceEntity === 0
136
+ ? { serviceEntityGap: { entities, services } }
137
+ : {}),
138
+ ...(serviceEntity > 0 && containersWithService > 0 && containerEntity === 0
139
+ ? { containerEntityGap: { containersWithService, serviceEntity } }
140
+ : {}),
141
+ };
142
+ }
143
+ // @rule gate-capability-event
144
+ export async function runCapabilityEventCheck(session) {
145
+ const serviceEvent = await scalar(session, count("service_event"));
146
+ const capabilityService = await scalar(session, count("capability_service"));
147
+ const capabilityEvent = await scalar(session, count("capability_event"));
148
+ let authoredGap;
149
+ if (capabilityEvent > 0) {
150
+ const authored = await scalar(session, count("capability_event", `COALESCE("source", '') = 'authored'`));
151
+ const derived = await scalar(session, count("capability_event", `COALESCE("source", '') = 'derived'`));
152
+ if (authored === 0 && derived > 0)
153
+ authoredGap = { derived };
154
+ }
155
+ return {
156
+ ...(serviceEvent > 0 && capabilityService > 0 && capabilityEvent === 0
157
+ ? { derivableGap: { serviceEvent, capabilityService } }
158
+ : {}),
159
+ ...(authoredGap !== undefined ? { authoredGap } : {}),
160
+ };
161
+ }
162
+ // ─── 0161: feature_event derivation gap ──────────────────────────────────
163
+ // @rule gate-feature-event
164
+ export async function runFeatureEventCheck(session) {
165
+ const serviceEvent = await scalar(session, count("service_event"));
166
+ const featureService = await scalar(session, count("feature_service"));
167
+ const featureEvent = await scalar(session, count("feature_event"));
168
+ return serviceEvent > 0 && featureService > 0 && featureEvent === 0
169
+ ? { gap: { serviceEvent, featureService } }
170
+ : {};
171
+ }
172
+ // ─── 0162: forward platform-dependencies parity gap ──────────────────────
173
+ // @rule gate-platform-dependencies
174
+ export async function runPlatformDependenciesCheck(session, options) {
175
+ const forwardFile = join(options.sddRoot, "app-platform-dependencies.yaml");
176
+ const forwardAuthored = existsSync(forwardFile) &&
177
+ /^\s*platform_dependencies:/m.test(readFileSync(forwardFile, "utf-8"));
178
+ if (!forwardAuthored)
179
+ return { finding: false };
180
+ const ppd = await scalar(session, count("product_platform_dependencies"));
181
+ return { finding: ppd === 0 };
182
+ }
183
+ // ─── 0177 F1: feature-spec promotion ─────────────────────────────────────
184
+ /** Python's round(): half to even — the reference computes pct with it. */
185
+ function pythonRound(x) {
186
+ const floor = Math.floor(x);
187
+ const diff = x - floor;
188
+ if (diff > 0.5)
189
+ return floor + 1;
190
+ if (diff < 0.5)
191
+ return floor;
192
+ return floor % 2 === 0 ? floor : floor + 1;
193
+ }
194
+ // @rule gate-feature-spec-promotion
195
+ export async function runFeatureSpecPromotionCheck(session) {
196
+ const reader = await session.conn.runAndReadAll(`SELECT COALESCE("source", '') FROM "features";`);
197
+ let total = 0;
198
+ let resolvable = 0;
199
+ let intermediate = 0;
200
+ for (const r of reader.getRows()) {
201
+ const src = String(r[0] ?? "").trim();
202
+ total++;
203
+ if (src === "feature-spec" || src === "feature-tree")
204
+ resolvable++;
205
+ else if (src === "capability-spec" || src === "feature-grounding")
206
+ intermediate++;
207
+ }
208
+ if (total === 0 || intermediate === 0)
209
+ return {};
210
+ return {
211
+ finding: {
212
+ intermediate,
213
+ total,
214
+ pctResolvable: pythonRound((100 * resolvable) / total),
215
+ note: intermediate >= resolvable ? "predominantly un-promoted" : "partially promoted",
216
+ },
217
+ };
218
+ }
219
+ // ─── 0177 F2: near-duplicate feature surfaces (the borderline: engine for
220
+ // access, the token Jaccard host-language) ───────────────────────────
221
+ function tokens(slug) {
222
+ return new Set(slug.split("-").filter((t) => t !== ""));
223
+ }
224
+ function near(a, b) {
225
+ const ta = tokens(a);
226
+ const tb = tokens(b);
227
+ if (ta.size === 0 || tb.size === 0)
228
+ return false;
229
+ const inter = [...ta].filter((t) => tb.has(t)).length;
230
+ if (inter === ta.size || inter === tb.size)
231
+ return true; // subset either way
232
+ const union = ta.size + tb.size - inter;
233
+ return union > 0 && inter / union >= 0.5;
234
+ }
235
+ // @rule gate-feature-surface-duplication
236
+ export async function runFeatureSurfaceDuplicationCheck(session) {
237
+ const reader = await session.conn.runAndReadAll(`SELECT COALESCE("capability_id", ''), COALESCE("name", ''), COALESCE("source", '') FROM "features";`);
238
+ const rollup = new Map();
239
+ const detailed = new Map();
240
+ for (const r of reader.getRows()) {
241
+ const cap = String(r[0] ?? "").trim();
242
+ const slug = String(r[1] ?? "").trim();
243
+ const src = String(r[2] ?? "").trim();
244
+ if (cap === "" || slug === "")
245
+ continue;
246
+ if (src === "capability-spec") {
247
+ rollup.set(cap, [...(rollup.get(cap) ?? []), slug]);
248
+ }
249
+ else if (src === "feature-grounding") {
250
+ detailed.set(cap, [...(detailed.get(cap) ?? []), slug]);
251
+ }
252
+ }
253
+ let pairs = 0;
254
+ const caps = new Set();
255
+ for (const [cap, rollupSlugs] of rollup) {
256
+ const detailedSlugs = detailed.get(cap);
257
+ if (!detailedSlugs)
258
+ continue;
259
+ for (const r of rollupSlugs) {
260
+ for (const d of detailedSlugs) {
261
+ if (r !== d && near(r, d)) {
262
+ pairs++;
263
+ caps.add(cap);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ return pairs > 0 ? { finding: { pairs, capabilities: caps.size } } : {};
269
+ }
270
+ // @rule gate-relationship-coherence
271
+ export async function runRelationshipCoherenceCheck(session) {
272
+ const ownedUntraced = [];
273
+ let reader = await session.conn.runAndReadAll(`SELECT f."id", f."capability_id" FROM "features" f
274
+ WHERE COALESCE(f."capability_id", '') <> ''
275
+ AND NOT EXISTS (SELECT 1 FROM "capability_feature" cf
276
+ WHERE cf."capability_id" = f."capability_id"
277
+ AND cf."feature_id" = f."id")
278
+ ORDER BY f."id";`);
279
+ for (const r of reader.getRows()) {
280
+ ownedUntraced.push({ featureId: String(r[0]), capabilityId: String(r[1]) });
281
+ }
282
+ const publisherDrift = [];
283
+ reader = await session.conn.runAndReadAll(`SELECT se."service_id", se."event_id" FROM "service_event" se
284
+ WHERE se."role" = 'publishes'
285
+ AND NOT EXISTS (SELECT 1 FROM "events" e
286
+ WHERE e."id" = se."event_id"
287
+ AND COALESCE(e."publisher_service_id", '') = se."service_id")
288
+ ORDER BY 1, 2;`);
289
+ for (const r of reader.getRows()) {
290
+ publisherDrift.push({
291
+ serviceId: String(r[0]),
292
+ eventId: String(r[1]),
293
+ missing: "publisher-fk",
294
+ });
295
+ }
296
+ reader = await session.conn.runAndReadAll(`SELECT e."publisher_service_id", e."id" FROM "events" e
297
+ WHERE COALESCE(e."publisher_service_id", '') <> ''
298
+ AND NOT EXISTS (SELECT 1 FROM "service_event" se
299
+ WHERE se."role" = 'publishes'
300
+ AND se."event_id" = e."id"
301
+ AND se."service_id" = e."publisher_service_id")
302
+ ORDER BY 1, 2;`);
303
+ for (const r of reader.getRows()) {
304
+ publisherDrift.push({
305
+ serviceId: String(r[0]),
306
+ eventId: String(r[1]),
307
+ missing: "publishes-edge",
308
+ });
309
+ }
310
+ const supersessionDrift = [];
311
+ reader = await session.conn.runAndReadAll(`SELECT s."superseding_adr_id", s."superseded_adr_id" FROM "adr_supersedes" s
312
+ WHERE NOT EXISTS (SELECT 1 FROM "adrs" a
313
+ WHERE a."id" = s."superseded_adr_id"
314
+ AND COALESCE(a."superseded_by_id", '') = s."superseding_adr_id")
315
+ ORDER BY 1, 2;`);
316
+ for (const r of reader.getRows()) {
317
+ supersessionDrift.push({
318
+ supersedingId: String(r[0]),
319
+ supersededId: String(r[1]),
320
+ missing: "fk-backfill",
321
+ });
322
+ }
323
+ reader = await session.conn.runAndReadAll(`SELECT a."superseded_by_id", a."id" FROM "adrs" a
324
+ WHERE COALESCE(a."superseded_by_id", '') <> ''
325
+ AND NOT EXISTS (SELECT 1 FROM "adr_supersedes" s
326
+ WHERE s."superseded_adr_id" = a."id"
327
+ AND s."superseding_adr_id" = a."superseded_by_id")
328
+ ORDER BY 1, 2;`);
329
+ for (const r of reader.getRows()) {
330
+ supersessionDrift.push({
331
+ supersedingId: String(r[0]),
332
+ supersededId: String(r[1]),
333
+ missing: "edge-row",
334
+ });
335
+ }
336
+ return { ownedUntraced, publisherDrift, supersessionDrift };
337
+ }
338
+ //# sourceMappingURL=gate-checks.js.map