95 lines
4.9 KiB
JavaScript
95 lines
4.9 KiB
JavaScript
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;
|