@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,119 @@
1
+ // Domain discoverer — emits `domains.csv` rows from `docs/domains/<slug>/`.
2
+ //
3
+ // Per ADR 0134 and catalogue-builder-specification.md:
4
+ // - Domains are method-defined: each subdirectory under docs/domains/
5
+ // is one domain, named by its slug.
6
+ // - method_profile reflects the SDD's profile (the domain belongs to
7
+ // this SDD's method profile).
8
+ // - repo_count is computed by the build orchestrator after repositories
9
+ // are discovered, since this builder runs first.
10
+ //
11
+ // Per ADR 0136 ("App and Integration Products Do Not Belong to a Primary
12
+ // Domain"), only platform-profile SDDs own domains. Application-product
13
+ // and integration-product SDDs may have a `docs/domains/<umbrella>/`
14
+ // subdirectory as a structural-organisation convention (where their
15
+ // feature-trees and per-app evidence live), but those subdirectories
16
+ // don't represent domain ownership — they're just where the SDD's
17
+ // artefacts are organised. Emitting a domain row from a non-platform
18
+ // profile creates a duplicate-PK collision at aggregation time when the
19
+ // platform-profile SDD also emits a row for the same umbrella id (e.g.
20
+ // `applications`, `integrations`).
21
+ //
22
+ // The platform-profile SDD is the canonical owner; member SDDs reference
23
+ // their umbrella implicitly via profile + their own identity, not via
24
+ // a redundant domain row. This builder therefore returns an empty list
25
+ // for non-platform-profile SDDs.
26
+ import { readdir, readFile, stat } from "node:fs/promises";
27
+ import { join } from "node:path";
28
+ const DOMAINS_DIR_REL = join("docs", "domains");
29
+ export async function buildDomains(inputs) {
30
+ // Per ADR 0136: only platform-profile SDDs own domains. Non-platform
31
+ // SDDs return an empty list to avoid duplicate-PK collisions at
32
+ // aggregation against the canonical platform-profile contribution.
33
+ if (inputs.manifest.profile !== "platform-profile") {
34
+ return [];
35
+ }
36
+ const domainsDir = join(inputs.sddPath, DOMAINS_DIR_REL);
37
+ let entries;
38
+ try {
39
+ entries = (await readdir(domainsDir, { withFileTypes: true }))
40
+ .filter((e) => e.isDirectory())
41
+ .map((e) => ({ name: e.name }));
42
+ }
43
+ catch (err) {
44
+ const code = err.code;
45
+ if (code === "ENOENT") {
46
+ inputs.findings.add("coverage", `${DOMAINS_DIR_REL}/ not found - emitting empty domains.csv`);
47
+ return [];
48
+ }
49
+ throw err;
50
+ }
51
+ const rows = [];
52
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
53
+ const slug = entry.name;
54
+ const description = await readDomainDescription(join(domainsDir, slug), inputs.findings, slug);
55
+ rows.push({
56
+ id: slug,
57
+ name: titleCase(slug),
58
+ description,
59
+ method_profile: inputs.manifest.profile,
60
+ reference_architecture_equivalent: "",
61
+ // "active" is correct vocabulary here (active | deferred | absent):
62
+ // a domain directory that exists on disk is active by definition.
63
+ // deferred/absent rows have no authoring source yet — they would come
64
+ // from roadmap/archetype declarations, not directory discovery.
65
+ status: "active",
66
+ repo_count: 0, // populated by orchestrator after repositories build
67
+ source_sdd_id: "",
68
+ });
69
+ }
70
+ return rows;
71
+ }
72
+ /**
73
+ * Look for a one-line description in a domain README. Falls back to
74
+ * empty when no README or no usable first paragraph is present.
75
+ */
76
+ async function readDomainDescription(domainDir, findings, slug) {
77
+ const candidates = ["README.md", "README.markdown"];
78
+ for (const name of candidates) {
79
+ const path = join(domainDir, name);
80
+ try {
81
+ const s = await stat(path);
82
+ if (!s.isFile())
83
+ continue;
84
+ const text = await readFile(path, "utf-8");
85
+ const firstPara = extractFirstParagraph(text);
86
+ if (firstPara)
87
+ return firstPara.slice(0, 500);
88
+ }
89
+ catch (err) {
90
+ const code = err.code;
91
+ if (code !== "ENOENT")
92
+ throw err;
93
+ }
94
+ }
95
+ findings.add("coverage", `domain ${slug} has no README.md — description left empty`);
96
+ return "";
97
+ }
98
+ function extractFirstParagraph(markdown) {
99
+ // Strip an optional H1, then return the first non-empty paragraph
100
+ // (text up to the first blank line).
101
+ const lines = markdown.split(/\r?\n/);
102
+ let i = 0;
103
+ while (i < lines.length && (lines[i] === undefined || lines[i].trim() === "" || lines[i].startsWith("#"))) {
104
+ i++;
105
+ }
106
+ const para = [];
107
+ while (i < lines.length && lines[i].trim() !== "") {
108
+ para.push(lines[i].trim());
109
+ i++;
110
+ }
111
+ return para.join(" ").trim();
112
+ }
113
+ function titleCase(slug) {
114
+ return slug
115
+ .split("-")
116
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
117
+ .join(" ");
118
+ }
119
+ //# sourceMappingURL=domains.js.map
@@ -0,0 +1,19 @@
1
+ // F25 — one edge per node pair is the metamodel's rule (schema-stated since
2
+ // 2.30.0) and it stands. The defect was the SILENCE: distinct parallel
3
+ // labels merged first-seen-wins at member and aggregate tier, discarding
4
+ // authored facts without a word (measured: 20 labels already lost before
5
+ // the mechanism that exposed it existed). The merge now concatenates every
6
+ // distinct label — nothing dropped — and says so.
7
+ export function mergeEdgeLabel(row, label, catalogue, findings) {
8
+ if (label === "")
9
+ return;
10
+ const existing = String(row["interaction"] ?? "");
11
+ const parts = existing === "" ? [] : existing.split("; ");
12
+ if (parts.includes(label))
13
+ return;
14
+ row["interaction"] =
15
+ existing === "" ? label : `${existing}; ${label}`;
16
+ findings.add("convention", `${catalogue}: edge "${String(row["id"])}" is declared with multiple distinct labels — ` +
17
+ `one edge per node pair (schema rule); labels concatenated, nothing dropped (F25)`);
18
+ }
19
+ //# sourceMappingURL=edge-label-merge.js.map
@@ -0,0 +1,160 @@
1
+ // Forward-method walker for the entities catalogue per ADR 0159 §4.2.
2
+ // Reads `models[].entities[]` from `docs/platform/reference/glossary-alignment-manifest.yaml`.
3
+ //
4
+ // Same input file ADR 0158's entity_glossary walker reads — this walker
5
+ // promotes the entity entries to first-class entity rows; entity_glossary
6
+ // continues to emit a separate edge per (domain, entity, glossary_term).
7
+ //
8
+ // **Profile-gated to platform-profile only (per ADR 0159 §4.2 amendment,
9
+ // schema 2.13.0 patch).** The manifest is platform-owned ecosystem-wide
10
+ // content; adopters who syndicate it to sibling application-product /
11
+ // integration-product SDDs would otherwise see siblings re-emit the entire
12
+ // entity registry locally with unresolved-domain PKs, then collide en masse
13
+ // at aggregation. Profile-gating mirrors the precedent set by
14
+ // domain-models.ts per ADR 0142 (same scope, same syndication shape).
15
+ //
16
+ // Behaviour (Phase Pre Step E acceptance):
17
+ // - For each models[] entry, derive domain_id from its `domain` field.
18
+ // `platform-wide` and other non-canonical hints map to empty domain_id
19
+ // and the entity PK uses the `_platform-wide-{slug}` sentinel prefix
20
+ // (per ADR 0159 §4.2 amendment, schema 2.13.0 patch). The underscore
21
+ // prefix is unreachable via entitySlug (slugify strips underscores), so
22
+ // the sentinel can't collide with any real domain id including
23
+ // ecosystem-named domains. A [finding] convention surfaces the choice.
24
+ // - For each entities[] item with non-empty name, emit a row.
25
+ // - Within-model dedup by id; if attributes diverge, fire [finding] divergence
26
+ // on first re-encounter.
27
+ // - `status: superseded` entries carry extra `superseded_by` + `note` keys.
28
+ // Ingested as ordinary rows; superseded_by preserved in notes for future
29
+ // Phase 5 promotion to an entity_supersedes edge.
30
+ // - `glossary_term` resolved best-effort against glossary_items (exact term
31
+ // then entitySlug equality); leave empty and fire orphan on miss.
32
+ import { readFile } from "node:fs/promises";
33
+ import { join } from "node:path";
34
+ import { parse as parseYaml } from "yaml";
35
+ import { entitySlug } from "./entity-slug.js";
36
+ const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
37
+ const NON_CANONICAL_DOMAINS = new Set(["platform-wide", "", "platform_wide"]);
38
+ const PLATFORM_WIDE_PK_PREFIX = "_platform-wide";
39
+ export async function buildEntitiesFromManifest(inputs) {
40
+ // Profile gate per ADR 0159 §4.2 amendment: only platform-profile SDDs
41
+ // read the manifest. Sibling profiles (application-product, integration-
42
+ // product, support-repo, organisation) emit 0 rows even when the file is
43
+ // present via syndication.
44
+ if (inputs.profile !== "platform-profile")
45
+ return [];
46
+ const path = join(inputs.sddPath, MANIFEST_REL_PATH);
47
+ let text;
48
+ try {
49
+ text = await readFile(path, "utf-8");
50
+ }
51
+ catch (err) {
52
+ if (err.code === "ENOENT")
53
+ return [];
54
+ throw err;
55
+ }
56
+ let raw;
57
+ try {
58
+ raw = parseYaml(text);
59
+ }
60
+ catch (err) {
61
+ inputs.findings.add("convention", `${MANIFEST_REL_PATH}: failed to parse YAML — ${err.message}`);
62
+ return [];
63
+ }
64
+ if (!raw || typeof raw !== "object")
65
+ return [];
66
+ const obj = raw;
67
+ const modelsRaw = obj["models"];
68
+ if (!Array.isArray(modelsRaw))
69
+ return [];
70
+ const rows = [];
71
+ const byId = new Map();
72
+ for (const m of modelsRaw) {
73
+ if (!m || typeof m !== "object")
74
+ continue;
75
+ const model = m;
76
+ const rawDomain = String(model.domain ?? "").trim();
77
+ let domainId = rawDomain;
78
+ if (NON_CANONICAL_DOMAINS.has(rawDomain)) {
79
+ domainId = "";
80
+ inputs.findings.add("convention", `${MANIFEST_REL_PATH}: model ${model.path ?? "(unknown path)"} has non-canonical domain "${rawDomain}" — entity domain_id left empty`);
81
+ }
82
+ else if (rawDomain && !inputs.knownDomainIds.has(rawDomain)) {
83
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: model ${model.path ?? "(unknown path)"} declares unknown domain "${rawDomain}" — entity domain_id left empty`);
84
+ domainId = "";
85
+ }
86
+ const entities = (model.entities ?? []);
87
+ for (const e of entities) {
88
+ if (!e || typeof e !== "object")
89
+ continue;
90
+ const entity = e;
91
+ const name = String(entity.name ?? "").trim();
92
+ if (!name) {
93
+ inputs.findings.add("convention", `${MANIFEST_REL_PATH}: model ${model.path ?? "(unknown)"} has an entities[] entry with empty name — skipped`);
94
+ continue;
95
+ }
96
+ const slug = entitySlug(name);
97
+ const id = domainId
98
+ ? `${domainId}-${slug}`
99
+ : `${PLATFORM_WIDE_PK_PREFIX}-${slug}`;
100
+ const glossaryTerm = resolveGlossaryTerm(entity.glossary_term, inputs);
101
+ const noteParts = [];
102
+ if (entity.status)
103
+ noteParts.push(`status=${entity.status}`);
104
+ if (entity.superseded_by)
105
+ noteParts.push(`superseded_by=${entity.superseded_by}`);
106
+ if (entity.note)
107
+ noteParts.push(`note=${entity.note}`);
108
+ const row = {
109
+ id,
110
+ name,
111
+ domain_id: domainId,
112
+ source_domain_model_id: "",
113
+ persistence_kind: "",
114
+ glossary_term: glossaryTerm,
115
+ notes: noteParts.join(";"),
116
+ // The glossary-alignment manifest's entities[] shape has no evidence
117
+ // slot — manifest-sourced rows stay empty (ADR 0175; schema 2.20.0).
118
+ evidence_class: "",
119
+ source_sdd_id: "",
120
+ };
121
+ const existing = byId.get(id);
122
+ if (existing) {
123
+ const existingGt = String(existing["glossary_term"] ?? "");
124
+ if (existingGt !== glossaryTerm && glossaryTerm) {
125
+ inputs.findings.add("divergence", `${MANIFEST_REL_PATH}: entity ${id} declared in multiple models[] with conflicting glossary_term — keeping first ("${existingGt}"), discarded "${glossaryTerm}"`);
126
+ }
127
+ continue;
128
+ }
129
+ byId.set(id, row);
130
+ rows.push(row);
131
+ }
132
+ }
133
+ return rows;
134
+ }
135
+ function resolveGlossaryTerm(raw, inputs) {
136
+ const term = String(raw ?? "").trim();
137
+ if (!term)
138
+ return "";
139
+ // Try the term-name index plumbed by the glossary builder.
140
+ const lookups = inputs.idsByTermName?.get(term.toLowerCase());
141
+ if (lookups && lookups.length > 0) {
142
+ // Prefer the first; cross-domain collisions are rare and the entity row
143
+ // carries only one term FK.
144
+ const id = lookups[0]?.id;
145
+ if (id && inputs.knownGlossaryIds.has(id))
146
+ return id;
147
+ }
148
+ // Fall back to entity-slug equality scan: produces "vehicle-registration"
149
+ // type matches for terms authored as "Vehicle Registration".
150
+ const slug = entitySlug(term);
151
+ for (const id of inputs.knownGlossaryIds) {
152
+ // Glossary item ids follow {owning_domain_id}-{term_slug} style; suffix
153
+ // match avoids cross-domain false positives in most cases.
154
+ if (id.endsWith(`-${slug}`) || id === slug)
155
+ return id;
156
+ }
157
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: entity glossary_term "${term}" doesn't resolve to a known glossary item`);
158
+ return "";
159
+ }
160
+ //# sourceMappingURL=entities-manifest.js.map
@@ -0,0 +1,189 @@
1
+ // Reverse-method walker for the entities catalogue per ADR 0159 §4.3.
2
+ // Parses `erDiagram` blocks in `docs/domains/{domain}/architecture/data-model.mermaid.md`
3
+ // (or whatever file domain_models.csv references for that domain).
4
+ //
5
+ // Prototype findings (per ADR 0159 — mermaid authoring quirks):
6
+ // - Multi-erDiagram-per-file is the norm (asset has 7 blocks; identity has 4).
7
+ // Walker merges entities across all `erDiagram` fences in the file.
8
+ // - Naming style varies within a single file: UPPER_SNAKE_CASE, PascalCase,
9
+ // ALLCAPS-no-underscores, lowercase / camelCase. Regex accepts [A-Za-z_]
10
+ // as first char with explicit Mermaid-keyword exclusion.
11
+ // - Relationship lines (`A ||--o{ B : "label"`) start with an identifier but
12
+ // are not entity declarations; detected via arrow-token pattern + skipped.
13
+ // - Mermaid comments (`%% ...`) are tolerated inside entity blocks.
14
+ // - Files may mix `erDiagram` with other Mermaid diagram types (flowchart,
15
+ // sequenceDiagram). Each fenced block is independently checked for the
16
+ // erDiagram opener; non-ER blocks are skipped.
17
+ import { readFile } from "node:fs/promises";
18
+ import { entitySlug } from "./entity-slug.js";
19
+ import { extractFileEvidenceClass } from "./evidence-class.js";
20
+ const RE_MERMAID_OPEN = /^```\s*mermaid\s*$/i;
21
+ const RE_FENCE_CLOSE = /^```\s*$/;
22
+ const RE_COMMENT = /^\s*%%/;
23
+ const RE_ENTITY_OPEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$/;
24
+ const RE_REL_LINE = /^\s*[A-Za-z_][A-Za-z0-9_]*\s+\|?\|?[o|\-]+\{?\s+[A-Za-z]/;
25
+ const MERMAID_KEYWORDS = new Set(["erdiagram", "subgraph", "classdef", "style", "class"]);
26
+ export async function buildEntitiesFromMermaid(inputs) {
27
+ const rows = [];
28
+ const seenAcrossFiles = new Map(); // id → originating doc_path
29
+ for (const dm of inputs.domainModels) {
30
+ if (!dm.doc_path.endsWith(".mermaid.md"))
31
+ continue;
32
+ const docPath = dm.doc_path.startsWith("/")
33
+ ? dm.doc_path
34
+ : `${inputs.sddPath}/${dm.doc_path}`;
35
+ let text;
36
+ try {
37
+ text = await readFile(docPath, "utf-8");
38
+ }
39
+ catch (err) {
40
+ if (err.code === "ENOENT")
41
+ continue;
42
+ throw err;
43
+ }
44
+ const parsed = parseFile(text, dm, inputs.findings);
45
+ // File-level evidence class (leading ```yaml fence's `evidence-class:`,
46
+ // per the documentation-reverse anchor convention / ADR 0175). Every
47
+ // entity extracted from this file inherits it; absence → "".
48
+ const fileEvidenceClass = extractFileEvidenceClass(text);
49
+ // Within-file dedup by id is enforced inside parseFile via seen-set;
50
+ // cross-file dedup is enforced here (shouldn't fire in current Adopter A,
51
+ // defensive against future authoring that splits a domain across files).
52
+ for (const p of parsed) {
53
+ const id = `${p.domainId}-${entitySlug(p.name)}`;
54
+ const existing = seenAcrossFiles.get(id);
55
+ if (existing) {
56
+ inputs.findings.add("divergence", `entities: cross-file id collision on "${id}" — declared in ${existing} and ${dm.doc_path}; keeping first`);
57
+ continue;
58
+ }
59
+ seenAcrossFiles.set(id, dm.doc_path);
60
+ const glossaryTerm = resolveGlossaryTermBySlug(p.name, p.domainId, inputs.knownGlossaryIds);
61
+ rows.push({
62
+ id,
63
+ name: p.name,
64
+ domain_id: p.domainId,
65
+ source_domain_model_id: p.sourceDomainModelId,
66
+ persistence_kind: "",
67
+ glossary_term: glossaryTerm,
68
+ notes: `columns=${p.columnCount}`,
69
+ evidence_class: fileEvidenceClass,
70
+ source_sdd_id: "",
71
+ });
72
+ }
73
+ }
74
+ return rows;
75
+ }
76
+ function parseFile(text, dm, findings) {
77
+ const lines = text.split(/\r?\n/);
78
+ const out = [];
79
+ const seenInFile = new Set();
80
+ let inMermaid = false;
81
+ let isEr = false;
82
+ let firstKeywordSeen = false;
83
+ let inEntity = false;
84
+ let entityName = null;
85
+ let entityColumns = 0;
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const raw = lines[i] ?? "";
88
+ if (!inMermaid) {
89
+ if (RE_MERMAID_OPEN.test(raw)) {
90
+ inMermaid = true;
91
+ isEr = false;
92
+ firstKeywordSeen = false;
93
+ }
94
+ continue;
95
+ }
96
+ // in_mermaid == true
97
+ if (RE_FENCE_CLOSE.test(raw)) {
98
+ inMermaid = false;
99
+ isEr = false;
100
+ firstKeywordSeen = false;
101
+ // If we exit mid-entity, drop it silently — malformed block.
102
+ inEntity = false;
103
+ entityName = null;
104
+ continue;
105
+ }
106
+ const stripped = raw.trim();
107
+ if (!firstKeywordSeen && stripped !== "" && !RE_COMMENT.test(raw)) {
108
+ firstKeywordSeen = true;
109
+ isEr = stripped.toLowerCase().startsWith("erdiagram");
110
+ }
111
+ if (!isEr)
112
+ continue;
113
+ if (inEntity) {
114
+ if (stripped.startsWith("}")) {
115
+ if (entityName !== null) {
116
+ const slug = entitySlug(entityName);
117
+ if (!seenInFile.has(slug)) {
118
+ seenInFile.add(slug);
119
+ out.push({
120
+ name: entityName,
121
+ domainId: dm.domain_id,
122
+ sourceDomainModelId: dm.id,
123
+ columnCount: entityColumns,
124
+ });
125
+ }
126
+ }
127
+ inEntity = false;
128
+ entityName = null;
129
+ entityColumns = 0;
130
+ continue;
131
+ }
132
+ if (RE_COMMENT.test(raw))
133
+ continue;
134
+ if (stripped)
135
+ entityColumns++;
136
+ continue;
137
+ }
138
+ // Not in an entity block — look for an opener.
139
+ if (RE_COMMENT.test(raw))
140
+ continue;
141
+ if (RE_REL_LINE.test(raw))
142
+ continue;
143
+ const m = RE_ENTITY_OPEN.exec(raw);
144
+ if (m) {
145
+ const ident = m[1] ?? "";
146
+ if (MERMAID_KEYWORDS.has(ident.toLowerCase()))
147
+ continue;
148
+ entityName = ident;
149
+ inEntity = true;
150
+ entityColumns = 0;
151
+ }
152
+ }
153
+ // Defensive: if a block ended without closing an entity, log it once.
154
+ if (inEntity && entityName !== null) {
155
+ findings.add("convention", `entities-mermaid: ${dm.doc_path}: unclosed entity block for "${entityName}" — discarded`);
156
+ }
157
+ return out;
158
+ }
159
+ function resolveGlossaryTermBySlug(entityName, domainId, knownGlossaryIds) {
160
+ const slug = entitySlug(entityName);
161
+ if (!slug)
162
+ return "";
163
+ // Glossary ids follow {owning_domain_id}-{term_slug} or {slug} shape.
164
+ // Suffix-match keeps cross-domain ids resolvable while avoiding random
165
+ // substring collisions. Resolves single-word terms (slug has no internal
166
+ // hyphen, so it equals the glossary term-slug).
167
+ for (const id of knownGlossaryIds) {
168
+ if (id === slug || id.endsWith(`-${slug}`))
169
+ return id;
170
+ }
171
+ // ADR 0165 follow-on: the glossary builder collapses multi-word term slugs
172
+ // (CONTAINER_MESSAGING → "...-containermessaging"), while entitySlug
173
+ // hyphenates ("container-messaging"), so multi-word terms miss the suffix
174
+ // match above. Reconcile boundary-insensitively (compare hyphen-stripped
175
+ // forms), scoped to the entity's OWN domain to avoid cross-domain
176
+ // false positives.
177
+ if (domainId !== "") {
178
+ const prefix = `${domainId}-`;
179
+ const collapsed = slug.replace(/-/g, "");
180
+ for (const id of knownGlossaryIds) {
181
+ if (!id.startsWith(prefix))
182
+ continue;
183
+ if (id.slice(prefix.length).replace(/-/g, "") === collapsed)
184
+ return id;
185
+ }
186
+ }
187
+ return ""; // Most data-model entities have no glossary term; no orphan finding.
188
+ }
189
+ //# sourceMappingURL=entities-mermaid.js.map
@@ -0,0 +1,102 @@
1
+ // ADR 0164: backfill `entities.persistence_kind` from the reverse-method
2
+ // `## Data Store Mapping` table's `Store` column.
3
+ //
4
+ // persistence_kind is a reverse-only column (the services parity arc, ADR
5
+ // 0163): the reverse adopter's Data Store Mapping carries a `Store` engine
6
+ // per entity; forward-method adopters author a store-less table, so their
7
+ // rows correctly stay empty (no Store column → no fill, no finding).
8
+ //
9
+ // The Store engine is mapped to the canonical persistence_kind
10
+ // classification (relational-table / document-collection / event-payload /
11
+ // other) via data-store-mapping.derivePersistenceKind — a derived token,
12
+ // not the verbatim engine (the schema's recommended set is a classification;
13
+ // the raw engine is preserved in `notes`). erDiagram-only entities (not in
14
+ // any Data Store Mapping row) stay empty — legitimate partial coverage.
15
+ import { readFile } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { derivePersistenceKind, detectColumn, entitySlug, findDataStoreTable, normaliseEntity, } from "./data-store-mapping.js";
18
+ /**
19
+ * Returns the entity rows with `persistence_kind` filled (fill-if-empty)
20
+ * from the Data Store Mapping `Store` column, and the raw store recorded in
21
+ * `notes`. When no Store column exists anywhere (forward adopters) the rows
22
+ * are returned unchanged and no finding is emitted.
23
+ */
24
+ export async function enrichEntityPersistenceKind(inputs) {
25
+ const kindByEntityId = new Map();
26
+ const unmappedStores = new Set();
27
+ for (const dm of inputs.domainModels) {
28
+ if (!dm.doc_path.endsWith(".mermaid.md"))
29
+ continue;
30
+ const fullPath = dm.doc_path.startsWith("/")
31
+ ? dm.doc_path
32
+ : join(inputs.sddPath, dm.doc_path);
33
+ let text;
34
+ try {
35
+ text = await readFile(fullPath, "utf-8");
36
+ }
37
+ catch (err) {
38
+ if (err.code === "ENOENT")
39
+ continue;
40
+ throw err;
41
+ }
42
+ const table = findDataStoreTable(text);
43
+ if (table === null)
44
+ continue;
45
+ const entityColIdx = detectColumn(table.headers, (h) => h === "entity" || h.startsWith("entity"));
46
+ const storeColIdx = detectColumn(table.headers, (h) => h === "store");
47
+ // No Store column → forward-method store-less table; skip silently.
48
+ if (entityColIdx === -1 || storeColIdx === -1)
49
+ continue;
50
+ for (const cells of table.dataRows) {
51
+ if (cells.length <= Math.max(entityColIdx, storeColIdx))
52
+ continue;
53
+ const entityText = cells[entityColIdx].trim();
54
+ const store = cells[storeColIdx].trim();
55
+ if (!entityText || !store)
56
+ continue;
57
+ const { kind, recognised } = derivePersistenceKind(store);
58
+ if (kind === "")
59
+ continue; // null store cell (N/A, -, …)
60
+ if (!recognised)
61
+ unmappedStores.add(store);
62
+ const entSlug = entitySlug(normaliseEntity(entityText));
63
+ const entityId = inputs.entityIdByDomainSlug.get(`${dm.domain_id}|${entSlug}`) ??
64
+ inputs.entityIdBySlug.get(entSlug);
65
+ // Data-store-only row with no matching entity node — skip (partial
66
+ // coverage); the service-entity walker emits its own orphan finding.
67
+ if (entityId === undefined)
68
+ continue;
69
+ // First mapping wins (deterministic by domain-model order).
70
+ if (!kindByEntityId.has(entityId)) {
71
+ kindByEntityId.set(entityId, { kind, store });
72
+ }
73
+ }
74
+ }
75
+ if (kindByEntityId.size === 0)
76
+ return [...inputs.entityRows];
77
+ let filled = 0;
78
+ const out = inputs.entityRows.map((row) => {
79
+ if (String(row["persistence_kind"] ?? "") !== "")
80
+ return row; // fill-if-empty
81
+ const hit = kindByEntityId.get(String(row["id"]));
82
+ if (hit === undefined)
83
+ return row;
84
+ filled++;
85
+ const notes = String(row["notes"] ?? "");
86
+ const storeNote = `store=${hit.store}`;
87
+ return {
88
+ ...row,
89
+ persistence_kind: hit.kind,
90
+ notes: notes ? `${notes};${storeNote}` : storeNote,
91
+ };
92
+ });
93
+ if (filled > 0) {
94
+ const unmapped = unmappedStores.size > 0
95
+ ? ` (${unmappedStores.size} store value(s) matched no rule → other: ${[...unmappedStores].slice(0, 8).join(", ")})`
96
+ : "";
97
+ inputs.findings.add("coverage", `entities: persistence_kind backfilled on ${filled} ` +
98
+ `entit${filled === 1 ? "y" : "ies"} from Data Store Mapping Store column${unmapped}`);
99
+ }
100
+ return out;
101
+ }
102
+ //# sourceMappingURL=entity-persistence.js.map
@@ -0,0 +1,47 @@
1
+ // Entity-aware slug helper used by the data-model-entity walkers
2
+ // (entities-manifest, entities-mermaid, service-entity-tables) per ADR 0159 §4.6.
3
+ //
4
+ // Differs from the base `slugify` in events / capabilities / contracts: adds
5
+ // an ALLCAPS-then-Pascal split rule so naming-style variation across
6
+ // UPPER_SNAKE_CASE Mermaid authoring and PascalCase data-store-table
7
+ // authoring resolves to a single canonical slug.
8
+ //
9
+ // Examples:
10
+ // entitySlug("OBUConfigurationTemplate") → "obu-configuration-template"
11
+ // entitySlug("OBU_CONFIGURATION_TEMPLATE") → "obu-configuration-template"
12
+ // entitySlug("Vehicle") → "vehicle"
13
+ // entitySlug("APIKeyRotationRecord") → "api-key-rotation-record"
14
+ // entitySlug("customer") → "customer" (lowercase Mermaid)
15
+ // entitySlug("accountFeatures") → "account-features"
16
+ //
17
+ // Phase Pre finding (research/adr-0159-prototypes/PHASE-PRE-SYNTHESIS.md):
18
+ // without the ALLCAPS-then-Pascal rule, the Data Store Mapping table form
19
+ // (`OBUConfigurationTemplate`) never resolves to the Mermaid form
20
+ // (`OBU_CONFIGURATION_TEMPLATE`), breaking ~50% of expected service_entity
21
+ // edges in Adopter A.
22
+ //
23
+ // Acronym-run rule (ADR 0164 wave-2 follow-on): the first replace requires the
24
+ // leading uppercase run to be >=2 chars (`[A-Z]{2,}`). A *single* leading
25
+ // capital before a capitalised word is NOT split off, so a PascalCase form
26
+ // reconciles with its boundary-free UPPER_SNAKE form:
27
+ // entitySlug("OAuthClient") -> "oauth-client" (O is part of "OAuth")
28
+ // entitySlug("OAUTH_CLIENT") -> "oauth-client" (matches)
29
+ // Before the >=2 rule these produced "o-auth-client" vs "oauth-client".
30
+ // Verified zero PK churn across both reference adopters (263 + 414 entities).
31
+ // This cannot reconcile names whose PascalCase and UPPER_SNAKE forms genuinely
32
+ // disagree on word boundaries — e.g. "SigLink" (two words) vs "SIGLINK" (one)
33
+ // -> "sig-link" vs "siglink". That is adopter authoring drift, resolved by
34
+ // aligning the spellings (author `SIG_LINK`), not by this normaliser.
35
+ //
36
+ // Kept local to the entity walkers to avoid affecting events / contracts /
37
+ // capability id generation. Promote to a shared module only if a fourth
38
+ // walker needs it (per ADR 0159 §4.6 deferred decision).
39
+ export function entitySlug(s) {
40
+ return s
41
+ .replace(/([A-Z]{2,})([A-Z][a-z])/g, "$1-$2")
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9]+/g, "-")
45
+ .replace(/^-+|-+$/g, "");
46
+ }
47
+ //# sourceMappingURL=entity-slug.js.map