@sdd-method/sdd-cli 1.18.0 → 1.20.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/dist/lib/catalogue/aggregate.d.ts +18 -7
- package/dist/lib/catalogue/aggregate.d.ts.map +1 -1
- package/dist/lib/catalogue/aggregate.js +272 -43
- package/dist/lib/catalogue/aggregate.js.map +1 -1
- package/dist/lib/catalogue/aggregation-manifest-loader.d.ts +9 -0
- package/dist/lib/catalogue/aggregation-manifest-loader.d.ts.map +1 -1
- package/dist/lib/catalogue/aggregation-manifest-loader.js +16 -4
- package/dist/lib/catalogue/aggregation-manifest-loader.js.map +1 -1
- package/dist/lib/catalogue/builders/capability-spec.d.ts +4 -3
- package/dist/lib/catalogue/builders/capability-spec.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/capability-spec.js +19 -3
- package/dist/lib/catalogue/builders/capability-spec.js.map +1 -1
- package/dist/lib/catalogue/builders/feature-grounding.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/feature-grounding.js +15 -0
- package/dist/lib/catalogue/builders/feature-grounding.js.map +1 -1
- package/dist/lib/catalogue/builders/repo-edges.js +16 -0
- package/dist/lib/catalogue/builders/repo-edges.js.map +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +106 -5
- package/dist/lib/catalogue/manifest-loader.d.ts +8 -0
- package/dist/lib/catalogue/manifest-loader.d.ts.map +1 -1
- package/dist/lib/catalogue/manifest-loader.js +9 -0
- package/dist/lib/catalogue/manifest-loader.js.map +1 -1
- package/dist/lib/catalogue/rubric-coverage-map.yaml +1 -0
- package/package.json +1 -1
|
@@ -32,7 +32,14 @@ export interface AggregateResult {
|
|
|
32
32
|
export type Writer = (s: string) => void;
|
|
33
33
|
export declare function runCatalogueAggregate(inputs: AggregateInputs, write: Writer): Promise<AggregateResult>;
|
|
34
34
|
export declare function dedupExternalSystems(rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): void;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Stamp `column` = `value` on every row that does not already carry one
|
|
37
|
+
* (preserve-if-set — chained aggregation, M-ADR 0134 §4.5 step 4). Used for
|
|
38
|
+
* `source_sdd_id` and, per M-ADR 0208 §4.4, for `platform_sdd_id`. A null value
|
|
39
|
+
* means "do not fill" (organisation tier, where the stamp arrived from below or
|
|
40
|
+
* is legitimately unresolved). No-op when the catalogue lacks the column.
|
|
41
|
+
*/
|
|
42
|
+
declare function stampColumn(catalogue: CatalogueDef, rows: readonly CatalogueRow[], column: string, value: string | null): CatalogueRow[];
|
|
36
43
|
/**
|
|
37
44
|
* Resolve cross-SDD `screen_feature` edges (schema 2.24.0, R3).
|
|
38
45
|
*
|
|
@@ -144,11 +151,15 @@ export interface ContractRekeySummary {
|
|
|
144
151
|
*/
|
|
145
152
|
export declare function rekeyLegacyContractRefs(schema: CanonicalSchema, rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): ContractRekeySummary;
|
|
146
153
|
/**
|
|
147
|
-
* M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4
|
|
148
|
-
*
|
|
149
|
-
*
|
|
154
|
+
* M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4, TIER-GATED by
|
|
155
|
+
* M-ADR 0208 §4.2 — gated, not skipped: a skipped check fails toward success.
|
|
156
|
+
* platform tier: exactly one row with kind "platform" in the aggregate.
|
|
157
|
+
* organisation tier: exactly one "platform" row PER DISTINCT platform_sdd_id,
|
|
158
|
+
* and every "platform" row must carry a stamp — an empty
|
|
159
|
+
* stamp is unresolved, never a key (0208 §4.4).
|
|
160
|
+
* Exported for testing.
|
|
150
161
|
*/
|
|
151
|
-
export declare function checkPlatformKindUniqueness(systems: readonly CatalogueRow[], findings: FindingsLog): void;
|
|
162
|
+
export declare function checkPlatformKindUniqueness(systems: readonly CatalogueRow[], findings: FindingsLog, tier?: "platform" | "organisation"): void;
|
|
152
163
|
/**
|
|
153
164
|
* Flag c4_systems rows that share name AND description (M-ADR 0189 §4.5.3).
|
|
154
165
|
* PK uniqueness and FK validation cannot see this class: when a sibling's
|
|
@@ -186,9 +197,9 @@ export declare function checkDuplicateSystemIdentity(systems: readonly Catalogue
|
|
|
186
197
|
*
|
|
187
198
|
* Exported for testing.
|
|
188
199
|
*/
|
|
189
|
-
export declare function deriveCrossSddPlatformCapability(productPlatformDeps: readonly CatalogueRow[], capabilityServices: readonly CatalogueRow[], findings: FindingsLog): CatalogueRow[];
|
|
200
|
+
export declare function deriveCrossSddPlatformCapability(productPlatformDeps: readonly CatalogueRow[], capabilityServices: readonly CatalogueRow[], findings: FindingsLog, capabilities?: readonly CatalogueRow[]): CatalogueRow[];
|
|
190
201
|
export declare const __test_only__: {
|
|
191
|
-
|
|
202
|
+
stampColumn: typeof stampColumn;
|
|
192
203
|
parseCatalogueCsv: typeof parseCatalogueCsv;
|
|
193
204
|
};
|
|
194
205
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../../src/lib/catalogue/aggregate.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAuB,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAoB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../../src/lib/catalogue/aggregate.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,10 +30,14 @@ import { writeCatalogue } from "./csv-writer.js";
|
|
|
30
30
|
import { buildLegacyContractIdMap } from "./contract-id.js";
|
|
31
31
|
import { validateWithEngineToLog } from "./engine-validator.js";
|
|
32
32
|
import { FindingsLog } from "./findings.js";
|
|
33
|
+
import { readLockFile } from "../lock-file.js";
|
|
33
34
|
import { mergeEdgeLabel } from "./builders/edge-label-merge.js";
|
|
34
35
|
import { isAsyncContractType } from "./builders/product-platform-dependencies.js";
|
|
35
36
|
import { resolveContractPaths } from "./contract-path-resolution.js";
|
|
36
37
|
import { buildRepoEdges } from "./builders/repo-edges.js";
|
|
38
|
+
import { buildCapabilitySpec } from "./builders/capability-spec.js";
|
|
39
|
+
import { buildFeatureGrounding } from "./builders/feature-grounding.js";
|
|
40
|
+
import { loadFeatureGroundingAliases } from "./feature-grounding-aliases-loader.js";
|
|
37
41
|
import { buildFeatureContract } from "./builders/feature-contract.js";
|
|
38
42
|
import { buildCapabilityEvent } from "./builders/event-edges.js";
|
|
39
43
|
import { discoverVocabulary } from "./application-archetype/vocabulary.js";
|
|
@@ -49,7 +53,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
49
53
|
const rootManifest = await loadSddManifest(sddPath, {
|
|
50
54
|
cliVersion: CLI_VERSION,
|
|
51
55
|
});
|
|
52
|
-
write(`
|
|
56
|
+
write(` root: ${rootManifest.sddId} profile: ${rootManifest.profile} tier: ${rootManifest.profile === "organisation-profile" ? "organisation" : "platform"}\n`);
|
|
53
57
|
// 1. Resolve aggregation-manifest path + load.
|
|
54
58
|
//
|
|
55
59
|
// Two anchors, deliberately, because the terms are different KINDS of path:
|
|
@@ -75,8 +79,9 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
75
79
|
ecosystemRootPath: sddPath,
|
|
76
80
|
});
|
|
77
81
|
if (aggManifest.ecosystemRoot !== rootManifest.sddId) {
|
|
78
|
-
throw new Error(`aggregation manifest
|
|
79
|
-
` does not match
|
|
82
|
+
throw new Error(`aggregation manifest root_sdd_id="${aggManifest.ecosystemRoot}"\n` +
|
|
83
|
+
` does not match the owning SDD's sdd_id="${rootManifest.sddId}"\n` +
|
|
84
|
+
` (rule: root_sdd_id_consistent, M-ADR 0208 §4.3)\n` +
|
|
80
85
|
` (manifest: ${manifestPath})`);
|
|
81
86
|
}
|
|
82
87
|
write(` aggregation manifest: ${relative(sddPath, manifestPath)}\n`);
|
|
@@ -92,7 +97,12 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
92
97
|
// 2. Pull (filesystem only in v1) — pre-checked at manifest load. Now
|
|
93
98
|
// cross-check each sibling's own .sdd/sdd-manifest.yaml + its
|
|
94
99
|
// vendored schema vs the aggregator's.
|
|
95
|
-
await crossCheckSiblings(sddPath, aggManifest.siblings, schema, write);
|
|
100
|
+
const crossCheck = await crossCheckSiblings(sddPath, aggManifest.siblings, schema, write);
|
|
101
|
+
if (aggManifest.rootFieldDeprecated) {
|
|
102
|
+
findings.add("convention", `aggregation manifest uses deprecated key \`ecosystem_root\` — rename to \`root_sdd_id\` (M-ADR 0208 §4.3; alias removed at 2.0.0)`);
|
|
103
|
+
}
|
|
104
|
+
// M-ADR 0208 §4.2/§4.4: the tier decides the platform_sdd_id fill rule.
|
|
105
|
+
const rootIsOrganisation = rootManifest.profile === "organisation-profile";
|
|
96
106
|
// 3. Schema-version alignment was checked in crossCheckSiblings.
|
|
97
107
|
// 4 + 5. Concatenate per-catalogue rows from the ecosystem-root + every
|
|
98
108
|
// sibling, stamping source_sdd_id on node catalogues as we go.
|
|
@@ -101,9 +111,37 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
101
111
|
for (const cat of schema.catalogues) {
|
|
102
112
|
rowsByCatalogue.set(cat.name, []);
|
|
103
113
|
}
|
|
104
|
-
perSiblingCounts.set(rootManifest.sddId, await mergeCatalogues(schema, rowsByCatalogue, join(sddPath, "catalogues"), rootManifest.sddId));
|
|
114
|
+
perSiblingCounts.set(rootManifest.sddId, await mergeCatalogues(schema, rowsByCatalogue, join(sddPath, "catalogues"), rootManifest.sddId, rootIsOrganisation ? null : rootManifest.sddId));
|
|
105
115
|
for (const sib of aggManifest.siblings) {
|
|
106
|
-
|
|
116
|
+
// M-ADR 0208 §4.4 fill rule, by pull path. An empty stamp is UNRESOLVED and is
|
|
117
|
+
// reported below by checkPlatformKindUniqueness; it is never a key value.
|
|
118
|
+
let platformSddId;
|
|
119
|
+
if (!rootIsOrganisation) {
|
|
120
|
+
platformSddId = rootManifest.sddId; // every row in a platform-tier aggregate belongs to it
|
|
121
|
+
}
|
|
122
|
+
else if (sib.level === "organisation") {
|
|
123
|
+
platformSddId = null; // arrived stamped from the child's own platform-tier run
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// CATEGORY decides root-ness, not profile: platform-subsystem is platform-profile
|
|
127
|
+
// too, but it is a MEMBER contained by exactly one platform (M-ADR 0208 §4.1).
|
|
128
|
+
const sibCategory = crossCheck.siblingCategories.get(sib.sddId);
|
|
129
|
+
const declared = crossCheck.siblingDeclaredPlatform.get(sib.sddId);
|
|
130
|
+
if (sibCategory === "platform-root") {
|
|
131
|
+
platformSddId = sib.sddId; // a platform-root is its own platform
|
|
132
|
+
}
|
|
133
|
+
else if (declared !== undefined) {
|
|
134
|
+
platformSddId = declared; // a member that declares its platform gets it
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
platformSddId = null;
|
|
138
|
+
findings.add("coverage", `platform_sdd_id: sibling "${sib.sddId}" (${sibCategory ?? crossCheck.siblingProfiles.get(sib.sddId) ?? "?"}) is a MEMBER pulled directly at organisation tier with no declared platform — rows left unstamped (M-ADR 0208 §4.4; federate it under its platform's aggregate, or set platform_sdd_id in its manifest)`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (crossCheck.deprecatedPlatformField.has(sib.sddId)) {
|
|
142
|
+
findings.add("convention", `sibling "${sib.sddId}" manifest uses deprecated key \`ecosystem_root\` — rename to \`platform_sdd_id\` (M-ADR 0208 §4.3; alias removed at 2.0.0)`);
|
|
143
|
+
}
|
|
144
|
+
perSiblingCounts.set(sib.sddId, await mergeCatalogues(schema, rowsByCatalogue, sib.cataloguesPath, sib.sddId, platformSddId));
|
|
107
145
|
}
|
|
108
146
|
// 5a. Derive cross_sdd_platform_capability — federated edge, schema 2.9.0.
|
|
109
147
|
// Joins each consuming SDD's product_platform_dependencies rows against
|
|
@@ -113,7 +151,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
113
151
|
// emit nothing for this catalogue; the aggregator owns it entirely.
|
|
114
152
|
// See content/plans/v0-9-0-cohort-sweep-derived-bundle.md
|
|
115
153
|
// §"Cross-SDD platform-capability-edge" + ADR 0134.
|
|
116
|
-
const crossSddRows = deriveCrossSddPlatformCapability(rowsByCatalogue.get("product_platform_dependencies") ?? [], rowsByCatalogue.get("capability_service") ?? [], findings);
|
|
154
|
+
const crossSddRows = deriveCrossSddPlatformCapability(rowsByCatalogue.get("product_platform_dependencies") ?? [], rowsByCatalogue.get("capability_service") ?? [], findings, rowsByCatalogue.get("capabilities") ?? []);
|
|
117
155
|
rowsByCatalogue.set("cross_sdd_platform_capability", crossSddRows);
|
|
118
156
|
// Every SDD root in this aggregation, by sdd_id: the ecosystem root plus each
|
|
119
157
|
// sibling. This is precisely what the app tier does not have and cannot obtain.
|
|
@@ -190,15 +228,96 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
190
228
|
// it, and appending finished per-member joins never creates a row no input produced.
|
|
191
229
|
//
|
|
192
230
|
// §4.1's rule (a catalogue qualifies when it is derived AND at least one join input can
|
|
193
|
-
// carry a cross-SDD reference)
|
|
231
|
+
// carry a cross-SDD reference) selects five: feature_repo, repo_capability, repo_contract,
|
|
194
232
|
// feature_contract, capability_event. One shared mechanism (rederiveAppend below) runs
|
|
195
|
-
// all
|
|
233
|
+
// all five so a sixth qualifying catalogue is a new call, not new merge logic.
|
|
234
|
+
//
|
|
235
|
+
// feature_repo moved into scope under §4.1's "automatic movement" clause: its derived leg
|
|
236
|
+
// resolves free-text evidence tokens (M-ADR 0143 §4.4) against `repositories.id`, and the
|
|
237
|
+
// merged repositories table carries every sibling's repos — a platform spec that names a
|
|
238
|
+
// repo registered by a MEMBER SDD can only resolve here. The spec text is not a catalogue,
|
|
239
|
+
// so the derived leg is re-run the way §4.2 describes for every builder: the two feature
|
|
240
|
+
// extractors are invoked over each manifest-read contributor's docs with the MERGED
|
|
241
|
+
// repository / service / capability id sets as their resolution inputs. Only their
|
|
242
|
+
// feature_repo output is kept; feature rows and their per-row findings were already
|
|
243
|
+
// emitted at member tier and are not repeated. level:organisation siblings expose no docs
|
|
244
|
+
// (pre-built catalogues-aggregated/ only) and are skipped — their rows arrived from their
|
|
245
|
+
// own platform-tier run, where this pass has already happened.
|
|
196
246
|
//
|
|
197
247
|
// §4.2: only the derived leg is re-run — each builder is invoked with its authored-leg
|
|
198
248
|
// inputs (configs / manifests) empty, which runs zero iterations of that path and yields
|
|
199
249
|
// the derived rows alone; the authored rows are already correct via concatenation and are
|
|
200
250
|
// left untouched. §4.2/§4.3: rederiveAppend adds the result to what concatenation already
|
|
201
251
|
// produced, keeping the concatenated row on any primary-key collision — never a replace.
|
|
252
|
+
{
|
|
253
|
+
const mergedRepoIds = new Set((rowsByCatalogue.get("repositories") ?? []).map((r) => String(r["id"])));
|
|
254
|
+
const mergedServiceIds = new Set((rowsByCatalogue.get("services") ?? []).map((r) => String(r["id"])));
|
|
255
|
+
const mergedCapabilityIds = new Set((rowsByCatalogue.get("capabilities") ?? []).map((r) => String(r["id"])));
|
|
256
|
+
const mergedFeatureIds = new Set((rowsByCatalogue.get("features") ?? []).map((r) => String(r["id"])));
|
|
257
|
+
// Re-derivation FILLS ABSENCES; it does not widen. A feature its own member
|
|
258
|
+
// build already anchored keeps exactly those anchors: re-running bare-token
|
|
259
|
+
// resolution against every sibling's repo ids over an already-resolved row
|
|
260
|
+
// mostly finds collisions (measured on one estate: 13 of the 43 rows it would
|
|
261
|
+
// have added to anchored features resolved to repo ids like `web`, `proxy`,
|
|
262
|
+
// `service`, `batch` owned by unrelated SDDs; of the 261 rows added to
|
|
263
|
+
// UNANCHORED features, 4 did). The motivating case — and §4.1's clause — is a
|
|
264
|
+
// row no member could produce, which is exactly an unanchored feature.
|
|
265
|
+
const anchoredFeatureIds = new Set((rowsByCatalogue.get("feature_repo") ?? []).map((r) => String(r["feature_id"])));
|
|
266
|
+
const contributors = [
|
|
267
|
+
{ sddPath, profile: rootManifest.profile, sddId: rootManifest.sddId },
|
|
268
|
+
];
|
|
269
|
+
for (const sib of aggManifest.siblings) {
|
|
270
|
+
if (sib.level === "organisation")
|
|
271
|
+
continue;
|
|
272
|
+
const profile = crossCheck.siblingProfiles.get(sib.sddId);
|
|
273
|
+
if (profile === undefined)
|
|
274
|
+
continue;
|
|
275
|
+
contributors.push({ sddPath: sib.sddRoot, profile, sddId: sib.sddId });
|
|
276
|
+
}
|
|
277
|
+
const rederivedFeatureRepo = [];
|
|
278
|
+
// Both extractors, over several contributors, can yield the same edge (the same
|
|
279
|
+
// feature grounded in the same repo from two tables); rederiveAppend dedups only
|
|
280
|
+
// against the CONCATENATED rows, so dedup the re-derived set itself here or the
|
|
281
|
+
// duplicate-PK check fails the aggregate on two copies of one correct fact.
|
|
282
|
+
const rederivedIds = new Set();
|
|
283
|
+
const scratch = new FindingsLog(); // member-tier findings already emitted; not repeated
|
|
284
|
+
for (const c of contributors) {
|
|
285
|
+
const aliases = await loadFeatureGroundingAliases(c.sddPath);
|
|
286
|
+
const common = {
|
|
287
|
+
sddPath: c.sddPath,
|
|
288
|
+
findings: scratch,
|
|
289
|
+
profile: c.profile,
|
|
290
|
+
productId: c.sddId,
|
|
291
|
+
knownCapabilityIds: mergedCapabilityIds,
|
|
292
|
+
knownServiceIds: mergedServiceIds,
|
|
293
|
+
knownRepoKeys: mergedRepoIds,
|
|
294
|
+
existingFeatureIds: new Set(),
|
|
295
|
+
aliases,
|
|
296
|
+
};
|
|
297
|
+
const [grounding, spec] = await Promise.all([
|
|
298
|
+
buildFeatureGrounding(common),
|
|
299
|
+
buildCapabilitySpec(common),
|
|
300
|
+
]);
|
|
301
|
+
for (const fr of [...grounding.featureRepoRows, ...spec.featureRepoRows]) {
|
|
302
|
+
const featureId = String(fr["feature_id"] ?? "");
|
|
303
|
+
if (!mergedFeatureIds.has(featureId))
|
|
304
|
+
continue;
|
|
305
|
+
if (anchoredFeatureIds.has(featureId))
|
|
306
|
+
continue;
|
|
307
|
+
const id = String(fr["id"] ?? "");
|
|
308
|
+
if (id === "" || rederivedIds.has(id))
|
|
309
|
+
continue;
|
|
310
|
+
rederivedIds.add(id);
|
|
311
|
+
rederivedFeatureRepo.push(fr);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const before = (rowsByCatalogue.get("feature_repo") ?? []).length;
|
|
315
|
+
rederiveAppend(rowsByCatalogue, "feature_repo", rederivedFeatureRepo);
|
|
316
|
+
const added = (rowsByCatalogue.get("feature_repo") ?? []).length - before;
|
|
317
|
+
if (added > 0) {
|
|
318
|
+
findings.add("coverage", `feature_repo: ${added} row(s) re-derived at aggregate tier for features that had NO anchor at member tier — evidence tokens naming a repo registered by another contributor resolve only against the merged repositories; already-anchored features are not widened (M-ADR 0198 §4.1 as amended; ${contributors.length} contributor doc trees scanned)`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
202
321
|
const repoEdges = buildRepoEdges({
|
|
203
322
|
findings,
|
|
204
323
|
configs: [],
|
|
@@ -266,7 +385,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
266
385
|
// tier alongside an existing one), each contributes the same method ADR ids. Merge
|
|
267
386
|
// them (first-seen wins) rather than failing the duplicate-PK check. Platform
|
|
268
387
|
// / domain ADRs are NOT deduped — a collision there is a genuine error.
|
|
269
|
-
dedupMethodScopedAdrs(rowsByCatalogue, findings);
|
|
388
|
+
await dedupMethodScopedAdrs(rowsByCatalogue, findings, sddRootsById);
|
|
270
389
|
// 5c. Federated reverse-metadata enrichment (ADR 0163 P2). Both reference
|
|
271
390
|
// adopters centralise the service inventory in the platform-root, but for
|
|
272
391
|
// reverse-method federations the per-service evidence (service-architecture.md)
|
|
@@ -306,7 +425,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
306
425
|
`${adjudication.systemFallbackEdges} stuck edges → system_system_dependency at L1), ` +
|
|
307
426
|
`${adjudication.exempted} exempted, ${adjudication.unverified} unverified\n`);
|
|
308
427
|
// 5g. Platform-kind uniqueness (M-ADR 0133 §4.2c open hygiene).
|
|
309
|
-
checkPlatformKindUniqueness(rowsByCatalogue.get("c4_systems") ?? [], findings);
|
|
428
|
+
checkPlatformKindUniqueness(rowsByCatalogue.get("c4_systems") ?? [], findings, rootIsOrganisation ? "organisation" : "platform");
|
|
310
429
|
// 5h. M-ADR 0203 §4.4 cross-SDD re-key: the contract-id migration derives
|
|
311
430
|
// per-SDD, but contract-FK edges cross SDD boundaries — a federated SDD
|
|
312
431
|
// that owns no contracts has an empty map, so its references keep the 2.x
|
|
@@ -511,20 +630,58 @@ export function dedupExternalSystems(rowsByCatalogue, findings) {
|
|
|
511
630
|
}
|
|
512
631
|
rowsByCatalogue.set("c4_external_systems", [...byId.values()]);
|
|
513
632
|
}
|
|
514
|
-
// Method-
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
633
|
+
// Method-DISTRIBUTED ADRs are the shared method baseline, identical across
|
|
634
|
+
// method-based SDDs. Dedupe by id (first-seen wins), preserving order and leaving
|
|
635
|
+
// adopter-AUTHORED platform/domain ADRs untouched — a collision there is a genuine
|
|
636
|
+
// error and MUST still fail the duplicate-PK check. Mirrors dedupExternalSystems.
|
|
637
|
+
//
|
|
638
|
+
// WHAT COUNTS AS METHOD-DISTRIBUTED (M-ADR 0208 / gov F-E): a row whose doc_path is
|
|
639
|
+
// BUNDLE-MANAGED in the contributing SDD's method-baseline.lock. Provenance, not
|
|
640
|
+
// path prefix. The earlier test `startsWith("docs/method/adr/")` was a proxy for
|
|
641
|
+
// the same property and it failed the first time two platform-roots aggregated:
|
|
642
|
+
// the baseline also distributes docs/method/platform-adr/ (13 ADR-emitting files
|
|
643
|
+
// in platform-baseline v2.3.28), which the prefix missed, so identical
|
|
644
|
+
// bundle-managed rows from two roots collided as if adopter-authored. A prefix
|
|
645
|
+
// extension would have passed every test available that day and merged genuinely
|
|
646
|
+
// authored platform ADRs the next time an adopter wrote a colliding one; the lock
|
|
647
|
+
// already records exactly the property we mean.
|
|
648
|
+
//
|
|
649
|
+
// FALLBACK: a contributing SDD with no lock (fixtures; a repo that has never been
|
|
650
|
+
// synced) keeps the historical prefix test, so it is never LESS deduped than before.
|
|
651
|
+
async function dedupMethodScopedAdrs(rowsByCatalogue, findings, sddRootsById) {
|
|
519
652
|
const rows = rowsByCatalogue.get("adrs");
|
|
520
653
|
if (rows === undefined || rows.length === 0)
|
|
521
654
|
return;
|
|
655
|
+
// source_sdd_id -> set of bundle-managed paths, or null when that SDD has no lock.
|
|
656
|
+
const managedBySdd = new Map();
|
|
657
|
+
for (const [sddId, root] of sddRootsById) {
|
|
658
|
+
const lock = await readLockFile(root);
|
|
659
|
+
if (lock === null) {
|
|
660
|
+
managedBySdd.set(sddId, null);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const managed = new Set();
|
|
664
|
+
for (const b of lock.bundles)
|
|
665
|
+
for (const mp of b.managedPaths)
|
|
666
|
+
managed.add(mp);
|
|
667
|
+
managedBySdd.set(sddId, managed);
|
|
668
|
+
}
|
|
669
|
+
const isMethodDistributed = (row) => {
|
|
670
|
+
const docPath = String(row["doc_path"] ?? "");
|
|
671
|
+
if (docPath === "")
|
|
672
|
+
return false;
|
|
673
|
+
const managed = managedBySdd.get(String(row["source_sdd_id"] ?? ""));
|
|
674
|
+
if (managed === undefined || managed === null) {
|
|
675
|
+
// No lock for this contributor: historical prefix test.
|
|
676
|
+
return docPath.startsWith("docs/method/adr/");
|
|
677
|
+
}
|
|
678
|
+
return managed.has(docPath);
|
|
679
|
+
};
|
|
522
680
|
const seen = new Map();
|
|
523
681
|
const out = [];
|
|
524
682
|
for (const row of rows) {
|
|
525
|
-
const isMethod = String(row["doc_path"] ?? "").startsWith("docs/method/adr/");
|
|
526
683
|
const id = String(row["id"] ?? "");
|
|
527
|
-
if (!
|
|
684
|
+
if (!isMethodDistributed(row) || id === "") {
|
|
528
685
|
out.push(row);
|
|
529
686
|
continue;
|
|
530
687
|
}
|
|
@@ -538,10 +695,10 @@ function dedupMethodScopedAdrs(rowsByCatalogue, findings) {
|
|
|
538
695
|
const a = String(existing[attr] ?? "");
|
|
539
696
|
const b = String(row[attr] ?? "");
|
|
540
697
|
if (a !== "" && b !== "" && a !== b) {
|
|
541
|
-
findings.add("convention", `adrs: method-
|
|
698
|
+
findings.add("convention", `adrs: method-distributed id "${id}" has conflicting ${attr} across SDDs [${String(existing["source_sdd_id"] ?? "?")}, ${String(row["source_sdd_id"] ?? "?")}] — first-seen kept`);
|
|
542
699
|
}
|
|
543
700
|
}
|
|
544
|
-
// duplicate method ADR — already kept first-seen; drop this copy.
|
|
701
|
+
// duplicate method-distributed ADR — already kept first-seen; drop this copy.
|
|
545
702
|
}
|
|
546
703
|
rowsByCatalogue.set("adrs", out);
|
|
547
704
|
}
|
|
@@ -686,7 +843,9 @@ function findDuplicatePrimaryKeys(schema, rowsByCatalogue) {
|
|
|
686
843
|
}
|
|
687
844
|
return out;
|
|
688
845
|
}
|
|
689
|
-
async function mergeCatalogues(schema, rowsByCatalogue, cataloguesDir, sourceSddId
|
|
846
|
+
async function mergeCatalogues(schema, rowsByCatalogue, cataloguesDir, sourceSddId,
|
|
847
|
+
/** M-ADR 0208 §4.4: value to fill into `platform_sdd_id` when empty; null = do not fill. */
|
|
848
|
+
platformSddId) {
|
|
690
849
|
const counts = new Map();
|
|
691
850
|
let dirExists = true;
|
|
692
851
|
try {
|
|
@@ -717,26 +876,35 @@ async function mergeCatalogues(schema, rowsByCatalogue, cataloguesDir, sourceSdd
|
|
|
717
876
|
}
|
|
718
877
|
throw err;
|
|
719
878
|
}
|
|
720
|
-
const stamped =
|
|
879
|
+
const stamped = stampColumn(cat, stampColumn(cat, parsed.rows, "source_sdd_id", sourceSddId), "platform_sdd_id", platformSddId);
|
|
721
880
|
rowsByCatalogue.get(cat.name).push(...stamped);
|
|
722
881
|
counts.set(cat.name, stamped.length);
|
|
723
882
|
}
|
|
724
883
|
return counts;
|
|
725
884
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
885
|
+
/**
|
|
886
|
+
* Stamp `column` = `value` on every row that does not already carry one
|
|
887
|
+
* (preserve-if-set — chained aggregation, M-ADR 0134 §4.5 step 4). Used for
|
|
888
|
+
* `source_sdd_id` and, per M-ADR 0208 §4.4, for `platform_sdd_id`. A null value
|
|
889
|
+
* means "do not fill" (organisation tier, where the stamp arrived from below or
|
|
890
|
+
* is legitimately unresolved). No-op when the catalogue lacks the column.
|
|
891
|
+
*/
|
|
892
|
+
function stampColumn(catalogue, rows, column, value) {
|
|
893
|
+
const hasColumn = catalogue.columns.some((c) => c.name === column);
|
|
894
|
+
if (!hasColumn || value === null)
|
|
729
895
|
return rows.map((r) => ({ ...r }));
|
|
730
896
|
return rows.map((r) => {
|
|
731
|
-
if (r[
|
|
732
|
-
// Already stamped — preserve (chained aggregation).
|
|
897
|
+
if (String(r[column] ?? "").trim() !== "")
|
|
733
898
|
return { ...r };
|
|
734
|
-
}
|
|
735
|
-
return { ...r, source_sdd_id: sourceSddId };
|
|
899
|
+
return { ...r, [column]: value };
|
|
736
900
|
});
|
|
737
901
|
}
|
|
738
902
|
async function crossCheckSiblings(ecosystemRootPath, siblings, aggregatorSchema, write) {
|
|
739
903
|
const findings = new FindingsLog();
|
|
904
|
+
const siblingProfiles = new Map();
|
|
905
|
+
const siblingCategories = new Map();
|
|
906
|
+
const siblingDeclaredPlatform = new Map();
|
|
907
|
+
const deprecatedPlatformField = new Set();
|
|
740
908
|
for (const sib of siblings) {
|
|
741
909
|
let sibProfile;
|
|
742
910
|
if (sib.level === "organisation") {
|
|
@@ -761,7 +929,13 @@ async function crossCheckSiblings(ecosystemRootPath, siblings, aggregatorSchema,
|
|
|
761
929
|
` (rule: sibling_id_matches_sibling_manifest)`);
|
|
762
930
|
}
|
|
763
931
|
sibProfile = sibManifest.profile;
|
|
932
|
+
siblingCategories.set(sib.sddId, sibManifest.category);
|
|
933
|
+
if (sibManifest.platformSddId)
|
|
934
|
+
siblingDeclaredPlatform.set(sib.sddId, sibManifest.platformSddId);
|
|
935
|
+
if (sibManifest.platformFieldDeprecated)
|
|
936
|
+
deprecatedPlatformField.add(sib.sddId);
|
|
764
937
|
}
|
|
938
|
+
siblingProfiles.set(sib.sddId, sibProfile);
|
|
765
939
|
// Cross-check sibling's vendored catalogue-schema.yaml against the
|
|
766
940
|
// declared compat range. The aggregator's own canonical schema is the
|
|
767
941
|
// source of truth; the sibling's vendored copy is informational and
|
|
@@ -784,7 +958,7 @@ async function crossCheckSiblings(ecosystemRootPath, siblings, aggregatorSchema,
|
|
|
784
958
|
}
|
|
785
959
|
write(` ✓ ${sib.sddId} (${sibProfile})\n`);
|
|
786
960
|
}
|
|
787
|
-
return { findings };
|
|
961
|
+
return { findings, siblingProfiles, siblingCategories, siblingDeclaredPlatform, deprecatedPlatformField };
|
|
788
962
|
}
|
|
789
963
|
async function findVendoredSchema(sddRoot) {
|
|
790
964
|
// Adopters typically vendor canonical schema indirectly via sdd-cli.
|
|
@@ -1681,18 +1855,44 @@ export function rekeyLegacyContractRefs(schema, rowsByCatalogue, findings) {
|
|
|
1681
1855
|
return { rekeyed, deduped, ambiguous };
|
|
1682
1856
|
}
|
|
1683
1857
|
/**
|
|
1684
|
-
* M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1858
|
+
* M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4, TIER-GATED by
|
|
1859
|
+
* M-ADR 0208 §4.2 — gated, not skipped: a skipped check fails toward success.
|
|
1860
|
+
* platform tier: exactly one row with kind "platform" in the aggregate.
|
|
1861
|
+
* organisation tier: exactly one "platform" row PER DISTINCT platform_sdd_id,
|
|
1862
|
+
* and every "platform" row must carry a stamp — an empty
|
|
1863
|
+
* stamp is unresolved, never a key (0208 §4.4).
|
|
1864
|
+
* Exported for testing.
|
|
1687
1865
|
*/
|
|
1688
|
-
export function checkPlatformKindUniqueness(systems, findings) {
|
|
1689
|
-
const plats = systems
|
|
1690
|
-
|
|
1866
|
+
export function checkPlatformKindUniqueness(systems, findings, tier = "platform") {
|
|
1867
|
+
const plats = systems.filter((s) => String(s["kind"]) === "platform");
|
|
1868
|
+
if (tier === "platform") {
|
|
1869
|
+
const ids = plats.map((s) => String(s["id"])).sort((a, b) => a.localeCompare(b));
|
|
1870
|
+
if (ids.length > 1) {
|
|
1871
|
+
findings.add("convention", `c4_systems: ${ids.length} rows with kind "platform" [${ids.join(", ")}] — ` +
|
|
1872
|
+
`exactly one per platform (M-ADR 0208 §4.2); this is a platform-tier aggregate`);
|
|
1873
|
+
}
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const unstamped = plats
|
|
1877
|
+
.filter((s) => String(s["platform_sdd_id"] ?? "").trim() === "")
|
|
1691
1878
|
.map((s) => String(s["id"]))
|
|
1692
1879
|
.sort((a, b) => a.localeCompare(b));
|
|
1693
|
-
if (
|
|
1694
|
-
findings.add("
|
|
1695
|
-
|
|
1880
|
+
if (unstamped.length > 0) {
|
|
1881
|
+
findings.add("coverage", `c4_systems: ${unstamped.length} "platform" row(s) with EMPTY platform_sdd_id at organisation tier [${unstamped.join(", ")}] — unresolved, not a key (M-ADR 0208 §4.4)`);
|
|
1882
|
+
}
|
|
1883
|
+
const byPlatform = new Map();
|
|
1884
|
+
for (const s of plats) {
|
|
1885
|
+
const key = String(s["platform_sdd_id"] ?? "").trim();
|
|
1886
|
+
if (key === "")
|
|
1887
|
+
continue;
|
|
1888
|
+
const list = byPlatform.get(key) ?? [];
|
|
1889
|
+
list.push(String(s["id"]));
|
|
1890
|
+
byPlatform.set(key, list);
|
|
1891
|
+
}
|
|
1892
|
+
for (const [platform, ids] of byPlatform) {
|
|
1893
|
+
if (ids.length > 1) {
|
|
1894
|
+
findings.add("convention", `c4_systems: platform "${platform}" has ${ids.length} rows with kind "platform" [${ids.sort((a, b) => a.localeCompare(b)).join(", ")}] — exactly one per platform (M-ADR 0208 §4.2)`);
|
|
1895
|
+
}
|
|
1696
1896
|
}
|
|
1697
1897
|
}
|
|
1698
1898
|
/**
|
|
@@ -1767,7 +1967,32 @@ async function fileExists(absPath) {
|
|
|
1767
1967
|
*
|
|
1768
1968
|
* Exported for testing.
|
|
1769
1969
|
*/
|
|
1770
|
-
export function deriveCrossSddPlatformCapability(productPlatformDeps, capabilityServices, findings) {
|
|
1970
|
+
export function deriveCrossSddPlatformCapability(productPlatformDeps, capabilityServices, findings, capabilities = []) {
|
|
1971
|
+
// The edge's PLATFORM LEG. `capability_service` has no source_sdd_id column
|
|
1972
|
+
// (schema: id, capability_id, service_id, role), so the row that knows which
|
|
1973
|
+
// platform owns a capability is `capabilities` — its platform_sdd_id
|
|
1974
|
+
// (M-ADR 0208 §4.4) or, on pre-0208 output, its source_sdd_id. From
|
|
1975
|
+
// 2.9.0 (v0.22.0) until 1.19.2 this read `match["source_sdd_id"]` off the
|
|
1976
|
+
// capability_service row and was therefore "" on every emitted row, in
|
|
1977
|
+
// every estate — the id carried a visible empty slot (`a__b____cap__svc`).
|
|
1978
|
+
const platformByCapability = new Map();
|
|
1979
|
+
for (const cap of capabilities) {
|
|
1980
|
+
const id = String(cap["id"] ?? "").trim();
|
|
1981
|
+
if (id === "")
|
|
1982
|
+
continue;
|
|
1983
|
+
const leg = String(cap["platform_sdd_id"] ?? "").trim() ||
|
|
1984
|
+
String(cap["source_sdd_id"] ?? "").trim();
|
|
1985
|
+
if (leg !== "" && !platformByCapability.has(id))
|
|
1986
|
+
platformByCapability.set(id, leg);
|
|
1987
|
+
}
|
|
1988
|
+
const unresolvedLegs = new Set();
|
|
1989
|
+
const resolvePlatformLeg = (match) => {
|
|
1990
|
+
const capId = String(match["capability_id"] ?? "");
|
|
1991
|
+
const leg = platformByCapability.get(capId) ?? String(match["source_sdd_id"] ?? "").trim();
|
|
1992
|
+
if (leg === "" && capId !== "")
|
|
1993
|
+
unresolvedLegs.add(capId);
|
|
1994
|
+
return leg;
|
|
1995
|
+
};
|
|
1771
1996
|
// Index capability_service rows by service_id for O(1) lookup. A service
|
|
1772
1997
|
// may belong to multiple capabilities — keep them all so a dependency
|
|
1773
1998
|
// can resolve into every capability that owns its service. Filter out
|
|
@@ -1827,7 +2052,7 @@ export function deriveCrossSddPlatformCapability(productPlatformDeps, capability
|
|
|
1827
2052
|
continue;
|
|
1828
2053
|
}
|
|
1829
2054
|
for (const match of capMatches) {
|
|
1830
|
-
const platformSddId =
|
|
2055
|
+
const platformSddId = resolvePlatformLeg(match);
|
|
1831
2056
|
const id = [
|
|
1832
2057
|
consumingSddId,
|
|
1833
2058
|
consumingProductId,
|
|
@@ -1875,7 +2100,7 @@ export function deriveCrossSddPlatformCapability(productPlatformDeps, capability
|
|
|
1875
2100
|
}
|
|
1876
2101
|
}
|
|
1877
2102
|
for (const match of matches) {
|
|
1878
|
-
const platformSddId =
|
|
2103
|
+
const platformSddId = resolvePlatformLeg(match);
|
|
1879
2104
|
const platformCapabilityId = String(match["capability_id"] ?? "");
|
|
1880
2105
|
if (platformCapabilityId === "")
|
|
1881
2106
|
continue;
|
|
@@ -1899,9 +2124,13 @@ export function deriveCrossSddPlatformCapability(productPlatformDeps, capability
|
|
|
1899
2124
|
});
|
|
1900
2125
|
}
|
|
1901
2126
|
}
|
|
2127
|
+
if (unresolvedLegs.size > 0) {
|
|
2128
|
+
const caps = [...unresolvedLegs].sort((a, b) => a.localeCompare(b));
|
|
2129
|
+
findings.add("coverage", `cross_sdd_platform_capability: platform leg (platform_sdd_id) unresolved for ${caps.length} capability id(s) [${caps.join(", ")}] — no capabilities row carries platform_sdd_id or source_sdd_id for them; the edge id has an empty slot`);
|
|
2130
|
+
}
|
|
1902
2131
|
return rows;
|
|
1903
2132
|
}
|
|
1904
2133
|
// Used by tests that want to round-trip CSV through the aggregator's
|
|
1905
2134
|
// stamping logic without writing files.
|
|
1906
|
-
export const __test_only__ = {
|
|
2135
|
+
export const __test_only__ = { stampColumn, parseCatalogueCsv };
|
|
1907
2136
|
//# sourceMappingURL=aggregate.js.map
|