Add repeatable verification and establish fresh save checkpoint

Consolidate current readiness and existing rule decisions, preserve selective
subagents and automatic verified Git pushes, and record the efficiency pilot.
Add source-bound compact checks, private saved-game snapshots/online backups,
and bounded HTTP seat-restoration checks with 15 meaningful regressions.

At the user's explicit request, archive and clear 3,644 old local rooms and
related records while retaining schema/migrations; preserve future games from
a fresh empty baseline. Private backups and credentials are outside Git.

Validation: npm run check passes types, lint and all 4,177 offline tests.
Full report: 20260913-efficiency-check-3, 130.147 seconds; source fingerprint
344ce1d44475b0cd4ff10a915f28331cb4e2fcacbc7595636fbaa645c0568f64.
Two earlier broad attempts stopped at new type/lint errors before tests;
both were fixed. Independent review found cancellation falsely passing;
SIGINT/SIGTERM regressions and review confirm the fix. Documentation links,
example test selection, private backup integrity and empty baseline verified.
No app/build/dependency/HTTP implementation changed; no production build,
HTTP suite, browser acceptance or AI calibration claimed for this checkpoint.
The full game goal and existing release gates remain unfinished.
This commit is contained in:
Quintine committed 2026-09-13 10:39:34 +10:00
1 parent 06d0a66fc5
commit 6e26ba5daa
17 files changed
+1242 -2

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import {
checkSeatRestoration,
loadRestorationConfig,
} from './seat-restoration';
async function main() {
const { values } = parseArgs({
options: { config: { type: 'string' }, help: { type: 'boolean' } },
});
if (values.help) {
console.log(
'Usage: node --import tsx tools/restore-seats.ts --config /private/qa-seats.json\nRequires a private manifest outside the checkout and isolated, stable human QA rooms on a loopback server. GET can resume automatic game work. This checks HTTP restoration, not browser rendering.',
);
return;
}
if (!values.config) throw new Error();
const config = loadRestorationConfig(
values.config,
fileURLToPath(new URL('../', import.meta.url)),
);
console.log(JSON.stringify(await checkSeatRestoration(config)));
}
main().catch(() => {
console.error(
'Private-seat restoration failed. Check the private manifest, stable QA versions and local server; see --help.',
);
process.exitCode = 1;
});
+112
View File
@@ -0,0 +1,112 @@
import * as sqlite from 'node:sqlite';
import { createHash } from 'node:crypto';
import { chmodSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { privateOutputDirectory } from './verification';
type RoomProof = { code: string; version: number; hash: string };
export type SavedGameSnapshot = {
format: 1;
capturedAt: string;
rooms: RoomProof[];
};
export function readSavedGames(path: string): SavedGameSnapshot {
const db = new sqlite.DatabaseSync(path, { readOnly: true });
try {
const rows = db
.prepare('SELECT code, version, state FROM rooms ORDER BY code')
.all();
const rooms = rows.map((row) => {
if (
typeof row.code !== 'string' ||
!Number.isSafeInteger(row.version) ||
typeof row.state !== 'string'
)
throw new Error('Unexpected saved-game schema.');
return {
code: row.code,
version: row.version as number,
hash: createHash('sha256').update(row.state).digest('hex'),
};
});
return { format: 1, capturedAt: new Date().toISOString(), rooms };
} finally {
db.close();
}
}
export function compareSavedGames(
baseline: SavedGameSnapshot,
current: SavedGameSnapshot,
) {
for (const snapshot of [baseline, current]) {
if (
snapshot?.format !== 1 ||
!Array.isArray(snapshot.rooms) ||
snapshot.rooms.some(
(r) =>
!r ||
typeof r.code !== 'string' ||
!Number.isSafeInteger(r.version) ||
!/^[0-9a-f]{64}$/.test(r.hash),
) ||
new Set(snapshot.rooms.map((r) => r.code)).size !== snapshot.rooms.length
)
throw new Error('Invalid saved-game baseline.');
}
const now = new Map(current.rooms.map((r) => [r.code, r])),
before = new Set(baseline.rooms.map((r) => r.code));
const missing = baseline.rooms
.filter((r) => !now.has(r.code))
.map((r) => r.code);
const changed = baseline.rooms
.filter(
(r) =>
now.has(r.code) &&
(now.get(r.code)!.version !== r.version ||
now.get(r.code)!.hash !== r.hash),
)
.map((r) => r.code);
return {
original: baseline.rooms.length,
current: current.rooms.length,
added: current.rooms.filter((r) => !before.has(r.code)).length,
missing,
changed,
preserved: !missing.length && !changed.length,
};
}
export async function captureSavedGames(
root: string,
database: string,
output: string,
withBackup = false,
) {
// Validate without creating a missing database or destination directory.
readSavedGames(database);
if (withBackup && typeof sqlite.backup !== 'function')
throw new Error('Online backup requires Node.js 22.16 or newer.');
const directory = privateOutputDirectory(root, output);
let snapshot: SavedGameSnapshot;
if (withBackup) {
const db = new sqlite.DatabaseSync(database, { readOnly: true });
const destination = resolve(directory, 'games.sqlite');
try {
await sqlite.backup(db, destination);
} finally {
db.close();
}
chmodSync(destination, 0o600);
snapshot = readSavedGames(destination); // Bind the baseline to the actual consistent backup.
} else snapshot = readSavedGames(database);
writeFileSync(
resolve(directory, 'snapshot.json'),
JSON.stringify(snapshot, null, 2) + '\n',
{ flag: 'wx', mode: 0o600 },
);
return snapshot;
}
export function loadSavedGameSnapshot(path: string): SavedGameSnapshot {
const snapshot = JSON.parse(readFileSync(path, 'utf8')) as SavedGameSnapshot;
compareSavedGames(snapshot, snapshot);
return snapshot;
}
+67
View File
@@ -0,0 +1,67 @@
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import {
captureSavedGames,
compareSavedGames,
loadSavedGameSnapshot,
readSavedGames,
} from './saved-game-verification';
async function main() {
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
db: { type: 'string' },
out: { type: 'string' },
baseline: { type: 'string' },
backup: { type: 'boolean' },
help: { type: 'boolean' },
},
});
if (values.help) {
console.log(
'Usage: node --import tsx tools/saved-games.ts snapshot --db PATH --out /tmp/new-snapshot [--backup]\nOr: compare --db PATH --baseline /tmp/new-snapshot/snapshot.json\nRead-only source access. New rooms are allowed; any changed or missing original room fails. Backup requires Node >=22.16.',
);
return;
}
if (!values.db || positionals.length !== 1)
throw new Error('Provide a database and operation.');
if (positionals[0] === 'snapshot' && values.out && !values.baseline) {
const result = await captureSavedGames(
fileURLToPath(new URL('../', import.meta.url)),
values.db,
values.out,
values.backup,
);
console.log(
JSON.stringify({
captured: result.rooms.length,
backup: !!values.backup,
}),
);
} else if (
positionals[0] === 'compare' &&
values.baseline &&
!values.out &&
!values.backup
) {
const result = compareSavedGames(
loadSavedGameSnapshot(values.baseline),
readSavedGames(values.db),
);
console.log(
JSON.stringify({
...result,
missing: result.missing.length,
changed: result.changed.length,
}),
);
process.exitCode = result.preserved ? 0 : 1;
} else throw new Error('Invalid operation.');
}
main().catch(() => {
console.error(
'Saved-game verification failed. Check arguments, schema and file access; see --help.',
);
process.exitCode = 1;
});
+130
View File
@@ -0,0 +1,130 @@
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, relative, sep } from 'node:path';
type Seat = { code: string; playerId: string; version: number; token: string };
export type RestorationConfig = {
baseUrl: string;
isolatedQa: true;
seats: Seat[];
};
export function loadRestorationConfig(
path: string,
root: string,
): RestorationConfig {
const stat = lstatSync(path),
rel = relative(realpathSync(root), realpathSync(path));
if (
!stat.isFile() ||
(stat.mode & 0o077) !== 0 ||
(!rel.startsWith('..' + sep) && rel !== '..' && !isAbsolute(rel))
)
throw new Error(
'Seat credentials require a private file outside the checkout.',
);
return JSON.parse(readFileSync(path, 'utf8')) as RestorationConfig;
}
export async function checkSeatRestoration(
config: RestorationConfig,
request: typeof fetch = fetch,
) {
// These GETs can resume automatic server work. Only deliberately isolated QA
// fixtures are appropriate; callers must never probe arbitrary human games.
const url = new URL(config.baseUrl);
if (
!['http:', 'https:'].includes(url.protocol) ||
!['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) ||
url.username ||
url.password ||
url.pathname !== '/' ||
url.search ||
url.hash ||
config.isolatedQa !== true
)
throw new Error('Use an explicit loopback origin and isolated QA seats.');
if (
!Array.isArray(config.seats) ||
!config.seats.length ||
config.seats.some(
(s) =>
!s ||
!/^[A-Z2-9]{8}$/.test(s.code) ||
typeof s.playerId !== 'string' ||
!s.playerId ||
!Number.isSafeInteger(s.version) ||
s.version < 0 ||
typeof s.token !== 'string' ||
!/^[A-Za-z0-9_-]+$/.test(s.token),
)
)
throw new Error('Invalid seat manifest.');
for (const [index, seat] of config.seats.entries()) {
try {
const response = await request(new URL(`/api/rooms/${seat.code}`, url), {
method: 'GET',
headers: { cookie: `dune_${seat.code}=${seat.token}` },
redirect: 'error',
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw new Error();
const view = await response.json();
if (
!view ||
typeof view !== 'object' ||
!('players' in view) ||
!('code' in view) ||
!('me' in view) ||
!('version' in view) ||
view.code !== seat.code ||
view.me !== seat.playerId ||
view.version !== seat.version ||
!Array.isArray(view.players) ||
('botsPending' in view && view.botsPending)
)
throw new Error();
if (
view.players.some(
(p: unknown) =>
!p ||
typeof p !== 'object' ||
!('id' in p) ||
typeof p.id !== 'string',
)
)
throw new Error();
const own = view.players.filter(
(p: { id?: string }) => p.id === seat.playerId,
);
if (
own.length !== 1 ||
!Array.isArray(own[0].hand) ||
!Array.isArray(own[0].traitors) ||
!Number.isFinite(own[0].spice)
)
throw new Error();
const privateFields = [
'hand',
'traitors',
'traitorChoices',
'spice',
'faceDancers',
'prediction',
'bribes',
];
if (
view.players.some(
(p: Record<string, unknown>) =>
p.id !== seat.playerId && privateFields.some((key) => key in p),
)
)
throw new Error();
} catch {
// Never echo response bodies, session cookies or private hands.
throw new Error(
`Seat ${index + 1} failed restoration, version or privacy checks.`,
);
}
}
return { checked: config.seats.length, passed: true };
}
+177
View File
@@ -0,0 +1,177 @@
import { spawn, execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
closeSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
readlinkSync,
realpathSync,
writeFileSync,
} from 'node:fs';
import {
resolve,
relative,
isAbsolute,
dirname,
basename,
sep,
} from 'node:path';
export function sourceSnapshot(root: string) {
const git = (...args: string[]) =>
execFileSync('git', args, {
cwd: root,
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
});
const files = [
...new Set(
git('ls-files', '-z', '--cached', '--others', '--exclude-standard')
.split('\0')
.filter(Boolean),
),
].sort();
const digest = createHash('sha256');
for (const file of files) {
const path = resolve(root, file);
digest.update(JSON.stringify(file));
try {
const stat = lstatSync(path);
digest.update(
JSON.stringify([
stat.mode,
createHash('sha256')
.update(
stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path),
)
.digest('hex'),
]),
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
digest.update('deleted');
}
}
return {
commit: git('rev-parse', 'HEAD').trim(),
tree: digest.digest('hex'),
files: files.length,
};
}
export function privateOutputDirectory(root: string, output: string) {
// Resolve existing ancestors so a symlink cannot redirect artifacts into source.
const path = resolve(
realpathSync(dirname(resolve(output))),
basename(output),
);
const rel = relative(realpathSync(root), path);
if (!rel || (!rel.startsWith('..' + sep) && rel !== '..' && !isAbsolute(rel)))
throw new Error('Write verification artifacts outside the checkout.');
mkdirSync(path, { mode: 0o700 }); // Exclusive: existing results are never overwritten.
return path;
}
export type VerificationStep = {
name: string;
command: string;
args: string[];
};
export async function runVerification(
root: string,
output: string,
steps: VerificationStep[],
) {
if (!steps.length) throw new Error('Choose at least one verification step.');
if (
steps.some((s) => !/^[a-z0-9-]+$/.test(s.name)) ||
new Set(steps.map((s) => s.name)).size !== steps.length
)
throw new Error('Verification steps need unique simple names.');
const directory = privateOutputDirectory(root, output);
const report = {
format: 1,
startedAt: new Date().toISOString(),
finishedAt: '',
before: sourceSnapshot(root),
after: null as ReturnType<typeof sourceSnapshot> | null,
status: 'running' as 'running' | 'passed' | 'failed' | 'source-changed',
steps: [] as {
name: string;
command: string;
args: string[];
log: string;
exitCode: number;
durationMs: number;
}[],
};
const save = () =>
writeFileSync(
resolve(directory, 'report.json'),
JSON.stringify(report, null, 2) + '\n',
{ mode: 0o600 },
);
save();
for (const step of steps) {
const log = `${step.name}.log`,
fd = openSync(resolve(directory, log), 'wx', 0o600),
start = Date.now();
let exitCode = 1;
try {
const child = spawn(step.command, step.args, {
cwd: root,
stdio: ['ignore', fd, fd],
shell: false,
});
let interrupted = 0;
const interrupt = () => {
interrupted = 130;
child.kill('SIGINT');
},
terminate = () => {
interrupted = 143;
child.kill('SIGTERM');
};
process.on('SIGINT', interrupt);
process.on('SIGTERM', terminate);
try {
exitCode = await new Promise<number>((resolveExit) => {
child.once('error', () => resolveExit(1));
child.once('close', (code, signal) =>
resolveExit(
interrupted || (code ?? (signal === 'SIGINT' ? 130 : 1)),
),
);
});
} finally {
process.off('SIGINT', interrupt);
process.off('SIGTERM', terminate);
}
} finally {
closeSync(fd);
}
report.steps.push({
...step,
log,
exitCode,
durationMs: Date.now() - start,
});
save();
console.log(
`${step.name}: ${exitCode === 0 ? 'passed' : 'FAILED'} (${report.steps.at(-1)!.durationMs} ms); ${resolve(directory, log)}`,
);
if (exitCode !== 0) break;
}
report.after = sourceSnapshot(root);
report.finishedAt = new Date().toISOString();
report.status = report.steps.some((s) => s.exitCode !== 0)
? 'failed'
: report.before.tree !== report.after.tree ||
report.before.commit !== report.after.commit
? 'source-changed'
: 'passed';
save();
return report;
}
+84
View File
@@ -0,0 +1,84 @@
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { resolve } from 'node:path';
import { runVerification, type VerificationStep } from './verification';
async function main() {
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
out: { type: 'string' },
focus: { type: 'string', multiple: true },
games: { type: 'string' },
seed: { type: 'string' },
help: { type: 'boolean' },
},
});
if (values.help) {
console.log(
'Usage: node --import tsx tools/verify.ts --out /tmp/new-run check [build] [integration]\nOr: --out /tmp/new-run focused --focus fragment [--focus fragment]\nOr: --out /tmp/new-run base-games --games 20 --seed 20260913\nLogs and source-bound report.json stay outside the checkout. Commands stop on failure; changed source invalidates success.',
);
return;
}
if (!values.out || !positionals.length)
throw new Error('Provide --out and verification steps; see --help.');
if (values.focus?.some((fragment) => !fragment || fragment.startsWith('-')))
throw new Error('--focus accepts filename fragments, not runner options.');
const root = fileURLToPath(new URL('../', import.meta.url));
const steps: VerificationStep[] = positionals.map((name) => {
if (['check', 'build', 'integration'].includes(name))
return {
name,
command: 'npm',
args: ['run', name === 'integration' ? 'test:integration' : name],
};
if (name === 'focused' && values.focus?.length)
return { name, command: 'npm', args: ['test', '--', ...values.focus] };
if (name === 'base-games') {
const games = Number(values.games),
seed = Number(values.seed);
if (
!Number.isSafeInteger(games) ||
games < 1 ||
games > 120 ||
!Number.isSafeInteger(seed) ||
seed < 1 ||
seed > 0xffffffff
)
throw new Error(
'base-games requires --games 1..120 and --seed 1..4294967295.',
);
return {
name,
command: process.execPath,
args: [
'--import',
'tsx',
'tools/ai-calibration.ts',
'--games',
String(games),
'--seed',
String(seed),
'--out',
resolve(values.out!, 'base-games.json'),
],
};
}
throw new Error(`Unknown or incomplete step: ${name}`);
});
if (values.focus && !positionals.includes('focused'))
throw new Error('--focus requires the focused step.');
if ((values.games || values.seed) && !positionals.includes('base-games'))
throw new Error('--games/--seed require base-games.');
const report = await runVerification(root, values.out, steps);
console.log(
`Verification ${report.status}; ${resolve(values.out, 'report.json')}`,
);
process.exitCode = report.status === 'passed' ? 0 : 1;
}
main().catch(() => {
console.error(
'Verification could not complete. Check arguments, repository and output location; see --help.',
);
process.exitCode = 1;
});