@sdd-method/maintain-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/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +28 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +123 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/bundle.js +123 -0
- package/dist/lib/catalogue/aggregate.js +2404 -0
- package/dist/lib/catalogue/aggregation-manifest-loader.js +173 -0
- package/dist/lib/catalogue/application-archetype/expectations.js +119 -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/contract-path-resolution.js +149 -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-item-coverage.js +142 -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 +69 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/graph-run.js +64 -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/ci-binding/index.js +220 -0
- package/dist/lib/ci-binding/module.js +204 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/context-pull/index.js +151 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/hooks-seed/copilot.js +167 -0
- package/dist/lib/hooks-seed/entries.js +47 -0
- package/dist/lib/hooks-seed/index.js +17 -0
- package/dist/lib/hooks-seed/lifecycle-mapping.js +28 -0
- package/dist/lib/hooks-seed/merge.js +128 -0
- package/dist/lib/hooks-seed/types.js +20 -0
- package/dist/lib/init/ci-workflow.js +98 -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/mcp-seed/entries.js +14 -0
- package/dist/lib/mcp-seed/index.js +28 -0
- package/dist/lib/mcp-seed/merge.js +99 -0
- package/dist/lib/mcp-seed/types.js +13 -0
- package/dist/lib/pack/builder.js +303 -0
- package/dist/lib/pack/index.js +87 -0
- package/dist/lib/pack/leakage.js +144 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/publish-transform.js +233 -0
- package/dist/lib/pack/reference-closure.js +125 -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/repo-kind.js +129 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/snapshot/catalogue-diff.js +219 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/sync/adr-transitions.js +250 -0
- package/dist/lib/sync/apply.js +91 -0
- package/dist/lib/sync/briefing-merge.js +90 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/bundle-source.js +61 -0
- package/dist/lib/sync/config.js +72 -0
- package/dist/lib/sync/copilot-hooks-merge.js +108 -0
- package/dist/lib/sync/diff.js +251 -0
- package/dist/lib/sync/index.js +818 -0
- package/dist/lib/sync/settings-merge.js +216 -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-bin.js +8 -0
- package/dist/maintain-program.js +91 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/catalogue-maintain.js +110 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-build.js +31 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/sync.js +110 -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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage mode — `sdd-cli catalogue validate --coverage` (A4 CS-2).
|
|
3
|
+
*
|
|
4
|
+
* Runs the thirteen engine coverage predicates against a catalogues
|
|
5
|
+
* directory and prints EXACTLY the lines the reference shell scripts
|
|
6
|
+
* printed (coverage-render.ts holds the text contract), in a fixed block
|
|
7
|
+
* order: the four catalogue-classics (repo-capability, service-capability,
|
|
8
|
+
* event, contract), then the nine Phase-0 gate checks in the
|
|
9
|
+
* catalogue-integrity-gate's step order. Nothing else is written to
|
|
10
|
+
* stdout — the gate swaps N script invocations for one CLI call and the
|
|
11
|
+
* stream stays parseable by the same baseline machinery.
|
|
12
|
+
*
|
|
13
|
+
* Exit code is 0 in normal operation, exactly as the reference scripts:
|
|
14
|
+
* gating is the findings baseline's job, never this mode's. A classifier
|
|
15
|
+
* probe failure throws (the references exit 2).
|
|
16
|
+
*
|
|
17
|
+
* The one deliberately-unported check is check-repo-config-coverage.sh:
|
|
18
|
+
* it reads the AUTHORING surface, is text-shaped by design (A0's
|
|
19
|
+
* classification), and stays shell.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join, resolve } from "node:path";
|
|
23
|
+
import { readCatalogueCsv } from "./csv-reader.js";
|
|
24
|
+
import { createEngineSession } from "./engine-validator.js";
|
|
25
|
+
import { loadCanonicalSchema } from "./schema-loader.js";
|
|
26
|
+
import { runContractCoverage, runEventCoverage, runRepoCapabilityCoverage, runServiceCapabilityCoverage, } from "./coverage-predicates.js";
|
|
27
|
+
import { runCapabilityEventCheck, runDataModelEntitiesCheck, runEventCatalogueCheck, runFeatureEventCheck, runFeatureSpecPromotionCheck, runFeatureSurfaceDuplicationCheck, runGlossaryXrefCheck, runPlatformDependenciesCheck, runRelationshipCoherenceCheck, runRepoEdgesCheck, } from "./gate-checks.js";
|
|
28
|
+
import { runScoresCoverage } from "./scores-coverage.js";
|
|
29
|
+
import { renderScaffoldDelta, runScaffoldDelta } from "./scaffold-delta.js";
|
|
30
|
+
import { renderStalenessVerdicts, runStalenessVerdicts } from "./staleness-verdicts.js";
|
|
31
|
+
import { renderCapabilityEventCheck, renderContractCoverage, renderDataModelEntitiesCheck, renderEventCatalogueCheck, renderEventCoverage, renderFeatureEventCheck, renderFeatureSpecPromotionCheck, renderFeatureSurfaceDuplicationCheck, renderGlossaryXrefCheck, renderPlatformDependenciesCheck, renderRelationshipCoherenceCheck, renderRepoCapability, renderRepoEdgesCheck, renderScoresCoverage, renderServiceCapability, } from "./coverage-render.js";
|
|
32
|
+
export async function runCatalogueCoverage(inputs, write) {
|
|
33
|
+
const sddRoot = resolve(inputs.sddPath ?? process.cwd());
|
|
34
|
+
const dir = resolve(sddRoot, inputs.cataloguesDir ?? "catalogues");
|
|
35
|
+
const tier = inputs.tier ?? "member";
|
|
36
|
+
const schema = await loadCanonicalSchema();
|
|
37
|
+
const present = (name) => existsSync(join(dir, `${name}.csv`));
|
|
38
|
+
const rowsByCatalogue = new Map();
|
|
39
|
+
for (const cat of schema.catalogues) {
|
|
40
|
+
if (!present(cat.name))
|
|
41
|
+
continue;
|
|
42
|
+
const parsed = await readCatalogueCsv(join(dir, `${cat.name}.csv`));
|
|
43
|
+
rowsByCatalogue.set(cat.name, parsed.rows);
|
|
44
|
+
}
|
|
45
|
+
const session = await createEngineSession(schema, rowsByCatalogue);
|
|
46
|
+
const lines = [];
|
|
47
|
+
try {
|
|
48
|
+
// The four catalogue-classics, each anchored on its population file
|
|
49
|
+
// exactly as the reference scripts anchor (absent file → silent).
|
|
50
|
+
if (present("repositories")) {
|
|
51
|
+
const r = await runRepoCapabilityCoverage(session);
|
|
52
|
+
lines.push(...renderRepoCapability(r.findings, r.metrics));
|
|
53
|
+
}
|
|
54
|
+
if (present("services")) {
|
|
55
|
+
const r = await runServiceCapabilityCoverage(session, {
|
|
56
|
+
tier,
|
|
57
|
+
repoCapabilityAvailable: present("repo_capability"),
|
|
58
|
+
});
|
|
59
|
+
lines.push(...renderServiceCapability(r.findings, r.metrics, tier));
|
|
60
|
+
}
|
|
61
|
+
if (present("events")) {
|
|
62
|
+
const r = await runEventCoverage(session, { tier });
|
|
63
|
+
lines.push(...renderEventCoverage(r, tier));
|
|
64
|
+
}
|
|
65
|
+
if (present("contracts")) {
|
|
66
|
+
const r = await runContractCoverage(session, { tier, sddRoot });
|
|
67
|
+
lines.push(...renderContractCoverage(r, tier));
|
|
68
|
+
}
|
|
69
|
+
// The nine Phase-0 gate checks, in the gate's step order.
|
|
70
|
+
const eventCatalogue = await runEventCatalogueCheck(session, {
|
|
71
|
+
sddRoot,
|
|
72
|
+
eventsPresent: present("events"),
|
|
73
|
+
});
|
|
74
|
+
lines.push(...renderEventCatalogueCheck(eventCatalogue.gaps));
|
|
75
|
+
lines.push(...renderRepoEdgesCheck(await runRepoEdgesCheck(session, { sddRoot })));
|
|
76
|
+
lines.push(...renderGlossaryXrefCheck((await runGlossaryXrefCheck(session, {
|
|
77
|
+
sddRoot,
|
|
78
|
+
xrefPresent: present("glossary_xref"),
|
|
79
|
+
})).finding));
|
|
80
|
+
lines.push(...renderDataModelEntitiesCheck(await runDataModelEntitiesCheck(session)));
|
|
81
|
+
lines.push(...renderCapabilityEventCheck(await runCapabilityEventCheck(session)));
|
|
82
|
+
lines.push(...renderFeatureEventCheck(await runFeatureEventCheck(session)));
|
|
83
|
+
lines.push(...renderPlatformDependenciesCheck((await runPlatformDependenciesCheck(session, { sddRoot })).finding));
|
|
84
|
+
lines.push(...renderFeatureSpecPromotionCheck(await runFeatureSpecPromotionCheck(session)));
|
|
85
|
+
lines.push(...renderFeatureSurfaceDuplicationCheck(await runFeatureSurfaceDuplicationCheck(session)));
|
|
86
|
+
// The fourteenth check (M-ADR 0205 §4.5, Phase B): every eligible artefact
|
|
87
|
+
// carries a current score. Last in the block order because it is the newest
|
|
88
|
+
// and the baselines hold the stream positionally-agnostic but ordered.
|
|
89
|
+
const available = new Set(schema.catalogues.map((c) => c.name).filter((n) => present(n)));
|
|
90
|
+
lines.push(...renderScoresCoverage(await runScoresCoverage(session, { tier, available, sddRoot }), tier));
|
|
91
|
+
// Staleness VERDICTS — the second half of the fourteenth check's §4.5
|
|
92
|
+
// scope (ledger §3o), member-tier only with the aggregate skip stated.
|
|
93
|
+
// Same coverage class, same reporting-only staging as the rest of the
|
|
94
|
+
// scores family.
|
|
95
|
+
lines.push(...renderStalenessVerdicts(await runStalenessVerdicts(session, { tier, available, sddRoot }), tier));
|
|
96
|
+
// The fifteenth check (M-ADR 0206 §4.5): relationship coherence for the
|
|
97
|
+
// three families where drift or gaps mean something — owned-but-untraced
|
|
98
|
+
// features, publisher-projection drift, supersession drift. Reporting-only
|
|
99
|
+
// at both tiers; newest last in the block order, same as scores was.
|
|
100
|
+
lines.push(...renderRelationshipCoherenceCheck(await runRelationshipCoherenceCheck(session)));
|
|
101
|
+
// The sixteenth check (M-ADR 0207 §4.8): the scaffold declared-vs-derived
|
|
102
|
+
// delta, its resolution self-check, and the §4.6 staleness verdict.
|
|
103
|
+
// Reporting-only at both tiers per §4.9 — nothing here gates until the
|
|
104
|
+
// pilot's stated-in-advance figures land. Newest last in the block order,
|
|
105
|
+
// same as scores and coherence were.
|
|
106
|
+
lines.push(...renderScaffoldDelta(await runScaffoldDelta(session, { tier, available, schema, sddRoot }), tier));
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
session.dispose();
|
|
110
|
+
}
|
|
111
|
+
for (const line of lines)
|
|
112
|
+
write(`${line}\n`);
|
|
113
|
+
return { exitCode: 0 };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=coverage-run.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// CSV reader for catalogue files.
|
|
2
|
+
//
|
|
3
|
+
// RFC-4180-aware: handles `"` quoting (including doubled `""` for literal
|
|
4
|
+
// quotes), commas inside quoted cells, and CR/LF/CRLF line breaks. We
|
|
5
|
+
// don't try to be a general CSV library — the canonical writer's output
|
|
6
|
+
// is the input shape we support, plus tolerance for byte-identical
|
|
7
|
+
// catalogues authored by other conforming aggregator implementations.
|
|
8
|
+
//
|
|
9
|
+
// Headers come from the file's first line; column order is preserved
|
|
10
|
+
// for traceability but row access is by column name.
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
export async function readCatalogueCsv(path) {
|
|
13
|
+
const text = await readFile(path, "utf-8");
|
|
14
|
+
return parseCatalogueCsv(text);
|
|
15
|
+
}
|
|
16
|
+
export function parseCatalogueCsv(text) {
|
|
17
|
+
const records = parseRecords(text);
|
|
18
|
+
if (records.length === 0)
|
|
19
|
+
return { columns: [], rows: [] };
|
|
20
|
+
const columns = records[0] ?? [];
|
|
21
|
+
const rows = [];
|
|
22
|
+
for (let r = 1; r < records.length; r++) {
|
|
23
|
+
const record = records[r] ?? [];
|
|
24
|
+
if (record.length === 1 && record[0] === "")
|
|
25
|
+
continue; // empty trailing line
|
|
26
|
+
const row = {};
|
|
27
|
+
for (let c = 0; c < columns.length; c++) {
|
|
28
|
+
row[columns[c] ?? ""] = record[c] ?? "";
|
|
29
|
+
}
|
|
30
|
+
rows.push(row);
|
|
31
|
+
}
|
|
32
|
+
return { columns, rows };
|
|
33
|
+
}
|
|
34
|
+
function parseRecords(text) {
|
|
35
|
+
const records = [];
|
|
36
|
+
let current = [];
|
|
37
|
+
let cell = "";
|
|
38
|
+
let i = 0;
|
|
39
|
+
let inQuotes = false;
|
|
40
|
+
while (i < text.length) {
|
|
41
|
+
const ch = text[i] ?? "";
|
|
42
|
+
if (inQuotes) {
|
|
43
|
+
if (ch === '"') {
|
|
44
|
+
if (text[i + 1] === '"') {
|
|
45
|
+
// Escaped double-quote inside a quoted cell.
|
|
46
|
+
cell += '"';
|
|
47
|
+
i += 2;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
inQuotes = false;
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
cell += ch;
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === '"') {
|
|
59
|
+
inQuotes = true;
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === ",") {
|
|
64
|
+
current.push(cell);
|
|
65
|
+
cell = "";
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === "\r") {
|
|
70
|
+
if (text[i + 1] === "\n")
|
|
71
|
+
i++;
|
|
72
|
+
current.push(cell);
|
|
73
|
+
records.push(current);
|
|
74
|
+
current = [];
|
|
75
|
+
cell = "";
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === "\n") {
|
|
80
|
+
current.push(cell);
|
|
81
|
+
records.push(current);
|
|
82
|
+
current = [];
|
|
83
|
+
cell = "";
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
cell += ch;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
// Trailing cell + record.
|
|
91
|
+
if (cell !== "" || current.length > 0) {
|
|
92
|
+
current.push(cell);
|
|
93
|
+
records.push(current);
|
|
94
|
+
}
|
|
95
|
+
return records;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=csv-reader.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic CSV writer for catalogues.
|
|
3
|
+
*
|
|
4
|
+
* Emits headers + sorted rows per ADR 0134 builder contract:
|
|
5
|
+
* - column order matches the canonical schema exactly,
|
|
6
|
+
* - rows sorted by primary-key value,
|
|
7
|
+
* - cells with embedded comma/quote/newline are RFC-4180 quoted,
|
|
8
|
+
* - empty trailing newline (POSIX convention).
|
|
9
|
+
*
|
|
10
|
+
* Headers-only emission is supported — pass an empty rows array to write
|
|
11
|
+
* a CSV that downstream consumers can rely on the file existence of.
|
|
12
|
+
*
|
|
13
|
+
* ⚠ BUT A HEADERS-ONLY WRITE NEVER TRUNCATES A CATALOGUE THAT HAS ROWS.
|
|
14
|
+
* `conformance_finding` is written by a DIFFERENT VERB — `sdd-cli conformance
|
|
15
|
+
* --emit-finding` calls writeFindings() into the same `catalogues/` directory —
|
|
16
|
+
* and nothing in the `catalogue build` row-collection path ever sets it. So
|
|
17
|
+
* build emitted a bare header over it, unconditionally, and one verb destroyed
|
|
18
|
+
* another verb's output. Measured across one estate: every SDD that had
|
|
19
|
+
* authored a conformance finding lost it, while the gate registries went on
|
|
20
|
+
* citing those rows as CURRENT evidence for a hard-gated score.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ The first cut of this comment said `c4_system_entity` was a second
|
|
23
|
+
* producerless catalogue. It is not — `build.ts` sets it directly rather than
|
|
24
|
+
* via `builders/`, and the sweep that "found" it only searched `builders/`.
|
|
25
|
+
* An adopter disproved it by counterfactual, installing the defective version
|
|
26
|
+
* and measuring that its rows survived. Stated here because the wrong version
|
|
27
|
+
* shipped in 1.29.0 and a reader deserves the correction beside the claim.
|
|
28
|
+
*
|
|
29
|
+
* The guard is keyed on the CONDITION, not on those two names, so it also
|
|
30
|
+
* catches the case nobody has hit yet: a builder that regresses to zero rows
|
|
31
|
+
* silently deleting a populated catalogue.
|
|
32
|
+
*/
|
|
33
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
/** Data-row count of an existing catalogue file, or 0 when absent/unreadable/header-only. */
|
|
36
|
+
async function existingDataRows(filePath) {
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = await readFile(filePath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const lines = text.split("\n").filter((l) => l.trim() !== "");
|
|
45
|
+
return lines.length > 1 ? lines.length - 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
export async function writeCatalogue(opts) {
|
|
48
|
+
await mkdir(opts.outDir, { recursive: true });
|
|
49
|
+
const filePath = join(opts.outDir, `${opts.catalogue.name}.csv`);
|
|
50
|
+
// Refuse to replace rows with a bare header. Emitting one is correct when the
|
|
51
|
+
// file is absent or already empty — that is the "consumers can rely on the
|
|
52
|
+
// file existing" case — and is data loss when it is not.
|
|
53
|
+
if (opts.rows.length === 0) {
|
|
54
|
+
const onDisk = await existingDataRows(filePath);
|
|
55
|
+
if (onDisk > 0) {
|
|
56
|
+
return { rowCount: onDisk, path: filePath, preserved: true, preservedRows: onDisk };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const text = renderCatalogueCsv(opts.catalogue, opts.rows);
|
|
60
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
61
|
+
await writeFile(filePath, text, "utf-8");
|
|
62
|
+
return { rowCount: opts.rows.length, path: filePath };
|
|
63
|
+
}
|
|
64
|
+
export function renderCatalogueCsv(catalogue, rows) {
|
|
65
|
+
const columns = catalogue.columns.map((c) => c.name);
|
|
66
|
+
const lines = [];
|
|
67
|
+
lines.push(columns.map(escapeCell).join(","));
|
|
68
|
+
const sorted = [...rows].sort((a, b) => compareCell(a[catalogue.primaryKey], b[catalogue.primaryKey]));
|
|
69
|
+
for (const row of sorted) {
|
|
70
|
+
const cells = columns.map((col) => escapeCell(formatCell(row[col])));
|
|
71
|
+
lines.push(cells.join(","));
|
|
72
|
+
}
|
|
73
|
+
// Trailing newline per POSIX convention.
|
|
74
|
+
return `${lines.join("\n")}\n`;
|
|
75
|
+
}
|
|
76
|
+
function formatCell(value) {
|
|
77
|
+
if (value === null || value === undefined)
|
|
78
|
+
return "";
|
|
79
|
+
if (typeof value === "boolean")
|
|
80
|
+
return value ? "true" : "false";
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
function escapeCell(value) {
|
|
84
|
+
if (value === "")
|
|
85
|
+
return "";
|
|
86
|
+
if (/[",\n\r]/.test(value)) {
|
|
87
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function compareCell(a, b) {
|
|
92
|
+
const sa = a === undefined || a === null ? "" : String(a);
|
|
93
|
+
const sb = b === undefined || b === null ? "" : String(b);
|
|
94
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=csv-writer.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// feature_integration_support — coverage derived at aggregation (M-ADR 0216 §4.4).
|
|
2
|
+
//
|
|
3
|
+
// For each feature with at least one `required` requirement and each integration of the same
|
|
4
|
+
// platform: decide every requirement (an ungrouped item alone; an alternative_group across its
|
|
5
|
+
// members), then the feature takes the worst. `supported` means the items reach the canonical
|
|
6
|
+
// boundary, NOT that the feature works end to end. `enhances` never decides coverage. A feature
|
|
7
|
+
// with no requirement gets no row: nothing was declared, so nothing is decided.
|
|
8
|
+
//
|
|
9
|
+
// Two readings the ADR leaves to the builder, chosen conservatively:
|
|
10
|
+
// - a `not-applicable` item inside a feature that is not wholly not-applicable is unmet with
|
|
11
|
+
// reason source-absent (the integration does not have it);
|
|
12
|
+
// - when the members of an unmet requirement fail for different reasons, the reason reported
|
|
13
|
+
// is the most actionable: gateway-gap, then no-canonical-home, then source-absent.
|
|
14
|
+
const SUPPLY_ORDER = ["supported", "partial", "gateway-gap", "source-absent", "not-applicable"];
|
|
15
|
+
const REASON_ORDER = ["gateway-gap", "no-canonical-home", "source-absent"];
|
|
16
|
+
export function deriveFeatureIntegrationSupport(rows) {
|
|
17
|
+
const plat = (r) => String(r["platform_sdd_id"] ?? "");
|
|
18
|
+
const items = new Map();
|
|
19
|
+
for (const r of rows.get("data_items") ?? [])
|
|
20
|
+
items.set(`${plat(r)}\u0000${r["id"]}`, r);
|
|
21
|
+
const canonical = (p, item) => String(items.get(`${p}\u0000${item}`)?.["canonical_field"] ?? "") !== "";
|
|
22
|
+
const best = new Map(); // integration\0item → best verdict
|
|
23
|
+
const supplied = new Set();
|
|
24
|
+
for (const r of rows.get("integration_data_item") ?? []) {
|
|
25
|
+
const k = `${r["integration_id"]}\u0000${r["data_item_id"]}`;
|
|
26
|
+
const v = String(r["verdict"]);
|
|
27
|
+
supplied.add(String(r["integration_id"]));
|
|
28
|
+
const cur = best.get(k);
|
|
29
|
+
if (cur === undefined || SUPPLY_ORDER.indexOf(v) < SUPPLY_ORDER.indexOf(cur))
|
|
30
|
+
best.set(k, v);
|
|
31
|
+
}
|
|
32
|
+
// feature → requirement key → items
|
|
33
|
+
const reqs = new Map();
|
|
34
|
+
for (const r of rows.get("feature_data_requirement") ?? []) {
|
|
35
|
+
if (String(r["necessity"]) !== "required")
|
|
36
|
+
continue;
|
|
37
|
+
const f = String(r["feature_id"]);
|
|
38
|
+
const item = String(r["data_item_id"]);
|
|
39
|
+
const g = String(r["alternative_group"] ?? "");
|
|
40
|
+
const key = g !== "" ? `group:${g}` : `item:${item}`;
|
|
41
|
+
const m = reqs.get(f) ?? new Map();
|
|
42
|
+
m.set(key, [...(m.get(key) ?? []), item]);
|
|
43
|
+
reqs.set(f, m);
|
|
44
|
+
}
|
|
45
|
+
const features = rows.get("features") ?? [];
|
|
46
|
+
const integrations = rows.get("integrations") ?? [];
|
|
47
|
+
const out = [];
|
|
48
|
+
const byVerdict = {};
|
|
49
|
+
for (const f of features) {
|
|
50
|
+
const fid = String(f["id"]);
|
|
51
|
+
const fr = reqs.get(fid);
|
|
52
|
+
if (!fr)
|
|
53
|
+
continue;
|
|
54
|
+
const p = plat(f);
|
|
55
|
+
for (const integ of integrations) {
|
|
56
|
+
if (plat(integ) !== p)
|
|
57
|
+
continue;
|
|
58
|
+
const iid = String(integ["id"]);
|
|
59
|
+
const itemState = (item) => {
|
|
60
|
+
if (!canonical(p, item))
|
|
61
|
+
return { kind: "unmet", reason: "no-canonical-home" };
|
|
62
|
+
const v = best.get(`${iid}\u0000${item}`);
|
|
63
|
+
if (v === undefined)
|
|
64
|
+
return { kind: "unknown" };
|
|
65
|
+
if (v === "supported")
|
|
66
|
+
return { kind: "met" };
|
|
67
|
+
if (v === "partial")
|
|
68
|
+
return { kind: "partly" };
|
|
69
|
+
if (v === "not-applicable")
|
|
70
|
+
return { kind: "na" };
|
|
71
|
+
return { kind: "unmet", reason: v };
|
|
72
|
+
};
|
|
73
|
+
const decided = [...fr.entries()]
|
|
74
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
75
|
+
.map(([, members]) => {
|
|
76
|
+
const states = members.sort().map((m) => ({ m, s: itemState(m) }));
|
|
77
|
+
if (states.some((x) => x.s.kind === "met"))
|
|
78
|
+
return { kind: "met" };
|
|
79
|
+
if (states.some((x) => x.s.kind === "partly"))
|
|
80
|
+
return { kind: "partly" };
|
|
81
|
+
if (states.some((x) => x.s.kind === "unknown"))
|
|
82
|
+
return { kind: "unknown" };
|
|
83
|
+
if (states.every((x) => x.s.kind === "na"))
|
|
84
|
+
return { kind: "na", item: states[0].m };
|
|
85
|
+
const unmet = states
|
|
86
|
+
.map((x) => ({ m: x.m, reason: x.s.kind === "unmet" ? x.s.reason : "source-absent" }))
|
|
87
|
+
.sort((a, b) => REASON_ORDER.indexOf(a.reason) - REASON_ORDER.indexOf(b.reason));
|
|
88
|
+
return { kind: "unmet", item: unmet[0].m, reason: unmet[0].reason };
|
|
89
|
+
});
|
|
90
|
+
let verdict;
|
|
91
|
+
let limitingItem = "";
|
|
92
|
+
let limitingReason = "";
|
|
93
|
+
const firstUnmet = decided.find((d) => d.kind === "unmet");
|
|
94
|
+
if (firstUnmet && firstUnmet.kind === "unmet") {
|
|
95
|
+
verdict = "unsupported";
|
|
96
|
+
limitingItem = firstUnmet.item;
|
|
97
|
+
limitingReason = firstUnmet.reason;
|
|
98
|
+
}
|
|
99
|
+
else if (decided.every((d) => d.kind === "na")) {
|
|
100
|
+
verdict = "not-applicable";
|
|
101
|
+
}
|
|
102
|
+
else if (decided.some((d) => d.kind === "na")) {
|
|
103
|
+
// n/a mixed with anything else: the integration does not have the item.
|
|
104
|
+
const na = decided.find((d) => d.kind === "na");
|
|
105
|
+
verdict = "unsupported";
|
|
106
|
+
limitingItem = na.item;
|
|
107
|
+
limitingReason = "source-absent";
|
|
108
|
+
}
|
|
109
|
+
else if (decided.some((d) => d.kind === "unknown")) {
|
|
110
|
+
verdict = "unknown";
|
|
111
|
+
}
|
|
112
|
+
else if (decided.some((d) => d.kind === "partly")) {
|
|
113
|
+
verdict = "partial";
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
verdict = "supported";
|
|
117
|
+
}
|
|
118
|
+
byVerdict[verdict] = (byVerdict[verdict] ?? 0) + 1;
|
|
119
|
+
out.push({
|
|
120
|
+
id: `${fid}::${iid}`,
|
|
121
|
+
feature_id: fid,
|
|
122
|
+
integration_id: iid,
|
|
123
|
+
verdict,
|
|
124
|
+
limiting_item_id: limitingItem,
|
|
125
|
+
limiting_reason: limitingReason,
|
|
126
|
+
source_sdd_id: String(f["source_sdd_id"] ?? ""),
|
|
127
|
+
platform_sdd_id: p,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
rows: out.sort((a, b) => String(a["id"]).localeCompare(String(b["id"]))),
|
|
133
|
+
stats: {
|
|
134
|
+
featuresWithRequirements: reqs.size,
|
|
135
|
+
features: features.length,
|
|
136
|
+
integrations: integrations.length,
|
|
137
|
+
integrationsWithSupply: supplied.size,
|
|
138
|
+
byVerdict,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=data-item-coverage.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// data_plane ↔ product_platform_dependencies coherence cross-check
|
|
2
|
+
// (M-ADR 0193 §4.2, as amended 2026-07-19).
|
|
3
|
+
//
|
|
4
|
+
// The two surfaces are complementary views, not mutually derived: a data_plane
|
|
5
|
+
// binding carries pattern / fine locality / runtime that platform-dependencies
|
|
6
|
+
// lack, and a platform-dependency carries capability / contract / stability that
|
|
7
|
+
// a binding lacks. The method CROSS-CHECKS them for coherence rather than
|
|
8
|
+
// deriving one from the other.
|
|
9
|
+
//
|
|
10
|
+
// The check is deliberately COARSE, and per-target correspondence is NOT
|
|
11
|
+
// asserted. Under the gateway convention (M-ADR 0189: consumers reach a
|
|
12
|
+
// capability through a BFF/gateway), a `retrieve@internal-domain` binding names
|
|
13
|
+
// the DOMAIN service it logically reads (its finer-grained `target`), while the
|
|
14
|
+
// matching `product_platform_dependencies` row names the GATEWAY through which
|
|
15
|
+
// access physically flows. The two targets legitimately differ — that
|
|
16
|
+
// difference is the locality/granularity signal the axis exists to capture, not
|
|
17
|
+
// drift. Matching binding.target against platform-dep.target_service_id would
|
|
18
|
+
// flag every such binding falsely, and adopters would "fix" the false finding by
|
|
19
|
+
// coarsening `target` to the gateway id — destroying the axis's value. So the
|
|
20
|
+
// coherence check asserts only the coarse invariant: a product that declares
|
|
21
|
+
// platform-targeting data-plane bindings should also declare *some* platform
|
|
22
|
+
// dependency. A product with internal bindings and zero platform-deps has one
|
|
23
|
+
// surface without the other — a genuine coherence gap worth surfacing.
|
|
24
|
+
import { isOutputSidePattern } from "./manifest-loader.js";
|
|
25
|
+
const INTERNAL_LOCALITIES = new Set([
|
|
26
|
+
"internal-platform",
|
|
27
|
+
"internal-domain",
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Coarse coherence check: if a product declares platform-targeting INBOUND
|
|
31
|
+
* data-plane bindings (internal locality) but no `product_platform_dependencies`
|
|
32
|
+
* rows at all, the two complementary surfaces are out of step. Returns 1 if
|
|
33
|
+
* flagged, else 0. Per-target correspondence is intentionally NOT checked, and
|
|
34
|
+
* output-side bindings are excluded entirely (see header).
|
|
35
|
+
*/
|
|
36
|
+
// @rule data-plane-platform-dep-coherence
|
|
37
|
+
export function checkDataPlanePlatformDepCoherence(inputs) {
|
|
38
|
+
const { productId, bindings, platformDeps, findings } = inputs;
|
|
39
|
+
const platformTargeting = bindings.filter((b) => INTERNAL_LOCALITIES.has(String(b["locality"] ?? "")) &&
|
|
40
|
+
!isOutputSidePattern(String(b["pattern"] ?? "")));
|
|
41
|
+
if (platformTargeting.length === 0)
|
|
42
|
+
return 0; // no platform-targeting bindings
|
|
43
|
+
if (platformDeps.length > 0)
|
|
44
|
+
return 0; // both surfaces present — coherent
|
|
45
|
+
findings.add("convention", `data_plane coherence: ${productId} declares ${platformTargeting.length} platform-targeting ` +
|
|
46
|
+
`inbound data_plane binding(s) (internal locality) but no product_platform_dependencies rows — the two ` +
|
|
47
|
+
`complementary surfaces are out of step; declare the platform dependencies in ` +
|
|
48
|
+
`app-platform-dependencies.yaml (M-ADR 0193 §4.2). Per-target correspondence is not asserted ` +
|
|
49
|
+
`(gateway indirection, M-ADR 0189).`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Coherence cross-check between the `surface: api` trait and a `publish`
|
|
54
|
+
* data_plane binding. Returns 1 if a drift finding was raised, else 0.
|
|
55
|
+
*
|
|
56
|
+
* One-directional: only a headless (`surface: api`) product missing its
|
|
57
|
+
* `publish` binding is flagged. A `publish` binding on a `ui` surface is
|
|
58
|
+
* legitimate and never flagged (see header). Runs only over a DECLARED
|
|
59
|
+
* data-plane (bindings present) — same gating as the platform-dep check; a
|
|
60
|
+
* product that has not authored a data_plane is not nagged.
|
|
61
|
+
*/
|
|
62
|
+
// @rule publish-surface-coherence
|
|
63
|
+
export function checkPublishSurfaceCoherence(inputs) {
|
|
64
|
+
const { productId, surface, bindings, findings } = inputs;
|
|
65
|
+
const effectiveSurface = surface ?? "ui";
|
|
66
|
+
if (effectiveSurface !== "api")
|
|
67
|
+
return 0; // publish is orthogonal to a ui surface
|
|
68
|
+
const hasPublish = bindings.some((b) => String(b["pattern"] ?? "") === "publish");
|
|
69
|
+
if (hasPublish)
|
|
70
|
+
return 0; // both views present — coherent
|
|
71
|
+
findings.add("convention", `data_plane/surface coherence: ${productId} declares surface: api (headless, M-ADR 0167) but its ` +
|
|
72
|
+
`data_plane has no publish binding — a headless product's entire surface is programmatic, so the ` +
|
|
73
|
+
`contract that constitutes it is missing from the declared composition. Add a publish binding, or ` +
|
|
74
|
+
`reconsider the surface classification (M-ADR 0193 §4.7). Complementary views, not a derivation.`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=data-plane-coherence.js.map
|