@vgai/sdk 0.5.21 → 0.5.23

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `play.debugCommand.list` / `play.debugCommand.invoke` (Wave 5 — the
2
+ * `play.debugCommand.list` / `play.debugCommand.invoke` (the
3
3
  * editor/CLI/MCP door onto `ctx.debug.registerCommand` registrations).
4
4
  *
5
5
  * Debug commands are FIXTURES, never proofs (D16): they set up state so a
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `play.input.inject` (B4, §8 B4 "input injection through normal input
3
- * paths"; wired for real in Wave 5).
3
+ * paths"; wired for real).
4
4
  *
5
5
  * MUST exercise the NORMAL input action path, never a direct game-state
6
6
  * mutation (§8 B4 AC). Two tiers, both engine-native:
@@ -2,7 +2,7 @@
2
2
  * THE on-disk shape of `<project>/logs/play-*.jsonl` — one definition, shared
3
3
  * by the writer (`editor-server.ts`'s `/__editor/log-session` +
4
4
  * `/__editor/log-entries` handlers) and every reader (`play.log.*` here,
5
- * `vgai status`'s play-error banner, the project Logs utility).
5
+ * `vgai status`'s play-error banner, the project Analytics utility).
6
6
  *
7
7
  * A play log is JSONL with TWO record kinds:
8
8
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `play.gameplayState.inspect` / `play.gameplayState.list` (B4, §8 B4
3
3
  * "gameplay state/test-hook inspection when provided by the game";
4
- * implemented for real in Wave 5).
4
+ * implemented for real).
5
5
  *
6
6
  * The registration mechanism this file's old HONEST-GAP jsdoc anticipated
7
7
  * now exists: games register named state providers via
@@ -4,8 +4,7 @@
4
4
  *
5
5
  * status.playState is genuinely wired: `GET /__editor/state`'s `playState`
6
6
  * field, populated by `collectState()` (`packages/editor/src/command-listener.ts`)
7
- * after every relayed command. `seed`/`deterministic`/`timeScale` are ALL
8
- * real (D15/T-D15.6 landed seed/deterministic; timeScale since Wave 5) —
7
+ * after every relayed command. `seed`/`deterministic`/`timeScale` are ALL real:
9
8
  * `collectState` reports the live session's `ctx.random.seed`, whether its
10
9
  * manifest declares `determinism.seededRandom`, and the live `GameLoop
11
10
  * .timeScale`; each is `null`/`false` only when no session/Game is running
@@ -31,8 +31,8 @@
31
31
  * module jsdoc documents for its session-registry reader — a few lines
32
32
  * duplicated beats a much larger, unrelated interface coupling.
33
33
  *
34
- * REAL TRANSPORT SURFACE — what `HttpPlayTransport` actually talks to. Wave 5
35
- * closed the version-skew lie: `command-listener.ts`'s `handleCommand` now has
34
+ * REAL TRANSPORT SURFACE — what `HttpPlayTransport` actually talks to. There
35
+ * is no version-skew lie: `command-listener.ts`'s `handleCommand` has
36
36
  * a `default:` case answering `{ok:false, data:{code:'UNKNOWN_COMMAND_TYPE'}}`
37
37
  * for any command type the connected editor page predates, and the relay's
38
38
  * result leg carries `data` end to end (`reportCommandResult` →
@@ -72,10 +72,12 @@
72
72
  * `editor.console.subscribe`.
73
73
  */
74
74
 
75
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
76
- import { homedir } from 'node:os';
77
- import { join, resolve } from 'node:path';
78
75
  import { z } from 'zod';
76
+ import {
77
+ readLiveRegisteredSessions,
78
+ resolveRegisteredSession,
79
+ servedProjectAnswer,
80
+ } from '../editor/session-registry-format';
79
81
  import { ToolError } from '../errors.js';
80
82
  import type { ToolErrorDefinition } from '../registry.js';
81
83
  import type { ToolContext } from '../types.js';
@@ -178,7 +180,7 @@ export interface PlaySessionInfo {
178
180
  export interface PlayCommandResult {
179
181
  ok: boolean;
180
182
  error?: string;
181
- /** The relay's read-payload leg (Wave 5): the browser handler's structured
183
+ /** The relay's read-payload leg : the browser handler's structured
182
184
  * result on success, or a structured `{ code, ... }` failure marker
183
185
  * (registered-name lists, zod issues) on `ok: false`. */
184
186
  data?: unknown;
@@ -252,7 +254,7 @@ export interface PlayLogFollowMetadata {
252
254
  export type InputInjectionKind = 'axis' | 'vector2' | 'pointerDelta' | 'pointerPosition';
253
255
 
254
256
  /** Action-level injection (`kind: 'action'`, spec §3.2's `setVirtualAction` —
255
- * PRIMARY since Wave 5: the honest, focus-gated path whose relay result
257
+ * PRIMARY: the honest, focus-gated path whose relay result
256
258
  * carries `{delivered, reason?}`) plus the four legacy named-test-source
257
259
  * shapes (`InputManager.injectAxis` family, read back only through declared
258
260
  * `test_*` bindings). `atTick` (D15/T-D15.5, action-kind only) defers the
@@ -306,7 +308,7 @@ export interface PlayTransport {
306
308
  session: PlaySessionInfo,
307
309
  timeoutMs: number,
308
310
  ): Promise<PlayLogFollowMetadata | undefined>;
309
- /** Real since Wave 5 (relay case `inject-input`); `undefined` only against
311
+ /** Real (relay case `inject-input`); `undefined` only against
310
312
  * a stale editor page (UNKNOWN_COMMAND_TYPE marker). On success `data`
311
313
  * carries `{delivered, reason?}` for action-level injections. */
312
314
  injectInput(
@@ -330,14 +332,14 @@ export interface PlayTransport {
330
332
  seed: number,
331
333
  timeoutMs: number,
332
334
  ): Promise<PlayCommandResult | undefined>;
333
- /** Real since Wave 5 (relay case `set-time-scale`, reaching
335
+ /** Real (relay case `set-time-scale`, reaching
334
336
  * `GameLoop.timeScale`); `undefined` only against a stale editor page. */
335
337
  setTimeScale(
336
338
  session: PlaySessionInfo,
337
339
  timeScale: number,
338
340
  timeoutMs: number,
339
341
  ): Promise<PlayCommandResult | undefined>;
340
- /** Real since Wave 5 (relay case `inspect-gameplay-state`): success `data`
342
+ /** Real (relay case `inspect-gameplay-state`): success `data`
341
343
  * is `{ state: Record<string, unknown> | null }` (null when the running
342
344
  * game exposes no debug adapter); `undefined` only against a stale editor
343
345
  * page. Structured failures (`STATE_PROVIDER_NOT_FOUND`) ride
@@ -347,20 +349,20 @@ export interface PlayTransport {
347
349
  keys: string[] | undefined,
348
350
  timeoutMs: number,
349
351
  ): Promise<PlayCommandResult | undefined>;
350
- /** Real since Wave 5 (relay case `list-gameplay-state`): success `data` is
352
+ /** Real (relay case `list-gameplay-state`): success `data` is
351
353
  * `{ providers: {name, tier}[] }`. `undefined` = stale editor page. */
352
354
  listGameplayState(
353
355
  session: PlaySessionInfo,
354
356
  timeoutMs: number,
355
357
  ): Promise<PlayCommandResult | undefined>;
356
- /** Real since Wave 5 (relay case `list-debug-commands`): success `data` is
358
+ /** Real (relay case `list-debug-commands`): success `data` is
357
359
  * `{ commands: DebugCommandInfo[] }` (name/description/argsJsonSchema/
358
360
  * locus). `undefined` = stale editor page. */
359
361
  listDebugCommands(
360
362
  session: PlaySessionInfo,
361
363
  timeoutMs: number,
362
364
  ): Promise<PlayCommandResult | undefined>;
363
- /** Real since Wave 5 (relay case `invoke-debug-command`): success `data` is
365
+ /** Real (relay case `invoke-debug-command`): success `data` is
364
366
  * `{ result: unknown }`; structured failures (`DEBUG_COMMAND_NOT_REGISTERED`/
365
367
  * `DEBUG_COMMAND_ARGS_INVALID`/`DEBUG_COMMAND_FAILED`) ride `data.code`.
366
368
  * `undefined` = stale editor page. */
@@ -390,69 +392,6 @@ export interface PlayTransport {
390
392
  // Real transport
391
393
  // ---------------------------------------------------------------------------
392
394
 
393
- interface RegistrySessionEntry {
394
- project: string | null;
395
- port: number;
396
- pid: number;
397
- startedAt: string;
398
- kind?: string;
399
- }
400
-
401
- function isRegistrySessionEntry(v: unknown): v is RegistrySessionEntry {
402
- if (typeof v !== 'object' || v === null) return false;
403
- const s = v as Record<string, unknown>;
404
- return (
405
- (typeof s['project'] === 'string' || s['project'] === null) &&
406
- typeof s['port'] === 'number' &&
407
- typeof s['pid'] === 'number' &&
408
- typeof s['startedAt'] === 'string' &&
409
- (s['kind'] === undefined || typeof s['kind'] === 'string')
410
- );
411
- }
412
-
413
- function pidAlive(pid: number): boolean {
414
- try {
415
- process.kill(pid, 0);
416
- return true;
417
- } catch {
418
- return false;
419
- }
420
- }
421
-
422
- /** Deliberate light duplicate of `../editor/transport.ts`'s identical reader — see module jsdoc. */
423
- function readRegisteredSessions(): RegistrySessionEntry[] {
424
- const registryFile = join(homedir(), '.vgai', 'editor-sessions.json');
425
- if (!existsSync(registryFile)) return [];
426
- try {
427
- const raw: unknown = JSON.parse(readFileSync(registryFile, 'utf8'));
428
- return Array.isArray(raw)
429
- ? raw.filter(isRegistrySessionEntry).filter((s) => pidAlive(s.pid))
430
- : [];
431
- } catch {
432
- return [];
433
- }
434
- }
435
-
436
- /**
437
- * WHICH PROJECT a `/__editor/project` body says its server is serving — the
438
- * play lane's copy of `editor/transport.ts`'s `servedProjectPath` (deliberate
439
- * light duplicate of the read half, same as `readRegisteredSessions` above).
440
- *
441
- * `serving` is that server's own statement of "I AM serving this project, I
442
- * just cannot describe it" (its manifest is unparseable or fails strict
443
- * validation) — added to the route precisely because a bare `{ project: null }`
444
- * is indistinguishable from "no project open". Reading only `project.path`
445
- * collapses the two, and the cost is that every project-matched command loses
446
- * a live session the moment a save breaks its manifest.
447
- */
448
- function servedProjectPath(body: unknown): string | null {
449
- const b = body as {
450
- project?: { path?: string } | null;
451
- serving?: { path?: string } | null;
452
- };
453
- return b.project?.path ?? b.serving?.path ?? null;
454
- }
455
-
456
395
  async function fetchJson(url: string, timeoutMs: number): Promise<unknown | undefined> {
457
396
  try {
458
397
  const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
@@ -566,13 +505,13 @@ export class HttpPlayTransport implements PlayTransport {
566
505
  const base = editorUrl.replace(/\/+$/, '');
567
506
  const body = await fetchJson(`${base}/__editor/project`, timeoutMs);
568
507
  if (body === undefined) return undefined;
569
- const project = servedProjectPath(body);
508
+ const project = servedProjectAnswer(body).path;
570
509
  const port = url.port ? Number(url.port) : url.protocol === 'https:' ? 443 : 80;
571
510
  return { port, project, pid: null, url: base };
572
511
  }
573
512
 
574
513
  async listSessions(timeoutMs: number): Promise<PlaySessionInfo[]> {
575
- const registered = readRegisteredSessions();
514
+ const registered = readLiveRegisteredSessions();
576
515
  const perProbeTimeout = Math.min(PLAY_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
577
516
  const probes = await Promise.all(
578
517
  registered.map(async (s) => {
@@ -581,7 +520,7 @@ export class HttpPlayTransport implements PlayTransport {
581
520
  perProbeTimeout,
582
521
  );
583
522
  if (body === undefined) return undefined;
584
- const project = servedProjectPath(body);
523
+ const project = servedProjectAnswer(body).path;
585
524
  const info: PlaySessionInfo = {
586
525
  port: s.port,
587
526
  project,
@@ -631,7 +570,7 @@ export class HttpPlayTransport implements PlayTransport {
631
570
  }
632
571
  | undefined;
633
572
  if (!body) return undefined;
634
- // timeScale is real since Wave 5 (`collectState` reports the live loop's
573
+ // timeScale is real (`collectState` reports the live loop's
635
574
  // value; null while not playing or from an older editor page). seed/
636
575
  // deterministic are real since D15/T-D15.6 (`collectState` reports the
637
576
  // live session's `ctx.random.seed` + whether the manifest declares
@@ -785,24 +724,6 @@ export const PLAY_RUNTIME_NOT_AVAILABLE_ERROR: ToolErrorDefinition = {
785
724
  data: z.object({ editorUrl: z.string().optional() }),
786
725
  };
787
726
 
788
- function canonicalize(p: string): string {
789
- const absolute = resolve(p);
790
- try {
791
- return realpathSync(absolute);
792
- } catch {
793
- return absolute;
794
- }
795
- }
796
-
797
- function portOf(url: string): number | undefined {
798
- try {
799
- const port = new URL(url).port;
800
- return port ? Number(port) : undefined;
801
- } catch {
802
- return undefined;
803
- }
804
- }
805
-
806
727
  /**
807
728
  * Deterministic session selection for `play.*` ops — see module jsdoc for
808
729
  * why this duplicates (rather than imports) `../editor/transport.ts`'s
@@ -814,49 +735,14 @@ export async function resolvePlaySession(
814
735
  transport: Pick<PlayTransport, 'listSessions' | 'probeSessionUrl'>,
815
736
  ): Promise<PlaySessionInfo> {
816
737
  const editorUrl = ctx.editorUrl;
817
- const notRunning = (): never => {
818
- throw new ToolError('EDITOR_NOT_RUNNING', 'No editor connected to host a play runtime.', {
819
- ...(editorUrl !== undefined ? { editorUrl } : {}),
820
- });
821
- };
822
-
823
- if (editorUrl !== undefined && transport.probeSessionUrl) {
824
- try {
825
- const session = await withPlayTimeout(
826
- transport.probeSessionUrl(editorUrl, PLAY_PROBE_TIMEOUT_MS),
827
- PLAY_PROBE_TIMEOUT_MS,
828
- 'explicit editor URL probe',
829
- );
830
- if (session) return session;
831
- } catch {}
832
- return notRunning();
833
- }
834
-
835
- let sessions: PlaySessionInfo[];
836
- try {
837
- sessions = await withPlayTimeout(
838
- transport.listSessions(PLAY_SESSION_DISCOVERY_TIMEOUT_MS),
839
- PLAY_SESSION_DISCOVERY_TIMEOUT_MS,
840
- 'play session discovery',
841
- );
842
- } catch {
843
- return notRunning();
844
- }
845
- if (sessions.length === 0) return notRunning();
846
-
847
- if (editorUrl !== undefined) {
848
- const port = portOf(editorUrl);
849
- const match = sessions.find((s) => s.port === port);
850
- if (!match) return notRunning();
851
- return { ...match, url: editorUrl.replace(/\/+$/, '') };
852
- }
853
-
854
- if (ctx.projectRoot !== undefined) {
855
- const canon = canonicalize(ctx.projectRoot);
856
- const match = sessions.find((s) => s.project !== null && canonicalize(s.project) === canon);
857
- if (match) return match;
858
- return notRunning();
859
- }
860
-
861
- return [...sessions].sort((a, b) => a.port - b.port)[0]!;
738
+ return resolveRegisteredSession<PlaySessionInfo>(ctx, transport, {
739
+ probeTimeoutMs: PLAY_PROBE_TIMEOUT_MS,
740
+ discoveryTimeoutMs: PLAY_SESSION_DISCOVERY_TIMEOUT_MS,
741
+ withTimeout: withPlayTimeout,
742
+ notRunning: (): never => {
743
+ throw new ToolError('EDITOR_NOT_RUNNING', 'No editor connected to host a play runtime.', {
744
+ ...(editorUrl !== undefined ? { editorUrl } : {}),
745
+ });
746
+ },
747
+ });
862
748
  }
@@ -4,15 +4,15 @@
4
4
  * cheap project reads behind them.
5
5
  *
6
6
  * WHY THIS LIVES IN THE SDK. It started in the CLI, wired to `vgai status`
7
- * alone — and a measured 17-minute blind build ran the editor, `playtest` and
7
+ * alone — and a measured 17-minute blind build ran the editor and
8
8
  * `eval` while invoking `vgai status` ZERO times. The mechanisms were right;
9
9
  * the delivery assumption ("a building agent polls status constantly") was
10
10
  * false. Routing the SAME banners through the surfaces a build actually
11
- * crosses means three processes must compose them — the CLI, the editor dev
12
- * server, and a project's own `npm run playtest` so the text and the
13
- * thresholds move to the layer all three already depend on (this package: CLI
14
- * -> SDK -> engine, never the reverse). There is exactly ONE owner of the
15
- * wording and ONE owner of the numbers; every channel calls it.
11
+ * crosses means two processes must compose them — the CLI and the editor dev
12
+ * server so the text and the thresholds move to the layer both already
13
+ * depend on (this package: CLI -> SDK -> engine, never the reverse). There is
14
+ * exactly ONE owner of the wording and ONE owner of the numbers; every
15
+ * channel calls it.
16
16
  *
17
17
  * COMMIT CADENCE. Measured failure (blind-probe audit, three consecutive
18
18
  * probes): each quoted the project docs' "one commit per slice" bar back at
@@ -122,7 +122,7 @@ export function uncommittedWorkAge(
122
122
  }
123
123
 
124
124
  /** Which step the batch has reached — the ONE place the cadence thresholds are
125
- * compared, so every channel (status, playtest, dev server, eval) agrees. */
125
+ * compared, so every channel (status, dev server, eval) agrees. */
126
126
  export function commitCadenceTier(work: UncommittedWork | null): TripwireTier {
127
127
  if (!work || work.ageMs < CADENCE_NOTICE_MS) return 'silent';
128
128
  return work.ageMs < CADENCE_LOUD_MS ? 'notice' : 'loud';
@@ -223,7 +223,7 @@ function git(cwd: string, args: string[]): string | null {
223
223
  *
224
224
  * `null` — say nothing — when there is no project, no git work tree, or a
225
225
  * clean tree. Two `git` invocations plus one `stat` per dirty path (capped),
226
- * made on a caller's own event (a status request, a finished playtest run, a
226
+ * made on a caller's own event (a status request, a
227
227
  * save the dev server already validated) — never on a timer.
228
228
  */
229
229
  export function readUncommittedWork(
@@ -357,13 +357,11 @@ export function newestSourceMtime(projectRoot: string): number | null {
357
357
  * The newest mtime (epoch ms) among this project's LIVE-EVIDENCE artifacts, or
358
358
  * `null` when the game has never run.
359
359
  *
360
- * The two artifacts are the same pair the idiom checker's "has this ever been
361
- * played" rule reads, for the same reason: both are written by the tools
362
- * themselves while a real browser runs the real game, so neither can be
363
- * produced by intending to play.
360
+ * The artifact is the same one the idiom checker's "has this ever been
361
+ * played" rule reads, for the same reason: it is written by the editor server
362
+ * itself while a real browser runs the real game, so it cannot be produced by
363
+ * intending to play.
364
364
  * - `logs/play-*.jsonl` — one per Play session, opened by the editor server.
365
- * - `.vgai/last-run/**` — what a bot run leaves behind (screenshots, and the
366
- * `playtest.json` verdict `npm run playtest` files).
367
365
  */
368
366
  export function newestEvidenceMtime(projectRoot: string): number | null {
369
367
  let newest: number | null = null;
@@ -384,13 +382,6 @@ export function newestEvidenceMtime(projectRoot: string): number | null {
384
382
  } catch {
385
383
  /* no logs dir */
386
384
  }
387
-
388
- const runDir = join(projectRoot, '.vgai', 'last-run');
389
- try {
390
- for (const entry of readdirSync(runDir)) consider(join(runDir, entry));
391
- } catch {
392
- /* no run dir */
393
- }
394
385
  return newest;
395
386
  }
396
387
 
@@ -421,7 +412,7 @@ export function staleEvidenceBanner(
421
412
  if (newestEvidence !== null && newestEvidence >= newestSource) return null;
422
413
  const gap =
423
414
  newestEvidence === null
424
- ? ' no live evidence exists at all — no logs/play-*.jsonl, no .vgai/last-run artifacts.'
415
+ ? ' no live evidence exists at all — no logs/play-*.jsonl.'
425
416
  : ` newest source: ${new Date(newestSource).toISOString()}\n` +
426
417
  ` newest live evidence: ${new Date(newestEvidence).toISOString()}`;
427
418
  return [
@@ -436,8 +427,8 @@ export function staleEvidenceBanner(
436
427
  ' invisible mesh, camera inside the geometry) are invisible to every one',
437
428
  ' of them.',
438
429
  '',
439
- ' Play or playtest before claiming it works: `vgai play`, then look, or',
440
- ' `npm run playtest` for the bot proof.',
430
+ ' Play before claiming it works: `vgai play`, then look, and playtest',
431
+ ' live through `vgai eval`.',
441
432
  '================================================================',
442
433
  ].join('\n');
443
434
  }
@@ -481,7 +472,7 @@ export function unplayedSessionTier(
481
472
  * without ever producing live-play evidence — pure, driven directly by a test.
482
473
  *
483
474
  * The evidence signal is the SAME pair `newestEvidenceMtime` walks (a Play
484
- * session's `logs/play-*.jsonl`, a bot run's `.vgai/last-run/**`), so this
475
+ * session's `logs/play-*.jsonl`), so this
485
476
  * banner and the staleness banner can never disagree about what counts as
486
477
  * having run. Nothing new is instrumented on the game side: both artifacts are
487
478
  * already written by the tools themselves while a real browser runs the real
@@ -514,8 +505,8 @@ export function unplayedSessionBanner(
514
505
  ' nothing — are invisible to all of them, and the longer the first play',
515
506
  ' is deferred the more work is stacked on top of an unverified base.',
516
507
  '',
517
- ' Play it now: `vgai play`, then look — or `npm run playtest` for the',
518
- ' bot proof.',
508
+ ' Play it now: `vgai play`, then look — and direct the resident tester',
509
+ ' from the live session (`vgai eval`).',
519
510
  '================================================================',
520
511
  ].join('\n');
521
512
  }
@@ -38,14 +38,11 @@ import { registerInspectionOperations } from './inspection-operation.js';
38
38
  import { registerManifestOperations } from './manifest-operations.js';
39
39
 
40
40
  /**
41
- * Register every B2 `project.*` operation onto `registry`.
41
+ * Register every `project.*` operation onto `registry`.
42
42
  *
43
- * WO-8 removed the twelve `.vscn` document tools:
44
- * `project.scene.{read,validate,diff,apply}`,
45
- * `project.scene.entity.{add,update,move,remove}` and
46
- * `project.scene.component.{list,add,update,remove}`. They read and wrote the
47
- * deleted scene format; a three root is authored as TSX source now, so the source
48
- * tools (`project.file.*` / the editor's own adapter seam) are what act on it.
43
+ * There are no scene-document tools here: a three root is authored as TSX
44
+ * SOURCE, so the source tools (`project.file.*` / the editor's own adapter
45
+ * seam) are what act on it.
49
46
  */
50
47
  export function registerProjectOperations(registry: ToolRegistry): void {
51
48
  registerManifestOperations(registry);
@@ -3,62 +3,76 @@ import { GenerationBillingSchema } from '../generations.js';
3
3
 
4
4
  export const PROJECT_PROVENANCE_PATH = '.vgai/provenance.json';
5
5
 
6
- export const ProjectProvenanceOutputSchema = z.object({
7
- path: z.string(),
8
- bytes: z.number().int().nonnegative(),
9
- sha256: z.string().regex(/^[a-f0-9]{64}$/),
10
- mediaType: z.string().optional(),
11
- role: z.enum(['asset', 'prefab', 'provenance', 'other']).optional(),
12
- });
6
+ export const ProjectProvenanceOutputSchema = z
7
+ .object({
8
+ path: z.string(),
9
+ bytes: z.number().int().nonnegative(),
10
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
11
+ mediaType: z.string().optional(),
12
+ role: z.enum(['asset', 'prefab', 'provenance', 'other']).optional(),
13
+ })
14
+ .strict();
13
15
 
14
- export const ProjectProvenanceExecutionSchema = z.object({
15
- mode: z.enum(['mock', 'direct', 'managed']),
16
- provider: z.string(),
17
- operation: z.string().optional(),
18
- model: z.string().optional(),
19
- requestId: z.string().optional(),
20
- taskId: z.string().optional(),
21
- managedJobId: z.string().optional(),
22
- billing: GenerationBillingSchema.optional(),
23
- });
16
+ export const ProjectProvenanceExecutionSchema = z
17
+ .object({
18
+ mode: z.enum(['mock', 'direct', 'managed']),
19
+ provider: z.string(),
20
+ operation: z.string().optional(),
21
+ model: z.string().optional(),
22
+ requestId: z.string().optional(),
23
+ taskId: z.string().optional(),
24
+ managedJobId: z.string().optional(),
25
+ billing: GenerationBillingSchema.optional(),
26
+ })
27
+ .strict();
24
28
 
25
- export const ProjectProvenanceOperationSchema = z.object({
26
- createdAt: z.string().datetime(),
27
- operation: z.object({
28
- name: z.string(),
29
- source: z.string().optional(),
30
- }),
31
- execution: ProjectProvenanceExecutionSchema.optional(),
32
- executions: z.array(ProjectProvenanceExecutionSchema).min(2).optional(),
33
- input: z.json().optional(),
34
- outputs: z.array(ProjectProvenanceOutputSchema).min(1),
35
- });
29
+ export const ProjectProvenanceOperationSchema = z
30
+ .object({
31
+ createdAt: z.string().datetime(),
32
+ operation: z
33
+ .object({
34
+ name: z.string(),
35
+ source: z.string().optional(),
36
+ })
37
+ .strict(),
38
+ execution: ProjectProvenanceExecutionSchema.optional(),
39
+ executions: z.array(ProjectProvenanceExecutionSchema).min(2).optional(),
40
+ input: z.json().optional(),
41
+ outputs: z.array(ProjectProvenanceOutputSchema).min(1),
42
+ })
43
+ .strict();
36
44
 
37
- export const ProjectProvenanceDocumentSchema = z.object({
38
- version: z.literal(1),
39
- operations: z.record(z.string(), ProjectProvenanceOperationSchema),
40
- });
45
+ export const ProjectProvenanceDocumentSchema = z
46
+ .object({
47
+ version: z.literal(1),
48
+ operations: z.record(z.string(), ProjectProvenanceOperationSchema),
49
+ })
50
+ .strict();
41
51
 
42
52
  export type ProjectProvenanceDocument = z.infer<typeof ProjectProvenanceDocumentSchema>;
43
53
  export type ProjectProvenanceOperation = z.infer<typeof ProjectProvenanceOperationSchema>;
44
54
  export type ProjectProvenanceExecution = z.infer<typeof ProjectProvenanceExecutionSchema>;
45
55
 
46
- export const ProjectAttributionEntrySchema = z.object({
47
- key: z.string(),
48
- name: z.string().optional(),
49
- source: z.string().optional(),
50
- sourceUrl: z.string().url().optional(),
51
- author: z.string(),
52
- license: z.string(),
53
- text: z.string(),
54
- operationIds: z.array(z.string()),
55
- outputPaths: z.array(z.string()),
56
- });
56
+ export const ProjectAttributionEntrySchema = z
57
+ .object({
58
+ key: z.string(),
59
+ name: z.string().optional(),
60
+ source: z.string().optional(),
61
+ sourceUrl: z.string().url().optional(),
62
+ author: z.string(),
63
+ license: z.string(),
64
+ text: z.string(),
65
+ operationIds: z.array(z.string()),
66
+ outputPaths: z.array(z.string()),
67
+ })
68
+ .strict();
57
69
 
58
- export const ProjectAttributionReportSchema = z.object({
59
- version: z.literal(1),
60
- entries: z.array(ProjectAttributionEntrySchema),
61
- });
70
+ export const ProjectAttributionReportSchema = z
71
+ .object({
72
+ version: z.literal(1),
73
+ entries: z.array(ProjectAttributionEntrySchema),
74
+ })
75
+ .strict();
62
76
 
63
77
  export type ProjectAttributionEntry = z.infer<typeof ProjectAttributionEntrySchema>;
64
78
  export type ProjectAttributionReport = z.infer<typeof ProjectAttributionReportSchema>;
@@ -1,53 +1,18 @@
1
1
  /**
2
- * What a run is CALLED — one owner of the bound, and of the fallback a run
3
- * gets when nobody named it.
2
+ * What a run is CALLED — one owner of the bound.
4
3
  *
5
- * WHY THIS EXISTS. `vgai play --name <text>` and `npm run playtest --
6
- * '{"name":"…"}'` both exist so a run is findable by what it was testing, and
7
- * the measured answer to an optional field is that it stays empty: every play
8
- * event in the foundry probe's 45-minute session journal reads `"name":null`.
9
- * A label nobody supplies indexes nothing, so the run that DOES know what it
10
- * was testing supplies it — a route-targeted playtest is named after its
11
- * routes, and a full-suite run is named `playtest`. Nothing is taught and no
12
- * habit is required; the default carries the information.
4
+ * WHY THIS EXISTS. `vgai play --name <text>` exists so a run is findable by
5
+ * what it was testing.
13
6
  *
14
7
  * WHAT STAYS UNNAMED, deliberately: interactive `vgai play` with no `--name`.
15
8
  * A person pressing play is not testing a named thing, and inventing a label
16
9
  * for it would put noise in exactly the directory this makes greppable.
17
10
  *
18
11
  * Pure — no filesystem, no clock. `editor-server.ts`'s `playRunSlug` is the
19
- * OTHER half (slugging a name into a filename segment) and reads the same
20
- * bound from here, so the two can never disagree about how long a run name
21
- * may be.
12
+ * OTHER half (slugging a name into a filename segment) and reads the bound
13
+ * from here, so the two can never disagree about how long a run name may be.
22
14
  */
23
15
 
24
16
  /** Bound on a run's name — long enough to stay recognisable in a directory
25
17
  * listing, short enough that the timestamp beside it is still readable. */
26
18
  export const MAX_RUN_NAME = 40;
27
-
28
- /** The name a full-suite run gets: it targets no route in particular, and
29
- * "which run was that" is still a question worth being able to answer. */
30
- export const FULL_SUITE_RUN_NAME = 'playtest';
31
-
32
- /**
33
- * The name for one playtest run.
34
- *
35
- * An explicit name always wins — it is the caller saying what this run was,
36
- * and no derivation is better informed than that. Otherwise the routes name
37
- * it, joined with `+` (a separator that survives being read back as a list,
38
- * unlike the `-` that route names themselves use), bounded at
39
- * {@link MAX_RUN_NAME}; and a run that named no route is
40
- * {@link FULL_SUITE_RUN_NAME}.
41
- *
42
- * `null` is impossible by construction — every playtest run gets a name — but
43
- * an explicit blank/whitespace name is treated as no name at all, the same
44
- * case a missing one is.
45
- */
46
- export function derivePlaytestRunName(
47
- explicit: string | null | undefined,
48
- routes: readonly string[],
49
- ): string {
50
- if (typeof explicit === 'string' && explicit.trim() !== '') return explicit.trim();
51
- if (routes.length === 0) return FULL_SUITE_RUN_NAME;
52
- return routes.join('+').slice(0, MAX_RUN_NAME).replace(/\+$/, '');
53
- }