@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,182 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ /**
5
+ * Adopter pack seed file. Per ADR 0151 + context-pack-seed-schema.yaml.
6
+ *
7
+ * Loaded by `sdd-cli pack build` and `sdd-cli pack validate` from
8
+ * `orchestration/distribution/{pack-name}-seed.yaml` in adopter repos.
9
+ *
10
+ * Includes are resolved before validation: the merged seed presents
11
+ * all `include`'d managed_paths + forbidden_paths concatenated with
12
+ * the host seed's own.
13
+ */
14
+ /**
15
+ * Authored seed keys, snake_case → the camelCase field they map to.
16
+ *
17
+ * THE SINGLE SOURCE OF TRUTH for "what may appear in a seed file". It lived only
18
+ * inside the canonical-conformance test, which meant the loader had no idea what a
19
+ * valid key set was — so an unrecognised key was dropped by the hand-mapping below
20
+ * without a word. An adopter following a schema that documents a field their sdd-cli
21
+ * predates got a clean validation and a silent no-op: the same failure M-ADR 0175's
22
+ * history records for `packaging`, and the same one fixed for `.sdd/sdd-manifest.yaml`
23
+ * in 0.110.1 — but by a different mechanism, which is why that fix did not cover this.
24
+ *
25
+ * The conformance test now imports this rather than declaring its own copy. Two
26
+ * hand-maintained lists of the same facts is how the lifecycle expectation drifted.
27
+ */
28
+ export const SEED_KEY_TO_FIELD = {
29
+ version: "version",
30
+ scope: "scope",
31
+ forward_base: "forwardBase",
32
+ vendor_base: "vendorBase",
33
+ minimum_sdd_cli_version: "minimumSddCliVersion",
34
+ description: "description",
35
+ include: "include",
36
+ managed_paths: "managedPaths",
37
+ forbidden_paths: "forbiddenPaths",
38
+ scaffold_source_paths: "scaffoldSourcePaths",
39
+ requires_pack: "requiresPack",
40
+ };
41
+ /**
42
+ * Top-level keys in a seed file that this build does not recognise.
43
+ *
44
+ * Deliberately TOP-LEVEL only, stated as a limit rather than left implied: nested
45
+ * blocks (`forward_base`, `vendor_base`, `requires_pack`) are hand-mapped field by
46
+ * field too, so an unknown key inside one is still dropped silently today.
47
+ */
48
+ export function unknownSeedKeys(raw) {
49
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
50
+ return [];
51
+ return Object.keys(raw)
52
+ .filter((k) => !(k in SEED_KEY_TO_FIELD))
53
+ .sort();
54
+ }
55
+ /**
56
+ * Load a seed file from disk, resolve its `include:` directives,
57
+ * and return a merged PackSeed. Throws if any include cannot be read
58
+ * or if circular includes are detected.
59
+ *
60
+ * NOTE: This does not validate the seed structure — only parses YAML
61
+ * and assembles the typed shape. Use `validateSeed` (separate module)
62
+ * for schema enforcement.
63
+ */
64
+ export async function loadSeed(seedPath) {
65
+ const absPath = isAbsolute(seedPath) ? seedPath : resolve(seedPath);
66
+ const visited = new Set();
67
+ return loadSeedRecursive(absPath, visited);
68
+ }
69
+ async function loadSeedRecursive(absPath, visited) {
70
+ if (visited.has(absPath)) {
71
+ throw new Error(`Circular include detected at ${absPath}`);
72
+ }
73
+ visited.add(absPath);
74
+ let raw;
75
+ try {
76
+ raw = await readFile(absPath, "utf8");
77
+ }
78
+ catch (err) {
79
+ throw new Error(`Cannot read seed file ${absPath}: ${err.message}`);
80
+ }
81
+ const parsed = parseYaml(raw);
82
+ if (parsed === null || typeof parsed !== "object") {
83
+ throw new Error(`Seed file ${absPath} is not a YAML object`);
84
+ }
85
+ const baseDir = dirname(absPath);
86
+ const includes = Array.isArray(parsed.include) ? parsed.include : [];
87
+ const includedManaged = [];
88
+ const includedForbidden = [];
89
+ const includedUnknown = [];
90
+ for (const inc of includes) {
91
+ if (typeof inc !== "string") {
92
+ throw new Error(`Non-string include entry in ${absPath}: ${String(inc)}`);
93
+ }
94
+ const incPath = isAbsolute(inc) ? inc : resolve(baseDir, inc);
95
+ const includedSeed = await loadSeedRecursive(incPath, visited);
96
+ includedManaged.push(...includedSeed.managedPaths);
97
+ if (includedSeed.forbiddenPaths) {
98
+ includedForbidden.push(...includedSeed.forbiddenPaths);
99
+ }
100
+ if (includedSeed.unknownKeys)
101
+ includedUnknown.push(...includedSeed.unknownKeys);
102
+ }
103
+ // Unknown keys from this file, plus everything its includes reported. An included
104
+ // seed's stray key is as silently dropped as the root's, so the queue is unioned.
105
+ const ownUnknown = unknownSeedKeys(parsed);
106
+ const own = parsed;
107
+ const managedPaths = [
108
+ ...includedManaged,
109
+ ...asStringArray(own.managed_paths, "managed_paths", absPath),
110
+ ];
111
+ const forbiddenPaths = includedForbidden.length > 0 || Array.isArray(own.forbidden_paths)
112
+ ? [
113
+ ...includedForbidden,
114
+ ...asStringArray(own.forbidden_paths, "forbidden_paths", absPath, true),
115
+ ]
116
+ : undefined;
117
+ const scaffoldSourcePaths = Array.isArray(own.scaffold_source_paths)
118
+ ? asStringArray(own.scaffold_source_paths, "scaffold_source_paths", absPath, true)
119
+ : undefined;
120
+ const seed = {
121
+ version: asString(own.version, "version", absPath),
122
+ scope: asString(own.scope, "scope", absPath),
123
+ // forward_base optional per ADR 0176 §4.6 (profile-agnostic packs omit it)
124
+ ...(own.forward_base && {
125
+ forwardBase: {
126
+ bundle: asString(own.forward_base.bundle, "forward_base.bundle", absPath),
127
+ minimumVersion: asString(own.forward_base.minimum_version, "forward_base.minimum_version", absPath),
128
+ sourceRepo: own.forward_base.source_repo
129
+ ? asString(own.forward_base.source_repo, "forward_base.source_repo", absPath)
130
+ : "sdd-method",
131
+ },
132
+ }),
133
+ // vendor_base present only on a vendor-base pack (M-ADR 0175); the
134
+ // profile-agnostic/forward_base contradiction is caught in validateSeed.
135
+ ...(own.vendor_base && {
136
+ vendorBase: {
137
+ vendor: asString(own.vendor_base.vendor, "vendor_base.vendor", absPath),
138
+ vendorVersion: asString(own.vendor_base.vendor_version, "vendor_base.vendor_version", absPath),
139
+ },
140
+ }),
141
+ minimumSddCliVersion: asString(own.minimum_sdd_cli_version, "minimum_sdd_cli_version", absPath),
142
+ description: asString(own.description, "description", absPath),
143
+ managedPaths,
144
+ sourcePath: absPath,
145
+ ...(ownUnknown.length + includedUnknown.length > 0
146
+ ? { unknownKeys: [...new Set([...includedUnknown, ...ownUnknown])].sort() }
147
+ : {}),
148
+ ...(includes.length > 0 && { include: includes }),
149
+ ...(forbiddenPaths && { forbiddenPaths }),
150
+ ...(scaffoldSourcePaths && scaffoldSourcePaths.length > 0 && { scaffoldSourcePaths }),
151
+ ...(own.requires_pack && {
152
+ requiresPack: {
153
+ name: asString(own.requires_pack.name, "requires_pack.name", absPath),
154
+ version: asString(own.requires_pack.version, "requires_pack.version", absPath),
155
+ },
156
+ }),
157
+ };
158
+ return seed;
159
+ }
160
+ function asString(value, field, file) {
161
+ if (typeof value !== "string" || value.length === 0) {
162
+ throw new Error(`Seed field "${field}" must be a non-empty string in ${file}`);
163
+ }
164
+ return value;
165
+ }
166
+ function asStringArray(value, field, file, allowEmpty = false) {
167
+ if (value === undefined || value === null) {
168
+ if (allowEmpty)
169
+ return [];
170
+ throw new Error(`Seed field "${field}" is required in ${file}`);
171
+ }
172
+ if (!Array.isArray(value)) {
173
+ throw new Error(`Seed field "${field}" must be a list in ${file}`);
174
+ }
175
+ for (const entry of value) {
176
+ if (typeof entry !== "string") {
177
+ throw new Error(`Seed field "${field}" entries must be strings in ${file}`);
178
+ }
179
+ }
180
+ return value;
181
+ }
182
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1,49 @@
1
+ // `pack validate` — reads and checks a context-pack seed; writes nothing.
2
+ // Kept free of any import that reaches `./builder.js`, so that importing it
3
+ // cannot drag pack-building code into a package that must not author
4
+ // (M-ADR 0212 §4.3b). `index.ts` re-exports it, so existing imports are unchanged.
5
+ import { validateSeed } from "./schema.js";
6
+ import { loadSeed } from "./seed.js";
7
+ import { getPackageVersion } from "../package-version.js";
8
+ import { fail, info, pass, section } from "./log.js";
9
+ export async function runPackValidate(opts, stdout = (s) => process.stdout.write(s)) {
10
+ stdout(section("Pack validate"));
11
+ let seed;
12
+ try {
13
+ seed = await loadSeed(opts.seedPath);
14
+ }
15
+ catch (err) {
16
+ stdout(fail(`Seed load failed: ${err.message}`));
17
+ return { exitCode: 1, errors: [{ field: "(load)", message: err.message }] };
18
+ }
19
+ // A seed key this build does not know is REPORTED rather than dropped in silence.
20
+ // Not fatal: an older sdd-cli must still build a seed that declares something newer
21
+ // than itself, exactly as for `.sdd/sdd-manifest.yaml`. Silence here is what let an
22
+ // adopter follow the schema and get a clean validation with no effect.
23
+ if (seed.unknownKeys && seed.unknownKeys.length > 0) {
24
+ stdout(info(`${seed.unknownKeys.length} seed key(s) not understood by sdd-cli ${getPackageVersion()} ` +
25
+ `and therefore IGNORED: ${seed.unknownKeys.join(", ")}. If the seed follows a newer ` +
26
+ `context-pack-seed-schema than this build, upgrade sdd-cli; otherwise remove or correct the key.`));
27
+ }
28
+ const validation = validateSeed(seed);
29
+ if (!validation.ok) {
30
+ stdout(fail("Seed validation failed:"));
31
+ for (const e of validation.errors) {
32
+ stdout(fail(` ${e.field}: ${e.message}`));
33
+ }
34
+ return { exitCode: 1, errors: validation.errors };
35
+ }
36
+ stdout(pass(`Seed valid: ${seed.scope} v${seed.version}`));
37
+ stdout(info(seed.forwardBase
38
+ ? `Forward base: ${seed.forwardBase.bundle} >= ${seed.forwardBase.minimumVersion}`
39
+ : "Forward base: none (profile-agnostic pack, ADR 0176 §4.6)"));
40
+ if (seed.scaffoldSourcePaths) {
41
+ stdout(info(`Scaffold-source prefixes: ${seed.scaffoldSourcePaths.join(", ")}`));
42
+ }
43
+ stdout(info(`Managed paths: ${seed.managedPaths.length}`));
44
+ if (seed.requiresPack) {
45
+ stdout(info(`requires_pack: ${seed.requiresPack.name} ${seed.requiresPack.version}`));
46
+ }
47
+ return { exitCode: 0, errors: [] };
48
+ }
49
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Read the package version from package.json at runtime so the various
6
+ * version-string consumers (CLI --version, MCP server identity,
7
+ * catalogue error messages) cannot drift from the published version.
8
+ *
9
+ * Resolves relative to this module's compiled location:
10
+ * - In source layout: src/lib/package-version.ts → package.json (../../)
11
+ * - In dist layout: dist/lib/package-version.js → package.json (../../)
12
+ *
13
+ * Cached on first call.
14
+ */
15
+ let cached;
16
+ export function getPackageVersion() {
17
+ if (cached !== undefined)
18
+ return cached;
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const pkgPath = resolve(here, "..", "..", "package.json");
21
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
22
+ cached = pkg.version;
23
+ return cached;
24
+ }
25
+ //# sourceMappingURL=package-version.js.map
@@ -0,0 +1,129 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Forward-base bundles set `.sdd-repo-kind` and qualify as the base
5
+ * layer that additive extensions can compose on top of (per ADR 0135).
6
+ * The legacy self-contained `reverse-baseline` is retained as a
7
+ * forward base for compat with adopters who haven't migrated to the
8
+ * `reverse-extension-baseline` additive shape yet.
9
+ */
10
+ const FORWARD_BASE_BUNDLES = new Set([
11
+ "platform-baseline",
12
+ "app-baseline",
13
+ "integration-baseline",
14
+ "reverse-baseline",
15
+ ]);
16
+ export const REPO_KIND_FILE = ".sdd-repo-kind";
17
+ /**
18
+ * Legacy marker filename from the pre-rebrand era. Read-only fallback during
19
+ * the rebrand transition: `readRepoKind` accepts either filename; `sync`
20
+ * auto-migrates legacy → canonical. New writes always use `REPO_KIND_FILE`.
21
+ * Drop after one minor-version overlap.
22
+ */
23
+ export const LEGACY_REPO_KIND_FILE = ".iteraitive-repo-kind";
24
+ const PROFILE_BY_REPO_KIND = {
25
+ "platform-product": "platform",
26
+ "application-product": "application",
27
+ "integration-product": "integration",
28
+ "reverse-corpus": "reverse",
29
+ };
30
+ const REPO_KIND_BY_PROFILE = {
31
+ platform: "platform-product",
32
+ application: "application-product",
33
+ integration: "integration-product",
34
+ reverse: "reverse-corpus",
35
+ };
36
+ /**
37
+ * Forward-base bundles map 1:1 to a profile. Additive bundles
38
+ * (`reverse-extension-baseline`) have no profile of their own — they
39
+ * compose on top of whatever forward base is already locked.
40
+ */
41
+ const PROFILE_BY_BUNDLE_NAME = {
42
+ "platform-baseline": "platform",
43
+ "app-baseline": "application",
44
+ "integration-baseline": "integration",
45
+ "reverse-baseline": "reverse",
46
+ };
47
+ const BUNDLE_NAME_BY_PROFILE = {
48
+ platform: "platform-baseline",
49
+ application: "app-baseline",
50
+ integration: "integration-baseline",
51
+ reverse: "reverse-baseline",
52
+ };
53
+ export function profileFromRepoKind(kind) {
54
+ return PROFILE_BY_REPO_KIND[kind];
55
+ }
56
+ export function repoKindFromProfile(profile) {
57
+ return REPO_KIND_BY_PROFILE[profile];
58
+ }
59
+ /**
60
+ * Returns the profile a bundle authoritatively claims, or null for
61
+ * additive bundles that don't claim a profile of their own. Accepts
62
+ * arbitrary strings (adopter pack names per ADR 0151) and returns null
63
+ * for any name not in the closed sdd-method bundle set.
64
+ */
65
+ export function profileFromBundleName(name) {
66
+ if (!isBundleName(name))
67
+ return null;
68
+ return PROFILE_BY_BUNDLE_NAME[name] ?? null;
69
+ }
70
+ export function bundleNameFromProfile(profile) {
71
+ return BUNDLE_NAME_BY_PROFILE[profile];
72
+ }
73
+ /**
74
+ * Forward-base bundles set `.sdd-repo-kind` and qualify as the base
75
+ * layer that additive extensions compose on top of. Additive bundles
76
+ * (sdd-method's `reverse-extension-baseline` and ADR 0151 adopter
77
+ * packs) return false. Accepts arbitrary strings; unknown names return
78
+ * false.
79
+ */
80
+ export function isForwardBase(name) {
81
+ if (!isBundleName(name))
82
+ return false;
83
+ return FORWARD_BASE_BUNDLES.has(name);
84
+ }
85
+ /**
86
+ * Maps repo-kind → forward-base bundle name. Used when migrating a
87
+ * legacy v1 lock file (which doesn't record bundle name) to v2.
88
+ */
89
+ export function bundleNameFromRepoKind(kind) {
90
+ return BUNDLE_NAME_BY_PROFILE[PROFILE_BY_REPO_KIND[kind]];
91
+ }
92
+ export function isRepoKind(value) {
93
+ return value in PROFILE_BY_REPO_KIND;
94
+ }
95
+ const ALL_BUNDLE_NAMES = new Set([
96
+ "platform-baseline",
97
+ "app-baseline",
98
+ "integration-baseline",
99
+ "reverse-baseline",
100
+ "reverse-extension-baseline",
101
+ ]);
102
+ export function isBundleName(value) {
103
+ return ALL_BUNDLE_NAMES.has(value);
104
+ }
105
+ export async function readRepoKind(repoRoot) {
106
+ // Canonical first; fall back to legacy marker during the rebrand transition.
107
+ for (const filename of [REPO_KIND_FILE, LEGACY_REPO_KIND_FILE]) {
108
+ const path = join(repoRoot, filename);
109
+ let raw;
110
+ try {
111
+ raw = await readFile(path, "utf8");
112
+ }
113
+ catch (err) {
114
+ if (err.code === "ENOENT")
115
+ continue;
116
+ throw err;
117
+ }
118
+ const value = raw.trim();
119
+ if (!isRepoKind(value)) {
120
+ throw new Error(`Unrecognised ${filename} value: ${JSON.stringify(value)} at ${path}`);
121
+ }
122
+ return value;
123
+ }
124
+ return null;
125
+ }
126
+ export async function writeRepoKind(repoRoot, kind) {
127
+ await writeFile(join(repoRoot, REPO_KIND_FILE), `${kind}\n`, "utf8");
128
+ }
129
+ //# sourceMappingURL=repo-kind.js.map
@@ -0,0 +1,7 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolveBundledScript(name) {
4
+ const here = dirname(fileURLToPath(import.meta.url));
5
+ return resolve(here, "..", "..", "scripts", name);
6
+ }
7
+ //# sourceMappingURL=scripts.js.map
@@ -0,0 +1,35 @@
1
+ const WARN_MARKER = "[WARN]";
2
+ export function aggregateResults(results, opts) {
3
+ let passed = 0;
4
+ let failed = 0;
5
+ let skipped = 0;
6
+ let warned = 0;
7
+ for (const r of results) {
8
+ if (r.status === "skip") {
9
+ skipped++;
10
+ continue;
11
+ }
12
+ const hasWarn = r.stdout.includes(WARN_MARKER);
13
+ if (r.exitCode !== 0) {
14
+ failed++;
15
+ }
16
+ else if (hasWarn) {
17
+ warned++;
18
+ if (opts.strict)
19
+ failed++;
20
+ else
21
+ passed++;
22
+ }
23
+ else {
24
+ passed++;
25
+ }
26
+ }
27
+ return {
28
+ exitCode: failed > 0 ? 1 : 0,
29
+ passed,
30
+ failed,
31
+ skipped,
32
+ warned,
33
+ };
34
+ }
35
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1,69 @@
1
+ // Profile → bundle coherence (A2 of the bundle-migration plan).
2
+ //
3
+ // A repo's declared profile (`.sdd/sdd-manifest.yaml`) and the forward base
4
+ // its lock carries can silently disagree: a reclassification edits the
5
+ // manifest, nothing re-syncs the bundle, and no instrument notices — the
6
+ // registration/adoption-lag class. Measured instance: two platform-subsystem
7
+ // repos carried app-baseline for eight days, found by a human sweep.
8
+ //
9
+ // REPORTING-ONLY: a mismatch prints a [REPORT] line with the migration path
10
+ // and exits 0. Gating is a later, deliberate change-set once both estates
11
+ // read 0 (the scores-coverage precedent, M-ADR 0205 §4.5).
12
+ import { readLockFile, findForwardBase } from "../lock-file.js";
13
+ import { loadSddManifest, } from "../catalogue/manifest-loader.js";
14
+ import { getPackageVersion } from "../package-version.js";
15
+ /**
16
+ * Manifest profile → the forward base that profile expects. Profiles with no
17
+ * entry (support-repo, organisation-profile) carry no expectation — silence,
18
+ * not a guess. NOTE: platform-subsystem is a CATEGORY under platform-profile,
19
+ * so the two measured mismatch repos resolve through the platform-profile row.
20
+ */
21
+ const EXPECTED_FORWARD_BASE = {
22
+ "platform-profile": "platform-baseline",
23
+ "application-product": "app-baseline",
24
+ "integration-product": "integration-baseline",
25
+ };
26
+ export async function checkBundleCoherence(targetRepo) {
27
+ const lines = [];
28
+ const lock = await readLockFile(targetRepo);
29
+ const forwardBase = findForwardBase(lock);
30
+ if (!forwardBase) {
31
+ return {
32
+ exitCode: 0,
33
+ stdout: "",
34
+ skipped: "no forward base in method-baseline.lock — nothing to check",
35
+ };
36
+ }
37
+ let profile;
38
+ try {
39
+ const manifest = await loadSddManifest(targetRepo, {
40
+ cliVersion: getPackageVersion(),
41
+ });
42
+ profile = manifest.profile;
43
+ }
44
+ catch (err) {
45
+ // Reporting-only: an absent/invalid manifest is its own (already-gated)
46
+ // problem; this check must not double-report it as a bundle mismatch.
47
+ return {
48
+ exitCode: 0,
49
+ stdout: "",
50
+ skipped: `manifest unavailable (${err.message.split("\n")[0]})`,
51
+ };
52
+ }
53
+ const expected = EXPECTED_FORWARD_BASE[profile];
54
+ if (expected === undefined) {
55
+ lines.push(`[PASS] bundle-coherence: profile '${profile}' declares no forward-base expectation\n`);
56
+ return { exitCode: 0, stdout: lines.join("") };
57
+ }
58
+ if (forwardBase.name === expected) {
59
+ lines.push(`[PASS] bundle-coherence: profile '${profile}' ↔ forward base '${forwardBase.name}'\n`);
60
+ return { exitCode: 0, stdout: lines.join("") };
61
+ }
62
+ lines.push(`[REPORT] bundle-coherence: profile '${profile}' expects forward base '${expected}' ` +
63
+ `but the lock carries '${forwardBase.name}' — profile and bundle disagree. ` +
64
+ `Migration path: sdd-cli sync --bundle <${expected} tarball> --replace-bundle ${forwardBase.name} --dry-run, ` +
65
+ `review the three-way split, then re-run with --yes. ` +
66
+ `(Reporting-only; this check does not gate.)\n`);
67
+ return { exitCode: 0, stdout: lines.join("") };
68
+ }
69
+ //# sourceMappingURL=bundle-coherence.js.map