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