@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.
- package/LICENSE +201 -0
- package/NOTICE +17 -0
- package/README.md +6 -2
- package/dist/author-bin.js +8 -0
- package/dist/author-program.js +99 -0
- package/dist/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +37 -0
- package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
- package/dist/lib/api-surface/analyzers/library.js +95 -0
- package/dist/lib/api-surface/analyzers/serverless.js +336 -0
- package/dist/lib/api-surface/analyzers/spring.js +237 -0
- package/dist/lib/api-surface/analyzers/terraform.js +109 -0
- package/dist/lib/api-surface/detect.js +25 -0
- package/dist/lib/api-surface/emit.js +109 -0
- package/dist/lib/api-surface/file-set.js +43 -0
- package/dist/lib/api-surface/index.js +134 -0
- package/dist/lib/api-surface/types.js +17 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +132 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
- package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
- package/dist/lib/catalogue/archetype/resolver.js +103 -0
- package/dist/lib/catalogue/archetype/wireframe.js +418 -0
- package/dist/lib/catalogue/baseline-check.js +151 -0
- package/dist/lib/catalogue/build.js +1502 -0
- package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
- package/dist/lib/catalogue/builders/adrs.js +222 -0
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
- package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
- package/dist/lib/catalogue/builders/c4-components.js +229 -0
- package/dist/lib/catalogue/builders/c4-containers.js +240 -0
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
- package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
- package/dist/lib/catalogue/builders/c4-systems.js +98 -0
- package/dist/lib/catalogue/builders/capabilities.js +488 -0
- package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
- package/dist/lib/catalogue/builders/capability-edges.js +256 -0
- package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
- package/dist/lib/catalogue/builders/capability-spec.js +348 -0
- package/dist/lib/catalogue/builders/container-dependency.js +183 -0
- package/dist/lib/catalogue/builders/contracts.js +402 -0
- package/dist/lib/catalogue/builders/data-items.js +344 -0
- package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
- package/dist/lib/catalogue/builders/domain-models.js +175 -0
- package/dist/lib/catalogue/builders/domains.js +119 -0
- package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
- package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
- package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
- package/dist/lib/catalogue/builders/entity-slug.js +47 -0
- package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
- package/dist/lib/catalogue/builders/event-edges.js +327 -0
- package/dist/lib/catalogue/builders/events.js +168 -0
- package/dist/lib/catalogue/builders/evidence-class.js +43 -0
- package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
- package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
- package/dist/lib/catalogue/builders/feature-contract.js +77 -0
- package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
- package/dist/lib/catalogue/builders/feature-groups.js +63 -0
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
- package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
- package/dist/lib/catalogue/builders/features.js +300 -0
- package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
- package/dist/lib/catalogue/builders/glossary.js +204 -0
- package/dist/lib/catalogue/builders/implementations.js +214 -0
- package/dist/lib/catalogue/builders/index.js +64 -0
- package/dist/lib/catalogue/builders/integration-components.js +344 -0
- package/dist/lib/catalogue/builders/integrations.js +181 -0
- package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
- package/dist/lib/catalogue/builders/persona-container.js +133 -0
- package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
- package/dist/lib/catalogue/builders/personas.js +152 -0
- package/dist/lib/catalogue/builders/product-edges.js +106 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
- package/dist/lib/catalogue/builders/products.js +162 -0
- package/dist/lib/catalogue/builders/quality-scores.js +635 -0
- package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-edges.js +340 -0
- package/dist/lib/catalogue/builders/repositories.js +458 -0
- package/dist/lib/catalogue/builders/scaffolds.js +197 -0
- package/dist/lib/catalogue/builders/screen-tree.js +222 -0
- package/dist/lib/catalogue/builders/screens.js +396 -0
- package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
- package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
- package/dist/lib/catalogue/builders/service-contract.js +74 -0
- package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
- package/dist/lib/catalogue/builders/services.js +232 -0
- package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
- package/dist/lib/catalogue/builders/user-journeys.js +253 -0
- package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/contract-id.js +49 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +552 -0
- package/dist/lib/catalogue/coverage-run.js +115 -0
- package/dist/lib/catalogue/csv-reader.js +97 -0
- package/dist/lib/catalogue/csv-writer.js +96 -0
- package/dist/lib/catalogue/data-plane-coherence.js +77 -0
- package/dist/lib/catalogue/ddl-generator.js +233 -0
- package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
- package/dist/lib/catalogue/engine-validator.js +239 -0
- package/dist/lib/catalogue/ext-id.js +76 -0
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
- package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
- package/dist/lib/catalogue/finding-identity.js +183 -0
- package/dist/lib/catalogue/findings.js +46 -0
- package/dist/lib/catalogue/fk-validator.js +136 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +435 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/population-checks.js +87 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/reverse-product-quality.js +99 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
- package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
- package/dist/lib/catalogue/scaffold-delta.js +261 -0
- package/dist/lib/catalogue/schema-compat.js +52 -0
- package/dist/lib/catalogue/schema-loader.js +87 -0
- package/dist/lib/catalogue/scores-coverage.js +573 -0
- package/dist/lib/catalogue/screen-validation-loader.js +89 -0
- package/dist/lib/catalogue/service-architecture-parser.js +193 -0
- package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/traceability-loader.js +350 -0
- package/dist/lib/catalogue/validate.js +200 -0
- package/dist/lib/catalogue/version.js +18 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/conformance/finding.js +69 -0
- package/dist/lib/conformance/index.js +578 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/contracts-availability/index.js +267 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/list/discover.js +100 -0
- package/dist/lib/list/format.js +84 -0
- package/dist/lib/list/index.js +16 -0
- package/dist/lib/lock-file.js +437 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/schema.js +121 -0
- package/dist/lib/pack/seed.js +182 -0
- package/dist/lib/pack/validate.js +49 -0
- package/dist/lib/package-version.js +92 -0
- package/dist/lib/plan/index.js +69 -0
- package/dist/lib/plan/read-sdd.js +102 -0
- package/dist/lib/plan/render-template.js +84 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scaffold/domain.js +234 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/sdd-index/repo-config.js +245 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/snapshot/advance.js +454 -0
- package/dist/lib/snapshot/diff.js +190 -0
- package/dist/lib/snapshot/fold.js +208 -0
- package/dist/lib/snapshot/land.js +178 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/snapshot/targets.js +169 -0
- package/dist/lib/snapshot/verify.js +91 -0
- package/dist/lib/sync/briefing-merge.js +106 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/yaml-block-merge.js +120 -0
- package/dist/lib/validate/aggregate.js +51 -0
- package/dist/lib/validate/bundle-coherence.js +74 -0
- package/dist/lib/validate/dispatch.js +243 -0
- package/dist/lib/validate/index.js +110 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/api-surface.js +71 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/conformance.js +34 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/contracts.js +34 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/plan.js +42 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/scaffold.js +83 -0
- package/dist/verbs/snapshot.js +134 -0
- package/dist/verbs/validate.js +48 -0
- package/dist/verify-program.js +122 -0
- package/dist/verify-version.json +4 -0
- package/package.json +32 -4
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- 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
|