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,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;
|
||||
}
|
||||
Reference in new issue
Block a user