mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-10-11 03:13:48 +00:00
The repository now reads as the pipeline it is: crawler fetches, parser converts, assembler verifies and publishes, with data/ and web/ as the stores they hand work through. go-parser is renamed parser now that there is no other. The assembler replaces build-db.js and assemble-site.js. It compiles the parser, builds and verifies each database, compresses it, runs the Vite build and assembles _site — one command, and the only place that knows the order. It also closes a real hole: nothing previously asserted that a database reached the site. An empty staging directory assembled happily, so every page rendered, every query 404d and CI stayed green. The row-count and size guards could not catch that, since they only run when a database was built at all. Removing Node from the root forced the dataset list out of web/src/datasets.js, which the assembler cannot import. datasets.json is now the registry both sides read — JSON because Go and the browser both parse it without a dependency — while presentation stays in the web app, keyed by id and cross-checked against the registry so a half-added dataset fails instead of half-working. Guards verified by making each one fail: a missing database, and an expected row count one higher than the truth.
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 " |