@metaobjectsdev/cli 1.0.2 → 1.0.4-rc.1

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 (115) hide show
  1. package/README.md +13 -5
  2. package/dist/src/commands/deps.d.ts +3 -0
  3. package/dist/src/commands/deps.d.ts.map +1 -0
  4. package/dist/src/commands/deps.js +200 -0
  5. package/dist/src/commands/deps.js.map +1 -0
  6. package/dist/src/commands/docs.d.ts.map +1 -1
  7. package/dist/src/commands/docs.js +19 -3
  8. package/dist/src/commands/docs.js.map +1 -1
  9. package/dist/src/commands/eject.d.ts +2 -1
  10. package/dist/src/commands/eject.d.ts.map +1 -1
  11. package/dist/src/commands/eject.js +235 -75
  12. package/dist/src/commands/eject.js.map +1 -1
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +173 -30
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/init.d.ts +5 -9
  17. package/dist/src/commands/init.d.ts.map +1 -1
  18. package/dist/src/commands/init.js +92 -402
  19. package/dist/src/commands/init.js.map +1 -1
  20. package/dist/src/commands/migrate.d.ts.map +1 -1
  21. package/dist/src/commands/migrate.js +24 -19
  22. package/dist/src/commands/migrate.js.map +1 -1
  23. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  24. package/dist/src/commands/prompt-snapshot.js +2 -1
  25. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  26. package/dist/src/commands/verify.d.ts.map +1 -1
  27. package/dist/src/commands/verify.js +186 -29
  28. package/dist/src/commands/verify.js.map +1 -1
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +97 -15
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/lib/advisory.d.ts +6 -0
  33. package/dist/src/lib/advisory.d.ts.map +1 -1
  34. package/dist/src/lib/advisory.js +10 -0
  35. package/dist/src/lib/advisory.js.map +1 -1
  36. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  37. package/dist/src/lib/agent-context-staleness.js +7 -1
  38. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  39. package/dist/src/lib/args.d.ts +75 -3
  40. package/dist/src/lib/args.d.ts.map +1 -1
  41. package/dist/src/lib/args.js +54 -5
  42. package/dist/src/lib/args.js.map +1 -1
  43. package/dist/src/lib/catalog-listing.d.ts +92 -0
  44. package/dist/src/lib/catalog-listing.d.ts.map +1 -0
  45. package/dist/src/lib/catalog-listing.js +268 -0
  46. package/dist/src/lib/catalog-listing.js.map +1 -0
  47. package/dist/src/lib/catalog.d.ts +41 -0
  48. package/dist/src/lib/catalog.d.ts.map +1 -0
  49. package/dist/src/lib/catalog.js +89 -0
  50. package/dist/src/lib/catalog.js.map +1 -0
  51. package/dist/src/lib/collection-load-options.d.ts +13 -0
  52. package/dist/src/lib/collection-load-options.d.ts.map +1 -0
  53. package/dist/src/lib/collection-load-options.js +25 -0
  54. package/dist/src/lib/collection-load-options.js.map +1 -0
  55. package/dist/src/lib/dependency-sync.d.ts +170 -0
  56. package/dist/src/lib/dependency-sync.d.ts.map +1 -0
  57. package/dist/src/lib/dependency-sync.js +457 -0
  58. package/dist/src/lib/dependency-sync.js.map +1 -0
  59. package/dist/src/lib/install-set.d.ts +31 -0
  60. package/dist/src/lib/install-set.d.ts.map +1 -0
  61. package/dist/src/lib/install-set.js +103 -0
  62. package/dist/src/lib/install-set.js.map +1 -0
  63. package/dist/src/lib/library-eject.d.ts +79 -0
  64. package/dist/src/lib/library-eject.d.ts.map +1 -0
  65. package/dist/src/lib/library-eject.js +241 -0
  66. package/dist/src/lib/library-eject.js.map +1 -0
  67. package/dist/src/lib/library-listing.d.ts +63 -0
  68. package/dist/src/lib/library-listing.d.ts.map +1 -0
  69. package/dist/src/lib/library-listing.js +178 -0
  70. package/dist/src/lib/library-listing.js.map +1 -0
  71. package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
  72. package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
  73. package/dist/src/lib/library-prefix-advisory.js +67 -0
  74. package/dist/src/lib/library-prefix-advisory.js.map +1 -0
  75. package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
  76. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  77. package/dist/src/lib/load-metaobjects-config.js +11 -17
  78. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  79. package/dist/src/lib/migrate-scope.d.ts +32 -1
  80. package/dist/src/lib/migrate-scope.d.ts.map +1 -1
  81. package/dist/src/lib/migrate-scope.js +51 -3
  82. package/dist/src/lib/migrate-scope.js.map +1 -1
  83. package/dist/src/lib/overlay-lint.d.ts +37 -0
  84. package/dist/src/lib/overlay-lint.d.ts.map +1 -0
  85. package/dist/src/lib/overlay-lint.js +102 -0
  86. package/dist/src/lib/overlay-lint.js.map +1 -0
  87. package/dist/src/lib/requirement-check.d.ts +26 -3
  88. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  89. package/dist/src/lib/requirement-check.js +71 -13
  90. package/dist/src/lib/requirement-check.js.map +1 -1
  91. package/package.json +10 -10
  92. package/src/commands/deps.ts +231 -0
  93. package/src/commands/docs.ts +19 -3
  94. package/src/commands/eject.ts +304 -90
  95. package/src/commands/gen.ts +189 -29
  96. package/src/commands/init.ts +95 -407
  97. package/src/commands/migrate.ts +26 -16
  98. package/src/commands/prompt-snapshot.ts +2 -1
  99. package/src/commands/verify.ts +213 -30
  100. package/src/index.ts +97 -15
  101. package/src/lib/advisory.ts +13 -0
  102. package/src/lib/agent-context-staleness.ts +7 -1
  103. package/src/lib/args.ts +118 -8
  104. package/src/lib/catalog-listing.ts +369 -0
  105. package/src/lib/catalog.ts +107 -0
  106. package/src/lib/collection-load-options.ts +42 -0
  107. package/src/lib/dependency-sync.ts +656 -0
  108. package/src/lib/install-set.ts +116 -0
  109. package/src/lib/library-eject.ts +298 -0
  110. package/src/lib/library-listing.ts +258 -0
  111. package/src/lib/library-prefix-advisory.ts +78 -0
  112. package/src/lib/load-metaobjects-config.ts +15 -21
  113. package/src/lib/migrate-scope.ts +59 -4
  114. package/src/lib/overlay-lint.ts +129 -0
  115. package/src/lib/requirement-check.ts +100 -14
@@ -0,0 +1,170 @@
1
+ import { type ErrorCode } from "@metaobjectsdev/metadata";
2
+ import { type DependencyManifest, type DependencySpec, type Lock, type LockEntry } from "@metaobjectsdev/sdk";
3
+ /**
4
+ * DESIGN §4.1 step 1 — resolve a dependency spec's transport to a directory.
5
+ * `npm` / `python` refuse by design (Phase 1a ships `path` only — DESIGN
6
+ * §11.3's transports table); a `path` that does not resolve to a directory on
7
+ * disk is the SAME code (`ERR_DEPENDENCY_UNRESOLVED`) with a different
8
+ * detail, because both are "the transport could not locate a directory
9
+ * holding metaobjects.pkg.json" — the manifest read is a separate step
10
+ * (`readManifestDir`) that only runs once a directory is in hand.
11
+ */
12
+ export declare function resolveDependencyDir(configDir: string, spec: DependencySpec): string;
13
+ export interface ManifestRead {
14
+ readonly manifest: DependencyManifest;
15
+ readonly artifactContent: string;
16
+ }
17
+ /**
18
+ * DESIGN §4.1 step 2 — read `<dir>/metaobjects.pkg.json`, strict, and verify
19
+ * it is internally consistent with the artifact sitting beside it. Every
20
+ * failure here is `ERR_DEPENDENCY_MANIFEST_INVALID`: absent, not JSON, fails
21
+ * the schema, names a missing artifact, or the artifact's bytes don't hash
22
+ * to the recorded `integrity` — a publisher who bypassed `sharedModelFile()`
23
+ * is caught here, before the (more expensive) standalone load in
24
+ * {@link standaloneLoadCheck}.
25
+ */
26
+ export declare function readManifestDir(dir: string, name: string): Promise<ManifestRead>;
27
+ /**
28
+ * DESIGN §4.1 step 2 (the `name ≠ spec.name` arm) + step 3 (`metamodelVersion`
29
+ * major/minor). Returns an advisory warning string when the dependency was
30
+ * published against a newer metamodel MINOR (the metadata contract still
31
+ * loads; a MAJOR mismatch throws — ADR-0035 Amendment 2: the metadata
32
+ * contract is promised on the major alone), `undefined` otherwise.
33
+ */
34
+ export declare function validateAgainstSpec(manifest: DependencyManifest, spec: DependencySpec): string | undefined;
35
+ /**
36
+ * DESIGN §4.1 step 4 — load the artifact standalone with CORE providers only
37
+ * (a consumer loads it with ITS OWN providers, never the publisher's; a
38
+ * `MetaDataLoader` built with no `registry` option composes core providers by
39
+ * default), strict, and check it declares EXACTLY the manifest's `nodes` and
40
+ * `packages` — a publisher who hand-edited the manifest (or bypassed
41
+ * `sharedModelFile()` entirely) is caught here.
42
+ */
43
+ export declare function standaloneLoadCheck(manifest: DependencyManifest, artifactContent: string): Promise<void>;
44
+ /** One dependency this run will (re)sync — either brand new or a changed/
45
+ * unchanged re-sync of one already in the lock. */
46
+ export interface SyncedDependencyPlan {
47
+ readonly name: string;
48
+ readonly action: "sync" | "unchanged";
49
+ readonly manifest: DependencyManifest;
50
+ readonly artifactContent: string;
51
+ readonly resolvedFrom: LockEntry["resolvedFrom"];
52
+ readonly oldEntry: LockEntry | undefined;
53
+ }
54
+ /** A lock entry whose dependency is no longer declared in config. */
55
+ export interface PrunedDependencyPlan {
56
+ readonly name: string;
57
+ readonly entry: LockEntry;
58
+ }
59
+ export interface SyncPlan {
60
+ /** Target dependencies (all declared, or the requested subset), in name order. */
61
+ readonly plans: readonly SyncedDependencyPlan[];
62
+ /** Declared-but-not-targeted lock entries (only when a name filter was
63
+ * given) — carried into the new lock untouched. */
64
+ readonly carryForward: ReadonlyMap<string, LockEntry>;
65
+ /** Lock entries whose dependency config no longer declares, in name order. */
66
+ readonly prunes: readonly PrunedDependencyPlan[];
67
+ /** Advisory metamodel-minor-ahead warnings, one per affected dependency. */
68
+ readonly warnings: readonly string[];
69
+ }
70
+ /**
71
+ * DESIGN §4.1 steps 1-5, for every declared dependency (or the requested
72
+ * `filterNames` subset), in name order. Read-only: resolves each transport,
73
+ * validates its manifest and artifact, and checks the WHOLE resulting node
74
+ * set (freshly-resolved targets plus whatever untouched lock entries carry
75
+ * forward) for collisions — nothing is written here, which is what makes
76
+ * `--dry-run` "run this and skip `applySync`."
77
+ */
78
+ export declare function planSync(configDir: string, specs: readonly DependencySpec[], lock: Lock | undefined, filterNames?: readonly string[]): Promise<SyncPlan>;
79
+ export interface DependencyReportLine {
80
+ readonly name: string;
81
+ readonly action: "sync" | "unchanged" | "prune";
82
+ readonly line: string;
83
+ }
84
+ export interface ApplySyncResult {
85
+ readonly lock: Lock;
86
+ readonly report: readonly DependencyReportLine[];
87
+ }
88
+ /** First 8 hex chars after the `sha256-` (`INTEGRITY_PREFIX`) prefix — the
89
+ * report format's `<hash8>`. Exported so every caller — `deps.ts`'s `deps
90
+ * list` rendering, tests — shares this ONE definition rather than each
91
+ * reimplementing the slice with its own inlined `"sha256-"` literal. */
92
+ export declare function hash8(integrity: string): string;
93
+ /**
94
+ * DESIGN §4.1 steps 7-8. `opts.dryRun` runs the exact same reporting pass and
95
+ * writes nothing — no directory emptied, no artifact copied, no lock touched
96
+ * — which is what makes `--dry-run` a true preview of `planSync`'s verdict
97
+ * rather than a second code path that could drift from it.
98
+ */
99
+ export declare function applySync(configDir: string, plan: SyncPlan, opts: {
100
+ readonly dryRun: boolean;
101
+ }): Promise<ApplySyncResult>;
102
+ /**
103
+ * `readLock`, converted into a diagnostic that NAMES the file rather than
104
+ * letting a corrupted committed lock's raw `JSON.parse`/`ZodError` surface as
105
+ * an unhandled rejection. `deps.lock.json` is checked-in, hand-editable, and
106
+ * merge-conflictable — a realistic way for it to break — and `bin/meta.ts`'s
107
+ * `run(...).then((code) => process.exit(code))` has no top-level `.catch()`,
108
+ * so an uncaught throw here would crash the process with a stack trace
109
+ * instead of the clean exit code every other failure in this command gets.
110
+ *
111
+ * The ONE call site for the sdk's `readLock` in `cli/src` — `deps.ts`'s
112
+ * `sync`/`list`/`check` handlers and `verify.ts`'s `--deps` gate all call
113
+ * THIS wrapper instead, so the diagnostic can never go stale in one of them.
114
+ */
115
+ export declare function readLockOrThrow(configDir: string): Promise<Lock | undefined>;
116
+ /**
117
+ * `meta deps check` / `verify --deps`'s classification for one declared
118
+ * dependency (DESIGN §4.1 as amended by §11 — no usage-aware classifier, just
119
+ * "does the installed hash match the lock"):
120
+ * - `current` — resolved fine, and its installed artifact hashes to
121
+ * exactly what the lock pinned.
122
+ * - `drifted` — resolved fine, but the hash differs.
123
+ * - `unresolved` — step 1 (`resolveDependencyDir`) or step 2
124
+ * (`readManifestDir`) failed, OR the dependency is declared
125
+ * but the lock has no entry for it at all (nothing to
126
+ * compare against — "a check that cannot check must not
127
+ * pass"). `detail` carries WHY.
128
+ *
129
+ * `lockVersion`/`lockIntegrity` are `undefined` exactly when there is no lock
130
+ * entry; `installedVersion`/`installedIntegrity` are `undefined` exactly when
131
+ * resolution itself failed.
132
+ */
133
+ export interface DependencyCheckResult {
134
+ readonly name: string;
135
+ readonly status: "current" | "drifted" | "unresolved";
136
+ readonly lockVersion: string | undefined;
137
+ readonly lockIntegrity: string | undefined;
138
+ readonly installedVersion: string | undefined;
139
+ readonly installedIntegrity: string | undefined;
140
+ /** Populated for `unresolved` only — the reason, with the redundant
141
+ * `dependency "<name>": ` prefix (`manifestInvalid`/`resolveDependencyDir`'s
142
+ * own convention) stripped, since `formatCheckLine` already names the
143
+ * dependency once in its own lead-in. */
144
+ readonly detail: string | undefined;
145
+ }
146
+ /**
147
+ * DESIGN §4.1 as amended by §11 — resolve each declared dependency EXACTLY as
148
+ * `sync` steps 1-2 do (`resolveDependencyDir` then `readManifestDir`; never
149
+ * step 3's `validateAgainstSpec` — `check` compares hashes, it does not
150
+ * re-validate the spec), then compare the freshly-read artifact's TRUE hash
151
+ * (re-hashed here from the bytes `readManifestDir` returned, never the
152
+ * installed manifest's `integrity` field taken on faith) against the lock's.
153
+ *
154
+ * Read-only: nothing here writes the lock, the snapshot, or touches
155
+ * `.metaobjects/deps/` — that is `sync`'s job. Every declared dependency gets
156
+ * exactly one result, in name order.
157
+ */
158
+ export declare function checkDependencies(configDir: string, specs: readonly DependencySpec[], lock: Lock | undefined): Promise<DependencyCheckResult[]>;
159
+ /** The one code every `checkDependencies` failure (`drifted` or `unresolved`)
160
+ * is reported under — `deps.ts`'s `check` handler and `verify.ts`'s `--deps`
161
+ * gate both surface it, so it is named once here rather than inlined twice. */
162
+ export declare const ERR_DEPENDENCY_UPSTREAM_DRIFT: ErrorCode;
163
+ /**
164
+ * `meta deps check` / `verify --deps`'s one report line per dependency. The
165
+ * `drifted` shape is pinned exactly by the task brief:
166
+ * `<name>: drifted — lock <v> (<hash8>), installed <v> (<hash8>); run meta
167
+ * deps sync and review the artifact diff`.
168
+ */
169
+ export declare function formatCheckLine(result: DependencyCheckResult): string;
170
+ //# sourceMappingURL=dependency-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-sync.d.ts","sourceRoot":"","sources":["../../../src/lib/dependency-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,457 @@
1
+ // server/typescript/packages/cli/src/lib/dependency-sync.ts
2
+ //
3
+ // FR-023 Phase 1a, Task 14 — the algorithm `meta deps sync` runs (DESIGN §4.1
4
+ // steps 1-5, 7-8; step 6, the usage-aware classifier, is superseded — DESIGN
5
+ // §11.3, "the committed artifact's diff in the consumer's history is the
6
+ // review"). Five functions, split so `--dry-run` is "run everything except
7
+ // the write": `readManifestDir` / `validateAgainstSpec` / `standaloneLoadCheck`
8
+ // read and validate one already-resolved dependency directory;
9
+ // `planSync` composes them over every declared dependency (transport
10
+ // resolution, per-dependency validation, the cross-dependency node-collision
11
+ // check); `applySync` is the only function that touches the filesystem.
12
+ import { existsSync, statSync } from "node:fs";
13
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
14
+ import { join, resolve } from "node:path";
15
+ import { codeSource, InMemoryStringSource, MetaDataLoader, METAMODEL_VERSION, packageOfResolutionKey, ParseError, } from "@metaobjectsdev/metadata";
16
+ import { DEPS_DIR, DEFAULT_METAOBJECTS_DIR, INTEGRITY_PREFIX, LOCK_FILE, MANIFEST_FILE, DependencyManifestSchema, dependencyName, readLock, sha256Integrity, writeLock, } from "@metaobjectsdev/sdk";
17
+ /** `dependency-sync.ts`'s own `ParseError` factory for every
18
+ * `ERR_DEPENDENCY_MANIFEST_INVALID` case (DESIGN §4.1 step 2 and step 4):
19
+ * absent/invalid manifest, name mismatch, missing artifact, hash mismatch,
20
+ * a standalone load failure, or a `nodes`/`packages` mismatch against what
21
+ * the artifact actually declares. One thrower so every case names the
22
+ * dependency and carries the same code. */
23
+ function manifestInvalid(name, caller, detail) {
24
+ throw new ParseError(`dependency "${name}": ${detail}`, {
25
+ code: "ERR_DEPENDENCY_MANIFEST_INVALID",
26
+ source: codeSource(caller),
27
+ });
28
+ }
29
+ /** The MAJOR half of a `major.minor` metamodel version string — mirrors the
30
+ * private helper of the same name in `sdk/src/dependencies.ts` (not
31
+ * exported there; the contract, "compare on the major alone" per ADR-0035
32
+ * Amendment 2, is small enough to restate rather than widen that module's
33
+ * public surface for one caller). */
34
+ function metamodelMajor(version) {
35
+ return version.split(".")[0] ?? version;
36
+ }
37
+ /** The MINOR half, as a number — 0 when absent (defensive; the manifest
38
+ * schema's `^\d+\.\d+$` regex already guarantees a minor is present). */
39
+ function metamodelMinor(version) {
40
+ return Number(version.split(".")[1] ?? 0);
41
+ }
42
+ function sameSortedArray(a, b) {
43
+ if (a.length !== b.length)
44
+ return false;
45
+ const left = [...a].sort();
46
+ const right = [...b].sort();
47
+ return left.every((v, i) => v === right[i]);
48
+ }
49
+ /**
50
+ * DESIGN §4.1 step 1 — resolve a dependency spec's transport to a directory.
51
+ * `npm` / `python` refuse by design (Phase 1a ships `path` only — DESIGN
52
+ * §11.3's transports table); a `path` that does not resolve to a directory on
53
+ * disk is the SAME code (`ERR_DEPENDENCY_UNRESOLVED`) with a different
54
+ * detail, because both are "the transport could not locate a directory
55
+ * holding metaobjects.pkg.json" — the manifest read is a separate step
56
+ * (`readManifestDir`) that only runs once a directory is in hand.
57
+ */
58
+ export function resolveDependencyDir(configDir, spec) {
59
+ const name = dependencyName(spec);
60
+ if ("path" in spec) {
61
+ const dir = resolve(configDir, spec.path);
62
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
63
+ throw new ParseError(`dependency "${name}": the "path" transport resolved to ${dir}, but no directory exists there`, { code: "ERR_DEPENDENCY_UNRESOLVED", source: codeSource("resolveDependencyDir") });
64
+ }
65
+ return dir;
66
+ }
67
+ const kind = "npm" in spec ? "npm" : "python";
68
+ throw new ParseError(`dependency "${name}": transport \`${kind}\` is not supported by this toolchain yet; use \`path\``, { code: "ERR_DEPENDENCY_UNRESOLVED", source: codeSource("resolveDependencyDir") });
69
+ }
70
+ /**
71
+ * DESIGN §4.1 step 2 — read `<dir>/metaobjects.pkg.json`, strict, and verify
72
+ * it is internally consistent with the artifact sitting beside it. Every
73
+ * failure here is `ERR_DEPENDENCY_MANIFEST_INVALID`: absent, not JSON, fails
74
+ * the schema, names a missing artifact, or the artifact's bytes don't hash
75
+ * to the recorded `integrity` — a publisher who bypassed `sharedModelFile()`
76
+ * is caught here, before the (more expensive) standalone load in
77
+ * {@link standaloneLoadCheck}.
78
+ */
79
+ export async function readManifestDir(dir, name) {
80
+ const manifestPath = join(dir, MANIFEST_FILE);
81
+ let raw;
82
+ try {
83
+ raw = await readFile(manifestPath, "utf8");
84
+ }
85
+ catch {
86
+ manifestInvalid(name, "readManifestDir", `no ${MANIFEST_FILE} found at ${manifestPath}`);
87
+ }
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(raw);
91
+ }
92
+ catch (err) {
93
+ manifestInvalid(name, "readManifestDir", `${manifestPath} is not valid JSON: ${err.message}`);
94
+ }
95
+ const result = DependencyManifestSchema.safeParse(parsed);
96
+ if (!result.success) {
97
+ const issues = result.error.issues
98
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
99
+ .join("; ");
100
+ manifestInvalid(name, "readManifestDir", `${manifestPath} failed schema validation — ${issues}`);
101
+ }
102
+ const manifest = result.data;
103
+ const artifactPath = join(dir, manifest.artifact);
104
+ let artifactContent;
105
+ try {
106
+ artifactContent = await readFile(artifactPath, "utf8");
107
+ }
108
+ catch {
109
+ manifestInvalid(name, "readManifestDir", `the manifest names artifact "${manifest.artifact}", but it is missing at ${artifactPath}`);
110
+ }
111
+ const actual = sha256Integrity(artifactContent);
112
+ if (actual !== manifest.integrity) {
113
+ manifestInvalid(name, "readManifestDir", `${artifactPath} hashes to ${actual}, but the manifest records ${manifest.integrity}`);
114
+ }
115
+ return { manifest, artifactContent };
116
+ }
117
+ /**
118
+ * DESIGN §4.1 step 2 (the `name ≠ spec.name` arm) + step 3 (`metamodelVersion`
119
+ * major/minor). Returns an advisory warning string when the dependency was
120
+ * published against a newer metamodel MINOR (the metadata contract still
121
+ * loads; a MAJOR mismatch throws — ADR-0035 Amendment 2: the metadata
122
+ * contract is promised on the major alone), `undefined` otherwise.
123
+ */
124
+ export function validateAgainstSpec(manifest, spec) {
125
+ const name = dependencyName(spec);
126
+ if (manifest.name !== name) {
127
+ manifestInvalid(name, "validateAgainstSpec", `the manifest declares name "${manifest.name}", but it is declared in config as "${name}"`);
128
+ }
129
+ const manifestMajor = metamodelMajor(manifest.metamodelVersion);
130
+ const toolchainMajor = metamodelMajor(METAMODEL_VERSION);
131
+ if (manifestMajor !== toolchainMajor) {
132
+ throw new ParseError(`dependency "${name}" was published against metamodel ${manifest.metamodelVersion}; this ` +
133
+ `toolchain speaks ${METAMODEL_VERSION}. A different metamodel MAJOR is a different metadata ` +
134
+ `contract — upgrade the toolchain, or use a release of "${name}" built against it.`, { code: "ERR_DEPENDENCY_METAMODEL_INCOMPATIBLE", source: codeSource("validateAgainstSpec") });
135
+ }
136
+ if (metamodelMinor(manifest.metamodelVersion) > metamodelMinor(METAMODEL_VERSION)) {
137
+ return (`dependency "${name}" was published against metamodel ${manifest.metamodelVersion}, newer than ` +
138
+ `this toolchain's ${METAMODEL_VERSION} — vocabulary it uses that this toolchain does not yet ` +
139
+ `have may fail to load.`);
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * DESIGN §4.1 step 4 — load the artifact standalone with CORE providers only
145
+ * (a consumer loads it with ITS OWN providers, never the publisher's; a
146
+ * `MetaDataLoader` built with no `registry` option composes core providers by
147
+ * default), strict, and check it declares EXACTLY the manifest's `nodes` and
148
+ * `packages` — a publisher who hand-edited the manifest (or bypassed
149
+ * `sharedModelFile()` entirely) is caught here.
150
+ */
151
+ export async function standaloneLoadCheck(manifest, artifactContent) {
152
+ const loader = new MetaDataLoader({ strict: true });
153
+ const loaded = await loader.load([
154
+ new InMemoryStringSource(artifactContent, { id: manifest.artifact }),
155
+ ]);
156
+ if (loaded.errors.length > 0) {
157
+ manifestInvalid(manifest.name, "standaloneLoadCheck", `the artifact does not load standalone: ${loaded.errors[0].message}`);
158
+ }
159
+ // ADR-0039 sanctioned own-only case: `MetaRoot` never `extends`, so its own
160
+ // children ARE its effective children (the same reasoning
161
+ // `sharedModelFile()`'s step 2 documents on the publisher side).
162
+ const nodes = loaded.root.ownChildren().map((n) => n.resolutionKey());
163
+ const packages = [...new Set(nodes.map((n) => packageOfResolutionKey(n)))];
164
+ if (!sameSortedArray(nodes, manifest.nodes)) {
165
+ manifestInvalid(manifest.name, "standaloneLoadCheck", `the artifact's top-level nodes (${[...nodes].sort().join(", ") || "(none)"}) do not match the ` +
166
+ `manifest's "nodes" (${[...manifest.nodes].sort().join(", ") || "(none)"})`);
167
+ }
168
+ if (!sameSortedArray(packages, manifest.packages)) {
169
+ manifestInvalid(manifest.name, "standaloneLoadCheck", `the artifact's packages (${[...packages].sort().join(", ") || "(none)"}) do not match the ` +
170
+ `manifest's "packages" (${[...manifest.packages].sort().join(", ") || "(none)"})`);
171
+ }
172
+ }
173
+ /** The declared spec's transport, verbatim, minus `name` — a lock entry's
174
+ * `resolvedFrom` (DESIGN §3.3). */
175
+ function resolvedFromOf(spec) {
176
+ if ("path" in spec)
177
+ return { path: spec.path };
178
+ if ("npm" in spec)
179
+ return spec.dir === undefined ? { npm: spec.npm } : { npm: spec.npm, dir: spec.dir };
180
+ return spec.dir === undefined ? { python: spec.python } : { python: spec.python, dir: spec.dir };
181
+ }
182
+ /**
183
+ * DESIGN §4.1 steps 1-5, for every declared dependency (or the requested
184
+ * `filterNames` subset), in name order. Read-only: resolves each transport,
185
+ * validates its manifest and artifact, and checks the WHOLE resulting node
186
+ * set (freshly-resolved targets plus whatever untouched lock entries carry
187
+ * forward) for collisions — nothing is written here, which is what makes
188
+ * `--dry-run` "run this and skip `applySync`."
189
+ */
190
+ export async function planSync(configDir, specs, lock, filterNames = []) {
191
+ const allNames = specs.map(dependencyName);
192
+ const declared = new Set(allNames);
193
+ let targets;
194
+ if (filterNames.length === 0) {
195
+ targets = [...specs];
196
+ }
197
+ else {
198
+ const unknown = filterNames.filter((n) => !declared.has(n));
199
+ if (unknown.length > 0) {
200
+ throw new ParseError(`meta deps sync: not declared in ${DEFAULT_METAOBJECTS_DIR}/config.json: ${unknown.join(", ")}` +
201
+ (allNames.length > 0
202
+ ? ` (declared: ${[...declared].sort().join(", ")})`
203
+ : " (no dependencies declared)"), { code: "ERR_DEPENDENCY_UNRESOLVED", source: codeSource("planSync") });
204
+ }
205
+ targets = specs.filter((s) => filterNames.includes(dependencyName(s)));
206
+ }
207
+ targets.sort((a, b) => dependencyName(a).localeCompare(dependencyName(b)));
208
+ const targetNames = new Set(targets.map(dependencyName));
209
+ const oldEntries = lock?.dependencies ?? {};
210
+ // Node ownership seeds from untouched (carried-forward) lock entries, so a
211
+ // freshly-resolved target's nodes are checked against what already stands,
212
+ // not just against each other.
213
+ const nodeOwner = new Map();
214
+ const carryForward = new Map();
215
+ for (const [name, entry] of Object.entries(oldEntries)) {
216
+ if (declared.has(name) && !targetNames.has(name)) {
217
+ carryForward.set(name, entry);
218
+ for (const node of entry.nodes)
219
+ nodeOwner.set(node, name);
220
+ }
221
+ }
222
+ const warnings = [];
223
+ const plans = [];
224
+ for (const spec of targets) {
225
+ const name = dependencyName(spec);
226
+ const dir = resolveDependencyDir(configDir, spec);
227
+ const { manifest, artifactContent } = await readManifestDir(dir, name);
228
+ const warning = validateAgainstSpec(manifest, spec);
229
+ if (warning !== undefined)
230
+ warnings.push(warning);
231
+ await standaloneLoadCheck(manifest, artifactContent);
232
+ for (const node of manifest.nodes) {
233
+ const prior = nodeOwner.get(node);
234
+ if (prior !== undefined && prior !== name) {
235
+ throw new ParseError(`dependencies "${prior}" and "${name}" both export "${node}" — one fully-qualified node ` +
236
+ `cannot come from two places, and whichever loaded second would silently win`, { code: "ERR_DEPENDENCY_NODE_COLLISION", source: codeSource("planSync") });
237
+ }
238
+ nodeOwner.set(node, name);
239
+ }
240
+ const oldEntry = oldEntries[name];
241
+ const action = oldEntry !== undefined && oldEntry.integrity === manifest.integrity ? "unchanged" : "sync";
242
+ plans.push({ name, action, manifest, artifactContent, resolvedFrom: resolvedFromOf(spec), oldEntry });
243
+ }
244
+ const prunes = Object.entries(oldEntries)
245
+ .filter(([name]) => !declared.has(name))
246
+ .map(([name, entry]) => ({ name, entry }))
247
+ .sort((a, b) => a.name.localeCompare(b.name));
248
+ return { plans, carryForward, prunes, warnings };
249
+ }
250
+ /** First 8 hex chars after the `sha256-` (`INTEGRITY_PREFIX`) prefix — the
251
+ * report format's `<hash8>`. Exported so every caller — `deps.ts`'s `deps
252
+ * list` rendering, tests — shares this ONE definition rather than each
253
+ * reimplementing the slice with its own inlined `"sha256-"` literal. */
254
+ export function hash8(integrity) {
255
+ return integrity.slice(INTEGRITY_PREFIX.length, INTEGRITY_PREFIX.length + 8);
256
+ }
257
+ /**
258
+ * DESIGN §4.1 steps 7-8. `opts.dryRun` runs the exact same reporting pass and
259
+ * writes nothing — no directory emptied, no artifact copied, no lock touched
260
+ * — which is what makes `--dry-run` a true preview of `planSync`'s verdict
261
+ * rather than a second code path that could drift from it.
262
+ */
263
+ export async function applySync(configDir, plan, opts) {
264
+ const dependencies = {};
265
+ for (const [name, entry] of plan.carryForward)
266
+ dependencies[name] = entry;
267
+ const report = [];
268
+ for (const p of plan.plans) {
269
+ const newEntry = {
270
+ version: p.manifest.version,
271
+ metamodelVersion: p.manifest.metamodelVersion,
272
+ artifact: p.manifest.artifact,
273
+ integrity: p.manifest.integrity,
274
+ packages: p.manifest.packages,
275
+ nodes: p.manifest.nodes,
276
+ resolvedFrom: p.resolvedFrom,
277
+ };
278
+ dependencies[p.name] = newEntry;
279
+ if (p.action === "unchanged") {
280
+ report.push({ name: p.name, action: "unchanged", line: `unchanged ${p.name} ${p.manifest.version}` });
281
+ continue;
282
+ }
283
+ const oldVersion = p.oldEntry?.version ?? "(new)";
284
+ const oldHash = p.oldEntry !== undefined ? hash8(p.oldEntry.integrity) : "(new)";
285
+ const newHash = hash8(p.manifest.integrity);
286
+ const verb = opts.dryRun ? "would sync" : "synced";
287
+ report.push({
288
+ name: p.name,
289
+ action: "sync",
290
+ line: `${verb} ${p.name} ${oldVersion}→${p.manifest.version} (${oldHash}→${newHash})`,
291
+ });
292
+ if (!opts.dryRun) {
293
+ const depDir = join(configDir, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, p.name);
294
+ await rm(depDir, { recursive: true, force: true });
295
+ await mkdir(depDir, { recursive: true });
296
+ await writeFile(join(depDir, p.manifest.artifact), p.artifactContent, "utf8");
297
+ }
298
+ }
299
+ for (const pr of plan.prunes) {
300
+ report.push({
301
+ name: pr.name,
302
+ action: "prune",
303
+ line: opts.dryRun ? `would prune ${pr.name}` : `pruned ${pr.name}`,
304
+ });
305
+ if (!opts.dryRun) {
306
+ const depDir = join(configDir, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, pr.name);
307
+ await rm(depDir, { recursive: true, force: true });
308
+ }
309
+ }
310
+ const lock = { schema_version: 1, dependencies };
311
+ // Nothing to reconcile (no target dependency and no prune) is a true no-op —
312
+ // a project declaring no dependencies (and never having synced any) must
313
+ // not gain a `deps.lock.json` from a bare `meta deps sync`. `plan.prunes`
314
+ // being non-empty is what still writes when EVERY dependency was just
315
+ // removed from config: that run must produce a lock with an EMPTY
316
+ // `dependencies` map, not leave the stale one standing.
317
+ if (!opts.dryRun && (plan.plans.length > 0 || plan.prunes.length > 0)) {
318
+ await writeLock(configDir, lock);
319
+ }
320
+ return { lock, report };
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Task 15 — `meta deps check` / `verify --deps`: does the INSTALLED artifact
324
+ // (re-resolved live, exactly as `sync` steps 1-2 do) still hash to what the
325
+ // LOCK pinned? (DESIGN §2.3 "the lock pins bytes", §4.1 as amended by §11 —
326
+ // no usage-aware classification, just current/drifted/unresolved.)
327
+ // ---------------------------------------------------------------------------
328
+ /**
329
+ * `readLock`, converted into a diagnostic that NAMES the file rather than
330
+ * letting a corrupted committed lock's raw `JSON.parse`/`ZodError` surface as
331
+ * an unhandled rejection. `deps.lock.json` is checked-in, hand-editable, and
332
+ * merge-conflictable — a realistic way for it to break — and `bin/meta.ts`'s
333
+ * `run(...).then((code) => process.exit(code))` has no top-level `.catch()`,
334
+ * so an uncaught throw here would crash the process with a stack trace
335
+ * instead of the clean exit code every other failure in this command gets.
336
+ *
337
+ * The ONE call site for the sdk's `readLock` in `cli/src` — `deps.ts`'s
338
+ * `sync`/`list`/`check` handlers and `verify.ts`'s `--deps` gate all call
339
+ * THIS wrapper instead, so the diagnostic can never go stale in one of them.
340
+ */
341
+ export async function readLockOrThrow(configDir) {
342
+ try {
343
+ return await readLock(configDir);
344
+ }
345
+ catch (err) {
346
+ throw new Error(`${DEFAULT_METAOBJECTS_DIR}/${LOCK_FILE} is corrupted and could not be read: ${err.message}. ` +
347
+ "Fix it by hand, or delete it and re-run `meta deps sync` to regenerate it.");
348
+ }
349
+ }
350
+ /** Strips the `dependency "<name>": ` prefix `resolveDependencyDir` and
351
+ * `readManifestDir`'s errors always carry — `formatCheckLine` names the
352
+ * dependency itself, so repeating it verbatim inside `detail` would read
353
+ * twice. */
354
+ function stripDependencyPrefix(name, message) {
355
+ const prefix = `dependency "${name}": `;
356
+ return message.startsWith(prefix) ? message.slice(prefix.length) : message;
357
+ }
358
+ /**
359
+ * DESIGN §4.1 as amended by §11 — resolve each declared dependency EXACTLY as
360
+ * `sync` steps 1-2 do (`resolveDependencyDir` then `readManifestDir`; never
361
+ * step 3's `validateAgainstSpec` — `check` compares hashes, it does not
362
+ * re-validate the spec), then compare the freshly-read artifact's TRUE hash
363
+ * (re-hashed here from the bytes `readManifestDir` returned, never the
364
+ * installed manifest's `integrity` field taken on faith) against the lock's.
365
+ *
366
+ * Read-only: nothing here writes the lock, the snapshot, or touches
367
+ * `.metaobjects/deps/` — that is `sync`'s job. Every declared dependency gets
368
+ * exactly one result, in name order.
369
+ */
370
+ export async function checkDependencies(configDir, specs, lock) {
371
+ const entries = lock?.dependencies ?? {};
372
+ const sorted = [...specs].sort((a, b) => dependencyName(a).localeCompare(dependencyName(b)));
373
+ const results = [];
374
+ for (const spec of sorted) {
375
+ const name = dependencyName(spec);
376
+ const lockEntry = entries[name];
377
+ try {
378
+ const dir = resolveDependencyDir(configDir, spec);
379
+ const { manifest, artifactContent } = await readManifestDir(dir, name);
380
+ // Never trust the manifest's own `integrity` field for the comparison —
381
+ // re-hash the bytes actually sitting on disk right now.
382
+ //
383
+ // Right now this cannot observe a divergence from `manifest.integrity`:
384
+ // `readManifestDir` above (§4.1 step 2, :152-159) already throws
385
+ // `ERR_DEPENDENCY_MANIFEST_INVALID` unless `sha256Integrity(artifactContent)
386
+ // === manifest.integrity`, so by the time this line runs the two values are
387
+ // provably equal — this call is redundant WORK, not redundant SAFETY. The
388
+ // safety it buys is against `readManifestDir` changing out from under this
389
+ // function: if that check is ever relaxed, moved, or made conditional, this
390
+ // line becomes the ONLY thing standing between `check` and trusting a
391
+ // self-declared field, with no test that would fail to say so (the
392
+ // divergent state is unreachable through `checkDependencies`'s public
393
+ // signature today, precisely because `readManifestDir` forecloses it
394
+ // first). Read `manifest.integrity` here only if you have also confirmed
395
+ // `readManifestDir` still enforces the hash match unconditionally.
396
+ const installedIntegrity = sha256Integrity(artifactContent);
397
+ if (lockEntry === undefined) {
398
+ results.push({
399
+ name,
400
+ status: "unresolved",
401
+ lockVersion: undefined,
402
+ lockIntegrity: undefined,
403
+ installedVersion: manifest.version,
404
+ installedIntegrity,
405
+ detail: "declared, and resolves, but is not in the lock yet",
406
+ });
407
+ continue;
408
+ }
409
+ results.push({
410
+ name,
411
+ status: installedIntegrity === lockEntry.integrity ? "current" : "drifted",
412
+ lockVersion: lockEntry.version,
413
+ lockIntegrity: lockEntry.integrity,
414
+ installedVersion: manifest.version,
415
+ installedIntegrity,
416
+ detail: undefined,
417
+ });
418
+ }
419
+ catch (err) {
420
+ results.push({
421
+ name,
422
+ status: "unresolved",
423
+ lockVersion: lockEntry?.version,
424
+ lockIntegrity: lockEntry?.integrity,
425
+ installedVersion: undefined,
426
+ installedIntegrity: undefined,
427
+ detail: stripDependencyPrefix(name, err.message),
428
+ });
429
+ }
430
+ }
431
+ return results;
432
+ }
433
+ /** The one code every `checkDependencies` failure (`drifted` or `unresolved`)
434
+ * is reported under — `deps.ts`'s `check` handler and `verify.ts`'s `--deps`
435
+ * gate both surface it, so it is named once here rather than inlined twice. */
436
+ export const ERR_DEPENDENCY_UPSTREAM_DRIFT = "ERR_DEPENDENCY_UPSTREAM_DRIFT";
437
+ /**
438
+ * `meta deps check` / `verify --deps`'s one report line per dependency. The
439
+ * `drifted` shape is pinned exactly by the task brief:
440
+ * `<name>: drifted — lock <v> (<hash8>), installed <v> (<hash8>); run meta
441
+ * deps sync and review the artifact diff`.
442
+ */
443
+ export function formatCheckLine(result) {
444
+ const lockDesc = `${result.lockVersion ?? "(none)"} (${result.lockIntegrity !== undefined ? hash8(result.lockIntegrity) : "n/a"})`;
445
+ const installedDesc = `${result.installedVersion ?? "(none)"} (${result.installedIntegrity !== undefined ? hash8(result.installedIntegrity) : "n/a"})`;
446
+ switch (result.status) {
447
+ case "current":
448
+ return `${result.name}: current — ${lockDesc}`;
449
+ case "drifted":
450
+ return (`${result.name}: drifted — lock ${lockDesc}, installed ${installedDesc}; ` +
451
+ "run meta deps sync and review the artifact diff");
452
+ case "unresolved":
453
+ return (`${result.name}: unresolved — ${result.detail ?? "could not resolve the dependency"}; ` +
454
+ "run meta deps sync once the dependency is reachable");
455
+ }
456
+ }
457
+ //# sourceMappingURL=dependency-sync.js.map