diff --git a/package.json b/package.json index ef5193a..0d5670a 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,9 @@ "test:wp7-03:red": "node scripts/run-wp7-03-validation.mjs --phase red", "test:wp7-04": "node scripts/run-wp7-04-validation.mjs", "test:wp7-05": "node scripts/run-wp7-05-validation.mjs", - "test:wp7-05:unit": "node --test tests/package/wp7-05-ui-gate.test.mjs tests/package/wp7-05-coverage.test.mjs" + "test:wp7-05:unit": "node --test tests/package/wp7-05-ui-gate.test.mjs tests/package/wp7-05-coverage.test.mjs", + "test:wp7-06": "node scripts/run-wp7-06-validation.mjs", + "test:wp7-06:unit": "node --test tests/package/wp7-06-prefreeze.test.mjs" }, "devDependencies": { "@playwright/test": "1.62.0", diff --git a/scripts/lib/wp7-06-prefreeze.mjs b/scripts/lib/wp7-06-prefreeze.mjs new file mode 100644 index 0000000..db8d3bb --- /dev/null +++ b/scripts/lib/wp7-06-prefreeze.mjs @@ -0,0 +1,62 @@ +const EXPECTED_TRACE_SUMMARY = Object.freeze({ + acceptanceCriteria: 52, + errorCategories: 9, + featureModules: 13, + parentFamilies: 89, + penProductFrames: 18, + productContracts: 19, + requirements: 109, + tasks: 52, + testCases: 117, + uiPages: 22, +}); + +const REQUIRED_UPSTREAM = Object.freeze({ + "TASK-WP7-01": "passed", + "TASK-WP7-02": "passed", + "TASK-WP7-03": "deferred_nonblocking_first_version", + "TASK-WP7-04": "deferred_nonblocking_first_version", + "TASK-WP7-05": "passed", +}); + +const shaPattern = /^[0-9a-f]{40}$/i; + +export function buildWp706PrefreezeReport({ currentCommit, releaseExists, trace, upstream }) { + if (releaseExists) throw new Error("WP7_06_RELEASE_WRITTEN_PREMATURELY"); + if (!shaPattern.test(currentCommit ?? "")) throw new Error("WP7_06_CURRENT_COMMIT_INVALID"); + if (trace?.status !== "passed" || !Array.isArray(trace?.errors) || trace.errors.length > 0) { + throw new Error("WP7_06_TRACE_VALIDATION_FAILED"); + } + for (const [key, expected] of Object.entries(EXPECTED_TRACE_SUMMARY)) { + if (trace.summary?.[key] !== expected) throw new Error(`WP7_06_TRACE_COUNT_MISMATCH:${key}`); + } + + for (const [taskId, expectedStatus] of Object.entries(REQUIRED_UPSTREAM)) { + const item = upstream?.[taskId]; + if (!item || !shaPattern.test(item.head ?? "")) throw new Error(`WP7_06_UPSTREAM_HEAD_INVALID:${taskId}`); + if (item.merged !== true) throw new Error(`WP7_06_UPSTREAM_NOT_MERGED:${taskId}`); + if (item.status !== expectedStatus) throw new Error(`WP7_06_UNSUPPORTED_STATUS:${taskId}`); + } + + return { + schema_version: "1.0", + task_id: "TASK-WP7-06", + status: "passed", + current_commit: currentCommit.toLowerCase(), + release_json_written: false, + trace_summary: { ...EXPECTED_TRACE_SUMMARY }, + upstream: Object.fromEntries(Object.entries(REQUIRED_UPSTREAM).map(([taskId]) => [taskId, { + branch: upstream[taskId].branch, + head: upstream[taskId].head.toLowerCase(), + merged: true, + status: upstream[taskId].status, + }])), + deferred_external_tasks: Object.entries(REQUIRED_UPSTREAM) + .filter(([, status]) => status === "deferred_nonblocking_first_version") + .map(([taskId]) => taskId), + final_release_allowed: false, + next_task: "TASK-WP7-07", + }; +} + +export { EXPECTED_TRACE_SUMMARY, REQUIRED_UPSTREAM }; diff --git a/scripts/run-wp7-06-validation.mjs b/scripts/run-wp7-06-validation.mjs new file mode 100644 index 0000000..0023841 --- /dev/null +++ b/scripts/run-wp7-06-validation.mjs @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { validateTddTrace } from "./lib/tdd-trace.mjs"; +import { buildWp706PrefreezeReport, REQUIRED_UPSTREAM } from "./lib/wp7-06-prefreeze.mjs"; + +const runId = process.env.DADA_TDD_RUN_ID ?? `wp7-06-prefreeze-${new Date().toISOString().replace(/[^0-9]/g, "")}`; +const runDirectory = resolve("artifacts", "tdd", runId); +const caseDirectory = resolve(runDirectory, "cases", "TDD-WP7-AC-001-trace-structure"); +if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`); +mkdirSync(caseDirectory, { recursive: true }); + +function writeJson(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); +} + +function git(args) { + const result = spawnSync("git", args, { encoding: "utf8", timeout: 60_000 }); + if ((result.status ?? 1) !== 0) throw new Error(`WP7_06_GIT_COMMAND_FAILED:${args[0]}`); + return result.stdout.trim(); +} + +function runCommand(name, command) { + const startedAt = new Date().toISOString(); + const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", command], { + encoding: "utf8", + env: process.env, + maxBuffer: 64 * 1024 * 1024, + }); + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + return { + command, + exit_code: result.status ?? 1, + finished_at: new Date().toISOString(), + name, + started_at: startedAt, + }; +} + +const commands = [ + runCommand("prefreeze-unit", "node --test tests/package/wp7-06-prefreeze.test.mjs"), + runCommand("tdd-trace", "pnpm validate:tdd-trace"), +]; +if (commands.some((command) => command.exit_code !== 0)) { + writeJson(resolve(caseDirectory, "commands.json"), { commands, run_id: runId, schema_version: "1.0" }); + process.exit(1); +} + +const trace = validateTddTrace(); +const currentCommit = git(["rev-parse", "HEAD"]); +const remoteLines = git(["ls-remote", "--heads", "origin", "codex/wp7-01", "codex/wp7-02", "codex/wp7-03", "codex/wp7-04", "codex/wp7-05"]) + .split(/\r?\n/) + .filter(Boolean); +const remoteHeads = Object.fromEntries(remoteLines.map((line) => { + const [head, reference] = line.split(/\s+/); + return [reference.replace("refs/heads/", ""), head]; +})); +const upstream = Object.fromEntries(Object.entries(REQUIRED_UPSTREAM).map(([taskId, status]) => { + const branch = taskId.replace("TASK-", "codex/").toLowerCase(); + const head = remoteHeads[branch]; + const ancestry = spawnSync("git", ["merge-base", "--is-ancestor", head ?? "missing", "HEAD"], { encoding: "utf8", timeout: 30_000 }); + return [taskId, { branch, head, merged: ancestry.status === 0, status }]; +})); +const report = buildWp706PrefreezeReport({ + currentCommit, + releaseExists: existsSync(resolve("RELEASE.json")), + trace, + upstream, +}); + +const result = { + acceptance_criteria: Array.from({ length: 56 }, (_, index) => index + 1) + .filter((number) => ![8, 26, 37, 54].includes(number)) + .map((number) => `AC-${String(number).padStart(2, "0")}`), + automation: ["automated", "manual_review"], + commit: currentCommit, + evidence_refs: ["commands.json", "trace-summary.json", "upstream-lineage.json", "deferred-external.json"], + finished_at: new Date().toISOString(), + manifest: { + path: "tasks.manifest.json", + sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase(), + }, + missing_evidence: [], + release_gate: ["release:P0-A"], + requirements: ["NFR-05"], + run_id: runId, + schema_version: "1.0", + status: report.status, + task_id: "TASK-WP7-06", + test_id: "TDD-WP7-AC-001-trace-structure", + work_package: "WP-7", +}; + +writeJson(resolve(caseDirectory, "commands.json"), { commands, run_id: runId, schema_version: "1.0" }); +writeJson(resolve(caseDirectory, "trace-summary.json"), { status: trace.status, summary: trace.summary, schema_version: "1.0" }); +writeJson(resolve(caseDirectory, "upstream-lineage.json"), { current_commit: currentCommit, tasks: report.upstream, schema_version: "1.0" }); +writeJson(resolve(caseDirectory, "deferred-external.json"), { + policy: "product_owner_first_version_nonblocking", + tasks: report.deferred_external_tasks, + treated_as_real_provider_pass: false, + schema_version: "1.0", +}); +writeJson(resolve(caseDirectory, "result.json"), result); +writeJson(resolve(runDirectory, "evidence.json"), { cases: [{ missing_evidence: [], status: result.status, test_id: result.test_id }], run_id: runId, status: result.status, schema_version: "1.0" }); +console.log(JSON.stringify({ deferred_external_tasks: report.deferred_external_tasks, next_task: report.next_task, run_id: runId, status: report.status }, null, 2)); diff --git a/tests/package/wp7-06-prefreeze.test.mjs b/tests/package/wp7-06-prefreeze.test.mjs new file mode 100644 index 0000000..5955c5a --- /dev/null +++ b/tests/package/wp7-06-prefreeze.test.mjs @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { buildWp706PrefreezeReport } from "../../scripts/lib/wp7-06-prefreeze.mjs"; + +const expectedTrace = { + errors: [], + status: "passed", + summary: { + acceptanceCriteria: 52, + errorCategories: 9, + featureModules: 13, + parentFamilies: 89, + penProductFrames: 18, + productContracts: 19, + requirements: 109, + tasks: 52, + testCases: 117, + uiPages: 22, + }, +}; + +const upstream = { + "TASK-WP7-01": { branch: "codex/wp7-01", head: "1".repeat(40), merged: true, status: "passed" }, + "TASK-WP7-02": { branch: "codex/wp7-02", head: "2".repeat(40), merged: true, status: "passed" }, + "TASK-WP7-03": { branch: "codex/wp7-03", head: "3".repeat(40), merged: true, status: "deferred_nonblocking_first_version" }, + "TASK-WP7-04": { branch: "codex/wp7-04", head: "4".repeat(40), merged: true, status: "deferred_nonblocking_first_version" }, + "TASK-WP7-05": { branch: "codex/wp7-05", head: "5".repeat(40), merged: true, status: "passed" }, +}; + +test("builds the exact WP7-06 pre-freeze closure without pretending deferred suppliers passed", () => { + const report = buildWp706PrefreezeReport({ + currentCommit: "a".repeat(40), + releaseExists: false, + trace: expectedTrace, + upstream, + }); + + assert.equal(report.status, "passed"); + assert.equal(report.release_json_written, false); + assert.deepEqual(report.trace_summary, expectedTrace.summary); + assert.deepEqual(report.deferred_external_tasks, ["TASK-WP7-03", "TASK-WP7-04"]); +}); + +test("rejects missing lineage and premature RELEASE.json", () => { + assert.throws(() => buildWp706PrefreezeReport({ + currentCommit: "a".repeat(40), + releaseExists: false, + trace: expectedTrace, + upstream: { ...upstream, "TASK-WP7-05": { ...upstream["TASK-WP7-05"], merged: false } }, + }), /WP7_06_UPSTREAM_NOT_MERGED:TASK-WP7-05/); + + assert.throws(() => buildWp706PrefreezeReport({ + currentCommit: "a".repeat(40), + releaseExists: true, + trace: expectedTrace, + upstream, + }), /WP7_06_RELEASE_WRITTEN_PREMATURELY/); +}); + +test("rejects trace drift and unsupported upstream statuses", () => { + assert.throws(() => buildWp706PrefreezeReport({ + currentCommit: "a".repeat(40), + releaseExists: false, + trace: { ...expectedTrace, summary: { ...expectedTrace.summary, testCases: 116 } }, + upstream, + }), /WP7_06_TRACE_COUNT_MISMATCH:testCases/); + + assert.throws(() => buildWp706PrefreezeReport({ + currentCommit: "a".repeat(40), + releaseExists: false, + trace: expectedTrace, + upstream: { ...upstream, "TASK-WP7-03": { ...upstream["TASK-WP7-03"], status: "passed" } }, + }), /WP7_06_UNSUPPORTED_STATUS:TASK-WP7-03/); +});