// QA 게이트 — L0~L5 전 계층 실행. 1건 실패라도 있으면 exit 1 (배포 금지). // 사용: node tools/verify.mjs / npm run verify // 기준 화면 갱신: node tools/visual.mjs --update-baseline 후 커밋. import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import url from "node:url"; const here = path.dirname(url.fileURLToPath(import.meta.url)); const root = path.resolve(here, ".."); const npx = process.platform === "win32" ? "npx.cmd" : "npx"; const steps = [ { layer: "L0-정적/stylelint", cmd: () => spawnSync(npx, ["stylelint", "public/work/*/styles/*.css"], { cwd: root, encoding: "utf8", shell: true }), }, { layer: "L0-정식/html-validate", cmd: () => spawnSync(npx, ["html-validate", "public/work/gaon-lms/index.html", "public/work/dure-enrollment/index.html"], { cwd: root, encoding: "utf8", shell: true }), }, { layer: "L1-단위/계산로직", cmd: () => spawnSync(process.execPath, ["tools/unit/calc.test.mjs"], { cwd: root, encoding: "utf8" }) }, { layer: "L2-기능/감사", cmd: () => spawnSync(process.execPath, ["tools/audit.mjs"], { cwd: root, encoding: "utf8" }) }, { layer: "L2-불변식/렌더", cmd: () => spawnSync(process.execPath, ["tools/invariants.mjs"], { cwd: root, encoding: "utf8" }) }, { layer: "L3-시각회귀/diff", cmd: () => spawnSync(process.execPath, ["tools/visual.mjs"], { cwd: root, encoding: "utf8" }) }, { layer: "L4-WebKit", cmd: () => spawnSync(process.execPath, ["tools/webkit.mjs"], { cwd: root, encoding: "utf8" }) }, { layer: "L5-탐색/키보드", cmd: () => spawnSync(process.execPath, ["tools/exploratory.mjs"], { cwd: root, encoding: "utf8" }) }, ]; const report = []; const t0 = Date.now(); let failed = 0; for (const step of steps) { const s = Date.now(); const r = step.cmd(); const ok = r.status === 0; if (!ok) failed++; const out = (r.stdout || "") + (r.stderr || ""); const passLine = (out.match(/PASS\s+[^\n]/g) || []).length; const failLines = (out.match(/FAIL\s+[^\n]/g) || []).length; const tail = out.trim().split("\n").slice(-3).join(" | ").slice(0, 180); report.push({ layer: step.layer, ok, ms: Date.now() - s, passLine, failLines, tail }); console.log(`${ok ? "✓" : "✗"} ${step.layer} (${((Date.now() - s) / 1000).toFixed(1)}s)${passLine ? ` — PASS ${passLine}` : ""}${failLines ? ` FAIL ${failLines}` : ""}`); if (!ok) console.log(" " + tail); } const totalS = ((Date.now() - t0) / 1000).toFixed(1); const stamp = new Date().toISOString().replace("T", " ").slice(0, 16); const md = [ `# 검증 리포트 — ${stamp}`, ``, `게이트: ${failed ? `**${failed}개 계층 실패 — 배포 금지**` : "**전 계층 통과 — 배포 가능**"} · 총 ${totalS}초`, ``, `| 계층 | 결과 | 항목 | 소요 |`, `|---|---|---|---|`, ...report.map((r) => `| ${r.layer} | ${r.ok ? "PASS" : "**FAIL**"} | ${r.failLines ? `FAIL ${r.failLines}` : `PASS ${r.passLine || "—"}`} | ${(r.ms / 1000).toFixed(1)}s |`), ``, ...report.filter((r) => !r.ok).map((r) => `## ${r.layer} 실패 상세\n\n\`\`\`\n${r.tail}\n\`\`\``), ].join("\n"); fs.writeFileSync(path.join(here, "verify-report.md"), md); console.log(`\n${failed ? `게이트 실패(${failed}계층)` : "게이트 통과"} — 리포트: tools/verify-report.md`); process.exit(failed ? 1 : 0);