@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,350 @@
|
|
|
1
|
+
// Traceability manifest loader — walks `docs/domains/**/*.traceability.yaml`
|
|
2
|
+
// and returns parsed declarations.
|
|
3
|
+
//
|
|
4
|
+
// The traceability manifest is per-capability declarative metadata that
|
|
5
|
+
// the canonical builder reads to populate `capabilities.csv` and the
|
|
6
|
+
// five capability_* edge catalogues (services, adrs, features,
|
|
7
|
+
// contracts, events). One YAML per capability; the canonical layout is
|
|
8
|
+
// `docs/domains/{domain}/product/{capability}.traceability.yaml`.
|
|
9
|
+
//
|
|
10
|
+
// Schema reference: docs/method/methods/capability-traceability-manifest-template.yaml
|
|
11
|
+
// in sdd-method (per ADR 0047 / capability-spec-template.md).
|
|
12
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
13
|
+
import { join, relative, sep } from "node:path";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
const DOMAINS_DIR_REL = join("docs", "domains");
|
|
16
|
+
/**
|
|
17
|
+
* Normalise an authored `feature_directory` value (string, list of strings,
|
|
18
|
+
* or absent) to a clean `string[]`. Non-string entries and empties are
|
|
19
|
+
* dropped. Lets a capability declare one or several feature dirs.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeFeatureDirs(value) {
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return value.trim() === "" ? [] : [value];
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.filter((v) => typeof v === "string" && v.trim() !== "");
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* THE contract-kind list. Every consumer of `traceability.contracts` iterates
|
|
32
|
+
* THIS — never a hand-written array — because hand-copies are what let `graphql`
|
|
33
|
+
* be discoverable, mappable and loadable while the capability_contract EDGE
|
|
34
|
+
* builder silently ignored it. Measured cost: an estate authored 9 correct
|
|
35
|
+
* declarations, rebuilt, and got zero edges and 7 standing orphans.
|
|
36
|
+
*
|
|
37
|
+
* The list was hand-written at THREE consumer sites and `graphql` was missed at
|
|
38
|
+
* all three. A kind added here now reaches every consumer at once.
|
|
39
|
+
*
|
|
40
|
+
* Not in scope: repo-config's own `contracts` block (a different schema with its
|
|
41
|
+
* own three kinds) — that is not a stale copy of this list and must not be
|
|
42
|
+
* "fixed" with this helper.
|
|
43
|
+
*/
|
|
44
|
+
export const CONTRACT_LIST_KEYS = [
|
|
45
|
+
"openapi",
|
|
46
|
+
"proto",
|
|
47
|
+
"avro",
|
|
48
|
+
"spicedb",
|
|
49
|
+
"graphql",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Every contract path a contracts block declares, across all kinds and both
|
|
53
|
+
* entry forms (bare string, or `{path, external}` per D13).
|
|
54
|
+
*/
|
|
55
|
+
export function declaredContractPaths(block) {
|
|
56
|
+
if (block === undefined)
|
|
57
|
+
return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const key of CONTRACT_LIST_KEYS) {
|
|
60
|
+
const list = block[key];
|
|
61
|
+
if (!Array.isArray(list))
|
|
62
|
+
continue;
|
|
63
|
+
for (const entry of list) {
|
|
64
|
+
if (typeof entry === "string")
|
|
65
|
+
out.push(entry);
|
|
66
|
+
else if (entry && typeof entry.path === "string")
|
|
67
|
+
out.push(entry.path);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
export async function loadTraceabilityManifests(opts) {
|
|
73
|
+
const root = join(opts.sddPath, DOMAINS_DIR_REL);
|
|
74
|
+
let yamlPaths;
|
|
75
|
+
try {
|
|
76
|
+
yamlPaths = await collectYamlPaths(root, /\.traceability\.yaml$/);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const code = err.code;
|
|
80
|
+
if (code === "ENOENT") {
|
|
81
|
+
// No domains dir → no traceability data; return empty.
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
const manifests = [];
|
|
87
|
+
for (const path of yamlPaths.sort()) {
|
|
88
|
+
const text = await readFile(path, "utf-8");
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
raw = parseYaml(text);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
opts.findings.add("convention", `failed to parse YAML at ${relative(opts.sddPath, path)}: ${err.message}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!raw || typeof raw !== "object") {
|
|
98
|
+
opts.findings.add("convention", `${relative(opts.sddPath, path)}: not an object — skipped`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const obj = raw;
|
|
102
|
+
const cap = obj["capability"] ?? {};
|
|
103
|
+
const capId = String(cap["id"] ?? "").trim();
|
|
104
|
+
if (capId === "") {
|
|
105
|
+
opts.findings.add("convention", `${relative(opts.sddPath, path)}: missing capability.id`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const relPath = relative(opts.sddPath, path);
|
|
109
|
+
const domainFromPath = inferDomainFromPath(relPath);
|
|
110
|
+
const declaredDomain = String(cap["domain"] ?? "").trim();
|
|
111
|
+
if (declaredDomain && domainFromPath && declaredDomain !== domainFromPath) {
|
|
112
|
+
opts.findings.add("divergence", `${relPath}: capability.domain="${declaredDomain}" disagrees with path domain "${domainFromPath}"`);
|
|
113
|
+
}
|
|
114
|
+
const capability = {
|
|
115
|
+
id: capId,
|
|
116
|
+
domain: declaredDomain || domainFromPath,
|
|
117
|
+
...(typeof cap["capability_spec"] === "string"
|
|
118
|
+
? { capability_spec: cap["capability_spec"] }
|
|
119
|
+
: {}),
|
|
120
|
+
...(normalizeFeatureDirs(cap["feature_directory"]).length > 0
|
|
121
|
+
? { feature_directory: normalizeFeatureDirs(cap["feature_directory"]) }
|
|
122
|
+
: {}),
|
|
123
|
+
...(typeof cap["status"] === "string" ? { status: cap["status"] } : {}),
|
|
124
|
+
...(typeof cap["data_subset"] === "string"
|
|
125
|
+
? { data_subset: cap["data_subset"] }
|
|
126
|
+
: {}),
|
|
127
|
+
...(typeof cap["slice_scope"] === "string"
|
|
128
|
+
? { slice_scope: cap["slice_scope"] }
|
|
129
|
+
: {}),
|
|
130
|
+
...(typeof cap["evidence_class"] === "string"
|
|
131
|
+
? { evidence_class: cap["evidence_class"] }
|
|
132
|
+
: {}),
|
|
133
|
+
...(typeof cap["context"] === "string"
|
|
134
|
+
? { context: cap["context"] }
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
137
|
+
manifests.push({
|
|
138
|
+
path,
|
|
139
|
+
relPath,
|
|
140
|
+
domainFromPath,
|
|
141
|
+
capability,
|
|
142
|
+
traceability: extractTraceabilityBlock(obj["traceability"]),
|
|
143
|
+
data: raw,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return manifests;
|
|
147
|
+
}
|
|
148
|
+
function extractTraceabilityBlock(raw) {
|
|
149
|
+
if (!raw || typeof raw !== "object")
|
|
150
|
+
return {};
|
|
151
|
+
const t = raw;
|
|
152
|
+
const runway = t["runway"];
|
|
153
|
+
const adrs = t["adrs"];
|
|
154
|
+
const services = t["services"];
|
|
155
|
+
const contracts = t["contracts"];
|
|
156
|
+
const domainModel = t["domain_model"];
|
|
157
|
+
return {
|
|
158
|
+
...(runway && typeof runway === "object" ? { runway: extractRunway(runway) } : {}),
|
|
159
|
+
...(Array.isArray(adrs) ? { adrs: extractAdrs(adrs) } : {}),
|
|
160
|
+
...(Array.isArray(services) ? { services: extractServices(services) } : {}),
|
|
161
|
+
...(contracts && typeof contracts === "object"
|
|
162
|
+
? { contracts: extractContracts(contracts) }
|
|
163
|
+
: {}),
|
|
164
|
+
...(domainModel && typeof domainModel === "object"
|
|
165
|
+
? extractDomainModel(domainModel)
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function extractDomainModel(raw) {
|
|
170
|
+
if (!raw || typeof raw !== "object")
|
|
171
|
+
return {};
|
|
172
|
+
const dm = raw;
|
|
173
|
+
const path = String(dm["path"] ?? "").trim();
|
|
174
|
+
if (path === "")
|
|
175
|
+
return {};
|
|
176
|
+
const version = dm["version"];
|
|
177
|
+
return {
|
|
178
|
+
domain_model: {
|
|
179
|
+
path,
|
|
180
|
+
...(typeof version === "string" && version.trim() !== ""
|
|
181
|
+
? { version }
|
|
182
|
+
: {}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function extractRunway(raw) {
|
|
187
|
+
if (!raw || typeof raw !== "object")
|
|
188
|
+
return {};
|
|
189
|
+
const r = raw;
|
|
190
|
+
const enables = Array.isArray(r["enables"])
|
|
191
|
+
? r["enables"].filter((e) => typeof e === "string")
|
|
192
|
+
: undefined;
|
|
193
|
+
return {
|
|
194
|
+
...(typeof r["classification"] === "string"
|
|
195
|
+
? { classification: r["classification"] }
|
|
196
|
+
: {}),
|
|
197
|
+
...(enables ? { enables } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function extractAdrs(raw) {
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const item of raw) {
|
|
203
|
+
if (!item || typeof item !== "object")
|
|
204
|
+
continue;
|
|
205
|
+
const o = item;
|
|
206
|
+
const id = String(o["id"] ?? "").trim();
|
|
207
|
+
if (!id)
|
|
208
|
+
continue;
|
|
209
|
+
out.push({
|
|
210
|
+
id,
|
|
211
|
+
...(typeof o["path"] === "string" ? { path: o["path"] } : {}),
|
|
212
|
+
...(typeof o["relationship"] === "string"
|
|
213
|
+
? { relationship: o["relationship"] }
|
|
214
|
+
: {}),
|
|
215
|
+
...(typeof o["notes"] === "string" ? { notes: o["notes"] } : {}),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
function extractServices(raw) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const item of raw) {
|
|
223
|
+
if (!item || typeof item !== "object")
|
|
224
|
+
continue;
|
|
225
|
+
const o = item;
|
|
226
|
+
const id = String(o["id"] ?? "").trim();
|
|
227
|
+
if (!id)
|
|
228
|
+
continue;
|
|
229
|
+
out.push({
|
|
230
|
+
id,
|
|
231
|
+
...(typeof o["owner_domain"] === "string"
|
|
232
|
+
? { owner_domain: o["owner_domain"] }
|
|
233
|
+
: {}),
|
|
234
|
+
...(typeof o["owner_sdd"] === "string" && o["owner_sdd"].trim() !== ""
|
|
235
|
+
? { owner_sdd: o["owner_sdd"].trim() }
|
|
236
|
+
: {}),
|
|
237
|
+
...(typeof o["role"] === "string" ? { role: o["role"] } : {}),
|
|
238
|
+
...(typeof o["status"] === "string" ? { status: o["status"] } : {}),
|
|
239
|
+
...(typeof o["notes"] === "string" ? { notes: o["notes"] } : {}),
|
|
240
|
+
...(typeof o["contract"] === "string" && o["contract"].trim() !== ""
|
|
241
|
+
? { contract: o["contract"].trim() }
|
|
242
|
+
: {}),
|
|
243
|
+
...(typeof o["interaction_pattern"] === "string" && o["interaction_pattern"].trim() !== ""
|
|
244
|
+
? { interaction_pattern: o["interaction_pattern"].trim() }
|
|
245
|
+
: {}),
|
|
246
|
+
...(typeof o["qualifier"] === "string" && o["qualifier"].trim() !== ""
|
|
247
|
+
? { qualifier: o["qualifier"].trim() }
|
|
248
|
+
: {}),
|
|
249
|
+
...(typeof o["evidence"] === "string" && o["evidence"].trim() !== ""
|
|
250
|
+
? { evidence: o["evidence"].trim() }
|
|
251
|
+
: {}),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function extractContracts(raw) {
|
|
257
|
+
const o = raw;
|
|
258
|
+
// Keeps BOTH legal entry forms. The original version filtered to strings
|
|
259
|
+
// only — which silently discarded every object entry before any builder
|
|
260
|
+
// ran. Measured cost when an adopter marked 58 entries `external: true`
|
|
261
|
+
// for the D13 canary: 0 rows emitted AND the 56 orphan findings those
|
|
262
|
+
// entries used to raise disappeared — the declarations became invisible
|
|
263
|
+
// to row emission and to finding emission at once. The builder had been
|
|
264
|
+
// taught the object form (contracts.ts) while its only feeder had not:
|
|
265
|
+
// the F36 shape again, one surface ahead of the pipeline that supplies
|
|
266
|
+
// it. `spicedb` was likewise never extracted, so `.zed` declarations
|
|
267
|
+
// never reached any builder even as bare strings.
|
|
268
|
+
const arr = (key) => {
|
|
269
|
+
const v = o[key];
|
|
270
|
+
if (!Array.isArray(v))
|
|
271
|
+
return undefined;
|
|
272
|
+
return v.filter((e) => typeof e === "string" ||
|
|
273
|
+
(e !== null &&
|
|
274
|
+
typeof e === "object" &&
|
|
275
|
+
!Array.isArray(e) &&
|
|
276
|
+
typeof e["path"] === "string"));
|
|
277
|
+
};
|
|
278
|
+
const openapi = arr("openapi");
|
|
279
|
+
const proto = arr("proto");
|
|
280
|
+
const avro = arr("avro");
|
|
281
|
+
const spicedb = arr("spicedb");
|
|
282
|
+
const graphql = arr("graphql");
|
|
283
|
+
// `provenance` (3.5.0) MUST be extracted here or the builder that reads it
|
|
284
|
+
// sees nothing — this function reconstructs the block key-by-key rather than
|
|
285
|
+
// passing the parsed object through, which is precisely the F36 shape the
|
|
286
|
+
// comment above records. It was re-committed once during 3.5.0's own
|
|
287
|
+
// implementation: the builder was taught to read provenance while this feeder
|
|
288
|
+
// still dropped it, and only an end-to-end catalogue build caught it, because
|
|
289
|
+
// the builder's unit tests construct the manifest object directly and never
|
|
290
|
+
// traverse this function. Any future key added to this block needs a line here
|
|
291
|
+
// AND a loader-level test.
|
|
292
|
+
const provenance = typeof o["provenance"] === "string" ? o["provenance"].trim() : undefined;
|
|
293
|
+
return {
|
|
294
|
+
...(openapi ? { openapi } : {}),
|
|
295
|
+
...(proto ? { proto } : {}),
|
|
296
|
+
...(avro ? { avro } : {}),
|
|
297
|
+
...(spicedb ? { spicedb } : {}),
|
|
298
|
+
...(graphql ? { graphql } : {}),
|
|
299
|
+
...(provenance ? { provenance } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function inferDomainFromPath(relPath) {
|
|
303
|
+
const parts = relPath.split(sep);
|
|
304
|
+
const idx = parts.indexOf("domains");
|
|
305
|
+
if (idx === -1 || idx + 1 >= parts.length)
|
|
306
|
+
return "";
|
|
307
|
+
return parts[idx + 1] ?? "";
|
|
308
|
+
}
|
|
309
|
+
async function collectYamlPaths(dir, pattern) {
|
|
310
|
+
const out = [];
|
|
311
|
+
let entries;
|
|
312
|
+
try {
|
|
313
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
const code = err.code;
|
|
317
|
+
if (code === "ENOENT")
|
|
318
|
+
return [];
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
const full = join(dir, entry.name);
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
out.push(...(await collectYamlPaths(full, pattern)));
|
|
325
|
+
}
|
|
326
|
+
else if (entry.isFile() && pattern.test(entry.name)) {
|
|
327
|
+
out.push(full);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
/** Convenience: read a file's first non-empty line, returning the trimmed value. */
|
|
333
|
+
export async function readFirstNonEmptyLine(path) {
|
|
334
|
+
try {
|
|
335
|
+
const s = await stat(path);
|
|
336
|
+
if (!s.isFile())
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return "";
|
|
341
|
+
}
|
|
342
|
+
const text = await readFile(path, "utf-8");
|
|
343
|
+
for (const line of text.split(/\r?\n/)) {
|
|
344
|
+
const t = line.trim();
|
|
345
|
+
if (t !== "")
|
|
346
|
+
return t;
|
|
347
|
+
}
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=traceability-loader.js.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// `sdd-cli catalogue validate` — re-validate an existing catalogues/
|
|
2
|
+
// directory without rebuilding. Two checks per the canonical CLI spec:
|
|
3
|
+
//
|
|
4
|
+
// 1. Column-shape check: every CSV's header row must match the
|
|
5
|
+
// canonical schema's declared columns (exact set + order).
|
|
6
|
+
// 2. FK validation: full pass over the loaded rows, internal mode
|
|
7
|
+
// (external FKs treated per their declared flag — same semantics
|
|
8
|
+
// as `catalogue build`). Adopters who want post-merge validation
|
|
9
|
+
// run `catalogue aggregate` instead.
|
|
10
|
+
//
|
|
11
|
+
// Outputs the same shape of findings as `build` so consumers can pipe
|
|
12
|
+
// either through the same parser. Exit 0 on PASS; non-zero on shape
|
|
13
|
+
// drift OR internal-FK FAIL.
|
|
14
|
+
import { readFile, stat } from "node:fs/promises";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
17
|
+
import { readCatalogueCsv } from "./csv-reader.js";
|
|
18
|
+
import { writeCatalogue } from "./csv-writer.js";
|
|
19
|
+
import { validatedPopulation, validateWithEngineToLog } from "./engine-validator.js";
|
|
20
|
+
import { FindingsLog } from "./findings.js";
|
|
21
|
+
import { PLATFORM_SCOPED_AT_ORGANISATION } from "./platform-scope.js";
|
|
22
|
+
import { loadCanonicalSchema } from "./schema-loader.js";
|
|
23
|
+
export async function runCatalogueValidate(inputs, write) {
|
|
24
|
+
const sddPath = resolve(inputs.sddPath ?? process.cwd());
|
|
25
|
+
// An organisation root is aggregate-only by construction (M-ADR 0208): its own
|
|
26
|
+
// catalogues/ is deliberately empty and catalogues-aggregated/ is all it produces.
|
|
27
|
+
// Validating the empty member directory reported every file missing — a check
|
|
28
|
+
// that could only fail on such a root. Validate what it actually publishes.
|
|
29
|
+
const orgRoot = await isOrganisationRoot(sddPath);
|
|
30
|
+
const dirName = orgRoot ? "catalogues-aggregated" : "catalogues";
|
|
31
|
+
const cataloguesDir = join(sddPath, dirName);
|
|
32
|
+
const schema = await loadCanonicalSchema();
|
|
33
|
+
write(`sdd-cli catalogue validate (canonical schema ${schema.schemaVersion}, cli ${CLI_VERSION})\n`);
|
|
34
|
+
if (orgRoot)
|
|
35
|
+
write(`organisation root: validating ${dirName}/ (an organisation root publishes only its aggregate)\n`);
|
|
36
|
+
// Existence check on catalogues/ — fail fast with a clear message.
|
|
37
|
+
try {
|
|
38
|
+
const s = await stat(cataloguesDir);
|
|
39
|
+
if (!s.isDirectory()) {
|
|
40
|
+
throw new Error(`catalogues/ at ${cataloguesDir} exists but is not a directory`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === "ENOENT") {
|
|
45
|
+
throw new Error(`catalogues/ not found at ${cataloguesDir}\n` +
|
|
46
|
+
` Run \`sdd-cli catalogue build\` before \`validate\`.`);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
const findings = new FindingsLog();
|
|
51
|
+
const rowsByCatalogue = new Map();
|
|
52
|
+
let columnShapeOk = true;
|
|
53
|
+
for (const cat of schema.catalogues) {
|
|
54
|
+
const filePath = join(cataloguesDir, `${cat.name}.csv`);
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = await readCatalogueCsv(filePath);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err.code === "ENOENT") {
|
|
61
|
+
// Missing file is a shape failure — every catalogue declared in
|
|
62
|
+
// the schema MUST have a CSV (headers-only acceptable).
|
|
63
|
+
findings.add("convention", `${dirName}/${cat.name}.csv missing — every schema catalogue must have a file (headers-only acceptable)`);
|
|
64
|
+
columnShapeOk = false;
|
|
65
|
+
rowsByCatalogue.set(cat.name, []);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const expected = cat.columns.map((c) => c.name);
|
|
71
|
+
const drift = compareColumns(expected, parsed.columns);
|
|
72
|
+
if (drift !== null) {
|
|
73
|
+
findings.add("convention", `${dirName}/${cat.name}.csv: column shape drift — ${drift}`);
|
|
74
|
+
columnShapeOk = false;
|
|
75
|
+
}
|
|
76
|
+
rowsByCatalogue.set(cat.name, parsed.rows);
|
|
77
|
+
}
|
|
78
|
+
// Derive the SDD profile from products.csv so profile-external FKs (schema
|
|
79
|
+
// 2.23.0) resolve consistently with `catalogue build`. A per-SDD catalogue
|
|
80
|
+
// has one distinct profile → pass it (an app catalogue's capabilities.domain_id
|
|
81
|
+
// is then treated external, matching the build). An aggregated catalogue mixes
|
|
82
|
+
// profiles → leave undefined, so those FKs validate as internal and MUST
|
|
83
|
+
// resolve (the merged platform domains are present to satisfy them).
|
|
84
|
+
const profile = singleProfile(rowsByCatalogue.get("products") ?? []);
|
|
85
|
+
// FK + bound-enum validation on the embedded engine (M-ADR 0205 §4.4
|
|
86
|
+
// cutover; same semantics, findings text unchanged — the A2/A3 dual-run
|
|
87
|
+
// record is the evidence). Enum hard-fail behaviour per M-ADR 0203 §4.1
|
|
88
|
+
// is unchanged: an out-of-set value is an error-severity finding.
|
|
89
|
+
const { fk, enums } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, {
|
|
90
|
+
...(profile !== undefined ? { profile } : {}),
|
|
91
|
+
// At organisation tier `domains` is keyed (platform_sdd_id, id): two platforms may
|
|
92
|
+
// each own a domain of one name, exactly as the aggregator validates it.
|
|
93
|
+
...(orgRoot ? { platformScopedTargets: PLATFORM_SCOPED_AT_ORGANISATION } : {}),
|
|
94
|
+
});
|
|
95
|
+
// Report.
|
|
96
|
+
const findingsText = findings.render();
|
|
97
|
+
if (findingsText)
|
|
98
|
+
write(`\n${findingsText}\n`);
|
|
99
|
+
if (columnShapeOk) {
|
|
100
|
+
write(`\nColumn-shape check: PASS (${validatedPopulation(rowsByCatalogue)})\n`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
write(`\nColumn-shape check: FAIL\n`);
|
|
104
|
+
}
|
|
105
|
+
if (fk.internalPass) {
|
|
106
|
+
write(`FK validation (internal): PASS (${validatedPopulation(rowsByCatalogue)})\n`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
write(`FK validation (internal): FAIL (${fk.internalFailures.length} dangling)\n`);
|
|
110
|
+
for (const f of fk.internalFailures) {
|
|
111
|
+
write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value || "<empty>"}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (enums.pass) {
|
|
115
|
+
write(`Enum validation (column_enums): PASS (${validatedPopulation(rowsByCatalogue)})\n`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
write(`Enum validation (column_enums): FAIL (${enums.violations.length} out-of-set)\n`);
|
|
119
|
+
for (const v of enums.violations) {
|
|
120
|
+
write(` ${v.catalogue}.${v.column}: row pk=${v.rowPk} value "${v.value}" (allowed: ${v.allowed})\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (fk.externalFindings > 0) {
|
|
124
|
+
write(`External findings: ${fk.externalFindings} (validated by aggregator)\n`);
|
|
125
|
+
}
|
|
126
|
+
// Force-touch writeCatalogue to keep imports honest if a future
|
|
127
|
+
// refactor wants to re-emit a cleaned-up file from validate. Today
|
|
128
|
+
// validate is read-only.
|
|
129
|
+
void writeCatalogue;
|
|
130
|
+
const strictFail = inputs.strict === true && findings.count() > 0;
|
|
131
|
+
// Error-severity findings (the bound-enum FAIL path) fail validate by
|
|
132
|
+
// default, matching `catalogue build`'s exit-code contract.
|
|
133
|
+
const exitCode = !columnShapeOk || !fk.internalPass || !enums.pass || findings.errorCount() > 0 || strictFail
|
|
134
|
+
? 1
|
|
135
|
+
: 0;
|
|
136
|
+
return {
|
|
137
|
+
exitCode,
|
|
138
|
+
schemaVersion: schema.schemaVersion,
|
|
139
|
+
internalFkPass: fk.internalPass,
|
|
140
|
+
columnShapeOk,
|
|
141
|
+
enumPass: enums.pass,
|
|
142
|
+
findingsCount: findings.count(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Compare a CSV's actual column header against the schema's declared
|
|
147
|
+
* column order. Returns null if exactly equal (order included), or a
|
|
148
|
+
* one-line description of the drift otherwise.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* The single distinct non-empty `profile` across products rows, or undefined
|
|
152
|
+
* when there are zero or multiple (an aggregated catalogue). Drives
|
|
153
|
+
* profile-external FK handling (schema 2.23.0) — see the call site.
|
|
154
|
+
*/
|
|
155
|
+
function singleProfile(products) {
|
|
156
|
+
const profiles = new Set();
|
|
157
|
+
for (const p of products) {
|
|
158
|
+
const v = String(p["profile"] ?? "");
|
|
159
|
+
if (v !== "")
|
|
160
|
+
profiles.add(v);
|
|
161
|
+
}
|
|
162
|
+
return profiles.size === 1 ? [...profiles][0] : undefined;
|
|
163
|
+
}
|
|
164
|
+
function compareColumns(expected, actual) {
|
|
165
|
+
if (expected.length === actual.length) {
|
|
166
|
+
let equal = true;
|
|
167
|
+
for (let i = 0; i < expected.length; i++) {
|
|
168
|
+
if (expected[i] !== actual[i]) {
|
|
169
|
+
equal = false;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (equal)
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const expectedSet = new Set(expected);
|
|
177
|
+
const actualSet = new Set(actual);
|
|
178
|
+
const missing = expected.filter((c) => !actualSet.has(c));
|
|
179
|
+
const extra = actual.filter((c) => !expectedSet.has(c));
|
|
180
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
181
|
+
return `column order differs (expected: ${expected.join(",")} | got: ${actual.join(",")})`;
|
|
182
|
+
}
|
|
183
|
+
const parts = [];
|
|
184
|
+
if (missing.length > 0)
|
|
185
|
+
parts.push(`missing: ${missing.join(",")}`);
|
|
186
|
+
if (extra.length > 0)
|
|
187
|
+
parts.push(`unexpected: ${extra.join(",")}`);
|
|
188
|
+
return parts.join("; ");
|
|
189
|
+
}
|
|
190
|
+
/** True when the SDD's manifest declares it an organisation root (aggregate-only). */
|
|
191
|
+
async function isOrganisationRoot(sddPath) {
|
|
192
|
+
try {
|
|
193
|
+
const raw = await readFile(join(sddPath, ".sdd", "sdd-manifest.yaml"), "utf8");
|
|
194
|
+
return /^category:\s*"?organisation-root"?\s*$/m.test(raw);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `sdd-cli catalogue version` — prints the canonical schema version
|
|
2
|
+
// and the sdd-cli version pin so adopters can verify their tooling
|
|
3
|
+
// matches the schema they expect.
|
|
4
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
5
|
+
import { SCHEMA_COMPAT } from "./schema-compat.js";
|
|
6
|
+
import { loadCanonicalSchema } from "./schema-loader.js";
|
|
7
|
+
export async function runCatalogueVersion(write) {
|
|
8
|
+
const schema = await loadCanonicalSchema();
|
|
9
|
+
write([
|
|
10
|
+
`sdd-cli ${CLI_VERSION}`,
|
|
11
|
+
`catalogue schema ${schema.schemaVersion}`,
|
|
12
|
+
`accepted catalogue range ${SCHEMA_COMPAT.catalogue_schema}`,
|
|
13
|
+
`accepted sdd_manifest ${SCHEMA_COMPAT.sdd_manifest}`,
|
|
14
|
+
`accepted aggregation ${SCHEMA_COMPAT.aggregation_manifest}`,
|
|
15
|
+
"",
|
|
16
|
+
].join("\n"));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
export const BINDING_FILENAME = ".sdd/implementation-binding.yaml";
|
|
6
|
+
/** One binding per implementation, for an SDD that specifies several (M-ADR 0214 §4.9). */
|
|
7
|
+
export const BINDINGS_DIRNAME = ".sdd/implementation-bindings";
|
|
8
|
+
export function bindingPath(targetRepo) {
|
|
9
|
+
return resolve(targetRepo, BINDING_FILENAME);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every binding file of the SDD, relative to its root: the single-implementation file
|
|
13
|
+
* when present, then each `<implementation>.yaml` in the bindings directory, sorted.
|
|
14
|
+
*/
|
|
15
|
+
export function bindingFiles(targetRepo) {
|
|
16
|
+
const out = [];
|
|
17
|
+
if (existsSync(bindingPath(targetRepo)))
|
|
18
|
+
out.push(BINDING_FILENAME);
|
|
19
|
+
const dir = resolve(targetRepo, BINDINGS_DIRNAME);
|
|
20
|
+
if (existsSync(dir)) {
|
|
21
|
+
for (const f of readdirSync(dir).sort()) {
|
|
22
|
+
if (f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
23
|
+
out.push(`${BINDINGS_DIRNAME}/${f}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export function hasBinding(targetRepo) {
|
|
29
|
+
return bindingFiles(targetRepo).length > 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Flatten the binding's implementation members into derived checks. Each member's `checks[]` become
|
|
33
|
+
* Check objects (run as-is by the extractor core); the member's repo/role/lifecycle ride alongside so
|
|
34
|
+
* the emitted finding records *which implementation* drifted. A member with no checks contributes none
|
|
35
|
+
* (e.g. a decommissioned as-is member you no longer validate).
|
|
36
|
+
*/
|
|
37
|
+
export async function deriveChecksFromBinding(targetRepo) {
|
|
38
|
+
const derived = [];
|
|
39
|
+
// A check id is the conformance_finding primary key within the SDD, so one declared by two
|
|
40
|
+
// bindings would collide there: refused here, where both files can be named.
|
|
41
|
+
const declaredIn = new Map();
|
|
42
|
+
for (const file of bindingFiles(targetRepo)) {
|
|
43
|
+
for (const d of await deriveFromFile(resolve(targetRepo, file))) {
|
|
44
|
+
const prior = declaredIn.get(d.check.id);
|
|
45
|
+
if (prior !== undefined) {
|
|
46
|
+
throw new Error(`conformance check id '${d.check.id}' is declared by both ${prior} and ${file} — check ids are unique within the SDD (M-ADR 0214 §4.9)`);
|
|
47
|
+
}
|
|
48
|
+
declaredIn.set(d.check.id, file);
|
|
49
|
+
derived.push(d);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return derived;
|
|
53
|
+
}
|
|
54
|
+
async function deriveFromFile(path) {
|
|
55
|
+
const raw = parseYaml(await readFile(path, "utf-8"));
|
|
56
|
+
const derived = [];
|
|
57
|
+
for (const impl of raw?.implementations ?? []) {
|
|
58
|
+
const member = {
|
|
59
|
+
repo: impl.repo,
|
|
60
|
+
...(impl.role ? { role: impl.role } : {}),
|
|
61
|
+
...(impl.lifecycle ? { lifecycle: impl.lifecycle } : {}),
|
|
62
|
+
...(impl.stack ? { stack: impl.stack } : {}),
|
|
63
|
+
};
|
|
64
|
+
for (const check of impl.checks ?? []) {
|
|
65
|
+
derived.push({ check, member });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return derived;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=binding.js.map
|