#!/usr/bin/env node import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const docs = path.join(root, "docs", "p6"); const matrixPath = path.join(docs, "object-contract-matrix.json"); const matrix = JSON.parse(fs.readFileSync(matrixPath, "utf8")); const gateNames = [ "geometry-family-gate.json", "cam-generic-family-gate.json", "part-generic-family-gate.json", "fem-generic-family-gate.json", "mesh-geometry-family-gate.json", "fem-geometry-family-gate.json", "partdesign-generic-family-gate.json", "robot-generic-family-gate.json", "cam-geometry-family-gate.json", "import-geometry-family-gate.json", "inspection-geometry-family-gate.json", "part-geometry-family-gate.json", "partdesign-geometry-family-gate.json", "points-geometry-family-gate.json", "robot-geometry-family-gate.json", "sandbox-geometry-family-gate.json", "sketcher-geometry-family-gate.json", "surface-geometry-family-gate.json", "measure-object-family-gate.json", "data-property-family-gate.json", "document-graph-family-gate.json", "viewprovider-family-gate.json", ]; const gated = new Map(); const gatedByName = new Map(); const gateModules = { "cam-generic-family-gate.json": "CAM", "part-generic-family-gate.json": "Part", "fem-generic-family-gate.json": "Fem", "partdesign-generic-family-gate.json": "PartDesign", "robot-generic-family-gate.json": "Robot", }; for (const name of gateNames) { const file = path.join(docs, name); if (!fs.existsSync(file)) continue; const gate = JSON.parse(fs.readFileSync(file, "utf8")); for (const record of gate.records ?? []) { if (record.id) gated.set(record.id, record); if (record.name) { const module = gateModules[name]; if (module) gatedByName.set(`${module}:${record.name}`, record); for (const candidate of matrix.records.filter(item => item.name === record.name)) { gatedByName.set(`${candidate.module}:${record.name}`, record); } } } } const records = matrix.records.map((record) => { const existing = gated.get(record.id) ?? gatedByName.get(`${record.module}:${record.name}`); if (existing?.status) return { ...record, status: existing.status, detail: existing.detail ?? null, gate: existing.gate ?? null }; return { ...record, status: "blocked", detail: { code: "native_fixture_required", message: "No family-specific C-G native object fixture is registered yet", firstError: true, }, }; }); const counts = Object.fromEntries(["passed", "passed_boundary", "blocked"].map((status) => [status, records.filter((record) => record.status === status).length])); const report = { schema: "freecad-web-p6-object-contract-gate/v1", taskId: "FW-P6-FPA-D", result: records.length === matrix.records.length && counts.blocked > 0 ? "pass_with_blocked_records" : records.length === matrix.records.length ? "pass" : "fail", coverage: { inventoryRecords: matrix.records.length, terminalRecords: records.filter((record) => ["passed", "passed_boundary", "blocked"].includes(record.status)).length, counts }, records, source: { matrix: "docs/p6/object-contract-matrix.json", matrixSha256: crypto.createHash("sha256").update(fs.readFileSync(matrixPath)).digest("hex"), familyGates: gateNames.filter((name) => fs.existsSync(path.join(docs, name))) }, boundaries: ["blocked records retain source evidence but have no native fixture claim", "GUI-only view providers remain passed_boundary only when a family gate proves the boundary"], }; fs.writeFileSync(path.join(docs, "object-contract-gate.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, "docs", "freecad-p6-object-contract-d.log"), `result=${report.result}\ninventoryRecords=${report.coverage.inventoryRecords}\nterminalRecords=${report.coverage.terminalRecords}\npassed=${counts.passed}\npassed_boundary=${counts.passed_boundary}\nblocked=${counts.blocked}\n`); console.log(JSON.stringify({ result: report.result, ...report.coverage })); if (report.result === "fail") process.exitCode = 1;