132 lines
11 KiB
JavaScript
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("<svg") }, result: !unsafeTag && maliciousSvg.includes("<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 }));
|