@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FK validator per ADR 0134 builder-spec §"Run internal FK validation
|
|
3
|
+
* before exiting" + §"External FK handling".
|
|
4
|
+
*
|
|
5
|
+
* For each catalogue's foreign keys:
|
|
6
|
+
* - Internal FKs (external=false): every non-empty value MUST resolve
|
|
7
|
+
* to a primary-key in the referenced catalogue's emitted rows. Missing
|
|
8
|
+
* resolution is a hard failure.
|
|
9
|
+
* - External FKs (external=true, or external for the build's profile via
|
|
10
|
+
* `externalForProfiles` — schema 2.23.0): every non-empty value is logged
|
|
11
|
+
* as a `[finding] external` line and skipped from validation. The
|
|
12
|
+
* aggregator resolves these post-merge per ADR 0134 §4.5. A required
|
|
13
|
+
* (non-nullable) external FK still fails when EMPTY — the value must be
|
|
14
|
+
* named even if it resolves cross-SDD.
|
|
15
|
+
*
|
|
16
|
+
* Returns a structured result; the caller decides exit code + reporting.
|
|
17
|
+
*
|
|
18
|
+
* POST-CUTOVER ROLE (M-ADR 0205 §4.4, A4): no production path calls this
|
|
19
|
+
* module — validate/build/aggregate run the engine path. It remains as the
|
|
20
|
+
* reference side of the standing differential (parity-harness.ts and the
|
|
21
|
+
* estate sweep driver), so implementation drift between the two gets
|
|
22
|
+
* caught by the suite instead of lurking.
|
|
23
|
+
*/
|
|
24
|
+
/** The stamp column M-ADR 0208 §4.4 keys on. */
|
|
25
|
+
export const PLATFORM_STAMP_COLUMN = "platform_sdd_id";
|
|
26
|
+
/**
|
|
27
|
+
* Whether the FK `cat.fk → targetCat` resolves under the platform-scoped
|
|
28
|
+
* key (see FkValidationOptions.platformScopedTargets). One definition,
|
|
29
|
+
* shared with the query generator so the two paths cannot drift.
|
|
30
|
+
*/
|
|
31
|
+
export function isPlatformScopedFk(cat, targetCat, options) {
|
|
32
|
+
if (!(options.platformScopedTargets ?? []).includes(targetCat.name))
|
|
33
|
+
return false;
|
|
34
|
+
const has = (c) => c.columns.some((col) => col.name === PLATFORM_STAMP_COLUMN);
|
|
35
|
+
return has(cat) && has(targetCat);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build an in-memory map of every catalogue's primary-key set, then walk
|
|
39
|
+
* every row's FK columns and check resolution.
|
|
40
|
+
*/
|
|
41
|
+
export function validateForeignKeys(schema, rowsByCatalogue, findings, options = {}) {
|
|
42
|
+
// Index primary keys per catalogue, and — for platform-scoped targets —
|
|
43
|
+
// the set of stamps each pk is present under ('' for an unstamped row).
|
|
44
|
+
const pkIndex = new Map();
|
|
45
|
+
const stampIndex = new Map();
|
|
46
|
+
const catByName = new Map(schema.catalogues.map((c) => [c.name, c]));
|
|
47
|
+
for (const cat of schema.catalogues) {
|
|
48
|
+
const rows = rowsByCatalogue.get(cat.name) ?? [];
|
|
49
|
+
const pks = new Set();
|
|
50
|
+
const stamps = new Map();
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const pk = row[cat.primaryKey];
|
|
53
|
+
if (pk !== undefined && pk !== null && pk !== "") {
|
|
54
|
+
pks.add(String(pk));
|
|
55
|
+
const stamp = String(row[PLATFORM_STAMP_COLUMN] ?? "");
|
|
56
|
+
const set = stamps.get(String(pk)) ?? new Set();
|
|
57
|
+
set.add(stamp);
|
|
58
|
+
stamps.set(String(pk), set);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
pkIndex.set(cat.name, pks);
|
|
62
|
+
stampIndex.set(cat.name, stamps);
|
|
63
|
+
}
|
|
64
|
+
const failures = [];
|
|
65
|
+
let externalCount = 0;
|
|
66
|
+
for (const cat of schema.catalogues) {
|
|
67
|
+
const rows = rowsByCatalogue.get(cat.name) ?? [];
|
|
68
|
+
for (const fk of cat.foreignKeys) {
|
|
69
|
+
// Effective-external (2.23.0): declared external OR external for the
|
|
70
|
+
// profile under build. Governs the resolution skip only — the
|
|
71
|
+
// required-empty check below stays keyed on the DECLARED `external` so
|
|
72
|
+
// a profile-external FK still requires a (cross-SDD) value to be named.
|
|
73
|
+
const effectiveExternal = fk.external ||
|
|
74
|
+
(options.profile !== undefined &&
|
|
75
|
+
(fk.externalForProfiles ?? []).includes(options.profile));
|
|
76
|
+
const [targetCat, targetCol] = fk.references.split(".");
|
|
77
|
+
if (!targetCat || !targetCol) {
|
|
78
|
+
throw new Error(`malformed FK reference "${fk.references}" on ${cat.name}.${fk.column}`);
|
|
79
|
+
}
|
|
80
|
+
const targetPks = pkIndex.get(targetCat);
|
|
81
|
+
const targetDef = catByName.get(targetCat);
|
|
82
|
+
if (!targetPks || !targetDef) {
|
|
83
|
+
throw new Error(`FK ${cat.name}.${fk.column} references unknown catalogue "${targetCat}"`);
|
|
84
|
+
}
|
|
85
|
+
const scoped = isPlatformScopedFk(cat, targetDef, options);
|
|
86
|
+
const targetStamps = stampIndex.get(targetCat);
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const raw = row[fk.column];
|
|
89
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
90
|
+
if (!fk.nullable && !fk.external) {
|
|
91
|
+
// Required FK with no value — internal violation regardless of external flag.
|
|
92
|
+
failures.push({
|
|
93
|
+
catalogue: cat.name,
|
|
94
|
+
column: fk.column,
|
|
95
|
+
referencingRow: String(row[cat.primaryKey] ?? ""),
|
|
96
|
+
value: "",
|
|
97
|
+
target: fk.references,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const value = String(raw);
|
|
103
|
+
let resolved = targetPks.has(value);
|
|
104
|
+
if (resolved && scoped) {
|
|
105
|
+
const mine = String(row[PLATFORM_STAMP_COLUMN] ?? "");
|
|
106
|
+
const theirs = targetStamps.get(value) ?? new Set();
|
|
107
|
+
resolved = mine === "" || theirs.has("") || theirs.has(mine);
|
|
108
|
+
}
|
|
109
|
+
if (effectiveExternal && !options.treatExternalAsInternal) {
|
|
110
|
+
if (!resolved) {
|
|
111
|
+
// Per spec: optimistically resolve against own rows; only log when unresolved.
|
|
112
|
+
externalCount++;
|
|
113
|
+
findings.add("external", `${cat.name}.${fk.column} → ${fk.references}=${value} ` +
|
|
114
|
+
`(referencing row pk=${row[cat.primaryKey] ?? "?"}; resolves at aggregation)`);
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!resolved) {
|
|
119
|
+
failures.push({
|
|
120
|
+
catalogue: cat.name,
|
|
121
|
+
column: fk.column,
|
|
122
|
+
referencingRow: String(row[cat.primaryKey] ?? ""),
|
|
123
|
+
value,
|
|
124
|
+
target: fk.references,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
internalPass: failures.length === 0,
|
|
132
|
+
internalFailures: failures,
|
|
133
|
+
externalFindings: externalCount,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=fk-validator.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalogue build --check`: are the committed catalogues what the committed specs build to?
|
|
3
|
+
* (M-ADR 0218 §5, bead sdm-jts.6.)
|
|
4
|
+
*
|
|
5
|
+
* Builds into a temporary directory and compares, so it writes nothing into the SDD. Only what
|
|
6
|
+
* the build PRODUCES is compared: a hand-written file that happens to sit in catalogues/ (a
|
|
7
|
+
* README, say) is not a build output and is ignored. Stale means either a produced file whose
|
|
8
|
+
* bytes differ from the committed one, or a produced file the SDD has not committed, or a
|
|
9
|
+
* committed catalogue the build no longer produces.
|
|
10
|
+
*
|
|
11
|
+
* Measured before building it: an isolated clone (no siblings) of an application, an
|
|
12
|
+
* integration, a platform root and a packaged SDD rebuilt byte-identically to what each had
|
|
13
|
+
* committed, so a fresh SDD passes and the check flags only real drift. Catalogue content is a
|
|
14
|
+
* function of committed content, not of the commit: nothing stamps HEAD's sha.
|
|
15
|
+
*/
|
|
16
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { runCatalogueBuild } from "./build.js";
|
|
21
|
+
/** Stale files: produced but differing or uncommitted, and committed .csv the build no longer produces. Pure over two directories. */
|
|
22
|
+
export async function compareCatalogueDirs(producedDir, committedDir) {
|
|
23
|
+
const produced = (await readdir(producedDir)).sort();
|
|
24
|
+
const committed = existsSync(committedDir) ? (await readdir(committedDir)).sort() : [];
|
|
25
|
+
const stale = [];
|
|
26
|
+
for (const f of produced) {
|
|
27
|
+
if (!committed.includes(f)) {
|
|
28
|
+
stale.push(`${f} (produced, not committed)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const [a, b] = await Promise.all([readFile(join(producedDir, f)), readFile(join(committedDir, f))]);
|
|
32
|
+
if (!a.equals(b))
|
|
33
|
+
stale.push(`${f} (differs)`);
|
|
34
|
+
}
|
|
35
|
+
for (const f of committed) {
|
|
36
|
+
if (f.endsWith(".csv") && !produced.includes(f))
|
|
37
|
+
stale.push(`${f} (committed, no longer produced)`);
|
|
38
|
+
}
|
|
39
|
+
return stale;
|
|
40
|
+
}
|
|
41
|
+
export async function checkCataloguesFresh(sddPath, opts, out) {
|
|
42
|
+
const label = opts.label ?? "catalogue build --check";
|
|
43
|
+
const tmp = await mkdtemp(join(tmpdir(), "sdd-catalogue-check-"));
|
|
44
|
+
try {
|
|
45
|
+
const built = await runCatalogueBuild({
|
|
46
|
+
sddPath,
|
|
47
|
+
outDir: tmp,
|
|
48
|
+
...(opts.strict !== undefined ? { strict: opts.strict } : {}),
|
|
49
|
+
...(opts.snapshotStore !== undefined ? { snapshotStore: opts.snapshotStore } : {}),
|
|
50
|
+
}, () => { });
|
|
51
|
+
if (built.exitCode !== 0) {
|
|
52
|
+
out(`[FAIL] ${label}: the build itself failed (exit ${built.exitCode}); run catalogue build to see why.\n`);
|
|
53
|
+
return { exitCode: built.exitCode, stale: [] };
|
|
54
|
+
}
|
|
55
|
+
const produced = (await readdir(tmp)).sort();
|
|
56
|
+
const stale = await compareCatalogueDirs(tmp, join(sddPath, "catalogues"));
|
|
57
|
+
if (stale.length === 0) {
|
|
58
|
+
out(`[PASS] ${label}: ${produced.length} catalogue file(s) reproduce byte-identically from the committed specs.\n`);
|
|
59
|
+
return { exitCode: 0, stale };
|
|
60
|
+
}
|
|
61
|
+
out(`[FAIL] ${label}: ${stale.length} catalogue file(s) are stale against the committed specs — catalogue build (sdd-maintain or sdd-cli) rebuilds them:\n`);
|
|
62
|
+
for (const s of stale)
|
|
63
|
+
out(` ${s}\n`);
|
|
64
|
+
return { exitCode: 1, stale };
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(tmp, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=freshness.js.map
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-0 gate checks as SQL — M-ADR 0205 §4.4 (engine-evolution Phase A3,
|
|
3
|
+
* final tranche). The nine small catalogue-integrity-gate steps
|
|
4
|
+
* re-expressed on the engine: emptiness-vs-populated-inputs signals
|
|
5
|
+
* (repo-edges 0157, data-model-entities 0159, capability-event 0160,
|
|
6
|
+
* feature-event 0161, platform-dependencies 0162, glossary-xref 0158),
|
|
7
|
+
* the per-domain event-catalogue count compare (0156), and the two
|
|
8
|
+
* features.csv Phase-0 checks (0177 F1 promotion, F2 near-duplicate
|
|
9
|
+
* surfaces — the classified borderline: engine for access, the token
|
|
10
|
+
* Jaccard stays host-language).
|
|
11
|
+
*
|
|
12
|
+
* The shell scripts remain the REFERENCE implementations until A4. Known,
|
|
13
|
+
* deliberate divergence class: several references read CSV columns
|
|
14
|
+
* POSITIONALLY with a naive comma split (`awk -F, $5`) and count PHYSICAL
|
|
15
|
+
* lines rather than CSV records — this port reads by column name over
|
|
16
|
+
* parsed records. Where real data makes those disagree, the dual-run
|
|
17
|
+
* driver surfaces it and the adjudication is a defect in the shell side
|
|
18
|
+
* (the exact matcher-error class M-ADR 0205 §1 exists to kill), never a
|
|
19
|
+
* smoothing of this side.
|
|
20
|
+
*
|
|
21
|
+
* File and YAML ingestion stay host-side, mirroring each reference's
|
|
22
|
+
* refusal semantics (a missing key, an unparseable document, an absent
|
|
23
|
+
* tool each behave exactly as the reference behaves).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { parse as parseYaml } from "yaml";
|
|
28
|
+
async function scalar(session, sql) {
|
|
29
|
+
const reader = await session.conn.runAndReadAll(sql);
|
|
30
|
+
return Number(reader.getRows()[0][0]);
|
|
31
|
+
}
|
|
32
|
+
const count = (table, where = "") => `SELECT COUNT(*) FROM "${table}"${where ? ` WHERE ${where}` : ""};`;
|
|
33
|
+
// @rule gate-event-catalogue
|
|
34
|
+
export async function runEventCatalogueCheck(session, options) {
|
|
35
|
+
if (!options.eventsPresent)
|
|
36
|
+
return { skipped: true, gaps: [] };
|
|
37
|
+
const domainsDir = join(options.sddRoot, "docs", "domains");
|
|
38
|
+
if (!existsSync(domainsDir))
|
|
39
|
+
return { skipped: false, gaps: [] };
|
|
40
|
+
const gaps = [];
|
|
41
|
+
for (const domain of readdirSync(domainsDir).sort()) {
|
|
42
|
+
const catalogPath = join(domainsDir, domain, "contracts", "events", "event-catalog.yaml");
|
|
43
|
+
if (!existsSync(catalogPath))
|
|
44
|
+
continue;
|
|
45
|
+
// Mirror of `yq '.events | length' || echo 0`: missing key or null → 0,
|
|
46
|
+
// an array → its length, a mapping → its key count, unparseable → 0.
|
|
47
|
+
let declared = 0;
|
|
48
|
+
try {
|
|
49
|
+
const doc = parseYaml(readFileSync(catalogPath, "utf-8"));
|
|
50
|
+
const events = doc?.["events"];
|
|
51
|
+
if (Array.isArray(events))
|
|
52
|
+
declared = events.length;
|
|
53
|
+
else if (events !== null && events !== undefined && typeof events === "object")
|
|
54
|
+
declared = Object.keys(events).length;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
declared = 0;
|
|
58
|
+
}
|
|
59
|
+
if (declared <= 0)
|
|
60
|
+
continue;
|
|
61
|
+
const emitted = await scalar(session, count("events", `COALESCE("domain_id", '') = '${domain.replace(/'/g, "''")}'`));
|
|
62
|
+
if (emitted < declared)
|
|
63
|
+
gaps.push({ domain, declared, emitted });
|
|
64
|
+
}
|
|
65
|
+
return { skipped: false, gaps };
|
|
66
|
+
}
|
|
67
|
+
// @rule gate-repo-edges
|
|
68
|
+
export async function runRepoEdgesCheck(session, options) {
|
|
69
|
+
const repoCaps = await scalar(session, count("repo_capability"));
|
|
70
|
+
const capSvcs = await scalar(session, count("capability_service"));
|
|
71
|
+
const svcsWithRepo = await scalar(session, count("services", `COALESCE("primary_repo_id", '') <> ''`));
|
|
72
|
+
const repoContracts = await scalar(session, count("repo_contract"));
|
|
73
|
+
const contracts = await scalar(session, count("contracts"));
|
|
74
|
+
const svcContracts = await scalar(session, count("service_contract"));
|
|
75
|
+
// Mirror of the reference's two glob patterns (bash without globstar:
|
|
76
|
+
// `**` collapses to one level) — direct files AND one directory deep,
|
|
77
|
+
// schema.yaml deliberately NOT excluded because the reference scans it.
|
|
78
|
+
let repoConfigsWithContracts = 0;
|
|
79
|
+
const cfgDir = join(options.sddRoot, "orchestration", "repo-config");
|
|
80
|
+
if (existsSync(cfgDir)) {
|
|
81
|
+
const candidates = [];
|
|
82
|
+
for (const entry of readdirSync(cfgDir, { withFileTypes: true })) {
|
|
83
|
+
const p = join(cfgDir, entry.name);
|
|
84
|
+
if (entry.isFile() && entry.name.endsWith(".yaml"))
|
|
85
|
+
candidates.push(p);
|
|
86
|
+
else if (entry.isDirectory()) {
|
|
87
|
+
for (const f of readdirSync(p)) {
|
|
88
|
+
if (f.endsWith(".yaml"))
|
|
89
|
+
candidates.push(join(p, f));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const p of candidates) {
|
|
94
|
+
if (/^contracts:/m.test(readFileSync(p, "utf-8")))
|
|
95
|
+
repoConfigsWithContracts++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...(repoCaps === 0 && capSvcs > 0 && svcsWithRepo > 0
|
|
100
|
+
? { repoCapabilityGap: { capabilityService: capSvcs, servicesWithRepo: svcsWithRepo } }
|
|
101
|
+
: {}),
|
|
102
|
+
...(repoContracts === 0 && contracts > 0 && repoConfigsWithContracts > 0
|
|
103
|
+
? { repoContractForwardGap: { contracts, repoConfigsWithContracts } }
|
|
104
|
+
: {}),
|
|
105
|
+
...(repoContracts === 0 && svcContracts > 0 && svcsWithRepo > 0
|
|
106
|
+
? { repoContractPathXGap: { serviceContracts: svcContracts, servicesWithRepo: svcsWithRepo } }
|
|
107
|
+
: {}),
|
|
108
|
+
...(repoContracts === 0 &&
|
|
109
|
+
contracts > 0 &&
|
|
110
|
+
repoConfigsWithContracts === 0 &&
|
|
111
|
+
svcContracts === 0
|
|
112
|
+
? { repoContractReverseGap: { contracts } }
|
|
113
|
+
: {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// ─── 0158: glossary-xref ingestion gap ───────────────────────────────────
|
|
117
|
+
// @rule gate-glossary-xref
|
|
118
|
+
export async function runGlossaryXrefCheck(session, options) {
|
|
119
|
+
const manifest = join(options.sddRoot, "docs", "platform", "reference", "glossary-alignment-manifest.yaml");
|
|
120
|
+
if (!existsSync(manifest) || !options.xrefPresent)
|
|
121
|
+
return { finding: false };
|
|
122
|
+
const rows = await scalar(session, count("glossary_xref"));
|
|
123
|
+
return { finding: rows === 0 };
|
|
124
|
+
}
|
|
125
|
+
// @rule gate-data-model-entities
|
|
126
|
+
export async function runDataModelEntitiesCheck(session) {
|
|
127
|
+
const domainModels = await scalar(session, count("domain_models"));
|
|
128
|
+
const entities = await scalar(session, count("entities"));
|
|
129
|
+
const services = await scalar(session, count("services"));
|
|
130
|
+
const serviceEntity = await scalar(session, count("service_entity"));
|
|
131
|
+
const containerEntity = await scalar(session, count("c4_container_entity"));
|
|
132
|
+
const containersWithService = await scalar(session, count("c4_containers", `COALESCE("service_id", '') <> ''`));
|
|
133
|
+
return {
|
|
134
|
+
...(domainModels > 0 && entities === 0 ? { entitiesGap: { domainModels } } : {}),
|
|
135
|
+
...(entities > 0 && services > 0 && serviceEntity === 0
|
|
136
|
+
? { serviceEntityGap: { entities, services } }
|
|
137
|
+
: {}),
|
|
138
|
+
...(serviceEntity > 0 && containersWithService > 0 && containerEntity === 0
|
|
139
|
+
? { containerEntityGap: { containersWithService, serviceEntity } }
|
|
140
|
+
: {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// @rule gate-capability-event
|
|
144
|
+
export async function runCapabilityEventCheck(session) {
|
|
145
|
+
const serviceEvent = await scalar(session, count("service_event"));
|
|
146
|
+
const capabilityService = await scalar(session, count("capability_service"));
|
|
147
|
+
const capabilityEvent = await scalar(session, count("capability_event"));
|
|
148
|
+
let authoredGap;
|
|
149
|
+
if (capabilityEvent > 0) {
|
|
150
|
+
const authored = await scalar(session, count("capability_event", `COALESCE("source", '') = 'authored'`));
|
|
151
|
+
const derived = await scalar(session, count("capability_event", `COALESCE("source", '') = 'derived'`));
|
|
152
|
+
if (authored === 0 && derived > 0)
|
|
153
|
+
authoredGap = { derived };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
...(serviceEvent > 0 && capabilityService > 0 && capabilityEvent === 0
|
|
157
|
+
? { derivableGap: { serviceEvent, capabilityService } }
|
|
158
|
+
: {}),
|
|
159
|
+
...(authoredGap !== undefined ? { authoredGap } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ─── 0161: feature_event derivation gap ──────────────────────────────────
|
|
163
|
+
// @rule gate-feature-event
|
|
164
|
+
export async function runFeatureEventCheck(session) {
|
|
165
|
+
const serviceEvent = await scalar(session, count("service_event"));
|
|
166
|
+
const featureService = await scalar(session, count("feature_service"));
|
|
167
|
+
const featureEvent = await scalar(session, count("feature_event"));
|
|
168
|
+
return serviceEvent > 0 && featureService > 0 && featureEvent === 0
|
|
169
|
+
? { gap: { serviceEvent, featureService } }
|
|
170
|
+
: {};
|
|
171
|
+
}
|
|
172
|
+
// ─── 0162: forward platform-dependencies parity gap ──────────────────────
|
|
173
|
+
// @rule gate-platform-dependencies
|
|
174
|
+
export async function runPlatformDependenciesCheck(session, options) {
|
|
175
|
+
const forwardFile = join(options.sddRoot, "app-platform-dependencies.yaml");
|
|
176
|
+
const forwardAuthored = existsSync(forwardFile) &&
|
|
177
|
+
/^\s*platform_dependencies:/m.test(readFileSync(forwardFile, "utf-8"));
|
|
178
|
+
if (!forwardAuthored)
|
|
179
|
+
return { finding: false };
|
|
180
|
+
const ppd = await scalar(session, count("product_platform_dependencies"));
|
|
181
|
+
return { finding: ppd === 0 };
|
|
182
|
+
}
|
|
183
|
+
// ─── 0177 F1: feature-spec promotion ─────────────────────────────────────
|
|
184
|
+
/** Python's round(): half to even — the reference computes pct with it. */
|
|
185
|
+
function pythonRound(x) {
|
|
186
|
+
const floor = Math.floor(x);
|
|
187
|
+
const diff = x - floor;
|
|
188
|
+
if (diff > 0.5)
|
|
189
|
+
return floor + 1;
|
|
190
|
+
if (diff < 0.5)
|
|
191
|
+
return floor;
|
|
192
|
+
return floor % 2 === 0 ? floor : floor + 1;
|
|
193
|
+
}
|
|
194
|
+
// @rule gate-feature-spec-promotion
|
|
195
|
+
export async function runFeatureSpecPromotionCheck(session) {
|
|
196
|
+
const reader = await session.conn.runAndReadAll(`SELECT COALESCE("source", '') FROM "features";`);
|
|
197
|
+
let total = 0;
|
|
198
|
+
let resolvable = 0;
|
|
199
|
+
let intermediate = 0;
|
|
200
|
+
for (const r of reader.getRows()) {
|
|
201
|
+
const src = String(r[0] ?? "").trim();
|
|
202
|
+
total++;
|
|
203
|
+
if (src === "feature-spec" || src === "feature-tree")
|
|
204
|
+
resolvable++;
|
|
205
|
+
else if (src === "capability-spec" || src === "feature-grounding")
|
|
206
|
+
intermediate++;
|
|
207
|
+
}
|
|
208
|
+
if (total === 0 || intermediate === 0)
|
|
209
|
+
return {};
|
|
210
|
+
return {
|
|
211
|
+
finding: {
|
|
212
|
+
intermediate,
|
|
213
|
+
total,
|
|
214
|
+
pctResolvable: pythonRound((100 * resolvable) / total),
|
|
215
|
+
note: intermediate >= resolvable ? "predominantly un-promoted" : "partially promoted",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// ─── 0177 F2: near-duplicate feature surfaces (the borderline: engine for
|
|
220
|
+
// access, the token Jaccard host-language) ───────────────────────────
|
|
221
|
+
function tokens(slug) {
|
|
222
|
+
return new Set(slug.split("-").filter((t) => t !== ""));
|
|
223
|
+
}
|
|
224
|
+
function near(a, b) {
|
|
225
|
+
const ta = tokens(a);
|
|
226
|
+
const tb = tokens(b);
|
|
227
|
+
if (ta.size === 0 || tb.size === 0)
|
|
228
|
+
return false;
|
|
229
|
+
const inter = [...ta].filter((t) => tb.has(t)).length;
|
|
230
|
+
if (inter === ta.size || inter === tb.size)
|
|
231
|
+
return true; // subset either way
|
|
232
|
+
const union = ta.size + tb.size - inter;
|
|
233
|
+
return union > 0 && inter / union >= 0.5;
|
|
234
|
+
}
|
|
235
|
+
// @rule gate-feature-surface-duplication
|
|
236
|
+
export async function runFeatureSurfaceDuplicationCheck(session) {
|
|
237
|
+
const reader = await session.conn.runAndReadAll(`SELECT COALESCE("capability_id", ''), COALESCE("name", ''), COALESCE("source", '') FROM "features";`);
|
|
238
|
+
const rollup = new Map();
|
|
239
|
+
const detailed = new Map();
|
|
240
|
+
for (const r of reader.getRows()) {
|
|
241
|
+
const cap = String(r[0] ?? "").trim();
|
|
242
|
+
const slug = String(r[1] ?? "").trim();
|
|
243
|
+
const src = String(r[2] ?? "").trim();
|
|
244
|
+
if (cap === "" || slug === "")
|
|
245
|
+
continue;
|
|
246
|
+
if (src === "capability-spec") {
|
|
247
|
+
rollup.set(cap, [...(rollup.get(cap) ?? []), slug]);
|
|
248
|
+
}
|
|
249
|
+
else if (src === "feature-grounding") {
|
|
250
|
+
detailed.set(cap, [...(detailed.get(cap) ?? []), slug]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let pairs = 0;
|
|
254
|
+
const caps = new Set();
|
|
255
|
+
for (const [cap, rollupSlugs] of rollup) {
|
|
256
|
+
const detailedSlugs = detailed.get(cap);
|
|
257
|
+
if (!detailedSlugs)
|
|
258
|
+
continue;
|
|
259
|
+
for (const r of rollupSlugs) {
|
|
260
|
+
for (const d of detailedSlugs) {
|
|
261
|
+
if (r !== d && near(r, d)) {
|
|
262
|
+
pairs++;
|
|
263
|
+
caps.add(cap);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return pairs > 0 ? { finding: { pairs, capabilities: caps.size } } : {};
|
|
269
|
+
}
|
|
270
|
+
// @rule gate-relationship-coherence
|
|
271
|
+
export async function runRelationshipCoherenceCheck(session) {
|
|
272
|
+
const ownedUntraced = [];
|
|
273
|
+
let reader = await session.conn.runAndReadAll(`SELECT f."id", f."capability_id" FROM "features" f
|
|
274
|
+
WHERE COALESCE(f."capability_id", '') <> ''
|
|
275
|
+
AND NOT EXISTS (SELECT 1 FROM "capability_feature" cf
|
|
276
|
+
WHERE cf."capability_id" = f."capability_id"
|
|
277
|
+
AND cf."feature_id" = f."id")
|
|
278
|
+
ORDER BY f."id";`);
|
|
279
|
+
for (const r of reader.getRows()) {
|
|
280
|
+
ownedUntraced.push({ featureId: String(r[0]), capabilityId: String(r[1]) });
|
|
281
|
+
}
|
|
282
|
+
const publisherDrift = [];
|
|
283
|
+
reader = await session.conn.runAndReadAll(`SELECT se."service_id", se."event_id" FROM "service_event" se
|
|
284
|
+
WHERE se."role" = 'publishes'
|
|
285
|
+
AND NOT EXISTS (SELECT 1 FROM "events" e
|
|
286
|
+
WHERE e."id" = se."event_id"
|
|
287
|
+
AND COALESCE(e."publisher_service_id", '') = se."service_id")
|
|
288
|
+
ORDER BY 1, 2;`);
|
|
289
|
+
for (const r of reader.getRows()) {
|
|
290
|
+
publisherDrift.push({
|
|
291
|
+
serviceId: String(r[0]),
|
|
292
|
+
eventId: String(r[1]),
|
|
293
|
+
missing: "publisher-fk",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
reader = await session.conn.runAndReadAll(`SELECT e."publisher_service_id", e."id" FROM "events" e
|
|
297
|
+
WHERE COALESCE(e."publisher_service_id", '') <> ''
|
|
298
|
+
AND NOT EXISTS (SELECT 1 FROM "service_event" se
|
|
299
|
+
WHERE se."role" = 'publishes'
|
|
300
|
+
AND se."event_id" = e."id"
|
|
301
|
+
AND se."service_id" = e."publisher_service_id")
|
|
302
|
+
ORDER BY 1, 2;`);
|
|
303
|
+
for (const r of reader.getRows()) {
|
|
304
|
+
publisherDrift.push({
|
|
305
|
+
serviceId: String(r[0]),
|
|
306
|
+
eventId: String(r[1]),
|
|
307
|
+
missing: "publishes-edge",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const supersessionDrift = [];
|
|
311
|
+
reader = await session.conn.runAndReadAll(`SELECT s."superseding_adr_id", s."superseded_adr_id" FROM "adr_supersedes" s
|
|
312
|
+
WHERE NOT EXISTS (SELECT 1 FROM "adrs" a
|
|
313
|
+
WHERE a."id" = s."superseded_adr_id"
|
|
314
|
+
AND COALESCE(a."superseded_by_id", '') = s."superseding_adr_id")
|
|
315
|
+
ORDER BY 1, 2;`);
|
|
316
|
+
for (const r of reader.getRows()) {
|
|
317
|
+
supersessionDrift.push({
|
|
318
|
+
supersedingId: String(r[0]),
|
|
319
|
+
supersededId: String(r[1]),
|
|
320
|
+
missing: "fk-backfill",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
reader = await session.conn.runAndReadAll(`SELECT a."superseded_by_id", a."id" FROM "adrs" a
|
|
324
|
+
WHERE COALESCE(a."superseded_by_id", '') <> ''
|
|
325
|
+
AND NOT EXISTS (SELECT 1 FROM "adr_supersedes" s
|
|
326
|
+
WHERE s."superseded_adr_id" = a."id"
|
|
327
|
+
AND s."superseding_adr_id" = a."superseded_by_id")
|
|
328
|
+
ORDER BY 1, 2;`);
|
|
329
|
+
for (const r of reader.getRows()) {
|
|
330
|
+
supersessionDrift.push({
|
|
331
|
+
supersedingId: String(r[0]),
|
|
332
|
+
supersededId: String(r[1]),
|
|
333
|
+
missing: "edge-row",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return { ownedUntraced, publisherDrift, supersessionDrift };
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=gate-checks.js.map
|