@moldesk/core 0.0.1 → 0.0.3

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.
@@ -0,0 +1,31 @@
1
+ import type { ModelManifestV1, RuntimeKind } from "@moldesk/registry";
2
+ import type { SystemReport } from "@moldesk/runtime";
3
+ export type CompatibilityStatus = "compatible" | "warning" | "unsupported";
4
+ export interface CompatibilityReason {
5
+ code: string;
6
+ severity: "info" | "warning" | "error";
7
+ message: string;
8
+ remediation?: string;
9
+ }
10
+ export interface RuntimeCompatibility {
11
+ kind: RuntimeKind;
12
+ status: CompatibilityStatus;
13
+ reasons: CompatibilityReason[];
14
+ }
15
+ export interface CompatibilityResult {
16
+ status: CompatibilityStatus;
17
+ selectedRuntime?: RuntimeKind;
18
+ reasons: CompatibilityReason[];
19
+ /** Per-candidate breakdown, one entry per evaluated runtime, manifest order. */
20
+ runtimes: RuntimeCompatibility[];
21
+ }
22
+ export interface CompatibilityOptions {
23
+ requestedRuntime?: RuntimeKind;
24
+ }
25
+ /**
26
+ * Pure compatibility evaluator shared by `doctor`, `list`, and `install`.
27
+ * Never probes, never writes; all machine facts come from `report`.
28
+ */
29
+ export declare function evaluateCompatibility(manifest: ModelManifestV1, report: SystemReport, options?: CompatibilityOptions): CompatibilityResult;
30
+ /** Shortest important reason for one-line `list` rendering. */
31
+ export declare function shortReason(result: CompatibilityResult): string;
@@ -0,0 +1,377 @@
1
+ const BYTES_PER_GB = 1024 ** 3;
2
+ /** Managed-python runtimes support these OS/arch pairs via pinned uv bootstrap. */
3
+ const MANAGED_PYTHON_PLATFORMS = new Set(["darwin-arm64", "linux-x64"]);
4
+ /** Managed-python (uv-bootstrapped) interpreter versions Step 2 preflight can vouch for. */
5
+ const SUPPORTED_MANAGED_PYTHON_VERSIONS = new Set(["3.10", "3.11", "3.12"]);
6
+ /** Parse a `major.minor[.patch]` version string. Not a semver parser — just the two leading numeric components used for a floor comparison. */
7
+ function parseMajorMinor(version) {
8
+ const match = version.match(/^(\d+)\.(\d+)/);
9
+ if (!match)
10
+ return undefined;
11
+ return { major: Number(match[1]), minor: Number(match[2]) };
12
+ }
13
+ /** `detected >= required` as a major.minor floor — NOT a semver range comparison. */
14
+ function isCudaVersionSufficient(detected, required) {
15
+ if (detected.major !== required.major)
16
+ return detected.major > required.major;
17
+ return detected.minor >= required.minor;
18
+ }
19
+ function dedupeReasons(reasons) {
20
+ const seen = new Map();
21
+ for (const reason of reasons) {
22
+ const key = `${reason.code}::${reason.message}`;
23
+ if (!seen.has(key))
24
+ seen.set(key, reason);
25
+ }
26
+ return [...seen.values()];
27
+ }
28
+ function currentPlatformId(report) {
29
+ const arch = report.arch === "x86_64" ? "x64" : report.arch;
30
+ if (report.platform === "unknown")
31
+ return "unknown";
32
+ return `${report.platform}-${arch}`;
33
+ }
34
+ function gb(bytes) {
35
+ return bytes / BYTES_PER_GB;
36
+ }
37
+ function maxVramBytes(report) {
38
+ let max;
39
+ for (const gpu of report.nvidia.gpus) {
40
+ if (typeof gpu.totalVramBytes !== "number")
41
+ continue;
42
+ max = max === undefined ? gpu.totalVramBytes : Math.max(max, gpu.totalVramBytes);
43
+ }
44
+ return max;
45
+ }
46
+ /**
47
+ * Resolves the effective accelerator requirement (CUDA vs MPS/none) for the
48
+ * runtime entry matching the *current* host, not a single manifest-wide value.
49
+ * A per-platform python runtime entry's own `nvidiaGpu`/`cuda`/`minVramGb` — when
50
+ * set — wins field-by-field over the manifest-level `hardware` equivalent, so a
51
+ * two-entry Boltz-style manifest can declare CUDA-required on Linux and no CUDA
52
+ * requirement (MPS instead) on Darwin from the *same* manifest.
53
+ */
54
+ function effectiveAcceleratorRequirements(manifest, runtime) {
55
+ if (runtime.kind !== "python") {
56
+ return { nvidiaGpu: manifest.hardware.nvidiaGpu, cuda: manifest.hardware.cuda, minVramGb: manifest.hardware.minVramGb };
57
+ }
58
+ return {
59
+ nvidiaGpu: runtime.nvidiaGpu ?? manifest.hardware.nvidiaGpu,
60
+ cuda: runtime.cuda ?? manifest.hardware.cuda,
61
+ minVramGb: runtime.minVramGb ?? manifest.hardware.minVramGb,
62
+ };
63
+ }
64
+ function evaluateSharedHardware(manifest, report, currentId, accelerator) {
65
+ const errors = [];
66
+ const warnings = [];
67
+ const hw = manifest.hardware;
68
+ if (hw.platforms && hw.platforms.length > 0 && !hw.platforms.includes(currentId)) {
69
+ errors.push({
70
+ code: "PLATFORM_UNSUPPORTED",
71
+ severity: "error",
72
+ message: `platform ${currentId} not in manifest platforms (${hw.platforms.join(", ")})`,
73
+ remediation: `Use one of: ${hw.platforms.join(", ")}.`,
74
+ });
75
+ }
76
+ if (typeof hw.minRamGb === "number") {
77
+ const totalGb = gb(report.memory.totalBytes);
78
+ if (!(totalGb >= hw.minRamGb)) {
79
+ errors.push({
80
+ code: "INSUFFICIENT_RAM",
81
+ severity: "error",
82
+ message: `requires >= ${hw.minRamGb} GB RAM, detected ${totalGb.toFixed(1)} GB`,
83
+ remediation: "Free memory or use a machine with more RAM.",
84
+ });
85
+ }
86
+ }
87
+ if (typeof hw.minDiskGb === "number") {
88
+ if (report.disk.probeFailed) {
89
+ warnings.push({
90
+ code: "DISK_SPACE_UNKNOWN",
91
+ severity: "warning",
92
+ message: `requires >= ${hw.minDiskGb} GB free at ${report.disk.path}, but disk space could not be determined${report.disk.probeError ? ` (${report.disk.probeError})` : ""}`,
93
+ remediation: `Verify free disk space at ${report.disk.path} manually.`,
94
+ });
95
+ }
96
+ else {
97
+ const availGb = gb(report.disk.availableBytes);
98
+ if (!(availGb >= hw.minDiskGb)) {
99
+ errors.push({
100
+ code: "DISK_SPACE_LOW",
101
+ severity: "error",
102
+ message: `requires >= ${hw.minDiskGb} GB free at ${report.disk.path}, detected ${availGb.toFixed(1)} GB`,
103
+ remediation: `Free disk space at ${report.disk.path}.`,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ const gpuLevel = accelerator.nvidiaGpu ?? "unsupported";
109
+ if (gpuLevel === "required" && !report.nvidia.available) {
110
+ errors.push({
111
+ code: "NVIDIA_GPU_REQUIRED",
112
+ severity: "error",
113
+ message: "model requires an NVIDIA GPU but none was detected",
114
+ remediation: "Run on a Linux host with an NVIDIA GPU + driver, or pick a CPU-compatible model.",
115
+ });
116
+ }
117
+ else if (gpuLevel === "recommended" && !report.nvidia.available) {
118
+ warnings.push({
119
+ code: "NVIDIA_GPU_RECOMMENDED",
120
+ severity: "warning",
121
+ message: "NVIDIA GPU recommended but none detected; CPU execution will be slow",
122
+ remediation: "For full speed, use a CUDA host.",
123
+ });
124
+ }
125
+ if (typeof accelerator.minVramGb === "number") {
126
+ // Schema requires nvidiaGpu whenever minVramGb is set (superRefine, at both the
127
+ // manifest-hardware and per-runtime-entry level), so gpuLevel here is never the
128
+ // ambiguous "no signal" case.
129
+ const vramSeverity = gpuLevel === "required" ? "error" : "warning";
130
+ const vramList = vramSeverity === "error" ? errors : warnings;
131
+ const verb = vramSeverity === "error" ? "requires" : "recommends";
132
+ if (!report.nvidia.available) {
133
+ vramList.push({
134
+ code: "INSUFFICIENT_VRAM",
135
+ severity: vramSeverity,
136
+ message: `${verb} >= ${accelerator.minVramGb} GB VRAM but no NVIDIA GPU was detected`,
137
+ remediation: "Use a GPU host with enough VRAM.",
138
+ });
139
+ }
140
+ else {
141
+ const maxVram = maxVramBytes(report);
142
+ if (maxVram === undefined) {
143
+ vramList.push({
144
+ code: "INSUFFICIENT_VRAM",
145
+ severity: vramSeverity,
146
+ message: `${verb} >= ${accelerator.minVramGb} GB VRAM but VRAM could not be determined`,
147
+ remediation: "Verify nvidia-smi reports memory.total for each GPU.",
148
+ });
149
+ }
150
+ else if (!(gb(maxVram) >= accelerator.minVramGb)) {
151
+ vramList.push({
152
+ code: "INSUFFICIENT_VRAM",
153
+ severity: vramSeverity,
154
+ message: `${verb} >= ${accelerator.minVramGb} GB VRAM, detected max ${gb(maxVram).toFixed(1)} GB`,
155
+ remediation: "Use a GPU with more VRAM.",
156
+ });
157
+ }
158
+ }
159
+ }
160
+ const cudaLevel = accelerator.cuda?.level;
161
+ if (cudaLevel === "required" && (!report.nvidia.available || !report.nvidia.driverCudaVersion)) {
162
+ errors.push({
163
+ code: "CUDA_NOT_DETECTED",
164
+ severity: "error",
165
+ message: "model requires CUDA but no CUDA version was detected",
166
+ remediation: "Install a CUDA-capable driver/toolkit or use a CUDA host.",
167
+ });
168
+ }
169
+ else if (cudaLevel === "recommended" && (!report.nvidia.available || !report.nvidia.driverCudaVersion)) {
170
+ warnings.push({
171
+ code: "CUDA_NOT_DETECTED",
172
+ severity: "warning",
173
+ message: "CUDA recommended but not detected; CPU execution will be slow",
174
+ remediation: "For full speed, use a CUDA host.",
175
+ });
176
+ }
177
+ else if (cudaLevel && report.nvidia.driverCudaVersion && accelerator.cuda?.minDriverCudaVersion) {
178
+ const detected = parseMajorMinor(report.nvidia.driverCudaVersion);
179
+ const required = parseMajorMinor(accelerator.cuda.minDriverCudaVersion);
180
+ if (detected && required && !isCudaVersionSufficient(detected, required)) {
181
+ const severity = cudaLevel === "required" ? "error" : "warning";
182
+ (severity === "error" ? errors : warnings).push({
183
+ code: "CUDA_VERSION_UNSUPPORTED",
184
+ severity,
185
+ message: `requires driver CUDA >= ${accelerator.cuda.minDriverCudaVersion}, detected ${report.nvidia.driverCudaVersion}`,
186
+ remediation: "Update the NVIDIA driver to one supporting a newer CUDA version.",
187
+ });
188
+ }
189
+ }
190
+ return { errors, warnings };
191
+ }
192
+ function evaluateRuntime(runtime, manifest, report, currentId) {
193
+ const shared = evaluateSharedHardware(manifest, report, currentId, effectiveAcceleratorRequirements(manifest, runtime));
194
+ const errors = [...shared.errors];
195
+ const warnings = [...shared.warnings];
196
+ if (runtime.kind === "python") {
197
+ // Host Python is diagnostic only; evaluate managed uv bootstrap instead.
198
+ if (!MANAGED_PYTHON_PLATFORMS.has(currentId)) {
199
+ errors.push({
200
+ code: "MANAGED_PYTHON_PLATFORM_UNSUPPORTED",
201
+ severity: "error",
202
+ message: `managed Python ${runtime.python} via uv is not supported on ${currentId}`,
203
+ remediation: "Use darwin-arm64 or linux-x64, or pick the docker runtime if offered.",
204
+ });
205
+ }
206
+ if (!SUPPORTED_MANAGED_PYTHON_VERSIONS.has(runtime.python)) {
207
+ errors.push({
208
+ code: "MANAGED_PYTHON_VERSION_UNSUPPORTED",
209
+ severity: "error",
210
+ message: `managed Python ${runtime.python} is not a supported managed-uv version`,
211
+ remediation: `Use a manifest requesting one of: ${[...SUPPORTED_MANAGED_PYTHON_VERSIONS].join(", ")}, or pick the docker runtime if offered.`,
212
+ });
213
+ }
214
+ return { errors, warnings };
215
+ }
216
+ // Docker runtime.
217
+ if (!report.docker.available) {
218
+ errors.push({
219
+ code: "DOCKER_NOT_INSTALLED",
220
+ severity: "error",
221
+ message: "docker runtime requires Docker but it is not installed",
222
+ remediation: "Install Docker Desktop (macOS) or Docker Engine (Linux).",
223
+ });
224
+ return { errors, warnings };
225
+ }
226
+ if (!report.docker.running) {
227
+ errors.push({
228
+ code: "DOCKER_NOT_RUNNING",
229
+ severity: "error",
230
+ message: `docker runtime requires a running daemon${report.docker.error ? `: ${report.docker.error}` : ""}`,
231
+ remediation: "Start Docker (e.g. `open -a Docker` or `sudo systemctl start docker`).",
232
+ });
233
+ return { errors, warnings };
234
+ }
235
+ // Docker GPU access is Tier-2-authoritative: a remote Docker daemon may
236
+ // have GPU access the local host doesn't (and vice versa), so this
237
+ // deliberately never consults `report.nvidia.available` — only
238
+ // `report.docker.gpuAccess` (Tier 1 `docker info`, or an injected Tier 2
239
+ // `docker run --gpus all ... nvidia-smi` result) decides.
240
+ if (runtime.gpu === "required") {
241
+ if (report.docker.gpuAccess === "unavailable") {
242
+ errors.push({
243
+ code: "DOCKER_GPU_UNAVAILABLE",
244
+ severity: "error",
245
+ message: "docker GPU access is confirmed unavailable on the target daemon",
246
+ remediation: "Install/enable the NVIDIA container runtime on the Docker host, or verify GPU access manually.",
247
+ });
248
+ }
249
+ else if (report.docker.gpuAccess === "unknown") {
250
+ errors.push({
251
+ code: "DOCKER_GPU_UNVERIFIED",
252
+ severity: "error",
253
+ message: "docker GPU access has not been verified",
254
+ remediation: "Run `moldesk install <model>` interactively to verify GPU access, or pass --yes to confirm the verification image download.",
255
+ });
256
+ }
257
+ }
258
+ else if (runtime.gpu === "optional") {
259
+ if (report.docker.gpuAccess === "unavailable") {
260
+ warnings.push({
261
+ code: "DOCKER_GPU_UNAVAILABLE",
262
+ severity: "warning",
263
+ message: "Docker GPU access unavailable; CPU execution will be slow",
264
+ remediation: "Install nvidia-container-toolkit for GPU acceleration.",
265
+ });
266
+ }
267
+ else if (report.docker.gpuAccess === "unknown") {
268
+ warnings.push({
269
+ code: "DOCKER_GPU_UNVERIFIED",
270
+ severity: "warning",
271
+ message: "Docker GPU access not verified; GPU use is uncertain",
272
+ remediation: "Verify GPU access before expecting acceleration.",
273
+ });
274
+ }
275
+ }
276
+ return { errors, warnings };
277
+ }
278
+ /**
279
+ * Pure compatibility evaluator shared by `doctor`, `list`, and `install`.
280
+ * Never probes, never writes; all machine facts come from `report`.
281
+ */
282
+ export function evaluateCompatibility(manifest, report, options = {}) {
283
+ if (manifest.status === "planned") {
284
+ return {
285
+ status: "unsupported",
286
+ runtimes: [],
287
+ reasons: [
288
+ {
289
+ code: "MODEL_PLANNED",
290
+ severity: "error",
291
+ message: `model "${manifest.name}" is planned and cannot be installed yet`,
292
+ remediation: "Pick a model with status available or beta.",
293
+ },
294
+ ],
295
+ };
296
+ }
297
+ const currentId = currentPlatformId(report);
298
+ const kindCandidates = options.requestedRuntime
299
+ ? manifest.runtimes.filter((r) => r.kind === options.requestedRuntime)
300
+ : [...manifest.runtimes];
301
+ if (options.requestedRuntime && kindCandidates.length === 0) {
302
+ return {
303
+ status: "unsupported",
304
+ runtimes: [],
305
+ reasons: [
306
+ {
307
+ code: "RUNTIME_NOT_AVAILABLE",
308
+ severity: "error",
309
+ message: `model "${manifest.name}" offers no ${options.requestedRuntime} runtime`,
310
+ remediation: `Omit --runtime or choose one of: ${manifest.runtimes.map((r) => r.kind).join(", ")}.`,
311
+ },
312
+ ],
313
+ };
314
+ }
315
+ if (kindCandidates.length === 0) {
316
+ return {
317
+ status: "unsupported",
318
+ runtimes: [],
319
+ reasons: [
320
+ {
321
+ code: "NO_RUNTIMES_DECLARED",
322
+ severity: "error",
323
+ message: `model "${manifest.name}" declares no runtimes`,
324
+ remediation: "Report this manifest bug to the registry maintainers.",
325
+ },
326
+ ],
327
+ };
328
+ }
329
+ // A per-platform python runtime entry (its own `platforms` field, distinct from
330
+ // the manifest-wide `hardware.platforms` filter) only ever candidates on a
331
+ // matching host — this is what lets a two-entry Boltz-style manifest report the
332
+ // *matching* entry's accelerator requirement per host, instead of evaluating
333
+ // every platform's entry (and its accelerator reasons) on every host.
334
+ const candidates = kindCandidates.filter((r) => r.kind !== "python" || r.platforms === undefined || r.platforms.includes(currentId));
335
+ if (candidates.length === 0) {
336
+ return {
337
+ status: "unsupported",
338
+ runtimes: [],
339
+ reasons: [
340
+ {
341
+ code: "RUNTIME_PLATFORM_UNSUPPORTED",
342
+ severity: "error",
343
+ message: `model "${manifest.name}" declares no runtime entry for platform ${currentId}`,
344
+ remediation: "Run on a platform this model's runtimes[] declares support for.",
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ // Evaluate every candidate — a manifest offering multiple runtimes should
350
+ // never have its non-selected candidates' diagnostics silently discarded,
351
+ // even when none of them end up compatible.
352
+ const runtimes = candidates.map((runtime) => {
353
+ const { errors, warnings } = evaluateRuntime(runtime, manifest, report, currentId);
354
+ const status = errors.length > 0 ? "unsupported" : warnings.length > 0 ? "warning" : "compatible";
355
+ return { kind: runtime.kind, status, reasons: [...errors, ...warnings] };
356
+ });
357
+ const clean = runtimes.find((r) => r.status === "compatible");
358
+ if (clean) {
359
+ return { status: "compatible", selectedRuntime: clean.kind, reasons: [], runtimes };
360
+ }
361
+ const warned = runtimes.find((r) => r.status === "warning");
362
+ if (warned) {
363
+ return { status: "warning", selectedRuntime: warned.kind, reasons: warned.reasons, runtimes };
364
+ }
365
+ return {
366
+ status: "unsupported",
367
+ reasons: dedupeReasons(runtimes.flatMap((r) => r.reasons)),
368
+ runtimes,
369
+ };
370
+ }
371
+ /** Shortest important reason for one-line `list` rendering. */
372
+ export function shortReason(result) {
373
+ const first = result.reasons.find((r) => r.severity === "error")
374
+ ?? result.reasons.find((r) => r.severity === "warning")
375
+ ?? result.reasons[0];
376
+ return first ? `${first.code}: ${first.message}` : "";
377
+ }
@@ -0,0 +1 @@
1
+ export {};