Files
2026-09-22 16:33:35 +08:00

132 lines
11 KiB
JavaScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { execFileSync } from "node:child_process";
import { performance } from "node:perf_hooks";
import os from "node:os";
import { parseJpwabc } from "../dist/domain/jpwabc.js";
import { decodeProject, encodeProject } from "../dist/domain/project-codec.js";
import { modelFromParse, createEmptyDocument, createNoteEvent, rational, modelEquivalent } from "../dist/domain/score-model.js";
import { compilePlaybackTimeline } from "../dist/playback/playback.js";
import { DEFAULT_PAPER_CONFIG, layoutDocument, layoutSummary } from "../dist/layout/layout-engine.js";
import { createDefaultFontMeasurementProvider } from "../dist/layout/font-measure.js";
import { exportSnapshotSvg, buildPrintableHtml, outputGeometry } from "../dist/output/output.js";
import { probeSvg, renderSnapshotSvg } from "../dist/layout/svg-renderer.js";
const ROOT = "acceptance/m9";
const manifest = JSON.parse(await readFile("fixtures/manifest.json", "utf8"));
const environment = {
generatedAt: new Date().toISOString(),
node: process.version,
platform: `${process.platform} ${os.release()} ${os.arch()}`,
cpu: os.cpus()[0]?.model ?? "unknown",
memoryBytes: os.totalmem(),
chrome: (() => { try { return execFileSync("google-chrome", ["--version"], { encoding: "utf8" }).trim(); } catch { return "not available"; } })(),
firefox: (() => { try { return execFileSync("firefox", ["--version"], { encoding: "utf8" }).trim(); } catch { return "not available"; } })(),
project: "jianpu-web-editor",
buildCommand: "npm run build",
testCommand: "npm test",
note: "Node-side local evidence; browser, original JPW7, printer and external resource claims are listed separately and are not inferred."
};
for (const directory of [ROOT, `${ROOT}/projects`, `${ROOT}/svg`, `${ROOT}/png`, `${ROOT}/pdf`, `${ROOT}/screenshots`, `${ROOT}/native-roundtrip`]) await mkdir(directory, { recursive: true });
await writeFile(`${ROOT}/environment.json`, `${JSON.stringify(environment, null, 2)}\n`);
const importRows = [];
const layoutRows = [];
const playbackRows = [];
const exportRows = [];
const caseRows = [];
const fontProvider = createDefaultFontMeasurementProvider();
let firstSnapshot = null;
let maxPages = 0;
let preservedCount = 0;
let reopenCount = 0;
let svgCount = 0;
let blockedExportCount = 0;
for (const [index, fixture] of manifest.samples.entries()) {
const bytes = await readFile(fixture.rawPath);
const parsed = parseJpwabc(new Uint8Array(bytes));
const model = modelFromParse(parsed, { fileName: basename(fixture.rawPath) });
const projectText = encodeProject(model);
const reopened = decodeProject(projectText);
const reopenedEquivalent = modelEquivalent(model, reopened);
if (reopenedEquivalent) reopenCount += 1;
if (model.source?.rawBase64) preservedCount += 1;
const snapshot = await layoutDocument(model, DEFAULT_PAPER_CONFIG, fontProvider);
if (!firstSnapshot) firstSnapshot = snapshot;
const summary = layoutSummary(snapshot);
maxPages = Math.max(maxPages, summary.pageCount);
const timeline = compilePlaybackTimeline(model);
const safeName = `${String(index + 1).padStart(2, "0")}-${fixture.id.replace(/[^\p{L}\p{N}._-]+/gu, "_")}`;
await writeFile(`${ROOT}/projects/${safeName}.jianpu.json`, projectText);
const svgs = exportSnapshotSvg(snapshot);
for (const [pageIndex, svg] of svgs.entries()) {
await writeFile(`${ROOT}/svg/${safeName}-page-${pageIndex + 1}.svg`, svg);
svgCount += 1;
}
const probe = svgs[0] ? probeSvg(svgs[0]) : null;
importRows.push({ fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, rawPath: fixture.rawPath, parsedStatus: parsed.compatibility.status, encoding: parsed.source.encoding, sections: parsed.sections.length, diagnostics: parsed.diagnostics.length, unknownTokens: parsed.unknownTokens.length, unknownBlocks: parsed.unknownBlocks.length, sourcePreserved: Boolean(model.source?.rawBase64), result: "pass-local" });
layoutRows.push({ fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, summary, deterministicKey: snapshot.deterministicKey, svgProbe: probe, warnings: snapshot.warnings.length, result: "pass-local" });
playbackRows.push({ fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, voiceCount: timeline.voiceIds.length, eventCount: timeline.events.length, durationTicks: timeline.durationTicks, warnings: timeline.warnings, controlEventCount: timeline.controlEvents?.length ?? 0, result: "pass-local-semantic" });
const exportStatus = model.attachments.length || model.pageObjects.length || model.unknownBlocks.length || model.voices.some((voice) => voice.temporary) ? "blocked-by-safety-preflight" : "core-preflight-evaluated";
if (exportStatus === "blocked-by-safety-preflight") blockedExportCount += 1;
exportRows.push({ fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, svgPages: svgs.length, outputGeometry: outputGeometry(snapshot), svgIndependentArtifact: `${ROOT}/svg/${safeName}-page-1.svg`, printableHtmlBytes: buildPrintableHtml(snapshot).length, png: "not-executed-in-node; representative Chromium PNG remains in acceptance/m5", pdf: "not-executed-in-node; representative Chromium PDF remains in acceptance/m5", compatibilityPreflight: exportStatus, result: "pass-local-svg-and-engineering-export" });
caseRows.push({ caseId: `M9-33-${String(index + 1).padStart(2, "0")}`, fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, environment: "node-local", steps: "import -> model -> project save/reopen -> fixed layout -> timeline -> SVG", expected: "source retained, complete project round-trip, deterministic A4 SVG and bounded playback diagnostics", actual: { reopenedEquivalent, pages: summary.pageCount, lines: summary.lineCount, measures: summary.measureCount, symbols: summary.symbolCount, playbackEvents: timeline.events.length }, result: reopenedEquivalent && svgs.length > 0 ? "pass-local" : "failed", evidence: [`${ROOT}/projects/${safeName}.jianpu.json`, `${ROOT}/svg/${safeName}-page-1.svg`] });
}
const malicious = `JPW-ABC File Ver 7.30\n.Voice\n<svg onload="alert(1)"> <script>alert(2)</script> 1\n`;
const maliciousModel = modelFromParse(parseJpwabc(new TextEncoder().encode(malicious)), { fileName: "malicious-input.jpwabc" });
const maliciousSnapshot = await layoutDocument(maliciousModel, DEFAULT_PAPER_CONFIG, fontProvider);
const maliciousSvg = renderSnapshotSvg(maliciousSnapshot)[0];
const unsafeTag = /<[^>]*(?:onload|onclick|javascript:)[^>]*>/i.test(maliciousSvg);
const securityRow = { caseId: "ACC-Q-008", input: "malicious source text", expected: "data only; no executable SVG/HTML attributes or external load", actual: { unsafeTag, escapedText: maliciousSvg.includes("&lt;svg") }, result: !unsafeTag && maliciousSvg.includes("&lt;svg") ? "pass-local" : "failed", evidence: `${ROOT}/security-probe.json` };
await writeFile(`${ROOT}/security-probe.json`, `${JSON.stringify(securityRow, null, 2)}\n`);
caseRows.push(securityRow);
const performanceSamples = [];
const performanceFixture = manifest.samples[0];
const performanceBytes = await readFile(performanceFixture.rawPath);
for (let run = 0; run < 20; run += 1) {
const started = performance.now();
const parsed = parseJpwabc(new Uint8Array(performanceBytes));
const model = modelFromParse(parsed);
const snapshot = await layoutDocument(model, DEFAULT_PAPER_CONFIG, fontProvider);
performanceSamples.push({ run: run + 1, milliseconds: performance.now() - started, pages: snapshot.pages.length });
}
const sortedTimes = performanceSamples.map((sample) => sample.milliseconds).sort((a, b) => a - b);
const p95 = sortedTimes[Math.min(sortedTimes.length - 1, Math.ceil(sortedTimes.length * 0.95) - 1)];
const longDocument = createEmptyDocument("m9-10-page-synthetic");
const longMeasures = [];
for (let index = 0; index < 120; index += 1) longMeasures.push({ id: `m9-measure-${index}`, ordinal: index, beats: rational(4), events: [1, 2, 3, 4].map((degree, eventIndex) => createNoteEvent(`m9-event-${index}-${eventIndex}`, degree, rational(1))) });
longDocument.voices.push({ id: "m9-voice", name: "性能合成", sectionId: null, visible: true, measures: longMeasures });
const longStarted = performance.now();
const longSnapshot = await layoutDocument(longDocument, DEFAULT_PAPER_CONFIG, fontProvider);
const longMilliseconds = performance.now() - longStarted;
await writeFile(`${ROOT}/performance.json`, `${JSON.stringify({ method: "Node local approximation; does not replace 20 cold browser runs, 100-edit P95, 10-minute audio or 30-minute resource-release tests", firstFixture: performanceFixture.id, firstDisplayRuns: performanceSamples, p95Milliseconds: p95, tenPageSynthetic: { pages: longSnapshot.pages.length, milliseconds: longMilliseconds }, targets: { firstDisplayP95Milliseconds: 2000, editP95Milliseconds: 100, tenMinutePlaybackDriftMilliseconds: 20 }, status: "local-measured; browser/device portions not-executed" }, null, 2)}\n`);
await writeFile(`${ROOT}/import-report.jsonl`, `${importRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
await writeFile(`${ROOT}/layout-report.jsonl`, `${layoutRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
await writeFile(`${ROOT}/playback-timeline.jsonl`, `${playbackRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
await writeFile(`${ROOT}/export-report.jsonl`, `${exportRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
await writeFile(`${ROOT}/case-results.jsonl`, `${caseRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
let previousNativeRows = new Map();
try {
const previousNativeText = await readFile(`${ROOT}/native-roundtrip-report.jsonl`, "utf8");
previousNativeRows = new Map(previousNativeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)).filter((row) => row.result !== "not-executed").map((row) => [row.fixtureId, row]));
} catch {
// A fresh M9 run has no external native evidence to preserve.
}
const nativeRows = manifest.samples.map((fixture) => previousNativeRows.get(fixture.id) ?? ({ caseId: "ACC-X-012", fixtureId: fixture.id, fixtureSha256: fixture.rawSha256, result: "not-executed", reason: "JPW7 v7.30 original program/Windows replay is not available in this Linux Web-only run; Web round-trip is not substituted.", evidence: `${ROOT}/native-roundtrip-report.jsonl` }));
await writeFile(`${ROOT}/native-roundtrip-report.jsonl`, `${nativeRows.map((row) => JSON.stringify(row)).join("\n")}\n`);
let previousExternal = {};
try {
previousExternal = JSON.parse(await readFile(`${ROOT}/summary.json`, "utf8")).external ?? {};
} catch {
// A fresh M9 run has no previous external summary.
}
const nativeCount = nativeRows.filter((row) => row.result !== "not-executed").length;
const nativeStatus = nativeCount === 0 ? "not-executed" : nativeCount === manifest.samples.length ? `pass-${nativeCount}-of-${manifest.samples.length}` : `partial-${nativeCount}-of-${manifest.samples.length}`;
await writeFile(`${ROOT}/summary.json`, `${JSON.stringify({ sampleCount: manifest.samples.length, preservedCount, reopenCount, svgCount, maxPages, blockedExportCount, localCases: caseRows.length, security: securityRow.result, external: { nativeRoundtrip: nativeStatus, edge: "not-executed", firefox: "not-executed", printer: "not-executed", tenMinuteAudio: "not-executed", ...previousExternal, nativeRoundtrip: nativeStatus } }, null, 2)}\n`);
console.log(JSON.stringify({ samples: manifest.samples.length, preservedCount, reopenCount, svgCount, maxPages, blockedExportCount, p95Milliseconds: p95, longDocumentPages: longSnapshot.pages.length }));