@irtio/cli 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/api.d.ts +52 -0
  2. package/dist/api.js +15 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/{chunk-GBNHBWES.js → chunk-3HQMVCYA.js} +10 -6
  5. package/dist/{chunk-32QTPKVT.js → chunk-DKWG7MGO.js} +1 -1
  6. package/dist/{chunk-KRQUAEN2.js → chunk-OTSFRVJN.js} +12 -6
  7. package/dist/chunk-RNAH5T4W.js +96 -0
  8. package/dist/chunk-RQSJZWQC.js +452 -0
  9. package/dist/chunk-UPHQM6NZ.js +72 -0
  10. package/dist/chunk-ZD4ND6X6.js +31 -0
  11. package/dist/chunk-ZK5JLUD4.js +94 -0
  12. package/dist/credentials.d.ts +61 -0
  13. package/dist/credentials.js +20 -0
  14. package/dist/delete-project-VENS2B44.js +118 -0
  15. package/dist/deploy.d.ts +149 -0
  16. package/dist/{deploy-3SABPL3T.js → deploy.js} +166 -44
  17. package/dist/{dev-QJOGXLKM.js → dev-QM26ONKS.js} +2957 -231
  18. package/dist/index.js +97 -25
  19. package/dist/init.d.ts +1 -1
  20. package/dist/init.js +20 -4
  21. package/dist/{keys-XBORZAPI.js → keys-JHLMEGRA.js} +8 -4
  22. package/dist/leaderboard-SYPSBPS3.js +352 -0
  23. package/dist/{login-3EXB4CGX.js → login-2M73HBZT.js} +12 -5
  24. package/dist/{logs-2EOXLNWF.js → logs-2W7CPZO5.js} +9 -5
  25. package/dist/{migrate-WUO2GBMX.js → migrate-T3DZJREY.js} +12 -8
  26. package/dist/ratings-VG32WFDG.js +297 -0
  27. package/dist/{rollback-GA6UY772.js → rollback-SO74MVZV.js} +9 -5
  28. package/dist/{rooms-B66LQIIF.js → rooms-VI33P4RA.js} +36 -10
  29. package/dist/simulate.d.ts +147 -4
  30. package/dist/simulate.js +680 -53
  31. package/dist/{static-deploy-5TBH4VNA.js → static-deploy-KOWFKWZA.js} +6 -4
  32. package/dist/status-HF3ZEKB7.js +219 -0
  33. package/dist/usage-4G23QXCH.js +213 -0
  34. package/dist/{whoami-CI5D5RCC.js → whoami-KTMTQNHM.js} +8 -4
  35. package/package.json +23 -7
  36. package/dist/chunk-BPE452KF.js +0 -180
  37. package/dist/chunk-TV66QHFP.js +0 -167
@@ -0,0 +1,297 @@
1
+ import {
2
+ readProjectConfig
3
+ } from "./chunk-DKWG7MGO.js";
4
+ import {
5
+ HelpRequested,
6
+ helpFor,
7
+ helpRequested
8
+ } from "./chunk-ZD4ND6X6.js";
9
+ import {
10
+ createApiClient,
11
+ isLoginRequired
12
+ } from "./chunk-RNAH5T4W.js";
13
+ import {
14
+ resolveControlUrlForUser
15
+ } from "./chunk-UPHQM6NZ.js";
16
+
17
+ // src/ratings.ts
18
+ import pc from "picocolors";
19
+ var USAGE = `usage: irtio ratings <queue> [options]
20
+ irtio ratings list [options]
21
+ irtio ratings delete <queue> --player <id> --yes [options]
22
+
23
+ Prints a project's skill ratings for one queue, best first. Needs your login: a rating is a
24
+ number the matchmaker uses, not a scoreboard for players to read.
25
+
26
+ There is no submit command. A rating moves only from room code (room.ratings.report), which
27
+ is what makes it mean anything.
28
+
29
+ list every queue with ratings on the project, with its row count
30
+ delete remove one player's rating in one queue. Needs --player, your login and --yes;
31
+ without --yes it prints what would go and changes nothing.
32
+
33
+ options:
34
+ --limit <n> how many rows (default 20, max 100)
35
+ --cursor <c> continue from a previous page's nextCursor
36
+ --all page through the whole queue, following cursors
37
+ --player <id> with delete: whose rating to remove
38
+ --yes with delete: actually delete
39
+ --project <id> project id (default: the project file)
40
+ -c, --config <f> the project file to read (default irtio.json)
41
+ --url <control> control plane (default: your stored login)
42
+ -h, --help print this
43
+ `;
44
+ function parseRatingsArgs(args) {
45
+ if (helpRequested(args)) throw helpFor(USAGE);
46
+ const parsed = {};
47
+ if (args[0] === "list" || args[0] === "delete") parsed.action = args[0];
48
+ for (let i = parsed.action === void 0 ? 0 : 1; i < args.length; i++) {
49
+ const arg = args[i];
50
+ switch (arg) {
51
+ case "--cursor": {
52
+ const value = args[++i];
53
+ if (value === void 0 || value === "") {
54
+ throw new Error("irtio ratings: --cursor needs a value");
55
+ }
56
+ parsed.cursor = value;
57
+ break;
58
+ }
59
+ case "--all":
60
+ parsed.all = true;
61
+ break;
62
+ case "--yes":
63
+ parsed.yes = true;
64
+ break;
65
+ case "--player": {
66
+ const value = args[++i];
67
+ if (value === void 0 || value === "") {
68
+ throw new Error("irtio ratings: --player needs a player id");
69
+ }
70
+ parsed.player = value;
71
+ break;
72
+ }
73
+ case "--limit": {
74
+ const value = Number(args[++i]);
75
+ if (!Number.isInteger(value) || value < 1) {
76
+ throw new Error("irtio ratings: --limit needs a whole number of at least 1");
77
+ }
78
+ parsed.limit = value;
79
+ break;
80
+ }
81
+ case "--project": {
82
+ const value = args[++i];
83
+ if (value === void 0) throw new Error("irtio ratings: --project needs a value");
84
+ parsed.project = value;
85
+ break;
86
+ }
87
+ case "-c":
88
+ case "--config": {
89
+ const value = args[++i];
90
+ if (value === void 0 || value === "") {
91
+ throw new Error("irtio ratings: --config needs a value");
92
+ }
93
+ parsed.config = value;
94
+ break;
95
+ }
96
+ case "--url": {
97
+ const value = args[++i];
98
+ if (value === void 0) throw new Error("irtio ratings: --url needs a value");
99
+ parsed.url = value;
100
+ break;
101
+ }
102
+ default:
103
+ if (arg.startsWith("-")) {
104
+ throw new Error(`irtio ratings: unknown option ${JSON.stringify(arg)}`);
105
+ }
106
+ if (parsed.queue !== void 0) {
107
+ throw new Error("irtio ratings: give exactly one queue name");
108
+ }
109
+ parsed.queue = arg;
110
+ }
111
+ }
112
+ return parsed;
113
+ }
114
+ function formatRatings(body) {
115
+ const out = [pc.dim(`queue ${body.queue} \xB7 Glicko-2, higher is stronger`), ""];
116
+ if (body.entries.length === 0) {
117
+ out.push(pc.dim("no ratings in this queue yet"));
118
+ out.push(pc.dim("a rating appears when room code calls room.ratings.report"));
119
+ return out;
120
+ }
121
+ const rankWidth = Math.max(4, ...body.entries.map((e) => String(e.rank).length));
122
+ const idWidth = Math.max(6, ...body.entries.map((e) => e.playerId.length));
123
+ out.push(pc.dim(`${"RANK".padEnd(rankWidth)} ${"PLAYER".padEnd(idWidth)} RATING \xB1DEV GAMES`));
124
+ for (const e of body.entries) {
125
+ out.push(
126
+ `${String(e.rank).padEnd(rankWidth)} ${e.playerId.padEnd(idWidth)} ${e.rating.toFixed(0).padStart(6)} ${e.deviation.toFixed(0).padStart(4)} ${String(e.games).padStart(5)}`
127
+ );
128
+ }
129
+ out.push("");
130
+ out.push(
131
+ pc.dim("\xB1DEV is how sure the rating is. 350 means a new player; under 80 means settled.")
132
+ );
133
+ return out;
134
+ }
135
+ async function runRatings(options) {
136
+ const log = options.log ?? ((line) => console.log(line));
137
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
138
+ const client = options.client ?? await createApiClient(controlUrl);
139
+ const base = `/v1/projects/${encodeURIComponent(options.project)}/ratings/${encodeURIComponent(
140
+ options.queue
141
+ )}`;
142
+ const pageUrl = (cursor) => {
143
+ const query = new URLSearchParams();
144
+ if (options.limit !== void 0) query.set("limit", String(options.limit));
145
+ if (cursor !== void 0) query.set("cursor", cursor);
146
+ const q = query.toString();
147
+ return `${base}${q === "" ? "" : `?${q}`}`;
148
+ };
149
+ let body = await client.get(pageUrl(options.cursor));
150
+ const entries = [...body.entries];
151
+ if (options.all === true) {
152
+ let next = body.nextCursor;
153
+ for (let page = 1; next !== void 0 && page < 100; page++) {
154
+ body = await client.get(pageUrl(next));
155
+ for (const entry of body.entries) {
156
+ entries.push({ ...entry, rank: entries.length + 1 });
157
+ }
158
+ next = body.nextCursor;
159
+ }
160
+ if (next !== void 0) {
161
+ throw new Error(
162
+ "irtio ratings: --all stopped after a hundred pages. Read it in pieces with --cursor"
163
+ );
164
+ }
165
+ }
166
+ const combined = { ...body, entries };
167
+ for (const line of formatRatings(combined)) log(line);
168
+ if (options.all !== true && body.nextCursor !== void 0) {
169
+ log("");
170
+ log(pc.dim(`more rows: --cursor ${body.nextCursor}`));
171
+ }
172
+ return combined;
173
+ }
174
+ function formatQueueList(queues) {
175
+ if (queues.length === 0) {
176
+ return [
177
+ pc.dim("no ratings on this project yet"),
178
+ pc.dim("a rating reaches a queue from room code: room.ratings.report")
179
+ ];
180
+ }
181
+ const nameWidth = Math.max(5, ...queues.map((q) => q.queue.length));
182
+ const out = [pc.dim(`${"QUEUE".padEnd(nameWidth)} ${"ROWS".padStart(7)} MATCHING`)];
183
+ for (const q of queues) {
184
+ const matching = q.skill ? "skill" : "fifo";
185
+ const note = q.configured && q.rows === 0 ? pc.dim(" (configured, no ratings yet)") : !q.configured ? pc.dim(" (ratings only; not a configured queue)") : !q.skill ? pc.dim(" (ratings recorded, but this queue fills in arrival order)") : "";
186
+ out.push(`${q.queue.padEnd(nameWidth)} ${String(q.rows).padStart(7)} ${matching}${note}`);
187
+ }
188
+ return out;
189
+ }
190
+ async function runQueueList(options) {
191
+ const log = options.log ?? ((line) => console.log(line));
192
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
193
+ const client = options.client ?? await createApiClient(controlUrl);
194
+ const body = await client.get(
195
+ `/v1/projects/${encodeURIComponent(options.project)}/ratings`
196
+ );
197
+ for (const line of formatQueueList(body.queues)) log(line);
198
+ return body.queues;
199
+ }
200
+ async function runRatingDelete(options) {
201
+ const log = options.log ?? ((line) => console.log(line));
202
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
203
+ const client = options.client ?? await createApiClient(controlUrl);
204
+ const path = `/v1/projects/${encodeURIComponent(options.project)}/ratings/${encodeURIComponent(
205
+ options.queue
206
+ )}/${encodeURIComponent(options.player)}`;
207
+ if (!options.yes) {
208
+ log(
209
+ `this would delete ${pc.bold(options.player)}'s rating in queue ${pc.bold(options.queue)} in project ${options.project}`
210
+ );
211
+ log(pc.dim("nothing was deleted. This cannot be undone once it runs"));
212
+ log(`to proceed: irtio ratings delete ${options.queue} --player ${options.player} --yes`);
213
+ return false;
214
+ }
215
+ const body = await client.del(path);
216
+ log(
217
+ body.deleted ? pc.green(`deleted ${options.player}'s rating in queue ${options.queue}`) : pc.dim(`${options.player} had no rating in queue ${options.queue}; nothing to delete`)
218
+ );
219
+ return true;
220
+ }
221
+ async function ratings(args, deps = {}) {
222
+ const log = deps.log ?? ((line) => console.log(line));
223
+ const errorLog = deps.errorLog ?? ((line) => console.error(line));
224
+ try {
225
+ const parsed = parseRatingsArgs(args);
226
+ const config = await readProjectConfig(process.cwd(), "irtio ratings", parsed.config);
227
+ const project = parsed.project ?? config.project;
228
+ if (project === void 0) {
229
+ throw new Error(
230
+ "irtio ratings: no project id. Pass --project or run this from a project with irtio.json"
231
+ );
232
+ }
233
+ const common = {
234
+ project,
235
+ log,
236
+ ...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
237
+ ...deps.client !== void 0 ? { client: deps.client } : {}
238
+ };
239
+ if (parsed.action === "list") {
240
+ if (parsed.queue !== void 0) throw new Error("irtio ratings list: takes no queue name");
241
+ await runQueueList(common);
242
+ return;
243
+ }
244
+ if (parsed.action === "delete") {
245
+ if (parsed.queue === void 0) {
246
+ throw new Error(
247
+ "irtio ratings delete: which queue? e.g. irtio ratings delete ranked --player irt:abc --yes"
248
+ );
249
+ }
250
+ if (parsed.player === void 0) {
251
+ throw new Error("irtio ratings delete: --player is required; there is no whole-queue wipe");
252
+ }
253
+ await runRatingDelete({
254
+ ...common,
255
+ queue: parsed.queue,
256
+ player: parsed.player,
257
+ yes: parsed.yes === true
258
+ });
259
+ return;
260
+ }
261
+ if (parsed.queue === void 0) {
262
+ throw new Error("irtio ratings: which queue? e.g. irtio ratings ranked");
263
+ }
264
+ if (parsed.cursor !== void 0 && parsed.all === true) {
265
+ throw new Error("irtio ratings: --cursor and --all are two ways to do the same thing");
266
+ }
267
+ await runRatings({
268
+ ...common,
269
+ queue: parsed.queue,
270
+ ...parsed.limit !== void 0 ? { limit: parsed.limit } : {},
271
+ ...parsed.cursor !== void 0 ? { cursor: parsed.cursor } : {},
272
+ ...parsed.all !== void 0 ? { all: parsed.all } : {}
273
+ });
274
+ } catch (err) {
275
+ if (err instanceof HelpRequested) {
276
+ log(err.usage);
277
+ return;
278
+ }
279
+ if (isLoginRequired(err)) {
280
+ errorLog("irtio ratings: not logged in. Run `irtio login` first");
281
+ process.exitCode = 1;
282
+ return;
283
+ }
284
+ errorLog(err instanceof Error ? err.message : String(err));
285
+ process.exitCode = 1;
286
+ }
287
+ }
288
+ export {
289
+ USAGE,
290
+ formatQueueList,
291
+ formatRatings,
292
+ parseRatingsArgs,
293
+ ratings,
294
+ runQueueList,
295
+ runRatingDelete,
296
+ runRatings
297
+ };
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-32QTPKVT.js";
3
+ } from "./chunk-DKWG7MGO.js";
4
4
  import {
5
5
  HelpRequested,
6
- createApiClient,
7
6
  helpFor,
8
- helpRequested,
9
- isLoginRequired,
7
+ helpRequested
8
+ } from "./chunk-ZD4ND6X6.js";
9
+ import {
10
+ createApiClient,
11
+ isLoginRequired
12
+ } from "./chunk-RNAH5T4W.js";
13
+ import {
10
14
  resolveControlUrlForUser
11
- } from "./chunk-TV66QHFP.js";
15
+ } from "./chunk-UPHQM6NZ.js";
12
16
 
13
17
  // src/rollback.ts
14
18
  import "fs";
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-32QTPKVT.js";
3
+ } from "./chunk-DKWG7MGO.js";
4
4
  import {
5
5
  HelpRequested,
6
- createApiClient,
7
6
  helpFor,
8
- helpRequested,
9
- isLoginRequired,
7
+ helpRequested
8
+ } from "./chunk-ZD4ND6X6.js";
9
+ import {
10
+ createApiClient,
11
+ isLoginRequired
12
+ } from "./chunk-RNAH5T4W.js";
13
+ import {
10
14
  resolveControlUrlForUser
11
- } from "./chunk-TV66QHFP.js";
15
+ } from "./chunk-UPHQM6NZ.js";
12
16
 
13
17
  // src/rooms.ts
14
18
  import "fs";
@@ -18,15 +22,19 @@ import pc from "picocolors";
18
22
  var USAGE = `usage: irtio rooms [options]
19
23
  irtio rooms saves <room> [options]
20
24
  irtio rooms restore <room> --save <id> [options]
25
+ irtio rooms delete <room> [options]
21
26
 
22
- Lists a project's rooms. Rows not reported for longer than the control plane's retention window
23
- (30 days by default) are pruned.
27
+ Lists a project's rooms, with the retention window each one's type declares. Rows not reported for
28
+ longer than the control plane's row-pruning window (30 days by default) are pruned; that prunes the
29
+ listing only and never a room's stored state.
24
30
 
25
31
  subcommands:
26
32
  saves <room> list a room's save generations, newest first
27
33
  restore <room> --save <id>
28
34
  bring a room back from a save. This DISCARDS its current state, and
29
35
  stops the tenant if it is live
36
+ delete <room> delete a room's stored state: its snapshot, its saves and its alarms.
37
+ This is permanent. Refused while the room is awake
30
38
 
31
39
  options:
32
40
  --project <id> project id (default: the project file)
@@ -39,7 +47,7 @@ function parseRoomsArgs(args) {
39
47
  const parsed = { sub: "list" };
40
48
  let rest = args;
41
49
  const first = args[0];
42
- if (first === "saves" || first === "restore") {
50
+ if (first === "saves" || first === "restore" || first === "delete") {
43
51
  parsed.sub = first;
44
52
  const room = args[1];
45
53
  if (room === void 0 || room.startsWith("-")) {
@@ -109,10 +117,11 @@ function statusColor(status) {
109
117
  function formatTable(rows) {
110
118
  if (rows.length === 0) return [pc.dim("no rooms")];
111
119
  const idWidth = Math.max(4, ...rows.map((r) => r.roomId.length));
112
- const out = [pc.dim(`${"ROOM".padEnd(idWidth)} STATUS LAST SEEN`)];
120
+ const out = [pc.dim(`${"ROOM".padEnd(idWidth)} STATUS RETAIN LAST SEEN`)];
113
121
  for (const r of rows) {
122
+ const retention = r.retention ?? "forever";
114
123
  out.push(
115
- `${r.roomId.padEnd(idWidth)} ${statusColor(r.status).padEnd(10)} ${pc.dim(r.lastSeen)}`
124
+ `${r.roomId.padEnd(idWidth)} ${statusColor(r.status).padEnd(10)} ${retention.padEnd(6)} ${pc.dim(r.lastSeen)}`
116
125
  );
117
126
  }
118
127
  return out;
@@ -164,6 +173,20 @@ async function runRestore(options) {
164
173
  }
165
174
  return result;
166
175
  }
176
+ async function runDeleteRoom(options) {
177
+ const log = options.log ?? ((line) => console.log(line));
178
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
179
+ const client = options.client ?? await createApiClient(controlUrl);
180
+ log(
181
+ `${pc.yellow("deleting")} room ${pc.bold(options.room)}: its snapshot, its saves and its alarms`
182
+ );
183
+ const result = await client.del(
184
+ `/v1/projects/${options.project}/rooms/${encodeURIComponent(options.room)}`
185
+ );
186
+ log(pc.green(`deleted ${options.room}: ${result.objects} stored object(s) removed`));
187
+ log(pc.dim("player data and leaderboard scores are keyed to the player and are untouched"));
188
+ return result;
189
+ }
167
190
  async function runRooms(options) {
168
191
  const log = options.log ?? ((line) => console.log(line));
169
192
  const controlUrl = await resolveControlUrlForUser(options.controlUrl);
@@ -192,6 +215,8 @@ async function rooms(args, deps = {}) {
192
215
  };
193
216
  if (parsed.sub === "saves") {
194
217
  await runSaves({ ...common, room: parsed.room });
218
+ } else if (parsed.sub === "delete") {
219
+ await runDeleteRoom({ ...common, room: parsed.room });
195
220
  } else if (parsed.sub === "restore") {
196
221
  await runRestore({
197
222
  ...common,
@@ -220,6 +245,7 @@ export {
220
245
  USAGE,
221
246
  parseRoomsArgs,
222
247
  rooms,
248
+ runDeleteRoom,
223
249
  runRestore,
224
250
  runRooms,
225
251
  runSaves
@@ -1,6 +1,46 @@
1
- import { RunEnd, SimulationReport, TickHealthReading } from '@irtio/bots';
1
+ import { ScenarioDefinition, BotConditions, NetworkConditions, AssertionResult, RunEnd, SimulationReport, HitRow, TruthDiff, TickHealthReading } from '@irtio/bots';
2
2
  import { AnySchema } from '@irtio/schema';
3
3
 
4
+ /**
5
+ * D41: loading a scenario file, and reading the recorded authoritative timeline back off the dev
6
+ * server.
7
+ *
8
+ * Loading follows `loadProjectSchema`'s pattern exactly: esbuild the user's TypeScript to ESM,
9
+ * dynamic-import it with a cache-busting query, and shape-check the export. `bundleRoom` is the
10
+ * wrong tool here for the same reason it is wrong for a schema module, only more so: it enforces
11
+ * the room sandbox's fixed import menu, and a scenario's whole point is that it imports
12
+ * `@irtio/bots`.
13
+ *
14
+ * Reading follows the tick counters' pattern exactly: the dev server's own inspector routes, on
15
+ * the socket's port. A deployed tenant has no authoritative tap, so it has no timeline either,
16
+ * and this module says so rather than inventing one from what a client received.
17
+ */
18
+
19
+ /**
20
+ * A scenario that could not be loaded, or a timeline that could not be recorded. Both are "the
21
+ * run was not performed" rather than "the room failed": nothing was measured either way, and
22
+ * reporting a scenario as passing because its assertions never ran would be the worst outcome
23
+ * this command can produce.
24
+ */
25
+ declare class ScenarioNotRunError extends Error {
26
+ readonly cause?: unknown | undefined;
27
+ readonly name = "ScenarioNotRunError";
28
+ constructor(message: string, cause?: unknown | undefined);
29
+ }
30
+ interface LoadScenarioOptions {
31
+ readonly cwd: string;
32
+ readonly outDir: string;
33
+ /** The scenario file, absolute or relative to `cwd`. */
34
+ readonly file: string;
35
+ /** Redirects `@irtio/*` for the bundle; the monorepo's own `src/` by default. */
36
+ readonly irtioPackages?: Record<string, string> | undefined;
37
+ }
38
+ /** Bundles the scenario module and returns its default export. */
39
+ declare function loadScenario(options: LoadScenarioOptions): Promise<{
40
+ scenario: ScenarioDefinition;
41
+ file: string;
42
+ }>;
43
+
4
44
  /**
5
45
  * `irtio simulate`: point N real clients at a running room, play it randomly for a few
6
46
  * seconds, and print a pass/fail line per built-in invariant.
@@ -19,16 +59,29 @@ interface SimulateArgs {
19
59
  bots: number;
20
60
  seconds: number;
21
61
  cheat: boolean;
62
+ /** D42: bots named by `--cheat-bot`. Empty means "whatever `cheat` says". */
63
+ cheatBots?: number[];
64
+ /** D42: `--conditions`, applied to every bot. */
65
+ conditions?: NetworkConditions;
66
+ /** D42: `--conditions-bot <index>:<json>`, overriding `conditions` for those indices. */
67
+ conditionsPerBot?: Record<number, NetworkConditions>;
68
+ /** D43: `--truth`, take a save at the end of the run and diff it against the clients. */
69
+ truth?: boolean;
22
70
  room?: string;
23
71
  url?: string;
24
72
  key?: string;
25
73
  trace?: string;
74
+ scenario?: string;
26
75
  mispredictionMax?: number;
27
76
  snapsMax?: number;
28
77
  correctionsMax?: number;
29
78
  overrunsMax?: number;
79
+ /** D65: give every bot a bandwidth ledger and print the run's breakdown. */
80
+ profile?: boolean;
81
+ /** D65: rows in that table. Implies `profile`. */
82
+ profileTop?: number;
30
83
  }
31
- declare const USAGE = "usage: irtio simulate [options]\n\nDrives N real clients at a running room and checks the built-in invariants. Run it from a project\ndirectory so it can load irtio/schema.ts; without one it falls back to a relay run.\n\noptions:\n --bots <n> how many clients to spawn (default 5)\n --seconds <n> how long to play for (default 10)\n --room <code> join this room instead of creating one\n --url <ws://...> where to connect (default ws://localhost:7070)\n --key <projectKey> the project key to present\n --trace <path> write the frame trace here\n --cheat send illegal writes and expect corrections\n --misprediction-max <units> fail if one correction snaps a prediction further than this\n --snaps-max <n> fail above this many cap-exceeded reconciliations\n --corrections-max <perSec> fail above this correction rate per bot\n --overruns-max <n> fail above this many server tick overruns in the run window\n (default 0; the count is read from the server, and the run says\n so when it could not be read)\n -h, --help print this\n\nexit codes: 0 every invariant held, 1 something was measured and failed, 2 the run could not be\nperformed (nobody joined, or it passed its wall-clock ceiling).\n";
84
+ declare const USAGE = "usage: irtio simulate [options]\n\nDrives N real clients at a running room and checks the built-in invariants. Run it from a project\ndirectory so it can load irtio/schema.ts; without one it falls back to a relay run.\n\noptions:\n --bots <n> how many clients to spawn (default 5)\n --seconds <n> how long to play for (default 10)\n --room <code> join this room instead of creating one\n --url <ws://...> where to connect (default ws://localhost:7070)\n --key <projectKey> the project key to present\n --trace <path> write the frame trace here\n --scenario <file> run a scenario module and assert against the recorded timeline\n --cheat send illegal writes and expect corrections, from every bot\n --cheat-bot <index> only this bot cheats (repeatable)\n --conditions <json> inject network conditions into every bot, e.g.\n '{\"rttMs\":200,\"jitterMs\":20,\"loss\":0.02}'. Keys: rttMs, jitterMs,\n loss, duplicate, reorder, reorderMs. Loss, duplicate and reorder\n touch state frames only, so a join always completes\n --conditions-bot <i>:<json> conditions for one bot, overriding --conditions (repeatable)\n --truth save the room at the end and diff it against what each client\n received, within that client's visibility\n --misprediction-max <units> fail if one correction snaps a prediction further than this\n --snaps-max <n> fail above this many cap-exceeded reconciliations\n --corrections-max <perSec> fail above this correction rate per bot\n --overruns-max <n> fail above this many server tick overruns in the run window\n (default 0; the count is read from the server, and the run says\n so when it could not be read)\n --profile print where the run's bytes went, by collection and field, as the\n bots saw them\n --profile-top <n> rows in that table (default 12; implies --profile)\n -h, --help print this\n\nexit codes: 0 every invariant held, 1 something was measured and failed, 2 the run could not be\nperformed (nobody joined, or it passed its wall-clock ceiling).\n";
32
85
  /** Hand-rolled, like `parseDevArgs`: the CLI has no argument-parsing dependency. */
33
86
  declare function parseSimulateArgs(args: readonly string[]): SimulateArgs;
34
87
  interface LoadSchemaOptions {
@@ -63,7 +116,14 @@ interface LoadedWorld {
63
116
  * way a browser client would. The engine stays external — it must be the project's own copy,
64
117
  * resolved at import time, exactly as a room bundle resolves it.
65
118
  */
66
- declare function loadProjectWorld(options: LoadSchemaOptions): Promise<LoadedWorld | undefined>;
119
+ /** `bugs.md` #21: a 2D world module, recognised rather than refused. There is nothing to predict
120
+ * from it in this release, so the caller drops the predicted-world path and runs anyway. */
121
+ interface UnpredictedWorld {
122
+ readonly unpredicted: 'matter2d';
123
+ readonly file: string;
124
+ }
125
+ declare function isUnpredictedWorld(world: LoadedWorld | UnpredictedWorld | undefined): world is UnpredictedWorld;
126
+ declare function loadProjectWorld(options: LoadSchemaOptions): Promise<LoadedWorld | UnpredictedWorld | undefined>;
67
127
  /**
68
128
  * The build-twice determinism check: builds two worlds from the shared `setup` and compares
69
129
  * Rapier's own snapshots byte for byte. A builder that is not pure over synced inputs
@@ -82,6 +142,41 @@ interface RoomTickCounters {
82
142
  readonly overruns?: number;
83
143
  readonly maxTickMs?: number;
84
144
  }
145
+ /**
146
+ * `bugs.md` #16: which build of the room a run tested.
147
+ *
148
+ * A `simulate` report used to name the room code, the bot count and the URL, and nothing that
149
+ * identified the code under test. That is a false-green generator: edit `irtio/room.ts`, forget
150
+ * that an older `dev` is still holding the port, and the run measures the bundle you replaced.
151
+ * The information already existed on the inspector; nothing read it.
152
+ *
153
+ * Both fields matter and neither is redundant. The bundle hash catches an edit the server never
154
+ * picked up; the start time catches the case the hash cannot see, which is a *different process*
155
+ * that happens to be serving an identical bundle, and it is the field that tells a reader the
156
+ * server they thought they had restarted did not restart.
157
+ */
158
+ interface BuildIdentity {
159
+ /** The bundle hash the server is serving, as `irtio dev` prints it at startup. */
160
+ readonly bundleHash?: string;
161
+ /** The schema hash behind that bundle, when the server knows one. */
162
+ readonly schemaHash?: string;
163
+ /** Epoch ms when the serving process booted. */
164
+ readonly startedAt?: number;
165
+ /** Where it was read from. */
166
+ readonly source?: string;
167
+ /** Why there is no identity, when there is none. Never both this and the fields above. */
168
+ readonly unavailable?: string;
169
+ }
170
+ /**
171
+ * Reads the serving build's identity off the dev inspector, or says why it could not.
172
+ *
173
+ * Deliberately shaped like `readTickCounters`: a deployed tenant does not serve `state.json`, so
174
+ * against staging this returns an `unavailable` sentence rather than a wrong answer. An unknown
175
+ * build is a fact worth printing, and printing nothing is what caused the bug.
176
+ */
177
+ declare function readBuildIdentity(wsUrl: string): Promise<BuildIdentity>;
178
+ /** The one line `bugs.md` #16 asked for, beside the URL in the report header. */
179
+ declare function formatBuildIdentity(build: BuildIdentity): string;
85
180
  /**
86
181
  * Reads one room's tick counters off the dev server, or explains why it could not. Every failure
87
182
  * path returns a sentence rather than a zero: the `tick-health` invariant has an `unavailable`
@@ -146,9 +241,57 @@ declare class RunNotPerformedError extends Error {
146
241
  * are the two fatal run conditions, and both still print the report and write the trace.
147
242
  */
148
243
  type SimulationEnd = RunEnd | 'ceiling';
244
+ /** D42: the adversarial section. Present whenever anything was injected or anyone cheated. */
245
+ interface AdversarialRun {
246
+ /** Per bot: what was injected and what the wrapper counted. */
247
+ readonly conditions: readonly BotConditions[];
248
+ /** Bot indices that cheated. */
249
+ readonly cheated: readonly number[];
250
+ /** Corrections drawn per cheating bot, in the same order as `cheated`. */
251
+ readonly correctionsPerCheater: readonly number[];
252
+ }
253
+ /** D41: what a scenario run adds to the report. Absent when no `--scenario` ran. */
254
+ interface ScenarioRun {
255
+ readonly file: string;
256
+ /** Every assertion the scenario ran, in order. */
257
+ readonly assertions: readonly AssertionResult[];
258
+ readonly ok: boolean;
259
+ /** Recorded ticks the assertions could read. */
260
+ readonly ticks: number;
261
+ /** Ticks the recorder's caps evicted. Non-zero means the recording is a tail. */
262
+ readonly dropped: number;
263
+ /** Where the recording was written, so a failed run can be re-read without re-running it. */
264
+ readonly timelinePath: string;
265
+ }
266
+ /** D43: the truth-seam section, when a run asked for one. */
267
+ interface TruthRun {
268
+ /** The diff, when a save was taken and decoded. */
269
+ readonly diff?: TruthDiff;
270
+ /** The save generation the diff read. */
271
+ readonly saveId?: string;
272
+ /**
273
+ * The decoded save's state, in `inspectState` shape. Carried because it is the only place a
274
+ * caller can read what the server actually held without starting a room, which is the whole
275
+ * point of the decoder, and because a test asserting "the illegal write was accepted" has to
276
+ * read authoritative state rather than infer it from a correction that did not arrive.
277
+ */
278
+ readonly saveState?: Record<string, unknown>;
279
+ /** Why there is no diff, when there is none. Never both this and `diff`. */
280
+ readonly error?: string;
281
+ }
149
282
  interface SimulationRun {
150
283
  readonly report: SimulationReport;
151
284
  readonly tracePath: string;
285
+ /** `bugs.md` #16: which build of the room this run tested, or why that is not knowable. */
286
+ readonly build: BuildIdentity;
287
+ /** D41: the scenario section, when `--scenario` ran one. */
288
+ readonly scenario?: ScenarioRun;
289
+ /** D42: what was injected per bot, and which bots cheated. */
290
+ readonly adversarial?: AdversarialRun;
291
+ /** D42: one row per `bot.shot(...)`. Absent when nothing fired. */
292
+ readonly hits?: readonly HitRow[];
293
+ /** D43: the truth-seam verdict. Absent unless the run asked for it. */
294
+ readonly truth?: TruthRun;
152
295
  /** `false` when there was no schema module and the run fell back to a relay simulation. */
153
296
  readonly schema: boolean;
154
297
  /** `true` when the bots predicted physics from a shared world-builder module (D22 part 2). */
@@ -169,4 +312,4 @@ interface SimulationRun {
169
312
  declare function runSimulation(options?: RunSimulationOptions): Promise<SimulationRun>;
170
313
  declare function simulate(args: readonly string[]): Promise<void>;
171
314
 
172
- export { type LoadSchemaOptions, RunNotPerformedError, type RunSimulationOptions, type SimulateArgs, type SimulationEnd, type SimulationRun, USAGE, ceilingFor, checkWorldDeterminism, loadProjectSchema, loadProjectWorld, parseSimulateArgs, readTickCounters, runSimulation, simulate, stateUrlFor, tickHealthFrom };
315
+ export { type AdversarialRun, type BuildIdentity, type LoadSchemaOptions, RunNotPerformedError, type RunSimulationOptions, ScenarioNotRunError, type ScenarioRun, type SimulateArgs, type SimulationEnd, type SimulationRun, type TruthRun, USAGE, type UnpredictedWorld, ceilingFor, checkWorldDeterminism, formatBuildIdentity, isUnpredictedWorld, loadProjectSchema, loadProjectWorld, loadScenario, parseSimulateArgs, readBuildIdentity, readTickCounters, runSimulation, simulate, stateUrlFor, tickHealthFrom };