/*
RFC-0478: platform.consistency.validate — semantic hash drift guard and version bump enforcement.
- Does not bump package.json version — the operator does that in a separate commit.
- Does not validate migrator existence — that is RFC-0479's scope.
- RFC-0533: add PC-04 rule — CI-side check for X-Platform-Bump trailers on platform-scope commits.
- RFC-1159: step 4 — exec.exitcode.lint command, sweep remainder, packages-check wiring
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1249: service boot tests — import-based worker entry checks, cloudflare-workers stub, PC-04 cutoff advance
- RFC-1249: advance PC-04 cutoff past RFC-1252 publish-cascade bump commits
RFC-0478
*/
import path from "node:path";
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { resolvePlatformSemanticHash } from "./bundle-io.ts";
import { hasPlatformScopeFiles, hasTrailer } from "./platform-scope.ts";
import { compareSemver } from "./semver.ts";
import {
createDefaultIO,
execOrThrow,
type WorkspaceIO,
} from "@warpgogol/werkstatt-shared/kernel/workspace-io";
export interface PlatformConsistencyData {
currentHash: string;
lastHash: string | null;
currentVersion: string;
lastVersion: string | null;
driftDetected: boolean;
validatedAt: string;
violations?: PlatformConsistencyViolation[];
}
export interface PlatformConsistencyViolation {
rule: "PC-01" | "PC-02" | "PC-03" | "PC-04";
message: string;
severity: "error" | "warning";
}
interface PlatformVersionLogEntry {
hash: string;
version: string;
validatedAt: string;
}
function flagBoolean(input: KernelCommandInput, key: string): boolean {
const v = input.flags[key];
return typeof v === "boolean" ? v : false;
}
interface RfcVersionBumpMatch {
versionBump: string | undefined;
updatedAt: string;
}
async function findRfcsWithVersionBump(
rfcDir: string,
since: string,
predicate: (vb: string) => boolean,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const results: RfcVersionBumpMatch[] = [];
try {
const rfcFiles = await io.readdir(rfcDir);
for (const file of rfcFiles) {
if (!file.name.endsWith(".md") || file.name.startsWith("rfc-0000")) continue;
const filePath = path.join(rfcDir, file.name);
const content = await io.readFile(filePath);
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) continue;
const fm = yamlParse(fmMatch[1]) as Record;
const vb = fm["versionBump"];
const updatedAt = String(fm["updatedAt"] ?? "");
if (typeof vb === "string" && predicate(vb) && updatedAt >= since) {
results.push({ versionBump: vb, updatedAt });
}
}
} catch {
// RFC dir not readable — return empty
}
return results;
}
/**
* RFC-0533 PC-04: commits at-or-before this SHA are exempt — the rule is not
* retroactive. Set to the last commit before RFC-1249's platform-scope wave:
* the imported placeholder SHA (940c025cc) never existed in this repository's
* history, so the check silently scanned the whole rev-list. All pre-existing
* commits are grandfathered; platform-scope commits after this SHA must carry
* the X-Platform-Bump trailer via ecosystem.commit.
*
* Advanced to 8e22bf6ef (2026-10-10): two post-cutoff RFC-1252 commits used
* plain `git commit` on platform-scope files — 5e3a0c74a (forge version bump)
* and d0a8a1543 (packages/AGENTS.md doc bullet). Trailers cannot be added
* retroactively without history rewrite, so they are grandfathered here;
* platform-scope commits after this SHA must carry the trailer.
*/
export const PC_04_CUTOFF_SHA = "8e22bf6ef";
interface GitLogCommit {
sha: string;
parents: string[];
message: string;
files: string[];
}
async function getGitLogSince(
workspaceRoot: string,
sinceSha: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
try {
// If cutoff is not a real SHA or doesn't exist in this repo, use the root commit
let effectiveSince = sinceSha;
if (sinceSha === "0000000" || sinceSha.length < 7) {
try {
const rootResult = await io.exec("git", ["rev-list", "--max-parents=0", "HEAD"], {
cwd: workspaceRoot,
});
if (rootResult.exitCode !== 0) {
throw new Error(`git rev-list exited ${rootResult.exitCode}`);
}
effectiveSince = rootResult.stdout.trim();
} catch {
return [];
}
} else {
// Verify the SHA exists in this repo
try {
const catResult = await io.exec("git", ["cat-file", "-e", sinceSha], {
cwd: workspaceRoot,
});
if (catResult.exitCode !== 0) {
throw new Error(`git cat-file exited ${catResult.exitCode}`);
}
} catch {
// SHA doesn't exist — fall back to root commit
try {
const rootResult = await io.exec("git", ["rev-list", "--max-parents=0", "HEAD"], {
cwd: workspaceRoot,
});
if (rootResult.exitCode !== 0) {
throw new Error(`git rev-list exited ${rootResult.exitCode}`);
}
effectiveSince = rootResult.stdout.trim();
} catch {
return [];
}
}
}
const { stdout } = await execOrThrow(
io,
"git",
["log", `${effectiveSince}..HEAD`, "--format=__COMMIT__%H%n%P%n%B%n__FILES__", "--name-only"],
{ cwd: workspaceRoot },
);
const commits: GitLogCommit[] = [];
const blocks = stdout.split("__COMMIT__");
for (const block of blocks) {
const trimmed = block.trim();
if (!trimmed) continue;
const lines = trimmed.split("\n");
const sha = lines[0]?.trim();
if (!sha) continue;
const parentsStr = lines[1]?.trim() ?? "";
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
const filesStartIdx = lines.indexOf("__FILES__");
const messageEnd = filesStartIdx >= 0 ? filesStartIdx : lines.length;
const message = lines.slice(2, messageEnd).join("\n").trim();
const files =
filesStartIdx >= 0 ? lines.slice(filesStartIdx + 1).filter((l) => l.trim().length > 0) : [];
commits.push({ sha, parents, message, files });
}
return commits;
} catch {
return [];
}
}
async function checkPc04(workspaceRoot: string): Promise {
const violations: PlatformConsistencyViolation[] = [];
const commits = await getGitLogSince(workspaceRoot, PC_04_CUTOFF_SHA);
for (const commit of commits) {
// Skip merge commits (2+ parents)
if (commit.parents.length >= 2) continue;
// Skip commits with no platform-scope files
if (!hasPlatformScopeFiles(commit.files)) continue;
// Check for X-Platform-Bump trailer
if (!hasTrailer(commit.message, "X-Platform-Bump")) {
violations.push({
rule: "PC-04",
severity: "error",
message: `Commit ${commit.sha.slice(0, 7)} touches platform scope but lacks X-Platform-Bump trailer. Use ecosystem.commit for platform-scope changes (RFC-0533).`,
});
}
}
return violations;
}
export async function runPlatformConsistencyValidate(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const checkOnly = flagBoolean(input, "check");
const logPath = path.join(workspaceRoot, "docs", "platform-version-log.generated.yaml");
const packageJsonPath = path.join(workspaceRoot, "package.json");
const currentHash = await resolvePlatformSemanticHash(workspaceRoot);
let packageJson: { version?: string };
try {
packageJson = JSON.parse(await context.io.readFile(packageJsonPath)) as { version?: string };
} catch {
throw new Error("[platform.consistency.validate] Could not read root package.json");
}
const currentVersion = packageJson.version ?? "0.0.0";
let lastLog: PlatformVersionLogEntry | null = null;
try {
const logContent = await context.io.readFile(logPath);
lastLog = yamlParse(logContent) as PlatformVersionLogEntry;
} catch {
// first run — no log file
}
const lastHash = lastLog?.hash ?? null;
const lastVersion = lastLog?.version ?? null;
const validatedAt = new Date().toISOString();
const violations: PlatformConsistencyViolation[] = [];
const hashChanged = lastHash !== null && lastHash !== currentHash;
const versionChanged = lastVersion !== null && compareSemver(currentVersion, lastVersion) !== 0;
// PC-01: hash changed but version did not
if (hashChanged && !versionChanged) {
violations.push({
rule: "PC-01",
severity: "error",
message: `platformSemanticHash changed (${lastHash} → ${currentHash}) but package.json version did not (${currentVersion}). Bump the version or declare versionBump: none in the relevant RFC.`,
});
}
// PC-02: version bumped but no corresponding RFC with versionBump found
if (versionChanged && lastVersion !== null) {
const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
const since = lastLog?.validatedAt ?? "";
const bumpRfcs = await findRfcsWithVersionBump(
rfcDir,
since,
(vb) => vb === "minor" || vb === "patch",
);
if (bumpRfcs.length === 0) {
violations.push({
rule: "PC-02",
severity: "warning",
message: `package.json version bumped (${lastVersion} → ${currentVersion}) but no RFC with versionBump: minor|patch and updatedAt >= last validation was found.`,
});
}
}
// PC-03: versionBump: minor RFC merged but minor version was not bumped
if (lastVersion !== null) {
const minorBumped =
compareSemver(currentVersion, lastVersion) >= 0 &&
currentVersion.split(".")[1] !== lastVersion.split(".")[1];
if (!minorBumped && versionChanged) {
const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
const since = lastLog?.validatedAt ?? "";
const minorRfcs = await findRfcsWithVersionBump(rfcDir, since, (vb) => vb === "minor");
if (minorRfcs.length > 0) {
violations.push({
rule: "PC-03",
severity: "error",
message: `An RFC with versionBump: minor was merged but the minor version was not bumped (${lastVersion} → ${currentVersion}).`,
});
}
}
}
// PC-04 (RFC-0533): commits touching platform scope after cutoff must carry X-Platform-Bump trailer
const pc04Violations = await checkPc04(workspaceRoot);
violations.push(...pc04Violations);
const hasErrors = violations.some((v) => v.severity === "error");
const driftDetected = hashChanged && !versionChanged;
// Write log on success (unless --check mode). Covers both first-run and subsequent runs.
if (!hasErrors && !checkOnly) {
const logEntry: PlatformVersionLogEntry = {
hash: currentHash,
version: currentVersion,
validatedAt,
};
const logYaml = yamlStringify(logEntry);
await context.io.mkdir(path.dirname(logPath));
await context.io.writeFile(logPath, logYaml);
}
const data: PlatformConsistencyData = {
currentHash,
lastHash,
currentVersion,
lastVersion,
driftDetected,
validatedAt,
violations,
};
return {
data,
exitCode: hasErrors ? 1 : 0,
summary: hasErrors
? `[platform.consistency] Platform consistency check failed with ${violations.filter((v) => v.severity === "error").length} error${violations.filter((v) => v.severity === "error").length === 1 ? "" : "s"}`
: "[platform.consistency] Platform consistency check passed",
};
}