@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,229 @@
|
|
|
1
|
+
// C4 L3 components discoverer — emits `c4_components.csv` rows from
|
|
2
|
+
// C4-PlantUML-style mermaid in:
|
|
3
|
+
// - docs/domains/<slug>/architecture/diagrams/c4-l3-*.mermaid.md
|
|
4
|
+
// - docs/platform/diagrams/c4-l3-*.mermaid.md
|
|
5
|
+
// - docs/c4-l3-*.mermaid.md (per-product, application/integration SDDs)
|
|
6
|
+
//
|
|
7
|
+
// Mirrors c4-containers.ts at one level down. Source convention per
|
|
8
|
+
// `coordination/c4-l3-walker-and-path-convention-coordination.md`
|
|
9
|
+
// (sdd-method-gov, 2026-05-29). The walker is granularity-agnostic — both
|
|
10
|
+
// per-container files (one `Container_Boundary` block per file) and
|
|
11
|
+
// per-domain rollup files (multiple `Container_Boundary` blocks per file)
|
|
12
|
+
// produce identical row output. Adopters pick the file shape that suits
|
|
13
|
+
// their authoring workflow.
|
|
14
|
+
//
|
|
15
|
+
// Shape rules:
|
|
16
|
+
// Container_Boundary(local_id, "Container Display Name") {
|
|
17
|
+
// Component(component_id, "Component Name", "tech", "description")
|
|
18
|
+
// Component(...)
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
// container_id resolution: the walker slugifies the Container_Boundary's
|
|
22
|
+
// display name and matches against `c4_containers.id` rows that end in
|
|
23
|
+
// `-{slug}` within this SDD's system. Unmatched containers fire
|
|
24
|
+
// `[finding] orphan` and the contained Components are skipped.
|
|
25
|
+
//
|
|
26
|
+
// service_id is denormalised from the resolved container's service_id —
|
|
27
|
+
// populated when the parent container represents a runtime service,
|
|
28
|
+
// empty otherwise.
|
|
29
|
+
//
|
|
30
|
+
// Row id shape: `{container_id}-{component-slug}` per schema 2.15.0
|
|
31
|
+
// description.
|
|
32
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
33
|
+
import { join, sep } from "node:path";
|
|
34
|
+
const L3_PREFIX = "c4-l3-";
|
|
35
|
+
const L3_SUFFIX = ".mermaid.md";
|
|
36
|
+
// Boundary opener — only Container_Boundary at L3. System_Boundary is L2's domain.
|
|
37
|
+
const BOUNDARY_OPEN_RE = /^Container_Boundary\s*\(\s*\w+\s*,\s*"([^"]+)"\s*\)\s*\{/;
|
|
38
|
+
// C4-PlantUML component entries inside Container_Boundary blocks.
|
|
39
|
+
const COMPONENT_RE = /Component\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*(?:,\s*"([^"]*)")?\s*\)/;
|
|
40
|
+
export async function buildC4Components(inputs) {
|
|
41
|
+
// No containers → nothing to attach components to. Skip silently; the
|
|
42
|
+
// L2 walker would have logged the absence.
|
|
43
|
+
if (inputs.c4ContainerRows.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
// Index containers by suffix-after-system-prefix for slug-of-name matching.
|
|
46
|
+
// c4_containers.id shape per c4-containers.ts:93 is `{systemId}-{slug}`.
|
|
47
|
+
// The suffix-match lets us resolve `Container_Boundary(_, "Asset Twin")`
|
|
48
|
+
// (slug = "asset-twin") to `{anything}-asset-twin`.
|
|
49
|
+
const containerBySuffix = new Map();
|
|
50
|
+
for (const row of inputs.c4ContainerRows) {
|
|
51
|
+
const id = String(row["id"] ?? "");
|
|
52
|
+
if (id === "")
|
|
53
|
+
continue;
|
|
54
|
+
// The full id IS the suffix in the trivial case; also index by the
|
|
55
|
+
// trailing `-<slug>` portion in case the system prefix varies.
|
|
56
|
+
containerBySuffix.set(id, row);
|
|
57
|
+
const dash = id.lastIndexOf("-");
|
|
58
|
+
if (dash > 0) {
|
|
59
|
+
const tail = id.slice(dash + 1);
|
|
60
|
+
// Only add tail-indexing when it's unique enough to be useful.
|
|
61
|
+
// (Multiple containers with the same tail across systems is the
|
|
62
|
+
// ambiguous case; we keep the first-seen and rely on orphan-finding
|
|
63
|
+
// for the others.)
|
|
64
|
+
if (!containerBySuffix.has(tail))
|
|
65
|
+
containerBySuffix.set(tail, row);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const files = await collectL3Files(inputs.sddPath);
|
|
69
|
+
const rows = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const content = await readFile(file.absPath, "utf-8");
|
|
73
|
+
for (const block of parseContainerBoundaries(content)) {
|
|
74
|
+
const boundarySlug = slugify(block.displayName);
|
|
75
|
+
const containerRow = resolveContainer(boundarySlug, containerBySuffix);
|
|
76
|
+
if (containerRow === undefined) {
|
|
77
|
+
inputs.findings.add("orphan", `c4_components: ${file.relPath}: Container_Boundary "${block.displayName}" doesn't resolve to a c4_containers row — ${block.components.length} component(s) skipped`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const containerId = String(containerRow["id"]);
|
|
81
|
+
const denormalisedServiceId = String(containerRow["service_id"] ?? "");
|
|
82
|
+
for (const c of block.components) {
|
|
83
|
+
const componentSlug = slugify(c.name);
|
|
84
|
+
if (componentSlug === "")
|
|
85
|
+
continue;
|
|
86
|
+
const id = `${containerId}-${componentSlug}`;
|
|
87
|
+
if (seen.has(id))
|
|
88
|
+
continue;
|
|
89
|
+
seen.add(id);
|
|
90
|
+
rows.push({
|
|
91
|
+
id,
|
|
92
|
+
name: c.name,
|
|
93
|
+
container_id: containerId,
|
|
94
|
+
service_id: denormalisedServiceId,
|
|
95
|
+
path: "",
|
|
96
|
+
responsibility: c.description,
|
|
97
|
+
source_sdd_id: "",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
function resolveContainer(slug, containerBySuffix) {
|
|
106
|
+
if (slug === "")
|
|
107
|
+
return undefined;
|
|
108
|
+
// Try the trailing-segment index first (the common case where slug is the
|
|
109
|
+
// tail of `{systemId}-{slug}`); fall back to scanning all rows for any
|
|
110
|
+
// id ending in `-{slug}`.
|
|
111
|
+
const tailHit = containerBySuffix.get(slug);
|
|
112
|
+
if (tailHit !== undefined)
|
|
113
|
+
return tailHit;
|
|
114
|
+
for (const [_, row] of containerBySuffix) {
|
|
115
|
+
if (String(row["id"]).endsWith(`-${slug}`))
|
|
116
|
+
return row;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
async function collectL3Files(sddPath) {
|
|
121
|
+
const out = [];
|
|
122
|
+
// 1. Per-domain L3 files: docs/domains/<dom>/architecture/diagrams/c4-l3-*.mermaid.md
|
|
123
|
+
const domainsDir = join(sddPath, "docs", "domains");
|
|
124
|
+
let domainEntries;
|
|
125
|
+
try {
|
|
126
|
+
domainEntries = (await readdir(domainsDir, { withFileTypes: true }))
|
|
127
|
+
.filter((e) => e.isDirectory())
|
|
128
|
+
.map((e) => ({ name: e.name }));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err.code !== "ENOENT")
|
|
132
|
+
throw err;
|
|
133
|
+
domainEntries = [];
|
|
134
|
+
}
|
|
135
|
+
for (const entry of domainEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
136
|
+
const diagramsDir = join(sddPath, "docs", "domains", entry.name, "architecture", "diagrams");
|
|
137
|
+
const files = await listMatching(diagramsDir, L3_PREFIX, L3_SUFFIX);
|
|
138
|
+
for (const filename of files.sort()) {
|
|
139
|
+
const abs = join(diagramsDir, filename);
|
|
140
|
+
out.push({
|
|
141
|
+
absPath: abs,
|
|
142
|
+
relPath: join("docs", "domains", entry.name, "architecture", "diagrams", filename)
|
|
143
|
+
.split(sep)
|
|
144
|
+
.join("/"),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 2. Platform-wide L3: docs/platform/diagrams/c4-l3-*.mermaid.md
|
|
149
|
+
const platformDir = join(sddPath, "docs", "platform", "diagrams");
|
|
150
|
+
const platformFiles = await listMatching(platformDir, L3_PREFIX, L3_SUFFIX);
|
|
151
|
+
for (const filename of platformFiles.sort()) {
|
|
152
|
+
const abs = join(platformDir, filename);
|
|
153
|
+
out.push({
|
|
154
|
+
absPath: abs,
|
|
155
|
+
relPath: join("docs", "platform", "diagrams", filename).split(sep).join("/"),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 3. Per-product L3 (application-product / integration-product SDDs).
|
|
159
|
+
const productDir = join(sddPath, "docs");
|
|
160
|
+
const productFiles = await listMatching(productDir, L3_PREFIX, L3_SUFFIX);
|
|
161
|
+
for (const filename of productFiles.sort()) {
|
|
162
|
+
const abs = join(productDir, filename);
|
|
163
|
+
out.push({
|
|
164
|
+
absPath: abs,
|
|
165
|
+
relPath: join("docs", filename).split(sep).join("/"),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
async function listMatching(dir, prefix, suffix) {
|
|
171
|
+
try {
|
|
172
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
173
|
+
return entries
|
|
174
|
+
.filter((e) => e.isFile() && e.name.startsWith(prefix) && e.name.endsWith(suffix))
|
|
175
|
+
.map((e) => e.name);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err.code === "ENOENT")
|
|
179
|
+
return [];
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function parseContainerBoundaries(content) {
|
|
184
|
+
const out = [];
|
|
185
|
+
const lines = content.split(/\r?\n/);
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < lines.length) {
|
|
188
|
+
const line = (lines[i] ?? "").trim();
|
|
189
|
+
const open = BOUNDARY_OPEN_RE.exec(line);
|
|
190
|
+
if (!open) {
|
|
191
|
+
i++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const displayName = open[1] ?? "";
|
|
195
|
+
const components = [];
|
|
196
|
+
i++;
|
|
197
|
+
let depth = 1; // we just saw `{`
|
|
198
|
+
while (i < lines.length && depth > 0) {
|
|
199
|
+
const inner = (lines[i] ?? "").trim();
|
|
200
|
+
if (inner.endsWith("{"))
|
|
201
|
+
depth++;
|
|
202
|
+
else if (inner === "}") {
|
|
203
|
+
depth--;
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const m = COMPONENT_RE.exec(inner);
|
|
208
|
+
if (m) {
|
|
209
|
+
components.push({
|
|
210
|
+
entryId: m[1] ?? "",
|
|
211
|
+
name: m[2] ?? "",
|
|
212
|
+
technology: m[3] ?? "",
|
|
213
|
+
description: m[4] ?? "",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
out.push({ displayName, components });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function slugify(s) {
|
|
223
|
+
return s
|
|
224
|
+
.toLowerCase()
|
|
225
|
+
.replace(/\s*\([^)]+\)/g, "")
|
|
226
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
227
|
+
.replace(/^-+|-+$/g, "");
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=c4-components.js.map
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// C4 L2 containers discoverer — emits `c4_containers.csv` rows from
|
|
2
|
+
// C4-PlantUML-style mermaid in:
|
|
3
|
+
// - docs/domains/<slug>/architecture/diagrams/c4-l2-container.mermaid.md
|
|
4
|
+
// - docs/platform/diagrams/c4-l2-container.mermaid.md
|
|
5
|
+
// - docs/c4-l2-container.mermaid.md (per-product, application/integration SDDs)
|
|
6
|
+
//
|
|
7
|
+
// Per schema 2.0.0:
|
|
8
|
+
// - system_id (required FK to c4_systems.id) — for single-system
|
|
9
|
+
// ecosystems we use {sddId}-system. Multi-system support (parsing a
|
|
10
|
+
// `**System:** <id>` metadata line) is a future hook.
|
|
11
|
+
// - domain_id (nullable, denormalised) derived from the L2 file path:
|
|
12
|
+
// docs/domains/<slug>/... → <slug>; platform-wide L2 → empty.
|
|
13
|
+
// - service_id (optional) — opportunistic match on the C4 entry's id
|
|
14
|
+
// or a slug of its display name to a known service id.
|
|
15
|
+
//
|
|
16
|
+
// We only consider entries inside System_Boundary or Container_Boundary
|
|
17
|
+
// blocks — mirrors the python reference and excludes Person/System_Ext
|
|
18
|
+
// declarations that decorate the diagram.
|
|
19
|
+
//
|
|
20
|
+
// Dedupe is by row id; first-seen wins (a container appearing in
|
|
21
|
+
// multiple L2 files becomes one row).
|
|
22
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
23
|
+
import { join, sep } from "node:path";
|
|
24
|
+
const L2_BASENAME = "c4-l2-container.mermaid.md";
|
|
25
|
+
// Boundary opener: `System_Boundary(id, "name") {` or `Container_Boundary(...)`.
|
|
26
|
+
const BOUNDARY_OPEN_RE = /^(System_Boundary|Container_Boundary)\s*\(\s*\w+/;
|
|
27
|
+
// C4-PlantUML container entries inside boundaries. Per ADR 0155 the
|
|
28
|
+
// canonical c4_container_type is Application | Data Store only:
|
|
29
|
+
// Container(id, "name", "tech", "desc") → Application
|
|
30
|
+
// ContainerDb(id, "name", "tech", "desc") → Data Store
|
|
31
|
+
// ContainerQueue(id, "name", "tech", "desc") → Data Store (a running
|
|
32
|
+
// broker/queue is a
|
|
33
|
+
// container, not "infra")
|
|
34
|
+
// `Container_Ext` is intentionally NOT matched here — external containers
|
|
35
|
+
// are catalogued as `c4_external_systems` rows by the c4-external-systems
|
|
36
|
+
// builder, never as internal container rows.
|
|
37
|
+
const ENTRY_RE = /(Container|ContainerDb|ContainerQueue)\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*(?:,\s*"([^"]*)")?\s*\)/;
|
|
38
|
+
const TYPE_BY_KIND = {
|
|
39
|
+
Container: "Application",
|
|
40
|
+
ContainerDb: "Data Store",
|
|
41
|
+
ContainerQueue: "Data Store",
|
|
42
|
+
};
|
|
43
|
+
export async function buildC4Containers(inputs) {
|
|
44
|
+
const systemId = inputs.manifest.c4SystemId ?? `${inputs.manifest.sddId}-system`;
|
|
45
|
+
if (!inputs.knownSystemIds.has(systemId)) {
|
|
46
|
+
// No system row, so no FK target for container rows. Never silent when
|
|
47
|
+
// that drops authored content, and never on the strength of a sibling
|
|
48
|
+
// builder's log (M-ADR 0133 §4.2c no-silent-drop): if L2 diagrams
|
|
49
|
+
// exist, THIS builder says it is dropping them.
|
|
50
|
+
const orphanedL2 = await collectL2Files(inputs.sddPath);
|
|
51
|
+
if (orphanedL2.length > 0) {
|
|
52
|
+
inputs.findings.add("coverage", `c4_containers: no c4_systems row '${systemId}' to own containers; ` +
|
|
53
|
+
`${orphanedL2.length} L2 diagram file(s) present but not catalogued`);
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const files = await collectL2Files(inputs.sddPath);
|
|
58
|
+
const rows = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const rowById = new Map();
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const content = await readFile(file.absPath, "utf-8");
|
|
63
|
+
for (const entry of parseBoundaryEntries(content)) {
|
|
64
|
+
const slug = slugify(entry.name);
|
|
65
|
+
if (slug === "")
|
|
66
|
+
continue;
|
|
67
|
+
const id = `${systemId}-${slug}`;
|
|
68
|
+
if (!inputs.containerIdByEntryIdOut?.has(entry.entryId)) {
|
|
69
|
+
inputs.containerIdByEntryIdOut?.set(entry.entryId, id);
|
|
70
|
+
}
|
|
71
|
+
if (!inputs.containerNameByEntryIdOut?.has(entry.entryId)) {
|
|
72
|
+
inputs.containerNameByEntryIdOut?.set(entry.entryId, entry.name);
|
|
73
|
+
}
|
|
74
|
+
if (!inputs.containerTechByEntryIdOut?.has(entry.entryId) && entry.technology !== "") {
|
|
75
|
+
inputs.containerTechByEntryIdOut?.set(entry.entryId, entry.technology);
|
|
76
|
+
}
|
|
77
|
+
const collapsed = rowById.get(id);
|
|
78
|
+
if (collapsed !== undefined) {
|
|
79
|
+
// F21: dedup-by-id is first-seen and was SILENT — two containers
|
|
80
|
+
// sharing a display name collapse into one row, and every edge
|
|
81
|
+
// referencing either binds to the merge. When their technology
|
|
82
|
+
// strings flatly contradict, that is two different things wearing
|
|
83
|
+
// one name, and the collapse is the loudest available signal.
|
|
84
|
+
if (techContradicts(String(collapsed["technology"] ?? ""), entry.technology)) {
|
|
85
|
+
inputs.findings.add("convention", `c4_containers: two containers share the display name "${entry.name}" with contradictory ` +
|
|
86
|
+
`technologies ("${String(collapsed["technology"])}" vs "${entry.technology}") — collapsed ` +
|
|
87
|
+
`into one row "${id}" — either different referents wearing one name (rename one) ` +
|
|
88
|
+
`or a misused technology field on a shared reference (correct it) (F21)`);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
seen.add(id);
|
|
93
|
+
const serviceId = inputs.knownServiceIds.has(entry.entryId)
|
|
94
|
+
? entry.entryId
|
|
95
|
+
: inputs.knownServiceIds.has(slug)
|
|
96
|
+
? slug
|
|
97
|
+
: "";
|
|
98
|
+
const domainId = file.domainId !== "" && inputs.knownDomainIds.has(file.domainId)
|
|
99
|
+
? file.domainId
|
|
100
|
+
: "";
|
|
101
|
+
if (file.domainId !== "" && domainId === "") {
|
|
102
|
+
inputs.findings.add("orphan", `c4_containers: L2 file at ${file.relPath} references unknown domain "${file.domainId}" — domain_id left empty`);
|
|
103
|
+
}
|
|
104
|
+
const row = {
|
|
105
|
+
id,
|
|
106
|
+
name: entry.name,
|
|
107
|
+
system_id: systemId,
|
|
108
|
+
domain_id: domainId,
|
|
109
|
+
service_id: serviceId,
|
|
110
|
+
type: TYPE_BY_KIND[entry.kind] ?? "",
|
|
111
|
+
technology: entry.technology,
|
|
112
|
+
description: entry.description,
|
|
113
|
+
source_sdd_id: "",
|
|
114
|
+
};
|
|
115
|
+
rowById.set(id, row);
|
|
116
|
+
rows.push(row);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
120
|
+
return rows;
|
|
121
|
+
}
|
|
122
|
+
async function collectL2Files(sddPath) {
|
|
123
|
+
const out = [];
|
|
124
|
+
// 1. Per-domain L2 files.
|
|
125
|
+
const domainsDir = join(sddPath, "docs", "domains");
|
|
126
|
+
let domainEntries;
|
|
127
|
+
try {
|
|
128
|
+
domainEntries = (await readdir(domainsDir, { withFileTypes: true }))
|
|
129
|
+
.filter((e) => e.isDirectory())
|
|
130
|
+
.map((e) => ({ name: e.name }));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err.code !== "ENOENT")
|
|
134
|
+
throw err;
|
|
135
|
+
domainEntries = [];
|
|
136
|
+
}
|
|
137
|
+
for (const entry of domainEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
138
|
+
const rel = join("docs", "domains", entry.name, "architecture", "diagrams", L2_BASENAME);
|
|
139
|
+
const abs = join(sddPath, rel);
|
|
140
|
+
if (await isFile(abs)) {
|
|
141
|
+
out.push({
|
|
142
|
+
absPath: abs,
|
|
143
|
+
relPath: rel.split(sep).join("/"),
|
|
144
|
+
domainId: entry.name,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 2. Platform-wide L2.
|
|
149
|
+
const platformRel = join("docs", "platform", "diagrams", L2_BASENAME);
|
|
150
|
+
const platformAbs = join(sddPath, platformRel);
|
|
151
|
+
if (await isFile(platformAbs)) {
|
|
152
|
+
out.push({
|
|
153
|
+
absPath: platformAbs,
|
|
154
|
+
relPath: platformRel.split(sep).join("/"),
|
|
155
|
+
domainId: "",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 3. Per-product L2 (application-product / integration-product SDDs).
|
|
159
|
+
// Lives at the SDD docs/ root; the system is the SDD itself (one
|
|
160
|
+
// c4_systems row per SDD), so the boundary in the L2 is the product
|
|
161
|
+
// boundary. domain_id is left empty — federated app/integration
|
|
162
|
+
// products do not have a primary domain per ADR 0136.
|
|
163
|
+
const productRel = join("docs", L2_BASENAME);
|
|
164
|
+
const productAbs = join(sddPath, productRel);
|
|
165
|
+
if (await isFile(productAbs)) {
|
|
166
|
+
out.push({
|
|
167
|
+
absPath: productAbs,
|
|
168
|
+
relPath: productRel.split(sep).join("/"),
|
|
169
|
+
domainId: "",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
async function isFile(path) {
|
|
175
|
+
try {
|
|
176
|
+
const s = await stat(path);
|
|
177
|
+
return s.isFile();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err.code === "ENOENT")
|
|
181
|
+
return false;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function parseBoundaryEntries(content) {
|
|
186
|
+
const out = [];
|
|
187
|
+
const stack = [];
|
|
188
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
189
|
+
const line = raw.trim();
|
|
190
|
+
if (BOUNDARY_OPEN_RE.test(line) && line.includes("{")) {
|
|
191
|
+
stack.push("boundary");
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (line === "}" && stack.length > 0) {
|
|
195
|
+
stack.pop();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (stack.length === 0)
|
|
199
|
+
continue;
|
|
200
|
+
const m = ENTRY_RE.exec(line);
|
|
201
|
+
if (!m)
|
|
202
|
+
continue;
|
|
203
|
+
out.push({
|
|
204
|
+
kind: m[1] ?? "",
|
|
205
|
+
entryId: m[2] ?? "",
|
|
206
|
+
name: m[3] ?? "",
|
|
207
|
+
technology: m[4] ?? "",
|
|
208
|
+
description: m[5] ?? "",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
function slugify(s) {
|
|
214
|
+
return s
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.replace(/\s*\([^)]+\)/g, "")
|
|
217
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
218
|
+
.replace(/^-+|-+$/g, "");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* F21: two technology strings flatly contradict when both are present and
|
|
222
|
+
* share no token — ".NET 6 (in Linton)" vs "Java" needs no NLP. Absent or
|
|
223
|
+
* overlapping strings are NOT a contradiction (silence, not a guess).
|
|
224
|
+
*/
|
|
225
|
+
export function techContradicts(a, b) {
|
|
226
|
+
const tokens = (s) => new Set(s
|
|
227
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.split(/[^a-z0-9.+#]+/)
|
|
230
|
+
.filter((t) => t !== ""));
|
|
231
|
+
const ta = tokens(a);
|
|
232
|
+
const tb = tokens(b);
|
|
233
|
+
if (ta.size === 0 || tb.size === 0)
|
|
234
|
+
return false;
|
|
235
|
+
for (const t of ta)
|
|
236
|
+
if (tb.has(t))
|
|
237
|
+
return false;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=c4-containers.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Phase 3 C4-to-entity roll-up walkers per ADR 0159 §4.8.
|
|
2
|
+
// Both are pure transitive joins over already-FK-enforced inputs.
|
|
3
|
+
//
|
|
4
|
+
// buildC4ContainerEntity:
|
|
5
|
+
// service_entity ⋈ c4_containers.service_id
|
|
6
|
+
// → emit (container_id, entity_id, relation) inheriting relation from
|
|
7
|
+
// upstream service_entity. Dedup by (container_id, entity_id, relation).
|
|
8
|
+
//
|
|
9
|
+
// buildC4SystemEntity:
|
|
10
|
+
// c4_container_entity ⋈ c4_containers.system_id
|
|
11
|
+
// → aggregate (system_id, entity_id) with container_count.
|
|
12
|
+
//
|
|
13
|
+
// Both handle empty inputs gracefully (an adopter's pre-lock state has zero
|
|
14
|
+
// service_entity edges → both roll-up outputs are empty; no errors).
|
|
15
|
+
export function buildC4ContainerEntity(inputs) {
|
|
16
|
+
// Index containers by service_id.
|
|
17
|
+
const containersByServiceId = new Map();
|
|
18
|
+
for (const c of inputs.c4Containers) {
|
|
19
|
+
const sid = String(c["service_id"] ?? "");
|
|
20
|
+
if (!sid)
|
|
21
|
+
continue;
|
|
22
|
+
const cid = String(c["id"]);
|
|
23
|
+
const list = containersByServiceId.get(sid) ?? [];
|
|
24
|
+
list.push(cid);
|
|
25
|
+
containersByServiceId.set(sid, list);
|
|
26
|
+
}
|
|
27
|
+
const rowsByPk = new Map();
|
|
28
|
+
for (const se of inputs.serviceEntityRows) {
|
|
29
|
+
const sid = String(se["service_id"]);
|
|
30
|
+
const eid = String(se["entity_id"]);
|
|
31
|
+
const rel = String(se["relation"] ?? "");
|
|
32
|
+
const containers = containersByServiceId.get(sid);
|
|
33
|
+
if (!containers)
|
|
34
|
+
continue;
|
|
35
|
+
for (const cid of containers) {
|
|
36
|
+
const pk = `${cid}__${eid}`;
|
|
37
|
+
if (rowsByPk.has(pk))
|
|
38
|
+
continue; // first-relation-wins on dup
|
|
39
|
+
rowsByPk.set(pk, {
|
|
40
|
+
id: pk,
|
|
41
|
+
container_id: cid,
|
|
42
|
+
entity_id: eid,
|
|
43
|
+
relation: rel,
|
|
44
|
+
source_sdd_id: "",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...rowsByPk.values()];
|
|
49
|
+
}
|
|
50
|
+
export function buildC4SystemEntity(inputs) {
|
|
51
|
+
// Index containers by id for system lookup.
|
|
52
|
+
const containerById = new Map();
|
|
53
|
+
for (const c of inputs.c4Containers) {
|
|
54
|
+
containerById.set(String(c["id"]), c);
|
|
55
|
+
}
|
|
56
|
+
const counts = new Map(); // key: `${system_id}|${entity_id}`
|
|
57
|
+
const systemByKey = new Map();
|
|
58
|
+
for (const ce of inputs.containerEntityRows) {
|
|
59
|
+
const c = containerById.get(String(ce["container_id"]));
|
|
60
|
+
if (!c)
|
|
61
|
+
continue;
|
|
62
|
+
const sysId = String(c["system_id"] ?? "");
|
|
63
|
+
if (!sysId)
|
|
64
|
+
continue;
|
|
65
|
+
const eid = String(ce["entity_id"]);
|
|
66
|
+
const key = `${sysId}|${eid}`;
|
|
67
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
68
|
+
if (!systemByKey.has(key)) {
|
|
69
|
+
systemByKey.set(key, { system_id: sysId, entity_id: eid });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const rows = [];
|
|
73
|
+
for (const [key, count] of counts) {
|
|
74
|
+
const e = systemByKey.get(key);
|
|
75
|
+
if (!e)
|
|
76
|
+
continue;
|
|
77
|
+
rows.push({
|
|
78
|
+
id: `${e.system_id}__${e.entity_id}`,
|
|
79
|
+
system_id: e.system_id,
|
|
80
|
+
entity_id: e.entity_id,
|
|
81
|
+
container_count: count,
|
|
82
|
+
source_sdd_id: "",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=c4-entity-rollups.js.map
|