/* RFC-0221: filesystem IO for a handoff bundle — read/parse the lock and manifest, hash files, and resolve the recipient's current ecosystem facts. Do not copy or regenerate site files — that is pack/absorb's job. RFC-0221: initial bundle IO helpers. RFC-0533: extend resolvePlatformSemanticHash to cover packages/, integrations/, and services/ (full platform scope). */ import path from "node:path"; import { handoffLockSchema, handoffManifestSchema, type HandoffLock, type HandoffManifest, } from "@warpgogol/werkstatt-engine/schemas"; import { byteHash } from "@warpgogol/werkstatt-engine/fingerprint"; import { resolvePlatformSemanticHash } from "@warpgogol/werkstatt-engine/kernel"; import type { RegistryView } from "./types.ts"; import type { ValidationPack } from "./validation-pack.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export { resolvePlatformSemanticHash }; export function sha256OfBytes(bytes: Buffer | string): string { return byteHash(bytes); } export async function hashFile( filePath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const bytes = await io.readFile(filePath); return sha256OfBytes(bytes); } export async function readLock( bundleDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const raw = await io.readFile(path.join(bundleDir, "handoff-lock.json")); return handoffLockSchema.parse(JSON.parse(raw)); } export async function readManifest( bundleDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const raw = await io.readFile(path.join(bundleDir, "handoff-manifest.json")); return handoffManifestSchema.parse(JSON.parse(raw)); } /** Read the bundle's golden validation pack (validation/pack.json), or null if absent. */ export async function readGoldenPack( bundleDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const raw = await io.readFile(path.join(bundleDir, "validation", "pack.json")); return JSON.parse(raw) as ValidationPack; } catch { return null; } } export interface CurrentEcosystem { version: string; commit: string; } /** Resolve the recipient's current ecosystem version (root package.json) and git SHA. */ export async function resolveCurrentEcosystem( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const pkgRaw = await io.readFile(path.join(workspaceRoot, "package.json")); const version = String((JSON.parse(pkgRaw) as { version?: string }).version ?? "0.0.0"); let commit = "unknown"; try { const revResult = await io.exec("git", ["rev-parse", "HEAD"], { cwd: workspaceRoot, }); if (revResult.exitCode !== 0) { throw new Error(`git rev-parse exited ${revResult.exitCode}`); } commit = revResult.stdout.trim(); } catch { // not a git checkout — leave "unknown" } return { version, commit }; } /** * Legacy byte hash of the recipient's packages/ tree. * * Uses the committed git tree object id of `packages/` (cheap and deterministic), * wrapped as sha256 to satisfy the lock schema. Reflects COMMITTED state only — an * uncommitted local edit to packages/ is exactly the "ecosystem drift" the in-sync * branch warns about. Falls back to "sha256:" + 64 zeros when git is unavailable. * * RFC-0364: superseded by resolvePlatformSemanticHash for new writers. * Retained for dual-read during migration. */ export async function resolvePackagesHash( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const revResult = await io.exec("git", ["rev-parse", "HEAD:packages"], { cwd: workspaceRoot, }); if (revResult.exitCode !== 0) { throw new Error(`git rev-parse exited ${revResult.exitCode}`); } return sha256OfBytes(revResult.stdout.trim()); } catch { return `sha256:${"0".repeat(64)}`; } } interface UniRegistryEntry { id: string; semanticId?: string; version: string; intent?: string[]; } /** Build a RegistryView keyed by the unique entry id (semanticId is not unique across layers). */ export async function readRegistryView( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const raw = await io.readFile(path.join(workspaceRoot, "uni.registry.yaml")); const parsed = JSON.parse(raw) as { entries?: UniRegistryEntry[] }; const byId = new Map(); for (const entry of parsed.entries ?? []) { if (!entry.id) continue; byId.set(entry.id, { version: entry.version ?? "0.0.0", semanticId: entry.semanticId ?? entry.id, intent: entry.intent ?? [], }); } return { byId }; }