import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { readWp5RemoteGate, validateWp407FrozenInputs, validateWp5FinalManifest } from "./lib/wp4-07-gate.mjs"; function findFiles(directory, name) { if (!existsSync(directory)) return []; return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = resolve(directory, entry.name); return entry.isDirectory() ? findFiles(path, name) : entry.name === name ? [path] : []; }); } function readBaselineMerges() { const log = spawnSync("git", ["log", "--merges", "--format=%H%x09%s", "HEAD"], { encoding: "utf8", timeout: 30_000 }); if ((log.status ?? 1) !== 0) throw new Error("WP4_07_BASELINE_MERGES_UNREADABLE"); return log.stdout.trim().split(/\r?\n/).filter(Boolean).map((line) => { const [sha, ...subject] = line.split("\t"); return { sha, subject: subject.join("\t") }; }).filter((entry) => entry.subject.startsWith("merge: integrate WP5-")); } function readGitState() { const commitResult = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", timeout: 30_000 }); const statusResult = spawnSync("git", ["status", "--porcelain"], { encoding: "utf8", timeout: 30_000 }); if ((commitResult.status ?? 1) !== 0 || (statusResult.status ?? 1) !== 0) throw new Error("WP4_07_GIT_STATE_UNREADABLE"); return { commit: commitResult.stdout.trim(), worktree_under_test: statusResult.stdout.trim() ? "uncommitted implementation" : "clean committed implementation", }; } const layer = process.argv[2]; if (!['visual', 'performance'].includes(layer)) { console.error("Usage: node scripts/run-wp4-07-layer.mjs "); process.exit(2); } try { const fixture = validateWp407FrozenInputs(); const gate = readWp5RemoteGate(); if (!gate.complete) { console.error(JSON.stringify({ code: "WP4_07_WP5_GATE_INCOMPLETE", fixture_sha256: fixture.fixture_sha256, layer, candidate_baseline_branch: gate.candidate_baseline_branch, candidate_baseline_sha: gate.candidate_baseline_sha, missing_remote_tasks: gate.missing_tasks, observed_remote_heads: gate.heads, observed_task_shas: gate.task_shas, required_final_branches: gate.required_final_branches, status: "red", }, null, 2)); process.exit(1); } const runId = process.env.DADA_TDD_RUN_ID ?? `wp4-07-${layer}-${new Date().toISOString().replace(/[^0-9]/g, "")}`; const runDirectory = resolve("artifacts", "tdd", runId); const casesDirectory = resolve(runDirectory, "cases"); const releaseDirectory = resolve(runDirectory, "real-release"); mkdirSync(casesDirectory, { recursive: true }); let environment = { ...process.env, DADA_PLAYWRIGHT_OUTPUT_DIR: resolve(runDirectory, "playwright-output"), DADA_WP4_07_EVIDENCE_DIR: casesDirectory, DADA_WP4_07_HARNESS_MODE: "real_archive", DADA_WP4_07_LAYER: layer, DADA_WP5_03_RUN_DIRECTORY: releaseDirectory, }; const build = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "pnpm --filter @dada/shared-contracts build && pnpm --filter @dada/static-sticker-catalog build && pnpm --filter @dada/template-registry build && pnpm --filter @dada/asset-renderer build && pnpm --filter @dada/asset-compiler build"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 }); if (build.stdout) process.stdout.write(build.stdout); if (build.stderr) process.stderr.write(build.stderr); if ((build.status ?? 1) !== 0) process.exit(build.status ?? 1); let manifestPath = process.env.DADA_WP4_07_FINAL_ASSET_MANIFEST; if (!manifestPath) { const compile = spawnSync(process.execPath, ["scripts/compile-wp5-03-assets.mjs"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 }); if (compile.stdout) process.stdout.write(compile.stdout); if (compile.stderr) process.stderr.write(compile.stderr); if ((compile.status ?? 1) !== 0) process.exit(compile.status ?? 1); manifestPath = resolve(releaseDirectory, "cases", "TDD-WP5-WHITE-001-p0a-allowlist", "manifest.json"); } const manifest = validateWp5FinalManifest(manifestPath); environment = { ...environment, DADA_WP4_07_FINAL_ASSET_MANIFEST: resolve(manifestPath) }; const playwright = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "pnpm exec playwright test --config playwright.wp4-07.config.ts"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 }); if (playwright.stdout) process.stdout.write(playwright.stdout); if (playwright.stderr) process.stderr.write(playwright.stderr); if ((playwright.status ?? 1) !== 0) process.exit(playwright.status ?? 1); const caseDirectory = resolve(casesDirectory, layer === "visual" ? "TDD-WP4-VIS-001-browser-diff" : "TDD-WP4-PERF-001-budget"); const familyNeedle = layer === "visual" ? "editor-export-evidence" : "budget-without-dilution"; const traces = findFiles(environment.DADA_PLAYWRIGHT_OUTPUT_DIR, "trace.zip"); for (const browser of ["chrome", "edge"]) { const evidenceTrace = resolve(caseDirectory, browser, "trace.zip"); if (existsSync(evidenceTrace)) continue; const trace = traces.find((path) => path.toLowerCase().includes(familyNeedle) && path.toLowerCase().includes(browser)); if (!trace) throw new Error(`WP4_07_${layer.toUpperCase()}_${browser.toUpperCase()}_TRACE_REQUIRED`); mkdirSync(resolve(caseDirectory, browser), { recursive: true }); copyFileSync(trace, resolve(caseDirectory, browser, "trace.zip")); } const aggregator = spawnSync(process.execPath, [layer === "visual" ? "scripts/compare-wp4-07-screenshots.mjs" : "scripts/aggregate-wp4-07-performance.mjs", "--evidence", caseDirectory, "--phase", "green"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 }); if (aggregator.stdout) process.stdout.write(aggregator.stdout); if (aggregator.stderr) process.stderr.write(aggregator.stderr); if ((aggregator.status ?? 1) !== 0) process.exit(aggregator.status ?? 1); writeFileSync(resolve(caseDirectory, "release-inputs.json"), `${JSON.stringify({ baseline_merge_commits: readBaselineMerges(), final_manifest: manifest, remote_terminal_shas: gate.terminal_branch_shas, source_task_shas: gate.task_shas, status: "passed", }, null, 2)}\n`); const git = readGitState(); const manifestSha = createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase(); const visual = layer === "visual"; const evidenceRefs = visual ? [ "pixel-diff.json", "layout-boxes.json", "manual-review.json", "release-inputs.json", "chrome/asset-sources.json", "chrome/editor.png", "chrome/canvas.png", "chrome/export-dialog.png", "chrome/layout-boxes.json", "chrome/trace.zip", "edge/asset-sources.json", "edge/editor.png", "edge/canvas.png", "edge/export-dialog.png", "edge/layout-boxes.json", "edge/trace.zip", ] : [ "performance.json", "memory.json", "dom-count.json", "environment.json", "release-inputs.json", "chrome/asset-sources.json", "chrome/performance-raw.json", "chrome/trace.zip", "edge/asset-sources.json", "edge/performance-raw.json", "edge/trace.zip", ]; const missingEvidence = evidenceRefs.filter((path) => !existsSync(resolve(caseDirectory, path))); const manualReviewPassed = !visual || JSON.parse(readFileSync(resolve(caseDirectory, "manual-review.json"), "utf8")).status === "passed"; const status = missingEvidence.length > 0 ? "failed" : manualReviewPassed ? "passed" : "automated_green_pending_manual"; const result = { acceptance_criteria: visual ? ["AC-19", "AC-23", "AC-32"] : ["AC-27", "AC-32"], automation: visual ? ["automated", "manual_review"] : ["automated"], commit: git.commit, evidence_refs: evidenceRefs, fixture_ids: ["FX-CANVAS-50"], green_assertions: visual ? ["Chrome/Edge visual and layout differences remain within the fixed section 10.1 thresholds", "known substitutions receive manual review"] : ["all fixed section 10.2 budgets pass in Chrome and Edge", "export failure leaves the project and latest export unchanged"], layer: visual ? ["VIS-PERF", "MANUAL"] : ["VIS-PERF"], manifest: { path: "tasks.manifest.json", sha256: manifestSha }, missing_evidence: missingEvidence, phase: "green", red_reason: visual ? "Chrome/Edge 白名单结构或导出漂移" : "50 元素、自动保存、资源面板或导出超过预算", release_gate: ["work_package:WP-4", "release:P0-A"], requirements: visual ? ["NFR-02"] : ["NFR-03"], run_id: runId, status, task_id: "TASK-WP4-07", test_id: visual ? "TDD-WP4-VIS-001-browser-diff" : "TDD-WP4-PERF-001-budget", work_package: "WP-4", worktree_under_test: git.worktree_under_test, }; writeFileSync(resolve(caseDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`); writeFileSync(resolve(runDirectory, "evidence.json"), `${JSON.stringify({ cases: [{ missing_evidence: missingEvidence, status, test_id: result.test_id }], commit: git.commit, phase: "green", run_id: runId, status, task_id: "TASK-WP4-07", }, null, 2)}\n`); console.log(JSON.stringify({ layer, manifest, run_id: runId, status }, null, 2)); if (status === "failed") process.exit(1); } catch (error) { console.error(JSON.stringify({ code: error instanceof Error ? error.message : String(error), layer, status: "failed" }, null, 2)); process.exit(1); }