@sdd-method/verify-cli 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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +7 -0
  4. package/dist/lib/catalogue/baseline-check.js +136 -0
  5. package/dist/lib/catalogue/canonical-schema.yaml +3557 -0
  6. package/dist/lib/catalogue/cli-version.js +10 -0
  7. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  8. package/dist/lib/catalogue/coverage-render.js +522 -0
  9. package/dist/lib/catalogue/coverage-run.js +115 -0
  10. package/dist/lib/catalogue/csv-reader.js +97 -0
  11. package/dist/lib/catalogue/csv-writer.js +96 -0
  12. package/dist/lib/catalogue/ddl-generator.js +233 -0
  13. package/dist/lib/catalogue/engine-validator.js +222 -0
  14. package/dist/lib/catalogue/finding-identity.js +183 -0
  15. package/dist/lib/catalogue/findings.js +46 -0
  16. package/dist/lib/catalogue/fk-validator.js +136 -0
  17. package/dist/lib/catalogue/gate-checks.js +338 -0
  18. package/dist/lib/catalogue/graph-projection.js +329 -0
  19. package/dist/lib/catalogue/impact-run.js +228 -0
  20. package/dist/lib/catalogue/manifest-loader.js +422 -0
  21. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  22. package/dist/lib/catalogue/rubric-coverage-map.yaml +764 -0
  23. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  24. package/dist/lib/catalogue/schema-compat.js +52 -0
  25. package/dist/lib/catalogue/schema-loader.js +87 -0
  26. package/dist/lib/catalogue/scores-coverage.js +535 -0
  27. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  28. package/dist/lib/catalogue/validate.js +176 -0
  29. package/dist/lib/catalogue/version.js +18 -0
  30. package/dist/lib/list/discover.js +100 -0
  31. package/dist/lib/list/format.js +84 -0
  32. package/dist/lib/list/index.js +16 -0
  33. package/dist/lib/lock-file.js +413 -0
  34. package/dist/lib/pack/log.js +18 -0
  35. package/dist/lib/pack/schema.js +121 -0
  36. package/dist/lib/pack/seed.js +182 -0
  37. package/dist/lib/pack/validate.js +49 -0
  38. package/dist/lib/package-version.js +25 -0
  39. package/dist/lib/repo-kind.js +129 -0
  40. package/dist/lib/scripts.js +7 -0
  41. package/dist/lib/validate/aggregate.js +35 -0
  42. package/dist/lib/validate/bundle-coherence.js +69 -0
  43. package/dist/lib/validate/dispatch.js +218 -0
  44. package/dist/lib/validate/index.js +88 -0
  45. package/dist/lib/validate/runner.js +85 -0
  46. package/dist/verbs/catalogue-verify.js +114 -0
  47. package/dist/verbs/list.js +45 -0
  48. package/dist/verbs/pack-validate.js +17 -0
  49. package/dist/verbs/path-option.js +25 -0
  50. package/dist/verbs/validate.js +22 -0
  51. package/dist/verify-bin.js +8 -0
  52. package/dist/verify-program.js +115 -0
  53. package/package.json +32 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Transitive impact analysis — `sdd-cli catalogue impact` (M-ADR 0206 §4.6).
3
+ *
4
+ * Impact traverses the graph-projection edge set BY DECLARED SEMANTICS: every
5
+ * relationship carries an impact-flow token in the rubric-coverage-map
6
+ * (M-ADR 0206 §4.1) naming which endpoint RECEIVES impact — `toward-source`
7
+ * (dependency shape: a change at the target propagates to the source),
8
+ * `toward-target` (membership shape: a part's change reaches its whole),
9
+ * `both` (realisation), `none` (xref, lineage records, attributes). One
10
+ * relationship (`service_event`) resolves its flow per row from a named
11
+ * column (§4.2). The closure is the set of nodes impact can reach from the
12
+ * seed along declared propagation directions, computed by recursive SQL
13
+ * (`WITH RECURSIVE`) on the Phase A engine.
14
+ *
15
+ * THE CO-DIRECTIONALITY INVARIANT (§4.4, a condition of 0206's acceptance):
16
+ * the Phase C mirror-dedup heuristic is retired because under the declared
17
+ * tokens every true-mirror pair propagates the SAME direction. That premise
18
+ * is exercised, not assumed: buildImpactPropagation asserts that no node
19
+ * pair receives propagation in BOTH directions from two DIFFERENT
20
+ * single-direction relationship types, and fails loudly naming the pair and
21
+ * types. A single relationship declaring `both` is deliberate bidirectional
22
+ * coupling and is exempt; a self-referential type contributing both
23
+ * directions from different rows (a hierarchy cycle) is likewise exempt.
24
+ * A future token edit that would silently bring the undirected flood back
25
+ * trips this assertion instead.
26
+ *
27
+ * Cycle discipline: the recursion carries NO depth column — recursive UNION
28
+ * deduplicates whole rows, and a depth column makes every lap of a cycle a
29
+ * "new" row: the query that looks more informative is the query that never
30
+ * terminates. As a bare id-set the closure saturates and stops.
31
+ */
32
+ import { existsSync } from "node:fs";
33
+ import { join, resolve } from "node:path";
34
+ import { readCatalogueCsv } from "./csv-reader.js";
35
+ import { loadCanonicalSchema } from "./schema-loader.js";
36
+ import { loadCatalogueKinds, loadRelationshipSemantics, } from "./relationship-semantics.js";
37
+ import { projectGraph } from "./graph-projection.js";
38
+ function resolveFlow(edge, semantics) {
39
+ const decl = edge.type.includes(".")
40
+ ? semantics.entityFks.get(edge.type)
41
+ : semantics.edges.get(edge.type);
42
+ if (decl === undefined) {
43
+ throw new Error(`impact: relationship ${edge.type} has no declaration in the ` +
44
+ `rubric-coverage-map (M-ADR 0206) — sync the vendored map.`);
45
+ }
46
+ if (decl.impactFlow !== undefined)
47
+ return decl.impactFlow;
48
+ const by = decl.impactFlowBy;
49
+ const value = (edge.properties?.[by.column] ?? "").trim();
50
+ const flow = by.values[value];
51
+ if (flow === undefined) {
52
+ throw new Error(`impact: ${edge.type} resolves flow by column '${by.column}' but row ` +
53
+ `value '${value}' is not in the declared map — extend the ` +
54
+ `declaration or fix the row.`);
55
+ }
56
+ return flow;
57
+ }
58
+ /**
59
+ * Resolve every declared edge into propagation steps per its flow, and
60
+ * exercise the §4.4 co-directionality invariant.
61
+ */
62
+ export function buildImpactPropagation(edges, semantics) {
63
+ const out = [];
64
+ const byFlow = new Map();
65
+ let flowNone = 0;
66
+ const pairs = new Map();
67
+ const notePair = (from, to, type) => {
68
+ const [a, b] = from < to ? [from, to] : [to, from];
69
+ const key = `${a}␟${b}`;
70
+ let rec = pairs.get(key);
71
+ if (rec === undefined) {
72
+ rec = { ab: new Set(), ba: new Set() };
73
+ pairs.set(key, rec);
74
+ }
75
+ (from === a ? rec.ab : rec.ba).add(type);
76
+ };
77
+ for (const e of edges) {
78
+ const flow = resolveFlow(e, semantics);
79
+ byFlow.set(flow, (byFlow.get(flow) ?? 0) + 1);
80
+ if (flow === "none") {
81
+ flowNone += 1;
82
+ continue;
83
+ }
84
+ if (flow === "toward-source" || flow === "both") {
85
+ out.push({ from: e.target, to: e.source, type: e.type });
86
+ notePair(e.target, e.source, e.type);
87
+ }
88
+ if (flow === "toward-target" || flow === "both") {
89
+ out.push({ from: e.source, to: e.target, type: e.type });
90
+ notePair(e.source, e.target, e.type);
91
+ }
92
+ }
93
+ // §4.4 invariant, refined on measured data (2026-08-06): a pair fails only
94
+ // when one type contributes EXCLUSIVELY a→b against another contributing
95
+ // EXCLUSIVELY b→a. A type that itself spans both directions on the pair —
96
+ // opposing role-rows of a role-conditional relationship (a service that
97
+ // publishes AND consumes the same event: five real pairs on the reverse
98
+ // estate, one service, all session-broadcast shaped), a self-referential
99
+ // hierarchy cycle, or a declared `both` flow — has DECLARED that pair
100
+ // bidirectional, so other types aligning with either direction are
101
+ // consistent with declarations, not drift. The flipped-token flood shape
102
+ // (two single-direction declarations opposing) still fails, loudly.
103
+ for (const [key, rec] of pairs) {
104
+ const abOnly = [...rec.ab].filter((t) => !rec.ba.has(t));
105
+ const baOnly = [...rec.ba].filter((t) => !rec.ab.has(t));
106
+ if (abOnly.length > 0 && baOnly.length > 0) {
107
+ const [a, b] = key.split("␟");
108
+ throw new Error(`impact: counter-directional reciprocal pair (M-ADR 0206 §4.4): ` +
109
+ `${abOnly[0]} propagates ${a} -> ${b} while ${baOnly[0]} propagates ` +
110
+ `${b} -> ${a}, and neither declares the pair bidirectional. The ` +
111
+ `declared tokens have re-created an undirected pair; fix the ` +
112
+ `classification before trusting any closure.`);
113
+ }
114
+ }
115
+ return { edges: out, flowNone, byFlow };
116
+ }
117
+ function sqlStringLiteral(value) {
118
+ return `'${value.replace(/'/g, "''")}'`;
119
+ }
120
+ /**
121
+ * Closure of `seed` over resolved propagation edges, on the engine.
122
+ * Exported for the fixture controls; the verb goes through runCatalogueImpact.
123
+ */
124
+ export async function computeImpactClosure(propagation, seed) {
125
+ const { DuckDBInstance } = await import("@duckdb/node-api");
126
+ const instance = await DuckDBInstance.create(":memory:");
127
+ const conn = await instance.connect();
128
+ try {
129
+ await conn.run(`CREATE TABLE prop (f VARCHAR, t VARCHAR);`);
130
+ const CHUNK = 500;
131
+ for (let i = 0; i < propagation.length; i += CHUNK) {
132
+ const tuples = propagation
133
+ .slice(i, i + CHUNK)
134
+ .map((e) => `(${sqlStringLiteral(e.from)}, ${sqlStringLiteral(e.to)})`)
135
+ .join(",\n");
136
+ await conn.run(`INSERT INTO prop VALUES\n${tuples};`);
137
+ }
138
+ const seedLit = sqlStringLiteral(seed);
139
+ // CYCLE GUARD: the recursion carries NO depth column, deliberately.
140
+ // Recursive UNION deduplicates whole rows; a depth column makes every
141
+ // lap around a cycle a "new" row (same id, bigger depth), so the query
142
+ // that looks more informative is the query that never terminates — the
143
+ // catalogue graph is not a DAG. As a bare id-set the closure saturates
144
+ // and stops.
145
+ const reader = await conn.runAndReadAll(`WITH RECURSIVE reach(id) AS (\n` +
146
+ ` SELECT t FROM prop WHERE f = ${seedLit}\n` +
147
+ ` UNION\n` +
148
+ ` SELECT p.t FROM prop p JOIN reach r ON p.f = r.id\n` +
149
+ `)\n` +
150
+ `SELECT DISTINCT id FROM reach WHERE id <> ${seedLit} ORDER BY id;`);
151
+ return reader.getRows().map((row) => String(row[0]));
152
+ }
153
+ finally {
154
+ conn.disconnectSync();
155
+ }
156
+ }
157
+ export async function runCatalogueImpact(inputs, write) {
158
+ const sddRoot = resolve(inputs.sddPath ?? process.cwd());
159
+ const dir = resolve(sddRoot, inputs.cataloguesDir ?? "catalogues");
160
+ const tier = inputs.tier ?? "member";
161
+ if (!existsSync(dir)) {
162
+ write(`catalogues directory not found at ${dir}\n` +
163
+ ` Run \`sdd-cli catalogue build\` (or aggregate) first.\n`);
164
+ return { exitCode: 2 };
165
+ }
166
+ const schema = await loadCanonicalSchema();
167
+ const kinds = await loadCatalogueKinds();
168
+ const semantics = await loadRelationshipSemantics();
169
+ const rowsByCatalogue = new Map();
170
+ for (const cat of schema.catalogues) {
171
+ const filePath = join(dir, `${cat.name}.csv`);
172
+ if (!existsSync(filePath))
173
+ continue;
174
+ rowsByCatalogue.set(cat.name, (await readCatalogueCsv(filePath)).rows);
175
+ }
176
+ const projection = projectGraph(schema, kinds, rowsByCatalogue, tier);
177
+ const lines = projection.text.trimEnd().split("\n");
178
+ const nodeIds = new Set();
179
+ const edges = [];
180
+ for (const line of lines) {
181
+ const o = JSON.parse(line);
182
+ if (o.t === "node")
183
+ nodeIds.add(o.id);
184
+ else if (o.t === "edge")
185
+ edges.push({
186
+ source: o.source,
187
+ target: o.target,
188
+ type: o.type,
189
+ ...(o.properties !== undefined ? { properties: o.properties } : {}),
190
+ });
191
+ }
192
+ if (!nodeIds.has(inputs.of)) {
193
+ write(`node not found in the projection: ${inputs.of}\n` +
194
+ ` Node ids have the form "<catalogue>/<id>", e.g. contracts/orders-api.\n`);
195
+ return { exitCode: 2 };
196
+ }
197
+ const prop = buildImpactPropagation(edges, semantics);
198
+ const closure = await computeImpactClosure(prop.edges, inputs.of);
199
+ const byCatalogue = new Map();
200
+ for (const id of closure) {
201
+ const cat = id.slice(0, id.indexOf("/"));
202
+ byCatalogue.set(cat, (byCatalogue.get(cat) ?? 0) + 1);
203
+ }
204
+ write(`impact closure of ${inputs.of} (${tier} tier, schema ${schema.schemaVersion})\n`);
205
+ // Required stated lines (M-ADR 0206 §4.6): what the number means, and what
206
+ // was traversed under each declared flow — visible per run, not
207
+ // discoverable from the code.
208
+ write(` semantics: change impact over classified relationships (M-ADR 0206) —\n` +
209
+ ` each relationship traversed per its declared impact-flow; the\n` +
210
+ ` co-directionality invariant is asserted on every run\n`);
211
+ const flowCounts = [...prop.byFlow.entries()]
212
+ .sort()
213
+ .map(([f, n]) => `${f}=${n}`)
214
+ .join(" ");
215
+ write(` declared edges by resolved flow: ${flowCounts}\n`);
216
+ write(` reachable nodes: ${closure.length}\n`);
217
+ for (const [cat, n] of [...byCatalogue.entries()].sort()) {
218
+ write(` ${cat}: ${n}\n`);
219
+ }
220
+ for (const id of closure) {
221
+ write(` ${id}\n`);
222
+ }
223
+ return {
224
+ exitCode: 0,
225
+ result: { seed: inputs.of, closure, byCatalogue },
226
+ };
227
+ }
228
+ //# sourceMappingURL=impact-run.js.map
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Loader for the per-SDD `.sdd/sdd-manifest.yaml` per the schema at
3
+ * docs/method/methods/sdd-manifest-schema.yaml in sdd-method.
4
+ *
5
+ * Validates schema_version against SCHEMA_COMPAT and the
6
+ * profile↔category invariant from product-category-taxonomy.md.
7
+ */
8
+ import { readFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { parse as parseYaml } from "yaml";
11
+ import { z } from "zod";
12
+ import { SCHEMA_COMPAT, formatSchemaVersionMismatch } from "./schema-compat.js";
13
+ export const MANIFEST_REL_PATH = ".sdd/sdd-manifest.yaml";
14
+ export const PROFILES = [
15
+ "platform-profile",
16
+ "application-product",
17
+ "integration-product",
18
+ "support-repo",
19
+ "organisation-profile",
20
+ ];
21
+ export const CATEGORIES = [
22
+ "platform-root",
23
+ "platform-subsystem",
24
+ "application-product",
25
+ "integration-provider",
26
+ "integration-umbrella",
27
+ "support-repo",
28
+ "organisation-root",
29
+ ];
30
+ const PROFILE_CATEGORY_INVARIANT = {
31
+ "platform-profile": ["platform-root", "platform-subsystem"],
32
+ "application-product": ["application-product"],
33
+ "integration-product": ["integration-provider", "integration-umbrella"],
34
+ "support-repo": ["support-repo"],
35
+ "organisation-profile": ["organisation-root"],
36
+ };
37
+ // M-ADR 0193 — the data-plane axis. `pattern` and `locality` are CLOSED
38
+ // method-tier enums; `transport`/`target`/`runtime`/`notes` are free
39
+ // (adopter-tier). Malformed bindings FAIL at load, consistent with every other
40
+ // fixed manifest enum.
41
+ export const DATA_PLANE_PATTERNS = [
42
+ // Inbound (data-sourcing) sub-set.
43
+ "retrieve",
44
+ "own-store",
45
+ "stream-ingest",
46
+ "project",
47
+ // Output-side sub-set (M-ADR 0193 §4.7): how the service produces or exposes
48
+ // data. `generate` produces a scheduled/periodic artefact (compute-bearing,
49
+ // carries a runtime); `publish` exposes an outward contract/API (not
50
+ // compute-bearing).
51
+ "generate",
52
+ "publish",
53
+ ];
54
+ // Inbound data-sourcing patterns MUST name where the other end sits. `own-store`
55
+ // is a self-binding (no other end); the output-side patterns (`generate`/
56
+ // `publish`) MAY carry a locality but do not require one (M-ADR 0193 §4.7).
57
+ const LOCALITY_REQUIRED_PATTERNS = new Set([
58
+ "retrieve",
59
+ "stream-ingest",
60
+ "project",
61
+ ]);
62
+ export const DATA_PLANE_LOCALITIES = [
63
+ "internal-platform",
64
+ "internal-domain",
65
+ "external-vendor",
66
+ ];
67
+ // The OUTPUT-SIDE sub-set (M-ADR 0193 §4.7). This is the single method-tier
68
+ // direction mapping: every consumer that needs to branch on direction reads it
69
+ // from here rather than re-deriving the pattern split, so the sub-set the ADR
70
+ // declares has exactly one definition in the codebase.
71
+ const OUTPUT_SIDE_PATTERNS = new Set(["generate", "publish"]);
72
+ /**
73
+ * Direction of a data-plane pattern. DERIVED, never authored — `direction` is not
74
+ * a manifest field, so it cannot disagree with `pattern`. Three §4.7 rules branch
75
+ * on it: platform-dep reconciliation is inbound-only, the archetype-default WARN
76
+ * compares like with like, and the M-ADR 0187 convergence reasoning differs by
77
+ * direction.
78
+ */
79
+ export function dataPlaneDirection(pattern) {
80
+ return OUTPUT_SIDE_PATTERNS.has(pattern) ? "output" : "inbound";
81
+ }
82
+ /** True for the output-side sub-set (`generate`/`publish`). */
83
+ export function isOutputSidePattern(pattern) {
84
+ return OUTPUT_SIDE_PATTERNS.has(pattern);
85
+ }
86
+ // Patterns that carry a backend-compute role and therefore a runtime choice
87
+ // (M-ADR 0193 §4.1, extended by §4.7). `stream-ingest`/`project` (inbound
88
+ // compute) and `generate` (scheduled-generation) carry a runtime; `retrieve`
89
+ // (BFF/client), `own-store` (private-api), and `publish` (an exposed contract,
90
+ // not compute in the sourcing sense) do not, so a `runtime` on them is malformed.
91
+ const COMPUTE_BEARING_PATTERNS = new Set([
92
+ "stream-ingest",
93
+ "project",
94
+ "generate",
95
+ ]);
96
+ const DataPlaneBindingSchema = z
97
+ .object({
98
+ pattern: z.enum(DATA_PLANE_PATTERNS),
99
+ locality: z.enum(DATA_PLANE_LOCALITIES).optional(),
100
+ transport: z.string().optional(),
101
+ runtime: z.string().optional(),
102
+ target: z.string().optional(),
103
+ notes: z.string().optional(),
104
+ })
105
+ .refine((b) => !LOCALITY_REQUIRED_PATTERNS.has(b.pattern) || b.locality !== undefined, {
106
+ message: "locality is required for a data-sourcing binding (retrieve/stream-ingest/project); own-store and the output-side patterns (generate/publish) may omit it",
107
+ path: ["locality"],
108
+ })
109
+ .refine((b) => b.runtime === undefined || COMPUTE_BEARING_PATTERNS.has(b.pattern), {
110
+ message: "runtime is only valid on a compute-bearing binding (stream-ingest/project/generate); retrieve/own-store/publish carry no runtime choice",
111
+ path: ["runtime"],
112
+ });
113
+ // A declared departure from the archetype's default composition (M-ADR 0191
114
+ // §4.3 overlay). `expectation` names the archetype-default pattern the product
115
+ // knowingly diverges from; the rationale makes it sanctioned variance.
116
+ const DataPlaneDeviationSchema = z.object({
117
+ expectation: z.string(),
118
+ rationale: z.string().min(1, {
119
+ message: "a data_plane deviation must carry a non-empty rationale",
120
+ }),
121
+ });
122
+ const DataPlaneSchema = z.object({
123
+ bindings: z.array(DataPlaneBindingSchema),
124
+ deviations: z.array(DataPlaneDeviationSchema).optional(),
125
+ });
126
+ // Conformance coverage (M-ADR 0191 §4.3, second overlay element — WS4). An
127
+ // application-product asserts which of its archetype's conformance expectations
128
+ // its own conformance suite exercises (`coverage`), and declares a rationale for
129
+ // any expected tag it deliberately does not cover (`deviations`). The method
130
+ // gates the DECLARATION — it does not run the adopter's conformance tests.
131
+ const ConformanceSchema = z.object({
132
+ coverage: z.array(z.string()).optional(),
133
+ deviations: z
134
+ .array(z.object({
135
+ tag: z.string(),
136
+ rationale: z.string().min(1, {
137
+ message: "a conformance deviation must carry a non-empty rationale",
138
+ }),
139
+ }))
140
+ .optional(),
141
+ });
142
+ /**
143
+ * Owner-declared product lifecycle. Exported as a runtime array, not just a type,
144
+ * so the canonical-conformance test can compare the CANONICAL vocabulary against
145
+ * the IMPLEMENTATION rather than against a list duplicated in the test — a
146
+ * hardcoded expectation cannot detect the implementation drifting.
147
+ *
148
+ * `planned` = real and intended but does not exist yet (sdd-manifest-schema
149
+ * 1.9.0). Absent still reads as `active`.
150
+ */
151
+ export const LIFECYCLES = ["planned", "active", "deprecated"];
152
+ const ManifestSchema = z.object({
153
+ schema_version: z.string(),
154
+ sdd_id: z.string().regex(/^[a-z][a-z0-9-]*$/, {
155
+ message: "sdd_id must match /^[a-z][a-z0-9-]*$/",
156
+ }),
157
+ profile: z.enum(PROFILES),
158
+ category: z.enum(CATEGORIES),
159
+ domain_id: z
160
+ .string()
161
+ .regex(/^[a-z][a-z0-9-]*$/)
162
+ .optional(),
163
+ ecosystem_root: z.string().optional(),
164
+ // M-ADR 0208 §4.3: the platform this member is contained by. Canonical name;
165
+ // `ecosystem_root` above is its deprecated alias (schema 1.10.0, removed at 2.0.0).
166
+ platform_sdd_id: z.string().optional(),
167
+ aggregation_manifest_path: z.string().optional(),
168
+ description: z.string().optional(),
169
+ // Optional canonical C4 system id. Defaults to `{sdd_id}-system`. Set this
170
+ // when the system was already catalogued under an established id elsewhere
171
+ // (e.g. a reverse-extracted system pulled into its own SDD) so the id stays
172
+ // stable across SDDs and aggregation.
173
+ c4_system_id: z
174
+ .string()
175
+ .regex(/^[a-z][a-z0-9-]*$/, {
176
+ message: "c4_system_id must match /^[a-z][a-z0-9-]*$/",
177
+ })
178
+ .optional(),
179
+ // Owner-declared product lifecycle (sdd-manifest-schema 1.1.0). Absent
180
+ // means "active". Unknown values are a hard load failure, consistent with
181
+ // every other manifest enum — a typo'd lifecycle silently defaulting to
182
+ // active would recreate the exact false-active bug the field exists to fix.
183
+ // `planned` (sdd-manifest-schema 1.9.0): the product is real and intended but
184
+ // does not exist yet — the normal OPENING state of a forward-authored product,
185
+ // where the SDD is written before the code. The absent-means-"active" default
186
+ // was the one state a pre-realisation estate cannot be in, so it silently
187
+ // asserted that every unbuilt product was running. A fact about the PRODUCT,
188
+ // deliberately not about the document (that is `status`).
189
+ lifecycle: z.enum(LIFECYCLES).optional(),
190
+ // Application-product surface trait (M-ADR 0167, sdd-manifest-schema 1.2.0).
191
+ // Fixed enum: ui (human-facing, default) | api (headless). Unknown values
192
+ // are a hard load failure like every other fixed manifest enum. Ignored off
193
+ // application-product (cleared with a convention finding in the products
194
+ // builder). Absent reads as "ui".
195
+ surface: z.enum(["ui", "api"]).optional(),
196
+ // Application-product packaging trait (M-ADR 0175, sdd-manifest-schema
197
+ // 1.7.0). Fixed enum, deliberately narrow: `vendor-saas` marks a
198
+ // packaged-SaaS product (vendor-operated runtime, vendor-versioned base);
199
+ // absent reads as self-operated, the unmarked norm. Unknown values are a hard
200
+ // load failure like every other fixed manifest enum. Ignored off
201
+ // application-product (cleared with a convention finding in the products
202
+ // builder). Per M-ADR 0175 §4 no builder may branch on this beyond
203
+ // evidence/deployment handling.
204
+ packaging: z.enum(["vendor-saas"]).optional(),
205
+ // Reference-exemplar trait (sdd-manifest-schema 1.8.0). PROFILE-AGNOSTIC,
206
+ // unlike the three traits it sits between — a demonstration
207
+ // application-product is as conceivable as the integration-products the first
208
+ // cohort came from — so the products builder must NOT clear it off
209
+ // application-product. Absent reads as a real SDD, the unmarked norm, with no
210
+ // finding. Only `true` is meaningful; `false` is redundant but accepted.
211
+ exemplar: z.boolean().optional(),
212
+ // Application-product archetype trait (M-ADR 0191, sdd-manifest-schema
213
+ // 1.2.0). RESOLVED enum, not fixed: the loader validates only the slug
214
+ // shape here. Whether the value resolves against the active vocabulary
215
+ // (method ∪ RA-bundle ∪ adopter) is checked in the build step, which has
216
+ // the bundle cache. Absent reads as "unclassified" (with a finding). Named
217
+ // application_archetype to stay distinct from the ADR 0173 domain archetype
218
+ // and the ADR 0169 openui archetype.
219
+ application_archetype: z
220
+ .string()
221
+ .regex(/^[a-z][a-z0-9-]*$/, {
222
+ message: "application_archetype must match /^[a-z][a-z0-9-]*$/",
223
+ })
224
+ .optional(),
225
+ deprecated_date: z
226
+ .string()
227
+ .regex(/^\d{4}-\d{2}-\d{2}$/, {
228
+ message: "deprecated_date must be a YYYY-MM-DD date",
229
+ })
230
+ .optional(),
231
+ successor_sdd_id: z
232
+ .string()
233
+ .regex(/^[a-z][a-z0-9-]*$/, {
234
+ message: "successor_sdd_id must match /^[a-z][a-z0-9-]*$/ (sdd_id vocabulary)",
235
+ })
236
+ .optional(),
237
+ // Application-product data-plane axis (M-ADR 0193, sdd-manifest-schema 1.3.0);
238
+ // `data_plane.deviations` added in 1.4.0 (M-ADR 0191 §4.3 overlay). A
239
+ // composition of typed bindings (malformed bindings FAIL at load) plus
240
+ // optional declared deviations from the archetype default, each carrying a
241
+ // rationale so the mismatch is sanctioned variance, not silent drift.
242
+ data_plane: DataPlaneSchema.optional(),
243
+ // Conformance coverage declaration (M-ADR 0191 §4.3 WS4, sdd-manifest-schema
244
+ // 1.5.0). Which archetype conformance expectations the product's suite
245
+ // exercises, plus rationales for any it deliberately does not.
246
+ conformance: ConformanceSchema.optional(),
247
+ });
248
+ /** What `products.packaging` carries when the manifest declares nothing. */
249
+ export const PACKAGING_DEFAULT = "self-operated";
250
+ /**
251
+ * Read and validate the `.sdd/sdd-manifest.yaml` at the given SDD repo root.
252
+ *
253
+ * Throws on:
254
+ * - file missing
255
+ * - YAML parse error
256
+ * - shape violation (missing fields, bad enum value, bad slug pattern)
257
+ * - profile↔category invariant violation
258
+ * - schema_version outside SCHEMA_COMPAT.sdd_manifest range
259
+ */
260
+ /**
261
+ * Top-level keys this build understands. Derived from the zod shape so it cannot
262
+ * drift from what is actually parsed — a hand-maintained list would go stale and
263
+ * start reporting known fields as unknown, which is worse than saying nothing.
264
+ */
265
+ const KNOWN_MANIFEST_KEYS = new Set(Object.keys(ManifestSchema.shape));
266
+ /**
267
+ * Top-level keys present in the file that this build does not know.
268
+ *
269
+ * Deliberately TOP-LEVEL only. Nested blocks (`data_plane`, `conformance`) are
270
+ * validated by their own object schemas and strip their own unknowns; walking
271
+ * them would need a per-schema recursion this does not yet warrant. Stated so the
272
+ * limit is known rather than assumed — an unknown key nested inside `data_plane`
273
+ * is still silently dropped today.
274
+ */
275
+ function findUnknownTopLevelKeys(raw) {
276
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
277
+ return [];
278
+ return Object.keys(raw)
279
+ .filter((k) => !KNOWN_MANIFEST_KEYS.has(k))
280
+ .sort();
281
+ }
282
+ export async function loadSddManifest(sddPath, options) {
283
+ const sourcePath = join(sddPath, MANIFEST_REL_PATH);
284
+ let text;
285
+ try {
286
+ text = await readFile(sourcePath, "utf-8");
287
+ }
288
+ catch (err) {
289
+ const code = err.code;
290
+ if (code === "ENOENT") {
291
+ throw new Error(`SDD manifest not found at ${sourcePath}\n\n` +
292
+ `Every SDD repo MUST declare an .sdd/sdd-manifest.yaml per ADR 0134 §4.6.\n` +
293
+ `Schema: docs/method/methods/sdd-manifest-schema.yaml in sdd-method.\n` +
294
+ `Bootstrap: \`sdd-cli init\` (when migrated to support manifest authoring).`);
295
+ }
296
+ throw err;
297
+ }
298
+ let raw;
299
+ try {
300
+ raw = parseYaml(text);
301
+ }
302
+ catch (err) {
303
+ throw new Error(`failed to parse YAML at ${sourcePath}: ${err.message}`);
304
+ }
305
+ // Unknown top-level keys are REPORTED, not silently dropped and not fatal.
306
+ //
307
+ // zod's default is `strip`, so a key this build does not know vanished without a
308
+ // trace: an adopter following a schema that documents a field their sdd-cli
309
+ // predates got a clean validation and a silent no-op. Not hypothetical —
310
+ // M-ADR 0175's own version history records exactly this happening to
311
+ // `packaging`, where the ADR instructed adopters to declare a field the CLI had
312
+ // never implemented.
313
+ //
314
+ // `.strict()` is the WRONG fix: it turns the same situation into a hard failure,
315
+ // so an older CLI could not build an SDD whose manifest declares anything newer
316
+ // than itself. Newer baselines ship newer schemas by design, so that inverts the
317
+ // problem instead of solving it. Reporting keeps the declaration visible while
318
+ // leaving the build working.
319
+ const unknownKeys = findUnknownTopLevelKeys(raw);
320
+ const parsed = ManifestSchema.safeParse(raw);
321
+ if (!parsed.success) {
322
+ const issues = parsed.error.issues
323
+ .map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`)
324
+ .join("\n");
325
+ throw new Error(`invalid SDD manifest at ${sourcePath}:\n${issues}`);
326
+ }
327
+ // Compat range check.
328
+ if (!isInRange(parsed.data.schema_version, SCHEMA_COMPAT.sdd_manifest)) {
329
+ throw new Error(formatSchemaVersionMismatch({
330
+ schema: "sdd_manifest",
331
+ readVersion: parsed.data.schema_version,
332
+ cliVersion: options.cliVersion,
333
+ sourcePath,
334
+ }));
335
+ }
336
+ // Profile ↔ category invariant.
337
+ const permitted = PROFILE_CATEGORY_INVARIANT[parsed.data.profile];
338
+ if (!permitted.includes(parsed.data.category)) {
339
+ throw new Error(`invalid profile/category combination at ${sourcePath}:\n` +
340
+ ` profile=${parsed.data.profile}, category=${parsed.data.category}\n` +
341
+ ` permitted categories under profile=${parsed.data.profile}: ${permitted.join(", ")}\n` +
342
+ ` per docs/method/methods/product-category-taxonomy.md`);
343
+ }
344
+ return {
345
+ schemaVersion: parsed.data.schema_version,
346
+ sddId: parsed.data.sdd_id,
347
+ profile: parsed.data.profile,
348
+ category: parsed.data.category,
349
+ ...(parsed.data.domain_id !== undefined ? { domainId: parsed.data.domain_id } : {}),
350
+ ...(parsed.data.c4_system_id !== undefined ? { c4SystemId: parsed.data.c4_system_id } : {}),
351
+ ...(parsed.data.ecosystem_root !== undefined
352
+ ? { ecosystemRoot: parsed.data.ecosystem_root }
353
+ : {}),
354
+ ...((parsed.data.platform_sdd_id ?? parsed.data.ecosystem_root) !== undefined
355
+ ? {
356
+ platformSddId: (parsed.data.platform_sdd_id ?? parsed.data.ecosystem_root),
357
+ platformFieldDeprecated: parsed.data.platform_sdd_id === undefined && parsed.data.ecosystem_root !== undefined,
358
+ }
359
+ : {}),
360
+ ...(parsed.data.aggregation_manifest_path !== undefined
361
+ ? { aggregationManifestPath: parsed.data.aggregation_manifest_path }
362
+ : {}),
363
+ ...(parsed.data.description !== undefined ? { description: parsed.data.description } : {}),
364
+ ...(parsed.data.lifecycle !== undefined ? { lifecycle: parsed.data.lifecycle } : {}),
365
+ ...(parsed.data.surface !== undefined ? { surface: parsed.data.surface } : {}),
366
+ ...(parsed.data.packaging !== undefined ? { packaging: parsed.data.packaging } : {}),
367
+ ...(parsed.data.exemplar !== undefined ? { exemplar: parsed.data.exemplar } : {}),
368
+ ...(parsed.data.application_archetype !== undefined
369
+ ? { applicationArchetype: parsed.data.application_archetype }
370
+ : {}),
371
+ ...(parsed.data.deprecated_date !== undefined
372
+ ? { deprecatedDate: parsed.data.deprecated_date }
373
+ : {}),
374
+ ...(parsed.data.successor_sdd_id !== undefined
375
+ ? { successorSddId: parsed.data.successor_sdd_id }
376
+ : {}),
377
+ ...(parsed.data.data_plane !== undefined ? { dataPlane: parsed.data.data_plane } : {}),
378
+ ...(parsed.data.conformance !== undefined ? { conformance: parsed.data.conformance } : {}),
379
+ ...(unknownKeys.length > 0 ? { unknownKeys } : {}),
380
+ sourcePath,
381
+ };
382
+ }
383
+ /**
384
+ * Caret-prefixed semver range check. Accepts X in `^1.2.3` if X.major === 1
385
+ * and X >= 1.2.3.
386
+ *
387
+ * Restricted to the `^X.Y.Z` shape — that's the only form SCHEMA_COMPAT uses.
388
+ */
389
+ export function isInRange(version, range) {
390
+ if (!range.startsWith("^")) {
391
+ throw new Error(`unsupported range syntax: ${range}`);
392
+ }
393
+ const v = parseSemver(version);
394
+ const r = parseSemver(range.slice(1));
395
+ if (v === null || r === null)
396
+ return false;
397
+ if (v.major !== r.major)
398
+ return false;
399
+ if (v.major === 0) {
400
+ // Per semver, 0.x is unstable; ^0.x.y matches only 0.x.y itself.
401
+ return v.minor === r.minor && v.patch >= r.patch;
402
+ }
403
+ if (v.minor < r.minor)
404
+ return false;
405
+ if (v.minor === r.minor && v.patch < r.patch)
406
+ return false;
407
+ return true;
408
+ }
409
+ function parseSemver(s) {
410
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(s);
411
+ if (!m)
412
+ return null;
413
+ const [, maj, min, pat] = m;
414
+ if (maj === undefined || min === undefined || pat === undefined)
415
+ return null;
416
+ return {
417
+ major: parseInt(maj, 10),
418
+ minor: parseInt(min, 10),
419
+ patch: parseInt(pat, 10),
420
+ };
421
+ }
422
+ //# sourceMappingURL=manifest-loader.js.map