/*
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 };
}