/* Shared helpers for the three-phase build pipeline (build.prepare → astro build → build.post) used by mission.build, mission.validate, and release.prepare. Centralizes pipeline execution and build-input-hash computation to eliminate duplication. Does not define pipeline composition or command registration — that lives in site-kernel-checks pipelines. Does not run astro build directly — callers own the execSync call between build.prepare and build.post. ADR-0008: extracted runPipelinePhase and computeBuildInputHash from mission-materialization-commands.ts and release-commands.ts to eliminate 5x duplicated pipeline execution pattern and 2x duplicated build-input-hash computation. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import path from "node:path"; import type { DiscoveredSiteWorkspace, KernelPipelineReport, } from "@warpgogol/werkstatt-engine/kernel"; import { executeKernelPipeline } from "@warpgogol/werkstatt-engine/kernel"; import { fingerprintTree } from "@warpgogol/werkstatt-engine/fingerprint/semantic"; import { byteHash } from "@warpgogol/werkstatt-engine/fingerprint"; import { atomicWriteFile } from "../werkstatt/atomic.ts"; import { resolveCurrentEcosystem, resolvePlatformSemanticHash } from "./bundle-io.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; /** * Runs a kernel pipeline phase and throws a descriptive error if it fails. * Centralizes the executeKernelPipeline → unwrap → ok-check → format-failings pattern * that is used by mission.build, mission.validate, and release.prepare. * When `siteWorkspace` is provided, bypasses site discovery (needed for closed-mission * workpieces where registry.currentMission is null). */ export async function runPipelinePhase( workspaceRoot: string, pipelineName: string, siteName: string, siteWorkspace?: DiscoveredSiteWorkspace, ): Promise { const result = await executeKernelPipeline({ workspaceRoot, pipelineName, siteName, outputFormat: "pretty", siteWorkspace, }); const report: KernelPipelineReport = Array.isArray(result) ? result[0] : result; if (!report.ok) { const failed = report.steps .filter((s) => !s.ok) .map((s) => `${s.commandName} (exit ${s.exitCode})`); throw new Error(`${pipelineName} failed: ${failed.join(", ")}`); } return report; } export interface BuildInputHashResult { buildInputHash: string; workpieceTreeHash: string; platformVersion: string; platformSemanticHash: string; } /** * Computes the build input hash for a workpiece directory. * Centralizes the content-tree-hash + platform-version + platform-semantic-hash * computation used by mission.build (to write build-input-hash.json) and * release.prepare (to decide whether to reuse a distribution). * Returns intermediate values so release.prepare can populate the release manifest. */ export async function computeBuildInputHash( workspaceRoot: string, workpieceDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const { version: platformVersion } = await resolveCurrentEcosystem(workspaceRoot); const platformSemanticHash = await resolvePlatformSemanticHash(workspaceRoot); const contentDir = path.join(workpieceDir, "src", "content"); let workpieceTreeHash = "sha256:absent"; if (await io.exists(contentDir)) { const contentResult = await fingerprintTree(contentDir, { mode: "semantic" }); workpieceTreeHash = contentResult.value; } const buildInputHash = byteHash( `${workpieceTreeHash}|${platformVersion}|${platformSemanticHash}`, ); return { buildInputHash, workpieceTreeHash, platformVersion, platformSemanticHash }; } export interface PreliminaryBuildIdentityFields { releaseId: string; systemId: string; missionId: string; semver: string; } /** * Writes a preliminary build-identity.json to workpiece/public/.well-known/ before * astro build so the open-source-registry-section.astro component can read deployment * metadata (releaseId, buildTimestamp, commitSha) at build time. Without this file, * the component falls back to placeholder values ("—") which text.normalize.apply * later converts to "-" in the prerendered HTML. * * Returns the file path for later cleanup via cleanupPreliminaryBuildIdentity. */ export async function writePreliminaryBuildIdentity( workpieceDir: string, fields: PreliminaryBuildIdentityFields, logger: { info: (msg: string) => void }, io: WorkspaceIO = createDefaultIO().io, ): Promise { let commitSha = "0000000"; try { const revResult = await io.exec("sh", ["-c", "git rev-parse HEAD"], { cwd: workpieceDir, }); if (revResult.exitCode !== 0) { throw new Error(`git rev-parse exited ${revResult.exitCode}`); } commitSha = revResult.stdout.trim(); } catch { // Workpiece may not be a git repo — use placeholder } const publicWellKnownDir = path.join(workpieceDir, "public", ".well-known"); await io.mkdir(publicWellKnownDir); const preliminary = { releaseId: fields.releaseId, systemId: fields.systemId, missionId: fields.missionId, semver: fields.semver, distTreeHash: "", behaviorSnapshotHash: "", siteContentHash: "", platformVersion: "", platformSemanticHash: "", commitSha, buildTimestamp: new Date().toISOString(), targetPlatform: "cloudflare-workers", }; const filePath = path.join(publicWellKnownDir, "build-identity.json"); await atomicWriteFile(filePath, JSON.stringify(preliminary, null, 2) + "\n"); logger.info(` Wrote preliminary build-identity.json to workpiece/public/.well-known/`); return filePath; } /** * Removes the preliminary build-identity.json from workpiece/public/.well-known/ * after build.post completes. The copy in dist/client/.well-known/ is left intact — * release.prepare overwrites it with the final build-identity.json. */ export async function cleanupPreliminaryBuildIdentity( filePath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { await io.rm(filePath, { recursive: true }); }