import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const arg = (name) => { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; }; const staged = path.resolve(root, arg("--staged-root") ?? "tests/golden/corrective/C0-002/staged"); const reportPath = path.resolve(root, arg("--report") ?? "tests/golden/corrective/C0-002/resign-check.json"); const read = (file) => fs.readFileSync(file, "utf8"); const json = (file) => JSON.parse(read(file)); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); const check = []; const ok = (name, value, detail = {}) => { assert.ok(value, name); check.push({ name, status: "PASS", ...detail }); }; const registryPath = path.join(staged, "completed-gap-tasks.candidate.json"); const planPath = path.join(staged, "next-task-plan.candidate.json"); const indexPath = path.join(staged, "task-index", "task-index.json"); const catalogPath = path.join(staged, "task-index", "task-catalog.jsonl"); const snapshotPath = path.join(staged, "source-snapshot", "snapshot-manifest.json"); const registry = json(registryPath); const plan = json(planPath); const index = json(indexPath); const snapshot = json(snapshotPath); ok("registry-count", registry.length === 273, { value: registry.length }); ok("registry-unique", new Set(registry).size === registry.length); ok("plan-shape", plan.tasks.length === 6900 && plan.summary.completed === 273 && plan.summary.active === 1 && plan.firstTask === "M16-GAP-00274"); ok("plan-source-hash", plan.sources.completions.sha256 === sha256(registryPath)); ok("index-plan-hash", index.source.sha256 === sha256(planPath)); ok("index-catalog-hash", index.catalog.sha256 === sha256(catalogPath)); ok("index-active", index.activeTask === plan.firstTask); ok("snapshot-nonempty", snapshot.files.length > 0, { value: snapshot.files.length }); const snapshotByPath = new Map(); for (const file of snapshot.files) { const source = path.join(root, file.path); const copy = path.join(root, file.snapshotPath); assert.equal(sha256(source), file.sha256, file.path); assert.equal(sha256(copy), file.sha256, file.snapshotPath); snapshotByPath.set(file.path, file.sha256); } check.push({ name: "snapshot-hashes", status: "PASS", files: snapshot.files.length, bytes: snapshot.files.reduce((sum, file) => sum + file.bytes, 0) }); const manifestRoot = path.join(staged, "resigned-manifests"); const manifestTasks = fs.readdirSync(manifestRoot).filter((entry) => /^M(?:15|16)-/u.test(entry)).sort(); const m16Tasks = manifestTasks.filter((entry) => /^M16-GAP-\d{5}$/u.test(entry)); ok("resigned-manifest-count", m16Tasks.length === 274, { value: m16Tasks.length }); let doneCount = 0; for (const task of m16Tasks) { const manifest = json(path.join(manifestRoot, task, "manifest.json")); if (manifest.status === "done") doneCount += 1; for (const artifact of Object.values(manifest.artifacts ?? {})) { if (!artifact?.path || !artifact?.sha256) continue; assert.equal(snapshotByPath.get(artifact.path), artifact.sha256, `${task}:${artifact.path}`); } } ok("resigned-manifest-done-count", doneCount === 273, { value: doneCount }); const rootParent = json(path.join(manifestRoot, "M15-03E", "manifest.json")); ok("root-parent-repaired", rootParent.nextTask === "M16-GAP-00001"); const contextRoot = path.join(staged, "resigned-context"); const contexts = fs.readdirSync(contextRoot).filter((entry) => /^M16-GAP-\d{5}$/u.test(entry)).sort(); ok("repaired-context-count", contexts.length === 24, { value: contexts.length }); for (const task of contexts) { const context = json(path.join(contextRoot, task, "task-context.json")); assert.equal(context.task, task); assert.equal(context.parentTask, json(path.join(manifestRoot, task, "manifest.json")).parentTask); const parent = json(path.join(manifestRoot, context.parentTask, "manifest.json")); assert.equal(parent.nextTask, task, `${task}:parent-nextTask`); } check.push({ name: "repaired-context-links", status: "PASS", files: contexts.length }); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "corrective-resign-check-")); const regenerated = path.join(tempRoot, "plan.json"); try { execFileSync(process.execPath, [path.join(root, "tools/web/generate-blender-next-task-plan.mjs"), regenerated, "--completion-path", registryPath], { cwd: root, stdio: "ignore" }); ok("candidate-plan-deterministic", fs.readFileSync(regenerated).equals(fs.readFileSync(planPath))); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } const result = { schemaVersion: 1, operation: "CORRECTIVE_STATE_RECONCILIATION_RESIGN_CHECK", status: "PASS", queueMutation: false, stagedRoot: relative(staged), checks: check, artifactCount: snapshot.files.length, planTaskCount: plan.tasks.length, completedCount: doneCount, activeTask: plan.firstTask }; fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`); process.stdout.write(`corrective-resign-check-ok files=${snapshot.files.length} manifests=${m16Tasks.length} contexts=${contexts.length} active=${plan.firstTask}\n`);