dorfteich/scripts/gen-export-fixtures.mjs
Claude Opus 4.8 aaa9a253ae
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Failing after 27m51s
CI / Lint, typecheck, test (push) Successful in 3m11s
CI / Auth e2e pack (push) Successful in 4m0s
CI / Import/export fidelity gate (push) Failing after 36s
CI / Build container images (push) Has been skipped
Add import/export fidelity gate to CI (#69)
Make the "structure-true best effort" fidelity contract (ADR 0009) an
objective, pipeline-gated suite so "best effort" cannot erode silently.

- New CI job "Import/export fidelity gate" (.gitea/workflows/ci.yml) runs
  the corpus suites against the pinned sidecar images the stages use
  (pandoc/core:3.6, gotenberg/gotenberg:8), started via docker run and
  reached over the host gateway. Small and separate so it stays well under
  five minutes; the suites self-skip in the main checks job (no sidecars).
- Export fidelity: fixtures/export corpus + gen-export-fixtures.mjs +
  export.fidelity.test.ts — exports Markdown to docx/odt through the real
  pinned pandoc and reads it back, snapshotting the round trip so a writer
  drift (ours or a version bump) fails the gate.
- PDF smoke: pdf.fidelity.test.ts renders a page through real Gotenberg and
  asserts the extracted text and a sane page count (pdf-parse, dev-only).
- Fidelity contract doc: fixtures/README.md defines "corpus green = fidelity
  acceptable" and the fixture-first bug process; per-corpus READMEs updated.

Because the snapshots are byte-exact and generated with the pinned tools,
bumping a sidecar without regenerating shifts the output and fails the suite
(AC3). The import corpus (#63) is folded into the same gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-10 13:59:10 +02:00

55 lines
2.3 KiB
JavaScript

#!/usr/bin/env node
// Regenerate the export fidelity corpus (issue #69, ADR 0009) from the committed
// `*.md` sources: for each source and each office format, write the Markdown you
// get back after a full export → re-read round trip through the pinned pandoc.
// That round-trip snapshot is what the fidelity gate pins — a change in pandoc's
// docx/odt writer (e.g. a version bump) shifts the output and fails the suite.
//
// docker run --rm -p 3030:3030 pandoc/core:3.6 server
// PANDOC_URL=http://localhost:3030 node scripts/gen-export-fixtures.mjs
//
// Mirrors the export path (ExportService: gfm → <ext>, standalone) and then
// reads the document back (<ext> → gfm) exactly as export.fidelity.test.ts does.
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), '..', 'fixtures', 'export');
const PANDOC_URL = process.env.PANDOC_URL ?? 'http://localhost:3030';
// Match the import corpus reader so both directions normalise the same way.
const MARKDOWN_FORMAT = 'gfm-implicit_figures-raw_html';
const BINARY = new Set(['docx', 'odt']);
async function pandoc(params) {
const res = await fetch(`${PANDOC_URL}/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!res.ok) throw new Error(`pandoc ${res.status}: ${await res.text()}`);
return BINARY.has(params.to) ? Buffer.from(await res.arrayBuffer()) : res.text();
}
/** Export markdown to an office document, then read it back to markdown. */
async function roundTrip(markdown, ext) {
const document = await pandoc({ text: markdown, from: 'gfm', to: ext, standalone: true });
return pandoc({
text: document.toString('base64'),
from: ext,
to: MARKDOWN_FORMAT,
standalone: false,
wrap: 'none',
});
}
const sources = readdirSync(FIXTURES).filter((f) => f.endsWith('.md') && !f.includes('.expected.'));
for (const source of sources) {
const name = source.replace(/\.md$/, '');
const markdown = readFileSync(join(FIXTURES, source), 'utf8');
for (const ext of ['docx', 'odt']) {
writeFileSync(join(FIXTURES, `${name}.${ext}.expected.md`), await roundTrip(markdown, ext));
console.log(`wrote ${name}.${ext}.expected.md`);
}
}