Simplify telemetry to three game events

This commit is contained in:
2026-09-18 15:35:21 -04:00
parent 6d903adb31
commit 5bb49a776e
29 changed files with 658 additions and 1314 deletions
+38
View File
@@ -3,6 +3,8 @@ import { mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { config } from '../config.js';
import schemaSql from './schema.sql?raw';
import { requirement, type BetaPlan, type BetaRequirement } from '../../beta-plan.js';
import { emptyMetrics } from '../fleet/beta-evaluation.js';
let _db: Database.Database | null = null;
@@ -190,6 +192,40 @@ function assertSnapshotSchemeSafe(handle: Database.Database): void {
handle.prepare("INSERT INTO settings (key, value) VALUES ('snapshots_v1', '1')").run();
}
// Preserve old plans as reviewable checklists instead of silently passing checks
// that the three-event protocol can no longer perform. Existing evidence stays
// in history; active progress starts fresh under the new gameplay contract.
function migrateGameTelemetry(handle: Database.Database): void {
if (handle.prepare("SELECT 1 FROM settings WHERE key = 'game_telemetry_v2'").get()) return;
handle.transaction(() => {
const convert = (raw: string): string => {
const plan = JSON.parse(raw) as BetaPlan;
plan.requirements = plan.requirements.map(r => {
const { event: _event, ...rest } = r as BetaRequirement & { event?: string };
return ['feature', 'restart-budget', 'clean-logs'].includes(r.kind)
? { ...requirement('manual', r.id), label: `Review: ${r.label}`.slice(0, 200) } : rest;
});
return JSON.stringify(plan);
};
const plans = handle.prepare('SELECT beta_id, plan FROM beta_test_plans').all() as Array<{ beta_id: number; plan: string }>;
for (const row of plans) handle.prepare('UPDATE beta_test_plans SET plan = ? WHERE beta_id = ?').run(convert(row.plan), row.beta_id);
const presets = handle.prepare('SELECT name, plan FROM beta_test_presets').all() as Array<{ name: string; plan: string }>;
for (const row of presets) handle.prepare('UPDATE beta_test_presets SET plan = ? WHERE name = ?').run(convert(row.plan), row.name);
handle.prepare(`UPDATE beta_test_runs SET metrics = ?, cursors = '{}' WHERE retired_at IS NULL
AND beta_id IN (SELECT id FROM betas WHERE state IN ('running','ready-promote'))`).run(JSON.stringify(emptyMetrics()));
handle.exec(`UPDATE beta_test_plans SET outcome = 'active', reason = 'Gameplay telemetry updated; collecting fresh script events'
WHERE outcome IN ('active','passed') AND beta_id IN (SELECT id FROM betas WHERE state IN ('running','ready-promote'));
UPDATE script_versions SET state = 'untested' WHERE state = 'tested'
AND id IN (SELECT bs.script_version_id FROM beta_scripts bs JOIN beta_test_plans p ON p.beta_id = bs.beta_id WHERE p.outcome = 'active')
AND id NOT IN (SELECT current_version_id FROM scripts WHERE current_version_id IS NOT NULL)
AND NOT EXISTS (SELECT 1 FROM beta_scripts bs JOIN betas b ON b.id = bs.beta_id
WHERE bs.script_version_id = script_versions.id AND b.state = 'promoted');
UPDATE betas SET state = 'running' WHERE state = 'ready-promote' AND id IN (SELECT beta_id FROM beta_test_plans WHERE outcome = 'active');
UPDATE beta_servers SET stage = 'observing' WHERE stage = 'ready' AND beta_id IN (SELECT beta_id FROM beta_test_plans WHERE outcome = 'active');
INSERT INTO settings (key, value) VALUES ('game_telemetry_v2', '1');`);
})();
}
export function db(): Database.Database {
if (_db) return _db;
@@ -212,6 +248,7 @@ export function db(): Database.Database {
migrateTable(handle, 'beta_scripts', BETA_SCRIPTS_COLUMNS);
migrateTable(handle, 'deployment_scripts', DEPLOYMENT_SCRIPTS_COLUMNS);
migrateTable(handle, 'deployments', DEPLOYMENTS_COLUMNS);
migrateTable(handle, 'server_telemetry', [{ name: 'payload', ddl: 'ALTER TABLE server_telemetry ADD COLUMN payload TEXT' }]);
// Kind-referencing indexes: safe here because migrateTable above just
// ALTER'd the column into existence if it was missing.
handle.exec(
@@ -221,6 +258,7 @@ export function db(): Database.Database {
handle.exec(`CREATE INDEX IF NOT EXISTS idx_scripts_kind ON scripts(kind)`);
backfillMultiScriptJoins(handle);
assertSnapshotSchemeSafe(handle);
migrateGameTelemetry(handle);
} catch (err) {
handle.close();
throw err;
+4 -3
View File
@@ -39,8 +39,8 @@ CREATE TABLE IF NOT EXISTS servers (
-- and resume from the same anchor when the server comes back.
active_since TEXT, -- ISO ts of last 0 -> >0 player transition
inactive_since TEXT, -- ISO ts of last >0 -> 0 player transition
game_started_at TEXT, -- wall-clock ISO when we first saw a J; line for the current match cycle
game_ended_at TEXT, -- wall-clock ISO when we first saw ExitLevel/ShutdownGame end it
game_started_at TEXT, -- receipt time of the script's game_start
game_ended_at TEXT, -- receipt time of the script's game_end
game_cycle_key TEXT, -- opaque marker for the currently-tracked cycle (the first J; line after the latest InitGame); lets us detect same-players game restarts
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
@@ -314,7 +314,8 @@ CREATE TABLE IF NOT EXISTS server_telemetry (
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
round INTEGER NOT NULL,
players INTEGER NOT NULL,
received_at TEXT NOT NULL
received_at TEXT NOT NULL,
payload TEXT
);
-- Lifecycle history deliberately survives removal of the server registry row.
CREATE TABLE IF NOT EXISTS server_deployments (