import { createHash } from "node:crypto"; import { readFile, readdir } from "node:fs/promises"; import { resolve } from "node:path"; const root = resolve(import.meta.dirname, ".."); const sourcePath = resolve(root, "crates/nana-domain/src/lib.rs"); const hashPath = resolve(root, "contracts/.source.sha256"); const schemaDirectory = resolve(root, "contracts/schema"); const source = await readFile(sourcePath); const expectedHash = (await readFile(hashPath, "utf8")).trim(); const actualHash = createHash("sha256").update(source).digest("hex"); if (actualHash !== expectedHash) { throw new Error( "Generated contracts are stale. Install Rust and run `pnpm contracts:generate`." ); } const schemaFiles = (await readdir(schemaDirectory)) .filter((name) => name.endsWith(".schema.json")) .sort(); const required = [ "app-info.schema.json", "character-card.schema.json", "demo-pack-summary.schema.json", "item-spec.schema.json", "persona.schema.json", "player-view.schema.json", "plot-module.schema.json", "resource-bundle.schema.json", "resource-header.schema.json", "runtime-state.schema.json", "story-node.schema.json", "turn-failure.schema.json", "turn-request.schema.json", "turn-result.schema.json", "world-book.schema.json" ]; for (const name of required) { if (!schemaFiles.includes(name)) { throw new Error(`Missing generated schema: contracts/schema/${name}`); } } for (const name of schemaFiles) { const value = JSON.parse(await readFile(resolve(schemaDirectory, name), "utf8")); if (typeof value !== "object" || value === null || value.$schema === undefined) { throw new Error(`Invalid generated schema: contracts/schema/${name}`); } } const typeScript = await readFile(resolve(root, "contracts/ts/index.ts"), "utf8"); if (!typeScript.startsWith("// @generated by crates/nana-contracts")) { throw new Error("contracts/ts/index.ts is not marked as generated"); } console.log(`Verified ${schemaFiles.length} schemas and TypeScript DTO source hash.`);