mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-10-12 16:15:38 +00:00
Adds go-parser/, a Go reimplementation of the xlsxread parser, verified byte-for-byte against the Rust original before any cutover. Reader fidelity is exact across all 299 input files: the canonical cell dump of every sheet matches calamine's, locked in as a test against a committed hash oracle. Reaching that required replacing extrame/xls, which corrupted 69% of cells and dropped a further 28% on the BIFF corpus, with pbnjay/grate; correcting excelize's number-format application and trailing-cell trimming; restoring carriage returns that XML line-ending normalisation strips from 2,233 ten_cum_thi values; and gating numeric re-rendering on cell type so shared strings that merely look numeric keep their leading zeros. The differential gate compares both parsers over all four datasets: 3,265,641 rows with identical full-table SHA-256, identical per-column non-NULL counts, identical schema metadata and identical stdout. Config moves from TOML to YAML for both parsers, so they keep reading the same files and the gate stays meaningful. Verified by rebuilding 2016 and 2017-old2 with Rust under the new configs and matching the recorded counts. build-db.js now refuses to publish a database whose row count does not match the known figure, closing a path where an under-producing parser could ship a truncated public dataset with green CI. The deploy workflow gains a pull_request trigger and guards deploy to main, so branch verification can no longer publish to production.
208 lines
7.4 KiB
JavaScript
208 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Differential parity gate: proves the Go parser's databases are logically
|
|
// equivalent to the Rust parser's, across all four datasets.
|
|
//
|
|
// This is the entire safety argument for the migration. Unit and golden tests
|
|
// use synthetic fixtures; this is the only check against all 418 MB of real
|
|
// input.
|
|
//
|
|
// Deliberately NOT built on parser/scripts/verify-parity.js. That script
|
|
// compares a schema-shape change against a frozen baseline: its core check is
|
|
// "new columns must be all-NULL except an approved allowlist", which is vacuous
|
|
// here because both databases have the identical 22 columns, and its
|
|
// APPROVED_RECOVERY guard would emit seven spurious failures on a perfectly
|
|
// correct port. It also passes silently when a dataset is missing from both
|
|
// inputs. Leave it alone; it remains valid for the historical check it was
|
|
// written for.
|
|
//
|
|
// Uses node:sqlite, already the repo's only SQLite client — no new dependency,
|
|
// and no sqlite3 CLI (there isn't one on this box).
|
|
//
|
|
// Usage:
|
|
// node go-parser/scripts/differential-parity.mjs \
|
|
// --rust /tmp/rust-{id}.db --go /tmp/go-{id}.db \
|
|
// [--rust-stdout /tmp/rust-{id}.stdout --go-stdout /tmp/go-{id}.stdout]
|
|
//
|
|
// {id} is substituted per dataset. Exits non-zero on any mismatch.
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
const DATASETS = ["2016", "2017", "2017-old", "2017-old2"];
|
|
|
|
function arg(name, fallback) {
|
|
const i = process.argv.indexOf(name);
|
|
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
|
}
|
|
|
|
const rustPattern = arg("--rust", "/tmp/rust-{id}.db");
|
|
const goPattern = arg("--go", "/tmp/go-{id}.db");
|
|
const rustOutPattern = arg("--rust-stdout", "/tmp/rust-{id}.stdout");
|
|
const goOutPattern = arg("--go-stdout", "/tmp/go-{id}.stdout");
|
|
const expand = (p, id) => p.replace("{id}", id);
|
|
|
|
const failures = [];
|
|
const fail = (ds, msg) => failures.push(`${ds}: ${msg}`);
|
|
|
|
/** Column names in declaration order, from the table itself. */
|
|
function columns(db) {
|
|
return db.prepare("PRAGMA table_info(student)").all().map((r) => r.name);
|
|
}
|
|
|
|
/** PRAGMA metadata, normalised to a comparable string. */
|
|
function schemaSignature(db) {
|
|
const cols = db
|
|
.prepare("PRAGMA table_info(student)")
|
|
.all()
|
|
.map((r) => `${r.cid}:${r.name}:${r.type}:${r.notnull}:${r.pk}`)
|
|
.join("|");
|
|
const idx = db
|
|
.prepare("PRAGMA index_list(student)")
|
|
.all()
|
|
.map((r) => `${r.name}:${r.unique}:${r.partial}`)
|
|
.sort()
|
|
.join("|");
|
|
return `${cols}\n${idx}`;
|
|
}
|
|
|
|
/**
|
|
* Serialise one value so the hash is stable across drivers.
|
|
*
|
|
* Rust and Go embed different SQLite versions, so byte-identical files are
|
|
* impossible by construction; what must match is the logical content. NULL gets
|
|
* an explicit sentinel that no real value can collide with, and REALs are
|
|
* rendered with a fixed rule rather than whatever each driver's default
|
|
* formatting happens to be.
|
|
*/
|
|
function ser(v) {
|
|
if (v === null || v === undefined) return " |