@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,344 @@
|
|
|
1
|
+
// Data items, supply and demand (M-ADR 0216).
|
|
2
|
+
//
|
|
3
|
+
// data_items ← docs/platform/reference/data-items.yaml (platform-owned, like events)
|
|
4
|
+
// integration_data_item ← the integration's feature-grounding.md:
|
|
5
|
+
// every table with a "Data Item" column anywhere in the
|
|
6
|
+
// "Provider-to-Canonical Field Mapping" section (sub-headings
|
|
7
|
+
// included, up to the next `##`) → supported, or partial when
|
|
8
|
+
// its Notes begin with `partial`; and the "Data Items Not
|
|
9
|
+
// Supplied" section → the verdict it states.
|
|
10
|
+
// feature_data_requirement ← a "Data Requirements" table in a feature spec (the spec's own
|
|
11
|
+
// feature) or in the feature tree (a Feature column, by id or
|
|
12
|
+
// bullet slug — tree features have no spec file).
|
|
13
|
+
//
|
|
14
|
+
// `unknown` is never written: it is the reading of no row. A placeholder or an empty cell
|
|
15
|
+
// declares nothing — a row is evidence only when it names an item.
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
19
|
+
import { findIntegrationGroundingFile } from "./feature-grounding-walker.js";
|
|
20
|
+
import { findFeatureTreeFile } from "./feature-tree-walker.js";
|
|
21
|
+
export const DATA_ITEMS_FILE_REL = join("docs", "platform", "reference", "data-items.yaml");
|
|
22
|
+
const SUPPLY_VERDICTS = new Set(["supported", "partial", "source-absent", "gateway-gap", "not-applicable"]);
|
|
23
|
+
const NOT_SUPPLIED_VERDICTS = new Set(["partial", "source-absent", "gateway-gap", "not-applicable"]);
|
|
24
|
+
const POSITIVE = new Set(["supported", "partial"]);
|
|
25
|
+
const BASES = new Set(["code", "grounded", "declared", "inferred"]);
|
|
26
|
+
const NECESSITIES = new Set(["required", "enhances"]);
|
|
27
|
+
// ------------------------------------------------------------------ entity
|
|
28
|
+
export async function buildDataItems(sddPath, findings) {
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = await readFile(join(sddPath, DATA_ITEMS_FILE_REL), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
let doc;
|
|
37
|
+
try {
|
|
38
|
+
doc = parseYaml(raw);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
findings.add("convention", `${DATA_ITEMS_FILE_REL}: failed to parse YAML — ${err.message}`, "error");
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const items = doc?.data_items;
|
|
45
|
+
if (!Array.isArray(items)) {
|
|
46
|
+
findings.add("convention", `${DATA_ITEMS_FILE_REL}: expected a top-level \`data_items:\` list — no data items read`);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const rows = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
for (const it of items) {
|
|
52
|
+
const o = (it ?? {});
|
|
53
|
+
const id = String(o["id"] ?? "").trim();
|
|
54
|
+
const name = String(o["name"] ?? "").trim();
|
|
55
|
+
const category = String(o["category"] ?? "").trim();
|
|
56
|
+
if (id === "" || name === "" || category === "") {
|
|
57
|
+
findings.add("convention", `${DATA_ITEMS_FILE_REL}: a data item lacks id, name or category — skipped`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (seen.has(id)) {
|
|
61
|
+
findings.add("divergence", `${DATA_ITEMS_FILE_REL}: data item ${id} declared twice — first kept`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(id);
|
|
65
|
+
rows.push({
|
|
66
|
+
id,
|
|
67
|
+
name,
|
|
68
|
+
category,
|
|
69
|
+
canonical_field: String(o["canonical_field"] ?? "").trim(),
|
|
70
|
+
description: String(o["description"] ?? "").trim(),
|
|
71
|
+
source_sdd_id: "",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The tables in the `##` section whose heading matches `section`, sub-headings included, up
|
|
78
|
+
* to the next heading of level ≤ 2. Each table carries the governing `Checked:` line: the
|
|
79
|
+
* nearest one set at its own heading level or above, cleared by the next heading of the same
|
|
80
|
+
* or higher level.
|
|
81
|
+
*/
|
|
82
|
+
export function tablesInSection(text, section) {
|
|
83
|
+
const lines = text.split(/\r?\n/);
|
|
84
|
+
const out = [];
|
|
85
|
+
let inSection = false;
|
|
86
|
+
let inFence = false;
|
|
87
|
+
let level = 0; // level of the most recent heading inside the section
|
|
88
|
+
const checkedAtLevel = new Map();
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < lines.length) {
|
|
91
|
+
const line = lines[i];
|
|
92
|
+
const t = line.trim();
|
|
93
|
+
if (t.startsWith("```")) {
|
|
94
|
+
inFence = !inFence;
|
|
95
|
+
i++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (inFence) {
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(t);
|
|
103
|
+
if (h) {
|
|
104
|
+
const lv = h[1].length;
|
|
105
|
+
if (lv <= 2) {
|
|
106
|
+
inSection = lv === 2 && section.test(h[2].trim());
|
|
107
|
+
checkedAtLevel.clear();
|
|
108
|
+
}
|
|
109
|
+
for (const k of [...checkedAtLevel.keys()])
|
|
110
|
+
if (k >= lv)
|
|
111
|
+
checkedAtLevel.delete(k);
|
|
112
|
+
level = lv;
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!inSection) {
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (/^Checked:/i.test(t)) {
|
|
121
|
+
checkedAtLevel.set(level, t.replace(/^Checked:\s*/i, "").trim());
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (t.startsWith("|") && i + 1 < lines.length && /^\|?\s*:?-{2,}/.test(lines[i + 1].trim())) {
|
|
126
|
+
const headers = splitRow(t).map((c) => c.toLowerCase());
|
|
127
|
+
const rows = [];
|
|
128
|
+
let j = i + 2;
|
|
129
|
+
while (j < lines.length && lines[j].trim().startsWith("|")) {
|
|
130
|
+
rows.push({ cells: splitRow(lines[j].trim()), line: j + 1 });
|
|
131
|
+
j++;
|
|
132
|
+
}
|
|
133
|
+
let checkedAt = "";
|
|
134
|
+
for (let lv = level; lv >= 1; lv--) {
|
|
135
|
+
const c = checkedAtLevel.get(lv);
|
|
136
|
+
if (c !== undefined) {
|
|
137
|
+
checkedAt = c;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
out.push({ headers, rows, checkedAt });
|
|
142
|
+
i = j;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Split a table row on UNESCAPED pipes (GFM): `\|` is a literal pipe inside a cell. */
|
|
150
|
+
function splitRow(row) {
|
|
151
|
+
const inner = row.replace(/^\|/, "").replace(/(?<!\\)\|\s*$/, "");
|
|
152
|
+
return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim());
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A row whose cell count differs from its header's was split wrongly, almost always by an unescaped
|
|
156
|
+
* `|` inside a cell. Reading it would shift every later column (an evidence cell would land in the
|
|
157
|
+
* verdict), so it is reported and skipped rather than read.
|
|
158
|
+
*/
|
|
159
|
+
function malformed(r, headers, rel, findings) {
|
|
160
|
+
if (r.cells.length === headers.length)
|
|
161
|
+
return false;
|
|
162
|
+
findings.add("convention", `${rel}:${r.line}: table row has ${r.cells.length} cells, its header ${headers.length} — an unescaped | inside a cell? write it as \\| — row skipped`);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
/** A cell that names something: not empty, not a dash, not a `{placeholder}`. Backticks dropped. */
|
|
166
|
+
function named(cell) {
|
|
167
|
+
const v = (cell ?? "").replace(/`/g, "").trim();
|
|
168
|
+
if (v === "" || /^[-—–]+$/.test(v) || v.includes("{"))
|
|
169
|
+
return "";
|
|
170
|
+
return v;
|
|
171
|
+
}
|
|
172
|
+
function col(headers, name) {
|
|
173
|
+
return headers.indexOf(name);
|
|
174
|
+
}
|
|
175
|
+
export async function buildIntegrationSupply(inputs) {
|
|
176
|
+
if (inputs.integrationId === "")
|
|
177
|
+
return [];
|
|
178
|
+
const path = await findIntegrationGroundingFile(inputs.sddPath);
|
|
179
|
+
if (!path)
|
|
180
|
+
return [];
|
|
181
|
+
const rel = relative(inputs.sddPath, path);
|
|
182
|
+
const text = await readFile(path, "utf-8");
|
|
183
|
+
const rows = [];
|
|
184
|
+
const add = (item, verdict, basis, evidence, checkedAt) => {
|
|
185
|
+
rows.push({
|
|
186
|
+
id: `${inputs.integrationId}::${item}::${evidence}`,
|
|
187
|
+
integration_id: inputs.integrationId,
|
|
188
|
+
data_item_id: item,
|
|
189
|
+
verdict,
|
|
190
|
+
basis,
|
|
191
|
+
evidence_ref: evidence,
|
|
192
|
+
checked_at: checkedAt,
|
|
193
|
+
source_sdd_id: "",
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
for (const tbl of tablesInSection(text, /^Provider-to-Canonical Field Mapping\b/i)) {
|
|
197
|
+
const di = col(tbl.headers, "data item");
|
|
198
|
+
if (di < 0)
|
|
199
|
+
continue;
|
|
200
|
+
const notes = col(tbl.headers, "notes");
|
|
201
|
+
const basisCol = col(tbl.headers, "basis");
|
|
202
|
+
for (const r of tbl.rows) {
|
|
203
|
+
if (malformed(r, tbl.headers, rel, inputs.findings))
|
|
204
|
+
continue;
|
|
205
|
+
const item = named(r.cells[di]);
|
|
206
|
+
if (item === "")
|
|
207
|
+
continue;
|
|
208
|
+
const verdict = notes >= 0 && /^partial\b/i.test(r.cells[notes] ?? "") ? "partial" : "supported";
|
|
209
|
+
add(item, verdict, basisOf(r.cells[basisCol], rel, r.line, inputs.findings), `${rel}:${r.line}`, tbl.checkedAt);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const tbl of tablesInSection(text, /^Data Items Not Supplied\b/i)) {
|
|
213
|
+
const di = col(tbl.headers, "data item");
|
|
214
|
+
const vc = col(tbl.headers, "verdict");
|
|
215
|
+
if (di < 0 || vc < 0) {
|
|
216
|
+
inputs.findings.add("convention", `${rel}: a "Data Items Not Supplied" table needs Data Item and Verdict columns — table skipped`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const basisCol = col(tbl.headers, "basis");
|
|
220
|
+
const evCol = col(tbl.headers, "evidence");
|
|
221
|
+
for (const r of tbl.rows) {
|
|
222
|
+
if (malformed(r, tbl.headers, rel, inputs.findings))
|
|
223
|
+
continue;
|
|
224
|
+
const item = named(r.cells[di]);
|
|
225
|
+
if (item === "")
|
|
226
|
+
continue;
|
|
227
|
+
const verdict = named(r.cells[vc]).toLowerCase();
|
|
228
|
+
if (!NOT_SUPPLIED_VERDICTS.has(verdict)) {
|
|
229
|
+
inputs.findings.add("convention", `${rel}:${r.line}: data item ${item} — verdict "${verdict}" is not one of ${[...NOT_SUPPLIED_VERDICTS].join(", ")} (a supplied item belongs in the mapping table; unknown is never written) — row skipped`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const ev = named(r.cells[evCol]) || `${rel}:${r.line}`;
|
|
233
|
+
add(item, verdict, basisOf(r.cells[basisCol], rel, r.line, inputs.findings), ev, tbl.checkedAt);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// @rule gate-supply-contradiction
|
|
237
|
+
// Several rows per item are normal; a positive and a negative row for one item contradict.
|
|
238
|
+
const byItem = new Map();
|
|
239
|
+
for (const r of rows)
|
|
240
|
+
byItem.set(String(r["data_item_id"]), [...(byItem.get(String(r["data_item_id"])) ?? []), r]);
|
|
241
|
+
for (const [item, rs] of byItem) {
|
|
242
|
+
const pos = rs.filter((r) => POSITIVE.has(String(r["verdict"])));
|
|
243
|
+
const neg = rs.filter((r) => !POSITIVE.has(String(r["verdict"])));
|
|
244
|
+
if (pos.length && neg.length) {
|
|
245
|
+
inputs.findings.add("divergence", `${rel}: data item ${item} is stated both supplied (${pos.map((r) => `${r["verdict"]} @ ${r["evidence_ref"]}`).join("; ")}) and not supplied (${neg.map((r) => `${r["verdict"]} @ ${r["evidence_ref"]}`).join("; ")}) — resolve one; coverage never picks a winner between them (M-ADR 0216 §4.2)`, "error", `data_item:${item}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return dedupe(rows);
|
|
249
|
+
}
|
|
250
|
+
function basisOf(cell, rel, line, findings) {
|
|
251
|
+
const b = named(cell).toLowerCase();
|
|
252
|
+
if (b === "")
|
|
253
|
+
return "";
|
|
254
|
+
if (!BASES.has(b)) {
|
|
255
|
+
findings.add("convention", `${rel}:${line}: basis "${b}" is not one of ${[...BASES].join(", ")} — left empty`);
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
return b;
|
|
259
|
+
}
|
|
260
|
+
function dedupe(rows) {
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
return rows.filter((r) => {
|
|
263
|
+
const k = String(r["id"]);
|
|
264
|
+
if (seen.has(k))
|
|
265
|
+
return false;
|
|
266
|
+
seen.add(k);
|
|
267
|
+
return true;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
export async function buildFeatureDemand(inputs) {
|
|
271
|
+
const rows = [];
|
|
272
|
+
const section = /^Data Requirements\b/i;
|
|
273
|
+
const read = async (rel, ownFeature) => {
|
|
274
|
+
let text;
|
|
275
|
+
try {
|
|
276
|
+
text = await readFile(join(inputs.sddPath, rel), "utf-8");
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
for (const tbl of tablesInSection(text, section)) {
|
|
282
|
+
const di = col(tbl.headers, "data item");
|
|
283
|
+
const nc = col(tbl.headers, "necessity");
|
|
284
|
+
const fc = col(tbl.headers, "feature");
|
|
285
|
+
if (di < 0 || nc < 0) {
|
|
286
|
+
inputs.findings.add("convention", `${rel}: a "Data Requirements" table needs Data Item and Necessity columns — table skipped`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const gc = col(tbl.headers, "alternative group");
|
|
290
|
+
const bc = col(tbl.headers, "basis");
|
|
291
|
+
for (const r of tbl.rows) {
|
|
292
|
+
if (malformed(r, tbl.headers, rel, inputs.findings))
|
|
293
|
+
continue;
|
|
294
|
+
const item = named(r.cells[di]);
|
|
295
|
+
if (item === "")
|
|
296
|
+
continue;
|
|
297
|
+
let feature = ownFeature;
|
|
298
|
+
if (fc >= 0) {
|
|
299
|
+
const f = named(r.cells[fc]);
|
|
300
|
+
if (f !== "")
|
|
301
|
+
feature = inputs.knownFeatureIds.has(f) ? f : (inputs.idBySlug.get(f) ?? "");
|
|
302
|
+
if (feature === "") {
|
|
303
|
+
inputs.findings.add("orphan", `${rel}:${r.line}: Data Requirements feature "${f}" is neither a feature id nor a feature slug of this SDD — row skipped`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (feature === "") {
|
|
308
|
+
inputs.findings.add("convention", `${rel}:${r.line}: a Data Requirements row outside a feature spec needs a Feature column — row skipped`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const necessity = named(r.cells[nc]).toLowerCase();
|
|
312
|
+
if (!NECESSITIES.has(necessity)) {
|
|
313
|
+
inputs.findings.add("convention", `${rel}:${r.line}: necessity "${necessity}" is not required or enhances — row skipped`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
rows.push({
|
|
317
|
+
id: `${feature}::${item}`,
|
|
318
|
+
feature_id: feature,
|
|
319
|
+
data_item_id: item,
|
|
320
|
+
necessity,
|
|
321
|
+
alternative_group: named(r.cells[gc]),
|
|
322
|
+
basis_ref: named(r.cells[bc]) || `${rel}:${r.line}`,
|
|
323
|
+
source_sdd_id: "",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
for (const [rel, id] of [...inputs.idByPath].sort(([a], [b]) => a.localeCompare(b)))
|
|
329
|
+
await read(rel, id);
|
|
330
|
+
const tree = await findFeatureTreeFile(inputs.sddPath);
|
|
331
|
+
if (tree)
|
|
332
|
+
await read(relative(inputs.sddPath, tree), "");
|
|
333
|
+
const seen = new Map();
|
|
334
|
+
for (const r of rows) {
|
|
335
|
+
const k = String(r["id"]);
|
|
336
|
+
if (seen.has(k)) {
|
|
337
|
+
inputs.findings.add("divergence", `feature ${r["feature_id"]} states its need for ${r["data_item_id"]} twice (${seen.get(k)["basis_ref"]}, ${r["basis_ref"]}) — first kept`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
seen.set(k, r);
|
|
341
|
+
}
|
|
342
|
+
return [...seen.values()];
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=data-items.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Shared parsing primitives for the `## Data Store Mapping` markdown table
|
|
2
|
+
// that reverse-method adopters author in
|
|
3
|
+
// `docs/domains/<domain>/architecture/data-model.mermaid.md`.
|
|
4
|
+
//
|
|
5
|
+
// Extracted so two consumers share one parser (ADR 0164):
|
|
6
|
+
// - service-entity-tables.ts — emits service_entity edges from the
|
|
7
|
+
// Entity + Owning Service columns (relation=owns).
|
|
8
|
+
// - entity-persistence.ts — backfills entities.persistence_kind from the
|
|
9
|
+
// Store column.
|
|
10
|
+
//
|
|
11
|
+
// The Store column is reverse-only: forward-method adopters author a
|
|
12
|
+
// store-less table (Entity | Owning Service | Notes), so detectColumn returns
|
|
13
|
+
// -1 for `store` there and persistence_kind stays empty (correctly).
|
|
14
|
+
import { entitySlug } from "./entity-slug.js";
|
|
15
|
+
export const RE_DATA_STORE_HEADING = /^##\s+Data Store\s+(Mapping|Inventory)\s*$/i;
|
|
16
|
+
/**
|
|
17
|
+
* Locate the first `## Data Store Mapping`/`Inventory` table in the file and
|
|
18
|
+
* return its split headers + data rows. Returns null when the heading is
|
|
19
|
+
* absent or no markdown table follows it (callers decide whether that's a
|
|
20
|
+
* finding).
|
|
21
|
+
*/
|
|
22
|
+
export function findDataStoreTable(text) {
|
|
23
|
+
const lines = text.split(/\r?\n/);
|
|
24
|
+
const headingIdx = lines.findIndex((l) => RE_DATA_STORE_HEADING.test(l));
|
|
25
|
+
if (headingIdx === -1)
|
|
26
|
+
return null;
|
|
27
|
+
const tableLines = collectTableLines(lines, headingIdx + 1);
|
|
28
|
+
if (tableLines === null || tableLines.length < 2)
|
|
29
|
+
return null;
|
|
30
|
+
const headers = splitRow(tableLines[0]);
|
|
31
|
+
const dataRows = [];
|
|
32
|
+
// tableLines[1] is the |---|---| separator; data rows start at index 2.
|
|
33
|
+
for (let i = 2; i < tableLines.length; i++)
|
|
34
|
+
dataRows.push(splitRow(tableLines[i]));
|
|
35
|
+
return { headers, dataRows };
|
|
36
|
+
}
|
|
37
|
+
/** Index of the first header (lowercased) satisfying `match`, or -1. */
|
|
38
|
+
export function detectColumn(headers, match) {
|
|
39
|
+
for (let i = 0; i < headers.length; i++) {
|
|
40
|
+
if (match(headers[i].toLowerCase()))
|
|
41
|
+
return i;
|
|
42
|
+
}
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
/** Strip a trailing parenthetical annotation: "Role (fleet)" → "Role". */
|
|
46
|
+
export function normaliseEntity(entityText) {
|
|
47
|
+
return entityText.replace(/\s*\([^)]+\)\s*$/, "").trim();
|
|
48
|
+
}
|
|
49
|
+
export function splitRow(line) {
|
|
50
|
+
// Strip leading/trailing `|`, split on `|`, trim cells.
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
const parts = trimmed.split("|");
|
|
53
|
+
return parts.slice(1, parts.length - 1).map((c) => c.trim());
|
|
54
|
+
}
|
|
55
|
+
export function collectTableLines(lines, startIdx) {
|
|
56
|
+
let i = startIdx;
|
|
57
|
+
while (i < lines.length) {
|
|
58
|
+
const l = lines[i] ?? "";
|
|
59
|
+
if (l.startsWith("## "))
|
|
60
|
+
return null;
|
|
61
|
+
if (l.trimStart().startsWith("|")) {
|
|
62
|
+
const out = [];
|
|
63
|
+
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
64
|
+
out.push(lines[i]);
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// ADR 0164: map a Data Store Mapping `Store` cell (a physical engine such as
|
|
74
|
+
// "MySQL (fleet-db)", "MongoDB", "DynamoDB", "Kinesis") to the canonical
|
|
75
|
+
// `entities.persistence_kind` classification. This mirrors the services
|
|
76
|
+
// builder's deriveLanguage (a normalised token derived from a raw cell) —
|
|
77
|
+
// faithful classification, not a verbatim engine. The raw engine string is
|
|
78
|
+
// preserved by the caller in `notes`.
|
|
79
|
+
const PERSISTENCE_KIND_RULES = [
|
|
80
|
+
[
|
|
81
|
+
/\b(mysql|mariadb|postgres|postgresql|aurora|oracle|mssql|sqlserver|sql server|rds|relational)\b/,
|
|
82
|
+
"relational-table",
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
/\b(mongodb|mongo|dynamodb|dynamo|documentdb|cosmos|couchbase|couchdb|firestore)\b/,
|
|
86
|
+
"document-collection",
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
/\b(kinesis|kafka|activemq|rabbitmq|sqs|sns|eventbridge|nats|pubsub|stream|queue|topic)\b/,
|
|
90
|
+
"event-payload",
|
|
91
|
+
],
|
|
92
|
+
// Transient / in-memory state (not a system of record) — the schema's
|
|
93
|
+
// `dto` classification. Real-world values: "In-memory (Vuex)",
|
|
94
|
+
// "Transient (API response)", "Runtime (Vue plugin)".
|
|
95
|
+
[/\b(in-memory|transient|ephemeral|runtime)\b/, "dto"],
|
|
96
|
+
];
|
|
97
|
+
/** Cells that mean "no store", treated as empty. */
|
|
98
|
+
const NULL_STORE = new Set(["", "n/a", "na", "-", "—", "none", "tbd"]);
|
|
99
|
+
export function derivePersistenceKind(store) {
|
|
100
|
+
const s = store.trim().toLowerCase();
|
|
101
|
+
if (NULL_STORE.has(s))
|
|
102
|
+
return { kind: "", recognised: true };
|
|
103
|
+
for (const [re, kind] of PERSISTENCE_KIND_RULES) {
|
|
104
|
+
if (re.test(s))
|
|
105
|
+
return { kind, recognised: true };
|
|
106
|
+
}
|
|
107
|
+
// Present but unmapped (e.g. Elasticsearch, in-memory/Vuex) → a valid
|
|
108
|
+
// recommended value, but flagged so the rule set can grow.
|
|
109
|
+
return { kind: "other", recognised: false };
|
|
110
|
+
}
|
|
111
|
+
// Re-export so callers needing the entity-aware slug import from one place.
|
|
112
|
+
export { entitySlug };
|
|
113
|
+
//# sourceMappingURL=data-store-mapping.js.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Node builder for domain_models — emits `domain_models.csv` rows.
|
|
2
|
+
//
|
|
3
|
+
// Platform-profile only. Non-platform SDDs return an empty list.
|
|
4
|
+
//
|
|
5
|
+
// Sources (combined; deduplicated by domain_id):
|
|
6
|
+
// - Forward: scan loaded traceability manifests for
|
|
7
|
+
// `traceability.domain_model.path`. The capability's domain (from the
|
|
8
|
+
// manifest's `capability.domain` or path) determines `domain_id`.
|
|
9
|
+
// - Reverse: scan `docs/domains/<domain>/architecture/data-model.mermaid.md`
|
|
10
|
+
// and, since schema 3.11.0, `data-model.tql` beside it (M-ADR 0210 §4.6
|
|
11
|
+
// Decision 2). Presence-only — content is not parsed in this phase.
|
|
12
|
+
//
|
|
13
|
+
// `notation` (3.11.0) is DERIVED from the artefact's filename — mermaid-erd or
|
|
14
|
+
// typeql — never authored. It is Decision 2's compliance instrument: every
|
|
15
|
+
// build reports `[metric] domain_models: rows=N typeql=T mermaid-erd=M`, and a
|
|
16
|
+
// mermaid-erd row is a `convention` finding, advisory before the sunset
|
|
17
|
+
// (2027-03-31) and an error at the first build after it.
|
|
18
|
+
//
|
|
19
|
+
// One row per unique domain_id. If forward declares a path AND reverse
|
|
20
|
+
// finds a file for the same domain, the forward entry wins (the manifest
|
|
21
|
+
// is the declarative source) and the reverse check is silent unless the
|
|
22
|
+
// paths disagree, in which case a `[finding] divergence` is emitted.
|
|
23
|
+
//
|
|
24
|
+
// Added in schema 2.4.0 per ADR 0142.
|
|
25
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { extractFileEvidenceClass } from "./evidence-class.js";
|
|
28
|
+
const DOMAINS_DIR_REL = join("docs", "domains");
|
|
29
|
+
const DATA_MODEL_FILENAME = "data-model.mermaid.md";
|
|
30
|
+
const DATA_MODEL_TQL_FILENAME = "data-model.tql";
|
|
31
|
+
/** M-ADR 0210 §4.6 Decision 2 (as amended 2026-09-18): the first build after this date fails on a mermaid-erd model. */
|
|
32
|
+
export const DECISION_2_SUNSET = "2027-03-31";
|
|
33
|
+
/** Derived from the artefact, never authored (M-ADR 0210 §4.6). */
|
|
34
|
+
export function notationOf(docPath) {
|
|
35
|
+
return /\.tql$/i.test(docPath) ? "typeql" : "mermaid-erd";
|
|
36
|
+
}
|
|
37
|
+
export async function buildDomainModels(inputs) {
|
|
38
|
+
if (inputs.manifest.profile !== "platform-profile") {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
// Step 1: Forward — collect declared paths from traceability manifests.
|
|
42
|
+
// Multiple capabilities in the same domain may declare the same path;
|
|
43
|
+
// deduplicate by domain_id.
|
|
44
|
+
const declaredByDomain = new Map();
|
|
45
|
+
for (const m of inputs.manifests) {
|
|
46
|
+
const dm = m.traceability.domain_model;
|
|
47
|
+
if (!dm)
|
|
48
|
+
continue;
|
|
49
|
+
const domainId = m.capability.domain;
|
|
50
|
+
if (!domainId || !inputs.knownDomainIds.has(domainId))
|
|
51
|
+
continue;
|
|
52
|
+
const existing = declaredByDomain.get(domainId);
|
|
53
|
+
if (existing && existing.path !== dm.path) {
|
|
54
|
+
inputs.findings.add("divergence", `domain_model.path for domain "${domainId}": "${existing.path}" vs "${dm.path}" — using first declaration`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
declaredByDomain.set(domainId, {
|
|
58
|
+
path: dm.path,
|
|
59
|
+
...(dm.version ? { version: dm.version } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Step 2: Reverse — scan filesystem for data-model.mermaid.md per domain.
|
|
63
|
+
// Domains without a forward declaration that have a file get a row;
|
|
64
|
+
// domains with both get a divergence finding if paths disagree.
|
|
65
|
+
const fsByDomain = await scanFilesystemModels(inputs.sddPath, inputs.findings);
|
|
66
|
+
for (const [domainId, fsPath] of fsByDomain) {
|
|
67
|
+
if (!inputs.knownDomainIds.has(domainId))
|
|
68
|
+
continue;
|
|
69
|
+
const declared = declaredByDomain.get(domainId);
|
|
70
|
+
if (!declared) {
|
|
71
|
+
declaredByDomain.set(domainId, { path: fsPath });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (normalisePath(declared.path) !== normalisePath(fsPath)) {
|
|
75
|
+
inputs.findings.add("divergence", `domain_model for "${domainId}": traceability declares "${declared.path}" but file found at "${fsPath}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Step 3: Coverage finding for domains with no model at all.
|
|
79
|
+
for (const domainId of inputs.knownDomainIds) {
|
|
80
|
+
if (!declaredByDomain.has(domainId)) {
|
|
81
|
+
inputs.findings.add("coverage", `no domain_model for domain "${domainId}" — neither traceability declares nor file exists`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Step 4: Emit rows sorted by id for determinism. `evidence_class` reads
|
|
85
|
+
// from the model file's leading ```yaml fence (`evidence-class:`) per the
|
|
86
|
+
// documentation-reverse anchor convention (ADR 0175); absent file or
|
|
87
|
+
// declaration → "".
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (const [domainId, model] of declaredByDomain) {
|
|
90
|
+
rows.push({
|
|
91
|
+
id: `${domainId}-data-model`,
|
|
92
|
+
name: `${titleCase(domainId)} Domain Model`,
|
|
93
|
+
domain_id: domainId,
|
|
94
|
+
doc_path: model.path,
|
|
95
|
+
version: model.version ?? "",
|
|
96
|
+
evidence_class: await readEvidenceClass(inputs.sddPath, model.path),
|
|
97
|
+
source_sdd_id: "",
|
|
98
|
+
notation: notationOf(model.path),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const sorted = rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
102
|
+
// Decision 2's instrument (M-ADR 0210 §4.6 as amended): a number that moves, and one finding.
|
|
103
|
+
const typeql = sorted.filter((r) => r["notation"] === "typeql").length;
|
|
104
|
+
const mermaid = sorted.length - typeql;
|
|
105
|
+
inputs.findings.add("metric", `domain_models: rows=${sorted.length} typeql=${typeql} mermaid-erd=${mermaid}`);
|
|
106
|
+
const today = inputs.today ?? new Date().toISOString().slice(0, 10);
|
|
107
|
+
const pastSunset = today > DECISION_2_SUNSET;
|
|
108
|
+
for (const r of sorted) {
|
|
109
|
+
if (r["notation"] !== "mermaid-erd")
|
|
110
|
+
continue;
|
|
111
|
+
inputs.findings.add("convention", `domain_models: "${String(r["id"])}" is in mermaid-erd ${pastSunset ? "after" : "before"} the Decision-2 sunset (${DECISION_2_SUNSET}) — migrate at the next requirements pass (M-ADR 0210 §4.6)`, pastSunset ? "error" : "warn");
|
|
112
|
+
}
|
|
113
|
+
return sorted;
|
|
114
|
+
}
|
|
115
|
+
async function scanFilesystemModels(sddPath, findings) {
|
|
116
|
+
const result = new Map();
|
|
117
|
+
const domainsDir = join(sddPath, DOMAINS_DIR_REL);
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = await readdir(domainsDir, { withFileTypes: true });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const code = err.code;
|
|
124
|
+
if (code === "ENOENT")
|
|
125
|
+
return result;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isDirectory())
|
|
130
|
+
continue;
|
|
131
|
+
// Both notations are looked for; the TypeQL artefact wins when both exist,
|
|
132
|
+
// because after migration the diagram is a generated projection of it
|
|
133
|
+
// (M-ADR 0210 §4.1) and the model's notation is the authored one.
|
|
134
|
+
for (const filename of [DATA_MODEL_TQL_FILENAME, DATA_MODEL_FILENAME]) {
|
|
135
|
+
if (result.has(entry.name))
|
|
136
|
+
break;
|
|
137
|
+
const candidate = join(domainsDir, entry.name, "architecture", filename);
|
|
138
|
+
try {
|
|
139
|
+
const s = await stat(candidate);
|
|
140
|
+
if (s.isFile()) {
|
|
141
|
+
result.set(entry.name, join(DOMAINS_DIR_REL, entry.name, "architecture", filename));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const code = err.code;
|
|
146
|
+
if (code !== "ENOENT") {
|
|
147
|
+
findings.add("convention", `failed to stat ${candidate}: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
async function readEvidenceClass(sddPath, docPath) {
|
|
155
|
+
const abs = docPath.startsWith("/") ? docPath : join(sddPath, docPath);
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = await readFile(abs, "utf-8");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Forward-declared paths may not exist on disk; absence → empty.
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
return extractFileEvidenceClass(text);
|
|
165
|
+
}
|
|
166
|
+
function normalisePath(p) {
|
|
167
|
+
return p.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
168
|
+
}
|
|
169
|
+
function titleCase(slug) {
|
|
170
|
+
return slug
|
|
171
|
+
.split(/[-_]/)
|
|
172
|
+
.map((s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s))
|
|
173
|
+
.join(" ");
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=domain-models.js.map
|