import { CAPABILITY_CONTRACT_VERSIONS, type KnownCapabilityName, compareProviderToRuntime, } from '@celilo/capabilities'; import type { Check } from './types'; export interface ManifestCapabilityClaim { name: string; version: string; } export interface ManifestForCapabilityCheck { id?: string; version?: string; provides?: { capabilities?: ManifestCapabilityClaim[] }; requires?: { capabilities?: ManifestCapabilityClaim[] }; } function isKnownCapability(name: string): name is KnownCapabilityName { return name in CAPABILITY_CONTRACT_VERSIONS; } /** * Strict-publish: every `provides[X].version` must match the framework's * runtime registry; every `requires[X].version` must be at most the * framework's runtime version (consumers can require older minors of * still-supported majors). * * Returns a list of human-readable error messages — empty list means OK. * Used both by `module publish` (which fails on any error) and * `module check` (which surfaces them as failed checks). */ export function validateCapabilityVersions(manifest: ManifestForCapabilityCheck): string[] { const errors: string[] = []; for (const p of manifest.provides?.capabilities ?? []) { if (!isKnownCapability(p.name)) continue; const runtime = CAPABILITY_CONTRACT_VERSIONS[p.name]; const r = compareProviderToRuntime(p.version, runtime); if (!r.compatible) { errors.push( `provides[${p.name}].version is ${p.version} but framework registry is ${runtime} ` + `(${r.reason}). Update the manifest, then retry.`, ); } } for (const need of manifest.requires?.capabilities ?? []) { if (!isKnownCapability(need.name)) continue; const runtime = CAPABILITY_CONTRACT_VERSIONS[need.name]; const r = compareProviderToRuntime(need.version, runtime); if (!r.compatible && r.reason === 'major_mismatch_higher') { errors.push( `requires[${need.name}].version is ${need.version} but framework registry is ${runtime}. Bump the framework before publishing, or lower the manifest version.`, ); } } return errors; } /** * Per-capability `Check[]` form for `module check`. Emits one OK row for * every known capability the manifest references and one FAIL row for * every mismatch. A manifest with no known-capability claims yields a * single synthetic OK so the report is never empty. */ export function checkCapabilityVersions(manifest: ManifestForCapabilityCheck): Check[] { const checks: Check[] = []; for (const p of manifest.provides?.capabilities ?? []) { if (!isKnownCapability(p.name)) continue; const runtime = CAPABILITY_CONTRACT_VERSIONS[p.name]; const r = compareProviderToRuntime(p.version, runtime); checks.push({ category: 'capability', name: `provides[${p.name}]`, status: r.compatible ? 'ok' : 'fail', message: r.compatible ? `provides ${p.name}@${p.version} matches framework registry ${runtime}` : `provides ${p.name}@${p.version} but framework registry is ${runtime} (${r.reason}). Update the manifest.`, currentValue: p.version, suggestedValue: r.compatible ? undefined : runtime, }); } for (const need of manifest.requires?.capabilities ?? []) { if (!isKnownCapability(need.name)) continue; const runtime = CAPABILITY_CONTRACT_VERSIONS[need.name]; const r = compareProviderToRuntime(need.version, runtime); const tooNew = !r.compatible && r.reason === 'major_mismatch_higher'; checks.push({ category: 'capability', name: `requires[${need.name}]`, status: tooNew ? 'fail' : 'ok', message: tooNew ? `requires ${need.name}@${need.version} but framework registry is ${runtime}. Bump the framework, or lower the manifest version.` : `requires ${need.name}@${need.version} can be satisfied by framework registry ${runtime}`, currentValue: need.version, suggestedValue: tooNew ? runtime : undefined, }); } if (checks.length === 0) { checks.push({ category: 'capability', name: 'no known-capability claims', status: 'ok', message: 'manifest declares no provides/requires for known framework capabilities', }); } return checks; }