@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,84 @@
1
+ import { profileFromRepoKind } from "../repo-kind.js";
2
+ const PROFILE_ORDER = ["platform", "application", "integration"];
3
+ export function format(repos, opts) {
4
+ const filtered = opts.profileFilter
5
+ ? repos.filter((r) => profileFromRepoKind(r.kind) === opts.profileFilter)
6
+ : repos;
7
+ if (opts.format === "json")
8
+ return formatJson(filtered);
9
+ return formatText(filtered, opts.verbose === true);
10
+ }
11
+ function formatJson(repos) {
12
+ const payload = repos.map((r) => ({
13
+ path: r.path,
14
+ profile: profileFromRepoKind(r.kind),
15
+ kind: r.kind,
16
+ bundle_version: r.bundleVersion,
17
+ bundles: r.bundles.map(toJsonBundle),
18
+ }));
19
+ return JSON.stringify(payload, null, 2) + "\n";
20
+ }
21
+ function toJsonBundle(b) {
22
+ const out = {
23
+ name: b.name,
24
+ version: b.version,
25
+ is_forward_base: b.isForwardBase,
26
+ managed_path_count: b.managedPathCount,
27
+ };
28
+ if (b.producedByAdopter)
29
+ out.produced_by_adopter = b.producedByAdopter;
30
+ if (b.scope)
31
+ out.scope = b.scope;
32
+ if (b.requiresForwardBase)
33
+ out.requires_forward_base = b.requiresForwardBase;
34
+ if (b.requiresPack)
35
+ out.requires_pack = b.requiresPack;
36
+ return out;
37
+ }
38
+ function formatText(repos, verbose) {
39
+ if (repos.length === 0)
40
+ return "No SDD repositories found.\n";
41
+ const grouped = new Map();
42
+ for (const repo of repos) {
43
+ const profile = profileFromRepoKind(repo.kind);
44
+ const list = grouped.get(profile) ?? [];
45
+ list.push(repo);
46
+ grouped.set(profile, list);
47
+ }
48
+ const lines = [];
49
+ for (const profile of PROFILE_ORDER) {
50
+ const list = grouped.get(profile);
51
+ if (!list || list.length === 0)
52
+ continue;
53
+ lines.push(`${profile} (${list.length}):`);
54
+ for (const repo of list) {
55
+ lines.push(formatRepoLine(repo, verbose));
56
+ // Render non-forward-base bundles (adopter packs) as indented
57
+ // '+' lines beneath the repo line. Forward-base info is already
58
+ // in the repo line itself.
59
+ for (const b of repo.bundles) {
60
+ if (b.isForwardBase)
61
+ continue;
62
+ lines.push(formatPackLine(b, verbose));
63
+ }
64
+ }
65
+ lines.push("");
66
+ }
67
+ return lines.join("\n");
68
+ }
69
+ function formatRepoLine(repo, verbose) {
70
+ const version = repo.bundleVersion ?? "no-lock";
71
+ const base = ` ${repo.path} [${version}]`;
72
+ if (!verbose)
73
+ return base;
74
+ const fwd = repo.bundles.find((b) => b.isForwardBase);
75
+ const count = fwd ? ` (${fwd.managedPathCount} managed paths)` : "";
76
+ return base + count;
77
+ }
78
+ function formatPackLine(b, verbose) {
79
+ const producer = b.producedByAdopter ? ` from ${b.producedByAdopter}` : "";
80
+ const requires = b.requiresPack ? ` requires ${b.requiresPack}` : "";
81
+ const count = verbose ? ` (${b.managedPathCount} managed paths)` : "";
82
+ return ` + ${b.name} ${b.version}${producer}${requires}${count}`;
83
+ }
84
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,16 @@
1
+ import { discoverSddRepos } from "./discover.js";
2
+ import { format } from "./format.js";
3
+ export async function runList(inputs, write) {
4
+ const roots = inputs.workspaceRoots.length > 0
5
+ ? inputs.workspaceRoots
6
+ : [process.cwd()];
7
+ const repos = await discoverSddRepos(roots);
8
+ const output = format(repos, {
9
+ format: inputs.format,
10
+ verbose: inputs.verbose === true,
11
+ ...(inputs.profileFilter ? { profileFilter: inputs.profileFilter } : {}),
12
+ });
13
+ write(output);
14
+ return { exitCode: 0 };
15
+ }
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,413 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { bundleNameFromRepoKind, isForwardBase, readRepoKind, } from "./repo-kind.js";
4
+ export const LOCK_FILE = "method-baseline.lock";
5
+ /**
6
+ * lock_version 3 adds a **per-path sha256** to each `managed_paths` entry.
7
+ *
8
+ * Why: a dry run could previously say only "N conflicting managed file(s)",
9
+ * which reads as "you have local edits at risk". Twice the truth was "N files
10
+ * differ because you are several minors behind and every one is untouched" —
11
+ * established both times by hand-extracting bundles and diffing. The lock
12
+ * identified WHICH bundle was applied but nothing about its CONTENT, and the
13
+ * content is unreachable: forward bases are deliberately not cached
14
+ * (M-ADR 0176 §4.2), `dist/` is gitignored, and lock_version 2 recorded paths
15
+ * without digests. Recording the digest of each file AS APPLIED makes the lock
16
+ * self-verifying: pristine-vs-edited is then decidable from the repo alone —
17
+ * no network, no bundle store, and it survives a fresh clone and CI, which is
18
+ * exactly where the answer is needed.
19
+ *
20
+ * NOT retroactive, by construction: a repo whose last sync predates v3 has no
21
+ * digests to compare against. That case must say so explicitly rather than
22
+ * guess — see `classifyConflicts`.
23
+ */
24
+ export const LOCK_VERSION = 3;
25
+ /** Lock versions this build can read. Older ones are migrated in-memory on read. */
26
+ const READABLE_LOCK_VERSIONS = [2, 3];
27
+ /** `- <64-hex sha256> <path>` — the sha256sum convention, one line per path. */
28
+ const DIGEST_ENTRY_RE = /^([0-9a-f]{64}) (.+)$/;
29
+ export async function readLockFile(repoRoot) {
30
+ let raw;
31
+ try {
32
+ raw = await readFile(join(repoRoot, LOCK_FILE), "utf8");
33
+ }
34
+ catch (err) {
35
+ if (err.code === "ENOENT")
36
+ return null;
37
+ throw err;
38
+ }
39
+ if (isVersioned(raw))
40
+ return parseLockFile(raw);
41
+ const repoKind = await readRepoKind(repoRoot);
42
+ if (!repoKind) {
43
+ throw new Error(`Legacy lock detected but cannot infer bundle name: ${LOCK_FILE} is v1 ` +
44
+ `and .sdd-repo-kind is missing. Set .sdd-repo-kind manually or ` +
45
+ `delete the lock file before re-syncing.`);
46
+ }
47
+ return parseLockFile(raw, { legacyBundleName: bundleNameFromRepoKind(repoKind) });
48
+ }
49
+ export async function writeLockFile(repoRoot, lock) {
50
+ await writeFile(join(repoRoot, LOCK_FILE), serialiseLockFile(lock), "utf8");
51
+ }
52
+ export function parseLockFile(raw, opts = {}) {
53
+ if (isVersioned(raw))
54
+ return parseVersioned(raw);
55
+ if (!opts.legacyBundleName) {
56
+ throw new Error(`Cannot parse legacy v1 lock without legacyBundleName option ` +
57
+ `(needed to migrate to v2 shape).`);
58
+ }
59
+ return migrateV1(raw, opts.legacyBundleName);
60
+ }
61
+ export function serialiseLockFile(lock) {
62
+ const lines = [
63
+ `method_repo: ${lock.methodRepo}`,
64
+ `lock_version: ${lock.lockVersion}`,
65
+ "bundles:",
66
+ ];
67
+ for (const b of lock.bundles) {
68
+ lines.push(` - name: ${b.name}`, ` bundle_version: "${b.bundleVersion}"`, ` source_commit: "${b.sourceCommit}"`, ` bundle_sha256: "${b.bundleSha256}"`, ` synced_at_utc: "${b.syncedAtUtc}"`, ` is_forward_base: ${b.isForwardBase}`);
69
+ if (b.producedByAdopter !== undefined) {
70
+ lines.push(` produced_by_adopter: "${b.producedByAdopter}"`);
71
+ }
72
+ if (b.scope !== undefined) {
73
+ lines.push(` scope: "${b.scope}"`);
74
+ }
75
+ if (b.requiresForwardBase) {
76
+ lines.push(` requires_forward_base:`, ` bundle: "${b.requiresForwardBase.bundle}"`, ` minimum_version: "${b.requiresForwardBase.minimumVersion}"`, ` source_repo: "${b.requiresForwardBase.sourceRepo}"`);
77
+ }
78
+ if (b.requiresPack) {
79
+ lines.push(` requires_pack:`, ` name: "${b.requiresPack.name}"`, ` version: "${b.requiresPack.version}"`);
80
+ }
81
+ lines.push(` managed_paths:`);
82
+ for (const p of b.managedPaths) {
83
+ // `- <digest> <path>` where a digest is known, bare `- <path>` otherwise.
84
+ // A bare entry is a truthful "not recorded", which is what a lock migrated
85
+ // from version 2 carries until its next sync.
86
+ const digest = b.pathDigests?.[p];
87
+ lines.push(digest ? ` - ${digest} ${p}` : ` - ${p}`);
88
+ }
89
+ }
90
+ return lines.join("\n") + "\n";
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Internal helpers
94
+ // ---------------------------------------------------------------------------
95
+ /** Declared lock version, or null for a legacy v1 lock (no version line). */
96
+ function lockVersionOf(raw) {
97
+ const m = /^lock_version: *"?(\d+)"?/m.exec(raw);
98
+ return m ? Number(m[1]) : null;
99
+ }
100
+ function isVersioned(raw) {
101
+ return lockVersionOf(raw) !== null;
102
+ }
103
+ function stripQuotes(value) {
104
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
105
+ return value.slice(1, -1);
106
+ }
107
+ return value;
108
+ }
109
+ function parseVersioned(raw) {
110
+ let methodRepo;
111
+ let lockVersion;
112
+ const bundles = [];
113
+ let inBundles = false;
114
+ let inPaths = false;
115
+ let inForwardBase = false;
116
+ let inPack = false;
117
+ let cur = {
118
+ managedPaths: [],
119
+ };
120
+ let forwardBase = {};
121
+ let pack = {};
122
+ const flushNestedObjects = () => {
123
+ if (inForwardBase && forwardBase.bundle && forwardBase.minimumVersion) {
124
+ cur.requiresForwardBase = {
125
+ bundle: forwardBase.bundle,
126
+ minimumVersion: forwardBase.minimumVersion,
127
+ sourceRepo: forwardBase.sourceRepo ?? "sdd-method",
128
+ };
129
+ }
130
+ if (inPack && pack.name && pack.version) {
131
+ cur.requiresPack = { name: pack.name, version: pack.version };
132
+ }
133
+ inForwardBase = false;
134
+ inPack = false;
135
+ forwardBase = {};
136
+ pack = {};
137
+ };
138
+ const flushBundle = () => {
139
+ if (cur.name === undefined)
140
+ return;
141
+ flushNestedObjects();
142
+ const required = [
143
+ "name",
144
+ "bundleVersion",
145
+ "sourceCommit",
146
+ "bundleSha256",
147
+ "syncedAtUtc",
148
+ "isForwardBase",
149
+ ];
150
+ for (const k of required) {
151
+ if (cur[k] === undefined) {
152
+ throw new Error(`${LOCK_FILE} bundle entry "${cur.name}" missing required key: ${k}`);
153
+ }
154
+ }
155
+ bundles.push(cur);
156
+ cur = { managedPaths: [] };
157
+ inPaths = false;
158
+ };
159
+ for (const rawLine of raw.split("\n")) {
160
+ const line = rawLine.replace(/\r$/, "");
161
+ if (!inBundles) {
162
+ if (line === "bundles:") {
163
+ inBundles = true;
164
+ continue;
165
+ }
166
+ const top = /^([a-z_][a-z0-9_]*): (.*)$/.exec(line);
167
+ if (top) {
168
+ const [, key, value] = top;
169
+ if (key === "method_repo")
170
+ methodRepo = stripQuotes(value);
171
+ else if (key === "lock_version")
172
+ lockVersion = Number(stripQuotes(value));
173
+ }
174
+ continue;
175
+ }
176
+ // Inside the bundles block.
177
+ if (line.startsWith(" - name:")) {
178
+ flushBundle();
179
+ const name = stripQuotes(line.slice(" - name:".length).trim());
180
+ cur.name = name;
181
+ continue;
182
+ }
183
+ if (line === " managed_paths:") {
184
+ flushNestedObjects();
185
+ inPaths = true;
186
+ continue;
187
+ }
188
+ if (inPaths && line.startsWith(" - ")) {
189
+ // Tolerate a double-quoted scalar. The writer emits these unquoted
190
+ // today, but the entries contain ": " (ADR filenames carry a colon), so
191
+ // a YAML reader parses 113 of 325 entries in one real lock as MAPPINGS
192
+ // rather than strings — silently, with no error. Quoting on write is the
193
+ // fix; reading both forms ships first so no lock can be written that an
194
+ // older reader cannot parse. See method-open-items §3l.
195
+ const entry = stripQuotes(line.slice(" - ".length));
196
+ const withDigest = DIGEST_ENTRY_RE.exec(entry);
197
+ if (withDigest) {
198
+ const [, digest, path] = withDigest;
199
+ cur.managedPaths.push(path);
200
+ (cur.pathDigests ??= {})[path] = digest;
201
+ }
202
+ else {
203
+ // Bare path: a version-2 entry, or a v3 path whose digest was not
204
+ // recorded. Left out of pathDigests so it reads as "not recorded".
205
+ cur.managedPaths.push(entry);
206
+ }
207
+ continue;
208
+ }
209
+ if (line === " requires_forward_base:") {
210
+ flushNestedObjects();
211
+ inForwardBase = true;
212
+ continue;
213
+ }
214
+ if (line === " requires_pack:") {
215
+ flushNestedObjects();
216
+ inPack = true;
217
+ continue;
218
+ }
219
+ // Two-space-indented nested keys (under requires_forward_base / requires_pack)
220
+ if (inForwardBase || inPack) {
221
+ const nested = /^ ([a-z_][a-z0-9_]*): (.*)$/.exec(line);
222
+ if (nested) {
223
+ const [, key, rawValue] = nested;
224
+ const value = stripQuotes(rawValue);
225
+ if (inForwardBase) {
226
+ if (key === "bundle")
227
+ forwardBase.bundle = value;
228
+ else if (key === "minimum_version")
229
+ forwardBase.minimumVersion = value;
230
+ else if (key === "source_repo")
231
+ forwardBase.sourceRepo = value;
232
+ }
233
+ else if (inPack) {
234
+ if (key === "name")
235
+ pack.name = value;
236
+ else if (key === "version")
237
+ pack.version = value;
238
+ }
239
+ continue;
240
+ }
241
+ // A non-nested line ends the object scope.
242
+ flushNestedObjects();
243
+ }
244
+ const meta = /^ ([a-z_][a-z0-9_]*): (.*)$/.exec(line);
245
+ if (meta) {
246
+ inPaths = false;
247
+ const [, key, rawValue] = meta;
248
+ const value = stripQuotes(rawValue);
249
+ switch (key) {
250
+ case "bundle_version":
251
+ cur.bundleVersion = value;
252
+ break;
253
+ case "source_commit":
254
+ cur.sourceCommit = value;
255
+ break;
256
+ case "bundle_sha256":
257
+ cur.bundleSha256 = value;
258
+ break;
259
+ case "synced_at_utc":
260
+ cur.syncedAtUtc = value;
261
+ break;
262
+ case "is_forward_base":
263
+ cur.isForwardBase = value === "true";
264
+ break;
265
+ case "produced_by_adopter":
266
+ cur.producedByAdopter = value;
267
+ break;
268
+ case "scope":
269
+ cur.scope = value;
270
+ break;
271
+ }
272
+ continue;
273
+ }
274
+ if (line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t")) {
275
+ inBundles = false;
276
+ }
277
+ }
278
+ flushBundle();
279
+ if (methodRepo === undefined) {
280
+ throw new Error(`${LOCK_FILE} is missing required key: method_repo`);
281
+ }
282
+ // Older readable versions migrate in-memory and are upgraded on the next
283
+ // write; a NEWER version is a hard failure, because this build cannot know
284
+ // what it would be silently dropping.
285
+ if (!READABLE_LOCK_VERSIONS.includes(lockVersion)) {
286
+ throw new Error(`${LOCK_FILE} lock_version is ${lockVersion}; this build reads ` +
287
+ `${READABLE_LOCK_VERSIONS.join(" or ")} and writes ${LOCK_VERSION}` +
288
+ (lockVersion !== undefined && lockVersion > LOCK_VERSION
289
+ ? ` — the lock was written by a newer sdd-cli; upgrade rather than downgrading the lock.`
290
+ : `.`));
291
+ }
292
+ return { methodRepo, lockVersion: LOCK_VERSION, bundles };
293
+ }
294
+ function migrateV1(raw, legacyBundleName) {
295
+ const scalars = new Map();
296
+ const managedPaths = [];
297
+ let inManagedList = false;
298
+ for (const rawLine of raw.split("\n")) {
299
+ const line = rawLine.replace(/\r$/, "");
300
+ if (inManagedList) {
301
+ if (line.startsWith(" - ")) {
302
+ managedPaths.push(line.slice(4));
303
+ continue;
304
+ }
305
+ inManagedList = false;
306
+ }
307
+ if (line === "managed_paths:") {
308
+ inManagedList = true;
309
+ continue;
310
+ }
311
+ const m = /^([a-z_][a-z0-9_]*): (.*)$/.exec(line);
312
+ if (m)
313
+ scalars.set(m[1], stripQuotes(m[2]));
314
+ }
315
+ const required = [
316
+ "method_repo",
317
+ "bundle_version",
318
+ "source_commit",
319
+ "bundle_sha256",
320
+ "synced_at_utc",
321
+ ];
322
+ for (const key of required) {
323
+ if (!scalars.has(key)) {
324
+ throw new Error(`${LOCK_FILE} (legacy) is missing required key: ${key}`);
325
+ }
326
+ }
327
+ return {
328
+ methodRepo: scalars.get("method_repo"),
329
+ lockVersion: LOCK_VERSION,
330
+ bundles: [
331
+ {
332
+ name: legacyBundleName,
333
+ bundleVersion: scalars.get("bundle_version"),
334
+ sourceCommit: scalars.get("source_commit"),
335
+ bundleSha256: scalars.get("bundle_sha256"),
336
+ syncedAtUtc: scalars.get("synced_at_utc"),
337
+ isForwardBase: isForwardBase(legacyBundleName),
338
+ managedPaths,
339
+ },
340
+ ],
341
+ };
342
+ }
343
+ // ---------------------------------------------------------------------------
344
+ // Convenience helpers for sync code.
345
+ // ---------------------------------------------------------------------------
346
+ export function findBundle(lock, name) {
347
+ if (!lock)
348
+ return null;
349
+ return lock.bundles.find((b) => b.name === name) ?? null;
350
+ }
351
+ /**
352
+ * Replace the bundle entry with `entry.name`, or append it. Returns a
353
+ * new LockFile preserving order (existing bundles keep their position;
354
+ * a new bundle goes at the end).
355
+ */
356
+ export function upsertBundle(lock, entry) {
357
+ const base = lock ?? {
358
+ methodRepo: "sdd-method",
359
+ lockVersion: LOCK_VERSION,
360
+ bundles: [],
361
+ };
362
+ const idx = base.bundles.findIndex((b) => b.name === entry.name);
363
+ const bundles = idx >= 0
364
+ ? base.bundles.map((b, i) => (i === idx ? entry : b))
365
+ : [...base.bundles, entry];
366
+ return { ...base, bundles };
367
+ }
368
+ /**
369
+ * Remove the bundle entry named `name`, preserving the order of the rest.
370
+ * Returns the lock unchanged if the name is absent. Pure — composed with
371
+ * `upsertBundle` this gives bundle MIGRATION as a single lock value
372
+ * (`upsertBundle(removeBundle(lock, old), incoming)`), so one
373
+ * `writeLockFile` call swaps the blocks with no intermediate state where
374
+ * neither bundle is recorded.
375
+ */
376
+ export function removeBundle(lock, name) {
377
+ const base = lock ?? {
378
+ methodRepo: "sdd-method",
379
+ lockVersion: LOCK_VERSION,
380
+ bundles: [],
381
+ };
382
+ return { ...base, bundles: base.bundles.filter((b) => b.name !== name) };
383
+ }
384
+ /**
385
+ * True if any bundle in the lock other than `excluding` is a forward
386
+ * base. Used to enforce requires_forward_base for additive layers.
387
+ */
388
+ export function hasForwardBase(lock, excluding) {
389
+ if (!lock)
390
+ return false;
391
+ return lock.bundles.some((b) => b.name !== excluding && b.isForwardBase);
392
+ }
393
+ /**
394
+ * Find the forward-base bundle entry in a lock, or null if none.
395
+ */
396
+ export function findForwardBase(lock, excluding) {
397
+ if (!lock)
398
+ return null;
399
+ return (lock.bundles.find((b) => b.name !== excluding && b.isForwardBase) ?? null);
400
+ }
401
+ export function pathToBundleMap(lock, excluding) {
402
+ const m = new Map();
403
+ if (!lock)
404
+ return m;
405
+ for (const b of lock.bundles) {
406
+ if (excluding !== undefined && b.name === excluding)
407
+ continue;
408
+ for (const p of b.managedPaths)
409
+ m.set(p, b.name);
410
+ }
411
+ return m;
412
+ }
413
+ //# sourceMappingURL=lock-file.js.map
@@ -0,0 +1,18 @@
1
+ // Output helpers shared by `pack build` and `pack validate`. Split out so
2
+ // `pack validate` can be imported without pulling in the builder — the
3
+ // verify-only CLI ships validation and must not ship the code that writes
4
+ // a pack (M-ADR 0212 §4.3b).
5
+ export const SECTION_RULE = "═".repeat(59);
6
+ export function section(title) {
7
+ return `\n${SECTION_RULE}\n ${title}\n${SECTION_RULE}\n\n`;
8
+ }
9
+ export function info(msg) {
10
+ return `[INFO] ${msg}\n`;
11
+ }
12
+ export function pass(msg) {
13
+ return `[PASS] ${msg}\n`;
14
+ }
15
+ export function fail(msg) {
16
+ return `[FAIL] ${msg}\n`;
17
+ }
18
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schema for adopter pack seeds. Mirrors
4
+ * docs/method/foundations/schemas/context-pack-seed-schema.yaml.
5
+ *
6
+ * Used by `sdd-cli pack validate` for fast feedback during authoring.
7
+ * The structural parse in seed.ts already enforces presence of required
8
+ * fields; this schema layer adds value-shape checks (semver patterns,
9
+ * known forward-base bundle names, non-empty managed_paths).
10
+ */
11
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
12
+ const VERSIONED_RANGE_PATTERN = /^(\^|~|>=)?v?\d+\.\d+\.\d+$/;
13
+ // Exported so the canonical-schema conformance test can assert this vocabulary
14
+ // matches context-pack-seed-schema.yaml's `forward_base.bundle` enum. The two had
15
+ // drifted (canonical carried three values, this four) with nothing to detect it.
16
+ export const KNOWN_FORWARD_BASES = [
17
+ "app-baseline",
18
+ "integration-baseline",
19
+ "platform-baseline",
20
+ "reverse-baseline",
21
+ ];
22
+ export const PackSeedSchema = z.object({
23
+ version: z
24
+ .string()
25
+ .regex(SEMVER_PATTERN, "version must be semver (X.Y.Z)"),
26
+ scope: z
27
+ .string()
28
+ .regex(/^[a-z0-9-]+$/, "scope must be lower-kebab-case slug"),
29
+ // Optional per ADR 0176 §4.6: absent = profile-agnostic pack.
30
+ forwardBase: z
31
+ .object({
32
+ bundle: z.enum(KNOWN_FORWARD_BASES),
33
+ minimumVersion: z
34
+ .string()
35
+ .regex(/^v\d+\.\d+\.\d+$/, "forward_base.minimum_version must be vX.Y.Z"),
36
+ sourceRepo: z.string().min(1),
37
+ })
38
+ .optional(),
39
+ // Vendor-base pack pin (M-ADR 0175, seed schema 1.2.0). Present only on a
40
+ // vendor-base pack: names the vendor and the vendor version whose documented
41
+ // truth this pack's content encodes.
42
+ //
43
+ // The point of a PACK-LEVEL pin is that a consuming SDD does not restate it. A
44
+ // consumer pins the pack exactly (its bundles-list entry + sha256) and the
45
+ // vendor version it inherits is DERIVED from here. Recording it consumer-side
46
+ // as well would be a second source of truth that can silently disagree, and the
47
+ // pack is authoritative for its own vendor pin.
48
+ //
49
+ // `vendorVersion` is free-form by design: it carries either a formal vendor
50
+ // version or the anchor schema's `<vendor>-docs@YYYY-MM-DD` snapshot form, and
51
+ // vendors' own version strings are not the method's to constrain.
52
+ vendorBase: z
53
+ .object({
54
+ vendor: z
55
+ .string()
56
+ .regex(/^[a-z][a-z0-9-]*$/, "vendor_base.vendor must be a lower-kebab-case slug"),
57
+ vendorVersion: z.string().min(1, "vendor_base.vendor_version must be non-empty"),
58
+ })
59
+ .optional(),
60
+ minimumSddCliVersion: z
61
+ .string()
62
+ .regex(SEMVER_PATTERN, "minimum_sdd_cli_version must be semver (X.Y.Z)"),
63
+ description: z.string().min(1),
64
+ include: z.array(z.string()).optional(),
65
+ managedPaths: z
66
+ .array(z.string().min(1))
67
+ .min(1, "managed_paths must have at least one entry"),
68
+ forbiddenPaths: z.array(z.string()).optional(),
69
+ // ADR 0176 §4.3: scaffold-source path prefixes; each must cover at
70
+ // least one managed_paths entry (checked in the superRefine below).
71
+ scaffoldSourcePaths: z.array(z.string().min(1)).optional(),
72
+ requiresPack: z
73
+ .object({
74
+ name: z.string().min(1),
75
+ version: z
76
+ .string()
77
+ .regex(VERSIONED_RANGE_PATTERN, "requires_pack.version must be a semver range (e.g. ^v1.2.0)"),
78
+ })
79
+ .optional(),
80
+ sourcePath: z.string(),
81
+ });
82
+ const PackSeedSchemaRefined = PackSeedSchema.superRefine((seed, ctx) => {
83
+ // A vendor-base pack is PROFILE-AGNOSTIC (M-ADR 0176 §4.6, M-ADR 0175): its
84
+ // content is vendor-defined, so it layers over any baseline and must not pin
85
+ // one. Declaring both is a contradiction the producer should hear at validate
86
+ // time rather than discover when a consumer on a different baseline cannot
87
+ // apply it.
88
+ if (seed.vendorBase && seed.forwardBase) {
89
+ ctx.addIssue({
90
+ code: z.ZodIssueCode.custom,
91
+ path: ["forwardBase"],
92
+ message: "a vendor_base pack is profile-agnostic and must omit forward_base " +
93
+ "(M-ADR 0176 §4.6); remove forward_base, or remove vendor_base if this " +
94
+ "is not a vendor-base pack",
95
+ });
96
+ }
97
+ if (!seed.scaffoldSourcePaths)
98
+ return;
99
+ for (const [i, prefix] of seed.scaffoldSourcePaths.entries()) {
100
+ const covers = seed.managedPaths.some((mp) => mp.startsWith(prefix));
101
+ if (!covers) {
102
+ ctx.addIssue({
103
+ code: z.ZodIssueCode.custom,
104
+ path: ["scaffoldSourcePaths", i],
105
+ message: `scaffold_source_paths entry "${prefix}" matches no managed_paths entry`,
106
+ });
107
+ }
108
+ }
109
+ });
110
+ export function validateSeed(seed) {
111
+ const result = PackSeedSchemaRefined.safeParse(seed);
112
+ if (result.success) {
113
+ return { ok: true, errors: [] };
114
+ }
115
+ const errors = result.error.issues.map((issue) => ({
116
+ field: issue.path.join("."),
117
+ message: issue.message,
118
+ }));
119
+ return { ok: false, errors };
120
+ }
121
+ //# sourceMappingURL=schema.js.map