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:
1 parent
06d0a66fc5
commit
6e26ba5daa
17 files changed
+1242
-2
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user