@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.
- package/LICENSE +201 -0
- package/NOTICE +17 -0
- package/README.md +7 -0
- package/dist/lib/catalogue/baseline-check.js +136 -0
- package/dist/lib/catalogue/canonical-schema.yaml +3557 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +522 -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/ddl-generator.js +233 -0
- package/dist/lib/catalogue/engine-validator.js +222 -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/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +329 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +422 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +764 -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 +535 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/validate.js +176 -0
- package/dist/lib/catalogue/version.js +18 -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 +413 -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 +25 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/validate/aggregate.js +35 -0
- package/dist/lib/validate/bundle-coherence.js +69 -0
- package/dist/lib/validate/dispatch.js +218 -0
- package/dist/lib/validate/index.js +88 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/verbs/catalogue-verify.js +114 -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/validate.js +22 -0
- package/dist/verify-bin.js +8 -0
- package/dist/verify-program.js +115 -0
- 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
|