Final phase. Adds:
scripts/distillation/release_freeze.ts ~330 lines, 6 release gates
docs/distillation/operator-handoff.md durable cold-start operator doc
docs/distillation/recovery-runbook.md failure-mode runbook by symptom
scripts/distillation/distill.ts +release-freeze subcommand
The release_freeze orchestrator runs every gate the system has:
1. Clean git state (tolerates auto-regenerated reports)
2. Full test suite (bun test tests/distillation auditor/schemas/distillation)
3. Phase commit verification (every Phase 0-8 commit resolves)
4. Acceptance gate (22-invariant fixture E2E)
5. audit-full (Phases 0-7 verified + drift detection)
6. Tag availability check (distillation-v1.0.0 not yet existing)
Outputs:
reports/distillation/release-freeze.md human-readable manifest
reports/distillation/release-manifest.json machine-readable manifest
Manifest captures:
- git_head + git_branch + released_at
- phase→commit map for all 9 commits (Phase 0+1+2 scaffold through Phase 8 audit)
- dataset counts at freeze (RAG/SFT/Preference/evidence/scored/quarantined)
- latest audit baseline row
- per-gate pass/fail with detail
Operator handoff doc covers:
- phase map with commits + report locations
- known-good commands
- how to rerun audit-full + inspect drift
- how to restore from last-good (git checkout distillation-v1.0.0)
- how to add future phases without contaminating corpus
- what NOT to modify casually (with file:reason mapping)
- cumulative commits at v1.0.0
Recovery runbook covers, by symptom:
- audit-full exit non-zero (per-phase diagnostics)
- drift table flags warn (intentional vs regression)
- acceptance fail vs audit-full pass divergence
- run-all empty exports (counter-bisection order)
- hash mismatch on identical input (determinism violation; CRITICAL)
- replay logs growing unbounded (rotation guidance)
- nuclear restore via git checkout distillation-v1.0.0
Spec constraints (per now.md Phase 9):
- DO NOT add new intelligence features ✓ (zero new logic)
- DO NOT change scoring/export logic ✓ (zero touches)
- DO NOT weaken gates ✓ (gates only added, never relaxed beyond the
auto-regen tolerance documented in checkCleanGit)
- DO NOT retrain anything ✓ (no model touches)
CLI:
./scripts/distill release-freeze # exit 0 = release-ready
Tag creation deferred to operator confirmation (the release-freeze
report prints the exact `git tag` command). Per CLAUDE.md guidance,
destructive/visible operations like tags require explicit user
authorization.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
363 lines
14 KiB
TypeScript
363 lines
14 KiB
TypeScript
// release_freeze.ts — Phase 9 final orchestrator. Runs every gate the
|
|
// distillation system has + writes a release manifest + verifies clean
|
|
// git state. Never creates the git tag itself — prints the command for
|
|
// J to authorize.
|
|
//
|
|
// USAGE
|
|
// bun run scripts/distillation/release_freeze.ts
|
|
//
|
|
// Exit code 0 = release-ready. Non-zero = one or more gates failed.
|
|
|
|
import {
|
|
existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync,
|
|
} from "node:fs";
|
|
import { resolve, dirname } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const DEFAULT_ROOT = process.env.LH_DISTILL_ROOT ?? "/home/profit/lakehouse";
|
|
const VERSION = "v1.0.0";
|
|
const TAG = `distillation-${VERSION}`;
|
|
|
|
// Phase → known commit. Sourced from git log; if a commit gets
|
|
// rewritten the manifest will surface the mismatch.
|
|
const PHASE_COMMITS: Array<{ phase: string; commit: string; subject: string }> = [
|
|
{ phase: "0+1+2 scaffold", commit: "27b1d27", subject: "distillation: Phase 0 recon + Phase 1 schemas + Phase 2 transforms scaffold" },
|
|
{ phase: "2 materializer", commit: "1ea8029", subject: "distillation: Phase 2 — Evidence View materializer + health audit" },
|
|
{ phase: "3 scorer", commit: "c989253", subject: "distillation: Phase 3 — deterministic Success Scorer" },
|
|
{ phase: "4 exports", commit: "68b6697", subject: "distillation: Phase 4 — dataset export layer" },
|
|
{ phase: "5 receipts", commit: "2cf359a", subject: "distillation: Phase 5 — receipts harness (system-level observability)" },
|
|
{ phase: "6 acceptance", commit: "1b433a9", subject: "distillation: Phase 6 — acceptance gate suite" },
|
|
{ phase: "auditor rebuild", commit: "20a039c", subject: "auditor: rebuild on mode runner + drop tree-split (use distillation substrate)" },
|
|
{ phase: "7 replay", commit: "681f39d", subject: "distillation: Phase 7 — replay-driven local model bootstrapping" },
|
|
{ phase: "8 audit-full", commit: "5bdd159", subject: "distillation: Phase 8 — full system audit" },
|
|
];
|
|
|
|
interface GateResult {
|
|
name: string;
|
|
passed: boolean;
|
|
detail: string;
|
|
}
|
|
|
|
const gates: GateResult[] = [];
|
|
function gate(name: string, passed: boolean, detail: string) {
|
|
gates.push({ name, passed, detail });
|
|
}
|
|
|
|
function shell(cmd: string, args: string[], cwd = DEFAULT_ROOT, timeoutMs = 600_000) {
|
|
return spawnSync(cmd, args, { cwd, encoding: "utf8", timeout: timeoutMs, env: { ...process.env, LH_DISTILL_ROOT: cwd } });
|
|
}
|
|
|
|
function gitOutput(args: string[]): string {
|
|
const r = spawnSync("git", ["-C", DEFAULT_ROOT, ...args], { encoding: "utf8" });
|
|
return r.status === 0 ? r.stdout.trim() : "";
|
|
}
|
|
|
|
// ─── Gate 1: clean git state ──────────────────────────────────────
|
|
|
|
function checkCleanGit() {
|
|
const status = gitOutput(["status", "--porcelain"]);
|
|
// Tolerate two classes of dirty:
|
|
// 1. Untracked artifacts (data/, exports/, /tmp/, reports/distillation/<run_id>/)
|
|
// 2. Auto-regenerated reports under reports/distillation/phase*-*.md +
|
|
// reports/distillation/release-*.{md,json} — release-freeze itself
|
|
// rewrites these before it can check the gate
|
|
const lines = status.split("\n").filter(Boolean);
|
|
const tracked = lines.filter(l => /^\s*[MADRCU]/.test(l));
|
|
const concerning = tracked.filter(l => {
|
|
// git status --porcelain format: "XY <path>" where XY is 2-char status
|
|
const m = l.match(/^[\sMADRCU]{2}\s+(.+?)$/);
|
|
const path = m ? m[1] : l.replace(/^\s*[MADRCU]+\s*/, "");
|
|
if (/^reports\/distillation\/phase\d+-.*\.md$/.test(path)) return false;
|
|
if (/^reports\/distillation\/release-.*\.(md|json)$/.test(path)) return false;
|
|
return true;
|
|
});
|
|
const passed = concerning.length === 0;
|
|
gate(
|
|
"clean git state (no source-tree modifications)",
|
|
passed,
|
|
passed
|
|
? `tree clean (${tracked.length - concerning.length} auto-regenerated reports tolerated)`
|
|
: `${concerning.length} concerning modified file(s):\n${concerning.slice(0, 6).map(l => " " + l).join("\n")}`,
|
|
);
|
|
}
|
|
|
|
// ─── Gate 2: full test suite ──────────────────────────────────────
|
|
|
|
function checkTests() {
|
|
const r = shell("bun", ["test", "tests/distillation/", "auditor/schemas/distillation/"]);
|
|
const out = (r.stdout ?? "") + (r.stderr ?? "");
|
|
const m = out.match(/(\d+)\s*pass\s*\n\s*(\d+)\s*fail/);
|
|
const pass = m ? Number(m[1]) : 0;
|
|
const fail = m ? Number(m[2]) : 1;
|
|
gate(
|
|
`full test suite (bun test tests/distillation/ auditor/schemas/distillation/)`,
|
|
r.status === 0 && fail === 0,
|
|
`${pass} pass, ${fail} fail (exit=${r.status})`,
|
|
);
|
|
}
|
|
|
|
// ─── Gate 3: acceptance gate ──────────────────────────────────────
|
|
|
|
function checkAcceptance() {
|
|
const r = shell("bun", ["run", "scripts/distillation/acceptance.ts"]);
|
|
const out = (r.stdout ?? "") + (r.stderr ?? "");
|
|
const m = out.match(/PASS\s*—\s*(\d+)\/(\d+)/);
|
|
const passed = r.status === 0 && m && m[1] === m[2];
|
|
gate(
|
|
"acceptance gate (22-invariant fixture E2E)",
|
|
!!passed,
|
|
m ? `${m[1]}/${m[2]} invariants` : `exit=${r.status}, no PASS line found`,
|
|
);
|
|
}
|
|
|
|
// ─── Gate 4: full audit ───────────────────────────────────────────
|
|
|
|
function checkAuditFull() {
|
|
const r = shell("bun", ["run", "scripts/distillation/audit_full.ts"]);
|
|
const out = (r.stdout ?? "") + (r.stderr ?? "");
|
|
const m = out.match(/PASS\s*—\s*(\d+)\/(\d+)\s*required/);
|
|
const passed = r.status === 0 && m && m[1] === m[2];
|
|
gate(
|
|
"audit-full (Phases 0-7 verified + drift)",
|
|
!!passed,
|
|
m ? `${m[1]}/${m[2]} required checks` : `exit=${r.status}`,
|
|
);
|
|
}
|
|
|
|
// ─── Gate 5: tag does not yet exist ──────────────────────────────
|
|
|
|
function checkTagAvailable() {
|
|
const tags = gitOutput(["tag", "-l", TAG]);
|
|
const exists = tags.trim() === TAG;
|
|
gate(
|
|
`tag ${TAG} available (does not yet exist)`,
|
|
!exists,
|
|
exists ? `tag already exists; bump VERSION or delete the prior tag` : "tag name is free",
|
|
);
|
|
}
|
|
|
|
// ─── Gather dataset/export counts ────────────────────────────────
|
|
|
|
interface DatasetCounts {
|
|
rag_rows: number;
|
|
sft_rows: number;
|
|
preference_pairs: number;
|
|
evidence_files: number;
|
|
evidence_rows: number;
|
|
scored_files: number;
|
|
scored_rows: number;
|
|
quarantined_total: number;
|
|
}
|
|
|
|
function countLines(path: string): number {
|
|
if (!existsSync(path)) return 0;
|
|
return readFileSync(path, "utf8").split("\n").filter(Boolean).length;
|
|
}
|
|
|
|
function walkCount(dir: string): { files: number; rows: number } {
|
|
if (!existsSync(dir)) return { files: 0, rows: 0 };
|
|
let files = 0, rows = 0;
|
|
function walk(p: string) {
|
|
for (const e of readdirSync(p)) {
|
|
const full = resolve(p, e);
|
|
const st = statSync(full);
|
|
if (st.isDirectory()) walk(full);
|
|
else if (e.endsWith(".jsonl")) { files++; rows += countLines(full); }
|
|
}
|
|
}
|
|
walk(dir);
|
|
return { files, rows };
|
|
}
|
|
|
|
function gatherCounts(root: string): DatasetCounts {
|
|
const ev = walkCount(resolve(root, "data/evidence"));
|
|
const sc = walkCount(resolve(root, "data/scored-runs"));
|
|
return {
|
|
rag_rows: countLines(resolve(root, "exports/rag/playbooks.jsonl")),
|
|
sft_rows: countLines(resolve(root, "exports/sft/instruction_response.jsonl")),
|
|
preference_pairs: countLines(resolve(root, "exports/preference/chosen_rejected.jsonl")),
|
|
evidence_files: ev.files, evidence_rows: ev.rows,
|
|
scored_files: sc.files, scored_rows: sc.rows,
|
|
quarantined_total: ["sft", "rag", "preference"]
|
|
.reduce((acc, n) => acc + countLines(resolve(root, `exports/quarantine/${n}.jsonl`)), 0),
|
|
};
|
|
}
|
|
|
|
// ─── Gather latest baseline ──────────────────────────────────────
|
|
|
|
function loadLatestBaseline(root: string): any {
|
|
const p = resolve(root, "data/_kb/audit_baselines.jsonl");
|
|
if (!existsSync(p)) return null;
|
|
const lines = readFileSync(p, "utf8").split("\n").filter(Boolean);
|
|
if (lines.length === 0) return null;
|
|
try { return JSON.parse(lines[lines.length - 1]); } catch { return null; }
|
|
}
|
|
|
|
// ─── Verify phase commits actually exist ─────────────────────────
|
|
|
|
function verifyPhaseCommits() {
|
|
const missing: string[] = [];
|
|
for (const p of PHASE_COMMITS) {
|
|
const full = gitOutput(["rev-parse", p.commit]);
|
|
if (!full || full.length < 40) missing.push(`${p.phase} (${p.commit})`);
|
|
}
|
|
gate(
|
|
"every phase commit resolves",
|
|
missing.length === 0,
|
|
missing.length === 0 ? `${PHASE_COMMITS.length}/${PHASE_COMMITS.length} commits verified` : `missing: ${missing.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
// ─── Build manifest + report ─────────────────────────────────────
|
|
|
|
interface Manifest {
|
|
schema: "distillation_release_manifest.v1";
|
|
version: string;
|
|
tag: string;
|
|
released_at: string;
|
|
git_head: string;
|
|
git_branch: string;
|
|
phase_commits: typeof PHASE_COMMITS;
|
|
dataset_counts: DatasetCounts;
|
|
latest_baseline: any;
|
|
gates: GateResult[];
|
|
passed: boolean;
|
|
}
|
|
|
|
function renderReport(m: Manifest): string {
|
|
const md: string[] = [];
|
|
md.push("# Distillation Release Freeze — " + m.version);
|
|
md.push("");
|
|
md.push(`**Tag (proposed):** \`${m.tag}\``);
|
|
md.push(`**Released at:** ${m.released_at}`);
|
|
md.push(`**Git head:** \`${m.git_head}\``);
|
|
md.push(`**Branch:** ${m.git_branch}`);
|
|
md.push("");
|
|
md.push(`## Result: ${m.passed ? "**RELEASE-READY** ✓" : "**NOT READY ✗** — one or more gates failed"}`);
|
|
md.push("");
|
|
|
|
md.push("## Gates");
|
|
md.push("");
|
|
md.push("| # | Gate | Status | Detail |");
|
|
md.push("|---|---|---|---|");
|
|
for (let i = 0; i < m.gates.length; i++) {
|
|
const g = m.gates[i];
|
|
md.push(`| ${i + 1} | ${g.name} | ${g.passed ? "✓" : "✗ FAIL"} | ${g.detail.split("\n")[0].slice(0, 100)} |`);
|
|
}
|
|
md.push("");
|
|
|
|
md.push("## Phase commits");
|
|
md.push("");
|
|
md.push("| Phase | Commit | Subject |");
|
|
md.push("|---|---|---|");
|
|
for (const p of m.phase_commits) {
|
|
md.push(`| ${p.phase} | \`${p.commit}\` | ${p.subject} |`);
|
|
}
|
|
md.push("");
|
|
|
|
md.push("## Dataset counts at freeze");
|
|
md.push("");
|
|
md.push("| Artifact | Count |");
|
|
md.push("|---|---|");
|
|
md.push(`| RAG rows | ${m.dataset_counts.rag_rows} |`);
|
|
md.push(`| SFT rows (strict accepted-only) | ${m.dataset_counts.sft_rows} |`);
|
|
md.push(`| Preference pairs | ${m.dataset_counts.preference_pairs} |`);
|
|
md.push(`| Evidence files | ${m.dataset_counts.evidence_files} |`);
|
|
md.push(`| Evidence rows | ${m.dataset_counts.evidence_rows} |`);
|
|
md.push(`| Scored-run files | ${m.dataset_counts.scored_files} |`);
|
|
md.push(`| Scored rows | ${m.dataset_counts.scored_rows} |`);
|
|
md.push(`| Quarantined total | ${m.dataset_counts.quarantined_total} |`);
|
|
md.push("");
|
|
|
|
if (m.latest_baseline) {
|
|
md.push("## Latest audit baseline");
|
|
md.push("");
|
|
md.push("```json");
|
|
md.push(JSON.stringify(m.latest_baseline, null, 2));
|
|
md.push("```");
|
|
md.push("");
|
|
}
|
|
|
|
md.push("## Tag command (run after release-ready confirmation)");
|
|
md.push("");
|
|
md.push("```bash");
|
|
md.push(`git tag -a ${m.tag} ${m.git_head.slice(0, 12)} -m "distillation v${m.version.replace(/^v/, "")} — 8-phase substrate frozen"`);
|
|
md.push(`git push origin ${m.tag}`);
|
|
md.push("```");
|
|
md.push("");
|
|
|
|
md.push("## Failure detail");
|
|
md.push("");
|
|
const failed = m.gates.filter(g => !g.passed);
|
|
if (failed.length === 0) {
|
|
md.push("(no failures)");
|
|
} else {
|
|
for (const g of failed) {
|
|
md.push(`### ${g.name}`);
|
|
md.push("");
|
|
md.push("```");
|
|
md.push(g.detail);
|
|
md.push("```");
|
|
md.push("");
|
|
}
|
|
}
|
|
|
|
return md.join("\n");
|
|
}
|
|
|
|
async function main() {
|
|
const root = DEFAULT_ROOT;
|
|
console.log("[release-freeze] running gates...");
|
|
|
|
checkCleanGit();
|
|
checkTests();
|
|
verifyPhaseCommits();
|
|
checkAcceptance();
|
|
checkAuditFull();
|
|
checkTagAvailable();
|
|
|
|
const counts = gatherCounts(root);
|
|
const baseline = loadLatestBaseline(root);
|
|
const manifest: Manifest = {
|
|
schema: "distillation_release_manifest.v1",
|
|
version: VERSION,
|
|
tag: TAG,
|
|
released_at: new Date().toISOString(),
|
|
git_head: gitOutput(["rev-parse", "HEAD"]),
|
|
git_branch: gitOutput(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
phase_commits: PHASE_COMMITS,
|
|
dataset_counts: counts,
|
|
latest_baseline: baseline,
|
|
gates,
|
|
passed: gates.every(g => g.passed),
|
|
};
|
|
|
|
const reportPath = resolve(root, "reports/distillation/release-freeze.md");
|
|
mkdirSync(dirname(reportPath), { recursive: true });
|
|
writeFileSync(reportPath, renderReport(manifest));
|
|
|
|
// Also persist the manifest JSON for machines.
|
|
const manifestPath = resolve(root, "reports/distillation/release-manifest.json");
|
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
|
|
console.log("");
|
|
console.log(`[release-freeze] ${manifest.passed ? "RELEASE-READY" : "NOT READY"} — ${gates.filter(g => g.passed).length}/${gates.length} gates passed`);
|
|
for (const g of gates) {
|
|
console.log(` ${g.passed ? "✓" : "✗"} ${g.name}`);
|
|
if (!g.passed) console.log(` ${g.detail.split("\n").slice(0, 2).join(" | ")}`);
|
|
}
|
|
console.log("");
|
|
console.log(`[release-freeze] manifest: ${manifestPath}`);
|
|
console.log(`[release-freeze] report: ${reportPath}`);
|
|
if (manifest.passed) {
|
|
console.log("");
|
|
console.log("To create the tag (manual step — operator must confirm):");
|
|
console.log(` git tag -a ${TAG} -m "distillation v${VERSION.replace(/^v/, "")} — 8-phase substrate frozen"`);
|
|
console.log(` git push origin ${TAG}`);
|
|
}
|
|
|
|
process.exit(manifest.passed ? 0 : 1);
|
|
}
|
|
|
|
if (import.meta.main) main().catch(e => { console.error(e); process.exit(1); });
|