import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))); const projectRoot = path.resolve(toolsRoot, ".."); const docsRoot = path.join(projectRoot, "docs"); function readJson(name) { return JSON.parse(fs.readFileSync(path.join(docsRoot, name), "utf8")); } function sha256(filePath) { return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); } function requireFile(name) { return fs.existsSync(path.join(docsRoot, name)); } const performance = readJson("freecad-performance-budget.json"); const compatibility = readJson("freecad-browser-compatibility.json"); const recovery = readJson("freecad-power-loss-recovery.json"); const licenseInventory = readJson("web-license-inventory.json"); const artifactMap = readJson("web-license-artifact-map.json"); const offlineManifest = readJson("freecad-web-offline-package-manifest.json"); const staticCheck = readJson("freecad-web-offline-static-check.json"); const functional = readJson("freecad-web-offline-functional.json"); const repro = readJson("freecad-web-offline-package-repro.json"); const rollback = readJson("freecad-web-upgrade-rollback.json"); const required = [ "freecad-performance-budget.json", "freecad-browser-compatibility.json", "freecad-power-loss-recovery.json", "web-license-evidence.json", "web-license-artifact-map.json", "web-license-inventory.json", "web-license-notices.md", "freecad-web-offline-package-manifest.json", "freecad-web-offline-static-check.json", "freecad-web-offline-functional.json", "freecad-web-offline-package-repro.json", "freecad-web-upgrade-rollback.json", ]; const missing = required.filter((name) => !requireFile(name)); const chromeOnly = [ "FreeCAD/docs/current/FREECAD_WEB_MIGRATION_ANALYSIS.md", "FreeCAD/docs/current/FREECAD_WEB_EXECUTION_PLAN.md", "FreeCAD/docs/current/FREECAD_WEB_PROGRESS.md", ].every((relative) => /Chrome|Chrome only|浏览器测试只执行 Google Chrome|浏览器测试铁律/.test(fs.readFileSync(path.join(projectRoot, relative.replace("FreeCAD/", "")), "utf8"))); const supportedCompatibility = compatibility.summary.supportedCaseFailures === 0 && compatibility.result === "pass_for_supported_cases_with_explicit_unavailable_and_pending_scope"; const noLicenseBlocks = licenseInventory.summary.blockedDependencies === 0 && artifactMap.summary.unmappedArtifactCount === 0; const offlinePass = staticCheck.summary.result === "pass_static_package_closed" && functional.summary.result === "pass_offline_core_functional_replay" && repro.summary.result === "pass_reproducible_except_declared_timestamps"; const rollbackPass = rollback.summary.result === "pass_upgrade_rollback_paths_have_independent_evidence"; const packageFiles = offlineManifest.files.map((entry) => path.join(projectRoot, "web", "offline-package", entry.path)); const packageHashesValid = offlineManifest.files.every((entry) => fs.existsSync(path.join(projectRoot, "web", "offline-package", entry.path)) && sha256(path.join(projectRoot, "web", "offline-package", entry.path)) === entry.sha256); const checks = [ { id: "required-evidence", pass: missing.length === 0, detail: missing.length === 0 ? `${required.length} evidence files present` : `missing: ${missing.join(", ")}` }, { id: "performance-budget", pass: performance.blocked === false && performance.result.startsWith("pass_"), detail: performance.result }, { id: "chrome-compatibility", pass: supportedCompatibility, detail: "supported Chrome cases have zero failures; non-Chrome excluded by iron rule" }, { id: "power-loss-recovery", pass: recovery.blocked === false && recovery.result.startsWith("pass_"), detail: recovery.result }, { id: "license-coverage", pass: noLicenseBlocks, detail: `${artifactMap.summary.artifactCount} artifacts mapped; ${licenseInventory.summary.blockedDependencies} license blocks` }, { id: "offline-package", pass: offlineManifest.summary.result === "assembled_verified_inputs" && packageHashesValid, detail: `${offlineManifest.summary.fileCount} files hash-verified` }, { id: "offline-static-functional-repro", pass: offlinePass, detail: `${staticCheck.summary.referenceCount} references, ${functional.summary.passedCases}/${functional.summary.caseCount} Chrome cases, reproducible` }, { id: "upgrade-rollback", pass: rollbackPass, detail: `${rollback.summary.pathCount} paths preserved previous package and FCStd` }, { id: "chrome-only-iron-rule", pass: chromeOnly, detail: "current baseline, index and checkpoint state Chrome-only" }, ]; const gate = { schema: "freecad-web-release-gate/v1", sourceRevision: offlineManifest.sourceRevision, browserPolicy: "Chrome only; Firefox/WebKit/Safari excluded from execution and release blocking", checks, pendingOutsideRelease: [ "FCStd save/reopen UI slice is pending because current browser UI exposes no FCStd save/reopen action", "native Safari, Firefox and WebKit runs are excluded by the execution iron rule", "network distribution security and automatic corrupt-package repair are outside this gate", ], package: { manifest: "FreeCAD/docs/freecad-web-offline-package-manifest.json", packageRoot: "FreeCAD/web/offline-package", packageFileCount: offlineManifest.summary.fileCount, packageHashesValid, manifestSha256: sha256(path.join(projectRoot, "web", "offline-package", "manifest.json")), }, summary: { checkCount: checks.length, passedChecks: checks.filter((check) => check.pass).length, failedChecks: checks.filter((check) => !check.pass).length, result: checks.every((check) => check.pass) ? "PASS_P5_RELEASE_READY_WITH_EXPLICIT_PENDING_SCOPE" : "BLOCKED_P5_RELEASE_GATE", }, }; fs.writeFileSync(path.join(docsRoot, "freecad-web-release-gate.json"), `${JSON.stringify(gate, null, 2)}\n`); const lines = [ "# FreeCAD 1.2 Web P5 发布出口", "", "浏览器执行铁律:只使用 Google Chrome;Firefox、WebKit、Safari 不执行、不纳入当前验收或阻断判定。", "", `Source revision: \`${gate.sourceRevision}\``, "", "| Check | Result | Detail |", "|---|---|---|", ...checks.map((check) => `| ${check.id} | ${check.pass ? "PASS" : "BLOCKED"} | ${check.detail} |`), "", `## Decision: **${gate.summary.result}**`, "", "Explicit pending scope:", ...gate.pendingOutsideRelease.map((item) => `- ${item}`), "", `Offline package manifest SHA-256: \`${gate.package.manifestSha256}\``, ]; fs.writeFileSync(path.join(docsRoot, "freecad-web-release-gate.md"), `${lines.join("\n")}\n`); console.log(`P5 gate ${gate.summary.result}; checks=${gate.summary.passedChecks}/${gate.summary.checkCount}`); if (gate.summary.failedChecks > 0) process.exitCode = 1;