@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.
- package/package.json +3 -2
- package/src/editor/console-operations.ts +1 -1
- package/src/editor/open-operations.ts +4 -13
- package/src/editor/session-registry-format.ts +218 -0
- package/src/editor/transport.ts +20 -140
- package/src/generations.ts +100 -51
- package/src/play/control-operations.ts +1 -1
- package/src/play/debug-command-operations.ts +1 -1
- package/src/play/input-operations.ts +1 -1
- package/src/play/log-format.ts +1 -1
- package/src/play/state-operations.ts +1 -1
- package/src/play/status-operations.ts +1 -2
- package/src/play/transport.ts +29 -143
- package/src/project/build-discipline.ts +18 -27
- package/src/project/index.ts +4 -7
- package/src/project/provenance.ts +61 -47
- package/src/project/run-name.ts +5 -40
- package/src/project/session-journal.ts +85 -14
- package/src/project/shared.ts +2 -5
- package/src/project/tab-census.ts +83 -0
- package/src/project-tool-catalog.ts +25 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `play.debugCommand.list` / `play.debugCommand.invoke` (
|
|
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
|
|
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:
|
package/src/play/log-format.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/src/play/transport.ts
CHANGED
|
@@ -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.
|
|
35
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
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
|
|
12
|
-
* server
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
361
|
-
* played" rule reads, for the same reason:
|
|
362
|
-
*
|
|
363
|
-
*
|
|
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
|
|
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
|
|
440
|
-
'
|
|
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
|
|
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 —
|
|
518
|
-
'
|
|
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
|
}
|
package/src/project/index.ts
CHANGED
|
@@ -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
|
|
41
|
+
* Register every `project.*` operation onto `registry`.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
* `project.
|
|
45
|
-
*
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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>;
|
package/src/project/run-name.ts
CHANGED
|
@@ -1,53 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a run is CALLED — one owner of the bound
|
|
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>`
|
|
6
|
-
*
|
|
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
|
|
20
|
-
*
|
|
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
|
-
}
|