import { mkdir, readFile, writeFile } from "node:fs/promises"; import { parseJpwabc, hasExactRoundTrip } from "../dist/domain/jpwabc.js"; const manifest = JSON.parse(await readFile("fixtures/manifest.json", "utf8")); const rows = []; const failures = []; for (const fixture of manifest.samples) { try { const bytes = await readFile(fixture.rawPath); const result = parseJpwabc(bytes); const exactRoundTrip = hasExactRoundTrip(result); const errors = result.diagnostics.filter((item) => item.severity === "error"); const warnings = result.diagnostics.filter((item) => item.severity === "warning"); const status = errors.length ? "recovered-error" : warnings.length || result.unknownTokens.length || result.unknownBlocks.length ? "parsed-with-diagnostics" : "parsed-clean"; const row = { id: fixture.id, rawPath: fixture.rawPath, decodedPath: fixture.decodedPath, rawSha256: fixture.rawSha256, rawBytes: bytes.byteLength, status, sourcePreserved: exactRoundTrip, encoding: result.source.encoding, bom: result.source.bom, formatVersion: result.header.formatVersion, productVersion: result.header.productVersion, sections: result.sections.map((section) => ({ name: section.name, known: section.known, lineCount: section.lines.length })), semanticCounts: { options: result.options.document.length, unknownOptions: result.options.unknownDocument.length, fonts: result.fonts.length, titles: result.titles.length, voiceLines: result.voices.length, voiceTokens: result.voices.reduce((count, line) => count + line.tokens.length, 0), wordBlocks: result.words.length, attachments: result.attachments.length, pages: result.pages.length }, preservationCounts: { rawTokens: result.rawTokens.length, unknownTokens: result.unknownTokens.length, unknownBlocks: result.unknownBlocks.length }, diagnostics: result.diagnostics.map(({ code, severity, message, recovery }) => ({ code, severity, message, recovery })), compatibility: result.compatibility }; rows.push(row); if (!exactRoundTrip) failures.push(`${fixture.id}: source bytes changed on no-edit serialization`); } catch (error) { failures.push(`${fixture.id}: parser threw ${error instanceof Error ? error.message : String(error)}`); rows.push({ id: fixture.id, rawPath: fixture.rawPath, status: "parser-threw", sourcePreserved: false, error: String(error) }); } } if (rows.length !== manifest.sampleCount || rows.length !== 33) failures.push(`expected 33 classifications, got ${rows.length}`); if (rows.some((row) => !row.status || row.sourcePreserved !== true)) failures.push("one or more samples has no status or failed source preservation"); const report = { reportVersion: 1, milestone: "M1", purpose: "33-sample parse, preservation and diagnostic classification; not an external JPW7 compatibility pass", sampleCountExpected: 33, sampleCountActual: rows.length, statusCounts: Object.fromEntries([...new Set(rows.map((row) => row.status))].sort().map((status) => [status, rows.filter((row) => row.status === status).length])), allSourceBytesPreserved: failures.length === 0, failures, samples: rows }; await mkdir("acceptance/m1", { recursive: true }); await writeFile("acceptance/m1/sample-classification.json", `${JSON.stringify(report, null, 2)}\n`, "utf8"); const markdown = [ "# M1 33 样例分类报告", "", "> 分类、诊断和保留结果是 Web parser 的实测结果,不代表 JPW7 原程序兼容通过;外部回验仍由 R0/R1 单独执行。", "", `- 样例:${report.sampleCountActual}/${report.sampleCountExpected}`, `- 无编辑字节级保留:${report.allSourceBytesPreserved ? "是" : "否"}`, `- 状态:${JSON.stringify(report.statusCounts)}`, `- 失败:${report.failures.length}`, "", "| 样例 | 状态 | 编码/BOM | 区块 | 未知 token | 未知区块 | 诊断 |", "|---|---|---|---:|---:|---:|---:|", ...rows.map((row) => `| ${row.id} | ${row.status} | ${row.encoding}/${row.bom} | ${row.sections?.length ?? 0} | ${row.preservationCounts?.unknownTokens ?? "-"} | ${row.preservationCounts?.unknownBlocks ?? "-"} | ${row.diagnostics?.length ?? "-"} |`), "", "## 解释边界", "", "- `parsed-with-diagnostics` 表示语义/未知内容被保留并有报告,不是全量语法支持声明。", "- `recovered-error` 表示解析器发现错误但保留了原始字节;安全兼容导出必须阻止。", "- `sourcePreserved` 仅证明无编辑序列化稳定,不替代 JPW7 原程序回验。", "" ].join("\n"); await writeFile("acceptance/m1/sample-classification.md", markdown, "utf8"); console.log(JSON.stringify({ ok: failures.length === 0, samples: rows.length, statusCounts: report.statusCounts, failures: failures.length }, null, 2)); if (failures.length) process.exitCode = 1;