@volter/world 3.0.193 → 3.0.194
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/dist/examples/customer-onboarding/README.md +1 -1
- package/dist/examples/customer-onboarding/demo.mjs +4 -4
- package/dist/examples/customer-onboarding/package.json +5 -5
- package/dist/examples/integration-branches/README.md +4 -4
- package/dist/examples/integration-branches/package.json +5 -5
- package/dist/examples/workplace-agent/README.md +1 -1
- package/dist/examples/workplace-agent/package.json +5 -5
- package/dist/skills/volter-world/AGENTS.md +6 -4
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +35 -7
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/world-browser.js +9 -1
- package/dist/src/world.d.ts +19 -10
- package/dist/src/world.js +45 -20
- package/examples/customer-onboarding/README.md +1 -1
- package/examples/customer-onboarding/demo.mjs +4 -4
- package/examples/customer-onboarding/package.json +5 -5
- package/examples/integration-branches/README.md +4 -4
- package/examples/integration-branches/package.json +5 -5
- package/examples/workplace-agent/README.md +1 -1
- package/examples/workplace-agent/package.json +5 -5
- package/package.json +5 -5
- package/skills/volter-world/AGENTS.md +6 -4
- package/src/cli.ts +26 -7
- package/src/index.ts +2 -0
- package/src/sdk.test.ts +2 -2
- package/src/world-browser.ts +9 -1
- package/src/world.ts +44 -24
- package/src/handlers-in-repo.test.ts +0 -51
|
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
|
|
|
13
13
|
Copy this starter into a new folder:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.194 example customer-onboarding --dir onboarding
|
|
17
17
|
$ cd onboarding
|
|
18
18
|
$ pnpm install
|
|
19
19
|
```
|
|
@@ -7,22 +7,22 @@ try {
|
|
|
7
7
|
await initialize();
|
|
8
8
|
switch (process.argv[2]) {
|
|
9
9
|
case 'baseline': {
|
|
10
|
-
world.setClock(at);
|
|
10
|
+
await world.setClock(at);
|
|
11
11
|
const rows = await listSignups();
|
|
12
12
|
if (rows.length > 1 || rows.some(r => r.name !== 'Grace')) throw new Error('This demo contains customer work; reset both your disposable application database and demo World explicitly.');
|
|
13
13
|
const grace = rows.find(r => r.name === 'Grace') ?? await onboard({ name: 'Grace', email: 'owner@world.test' });
|
|
14
|
-
world.advanceClock('5s');
|
|
14
|
+
await world.advanceClock('5s');
|
|
15
15
|
console.log('Baseline', JSON.stringify(summarize((await listSignups()).find(r => r.customer_id === grace.customer_id)))); break;
|
|
16
16
|
}
|
|
17
17
|
case 'present': {
|
|
18
18
|
const rows = await listSignups();
|
|
19
19
|
if (rows.length !== 1 || rows[0].name !== 'Grace') throw new Error('Prepare the Grace-only baseline first.');
|
|
20
|
-
world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
|
|
20
|
+
await world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
|
|
21
21
|
const partial = await onboard({ name: 'Ada', email: 'owner@world.test', from: 'Example <welcome@unverified.world.test>' });
|
|
22
22
|
console.log('Email refusal; customer and SQL row retained', JSON.stringify(summarize(partial)));
|
|
23
23
|
const retry = await retryWelcome(partial.customer_id);
|
|
24
24
|
console.log('Retry accepted on the same customer', JSON.stringify(summarize(retry)));
|
|
25
|
-
world.advanceClock('5s');
|
|
25
|
+
await world.advanceClock('5s');
|
|
26
26
|
console.log('Delivered', JSON.stringify((await listSignups()).map(summarize))); break;
|
|
27
27
|
}
|
|
28
28
|
default: throw new Error('Use baseline or present');
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"pg": "8.16.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.194",
|
|
15
|
+
"@volter/world-core": "3.0.167",
|
|
16
|
+
"@volter/world-runtime": "3.0.192",
|
|
17
|
+
"@volter/world-console": "3.0.162",
|
|
18
18
|
"@volter/twin-stripe": "3.0.2",
|
|
19
19
|
"@volter/twin-resend": "1.0.1",
|
|
20
|
-
"@volter/world-ui": "1.0.
|
|
20
|
+
"@volter/world-ui": "1.0.166"
|
|
21
21
|
},
|
|
22
22
|
"packageManager": "pnpm@12.15.0",
|
|
23
23
|
"devEngines": {
|
|
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
|
|
|
13
13
|
Use an empty parent folder. Create the baseline starter there:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir baseline
|
|
17
17
|
$ cd baseline
|
|
18
18
|
$ pnpm install
|
|
19
19
|
$ ./node_modules/.bin/volter world init --name baseline --twins stripe
|
|
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
|
|
|
38
38
|
Create three more copies in the same parent folder:
|
|
39
39
|
|
|
40
40
|
```console
|
|
41
|
-
$ npx @volter/world@3.0.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
41
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir integration
|
|
42
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir worker-a
|
|
43
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir worker-b
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"stripe": "17.7.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@volter/world": "3.0.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
12
|
+
"@volter/world": "3.0.194",
|
|
13
|
+
"@volter/world-core": "3.0.167",
|
|
14
|
+
"@volter/world-runtime": "3.0.192",
|
|
15
|
+
"@volter/world-console": "3.0.162",
|
|
16
16
|
"@volter/twin-stripe": "3.0.2",
|
|
17
|
-
"@volter/world-ui": "1.0.
|
|
17
|
+
"@volter/world-ui": "1.0.166"
|
|
18
18
|
},
|
|
19
19
|
"packageManager": "pnpm@12.15.0",
|
|
20
20
|
"devEngines": {
|
|
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
|
|
|
19
19
|
together; its recorded workflow does not establish behavior for a different release.
|
|
20
20
|
|
|
21
21
|
```console
|
|
22
|
-
$ npx @volter/world@3.0.
|
|
22
|
+
$ npx @volter/world@3.0.194 example workplace-agent --dir workplace
|
|
23
23
|
$ cd workplace
|
|
24
24
|
$ pnpm install
|
|
25
25
|
$ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"@slack/web-api": "7.19.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.194",
|
|
15
|
+
"@volter/world-core": "3.0.167",
|
|
16
|
+
"@volter/world-runtime": "3.0.192",
|
|
17
|
+
"@volter/world-console": "3.0.162",
|
|
18
18
|
"@volter/twin-github": "3.0.9",
|
|
19
19
|
"@volter/twin-linear": "3.0.5",
|
|
20
20
|
"@volter/twin-slack": "3.0.8",
|
|
21
|
-
"@volter/world-ui": "1.0.
|
|
21
|
+
"@volter/world-ui": "1.0.166"
|
|
22
22
|
},
|
|
23
23
|
"packageManager": "pnpm@12.15.0",
|
|
24
24
|
"devEngines": {
|
|
@@ -117,10 +117,12 @@ Identity comes from credentials in the vendor's manner; inspect `GET /twin` for
|
|
|
117
117
|
identity and time rules. Use `clock set` and `clock advance` for backdated history. Without a
|
|
118
118
|
frozen World clock, the runtime uses wall time.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
Place a vendor's judgment (a decision) and failures (faults) on a scenario's timeline,
|
|
121
|
+
`scenarios/<name>.json` beside `world.json` (typically `.volter/scenarios/`), and run it with
|
|
122
|
+
`volter world play <file>`: it resets to the default data, sets the clock to the scenario's
|
|
123
|
+
`start` and makes the scenario the World's, which every twin reads at each request. A twin turns
|
|
124
|
+
each input into its own answer and records it; nothing in a scenario answers in its place.
|
|
125
|
+
`GET /twin/scenario` shows what is placed on that twin and what it consumed.
|
|
124
126
|
Model twins serve deterministic scripts or labeled stubs, so verify calls and state changes,
|
|
125
127
|
not the quality of prose from an unconfigured model.
|
|
126
128
|
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 a shared simulated internet (the world of the current directory)\n volter world init [--install] [--name <world>] [--twins a,b] [--source vendor=package] [--allow-unknown] detect or select vendors, choose installed publishers, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world env [--template <file>] render deployment .env.world references into dotenv files\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world generate write seed.generated.ts from world.json's people, organizations and accounts\n volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>]
|
|
2
|
+
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 a shared simulated internet (the world of the current directory)\n volter world init [--install] [--name <world>] [--twins a,b] [--source vendor=package] [--allow-unknown] detect or select vendors, choose installed publishers, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world env [--template <file>] render deployment .env.world references into dotenv files\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world generate write seed.generated.ts from world.json's people, organizations and accounts\n volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)\n volter world seed load the default data\n volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties\n volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--remote <remote>] [--token <token>] [--force] send entries to the selected remote (default origin); vendor deployment follows its root policy\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin submit --source <id> --vendor <vendor> --package <name> --version <exact> prepare a catalog submission\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n --version the command's version\n";
|
package/dist/src/cli.js
CHANGED
|
@@ -60,9 +60,11 @@ world — a shared simulated internet (the world of the current directory)
|
|
|
60
60
|
volter world generate write seed.generated.ts from world.json's people, organizations and accounts
|
|
61
61
|
volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
|
|
62
62
|
volter world seed load the default data
|
|
63
|
+
volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties
|
|
64
|
+
volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)
|
|
63
65
|
volter world reset back to the default data
|
|
64
66
|
volter world down [--purge] stop the twins (--purge forgets this branch's state)
|
|
65
|
-
volter world branch [<name>]
|
|
67
|
+
volter world branch [<name>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) and check it out
|
|
66
68
|
volter world checkout <name> switch branches
|
|
67
69
|
volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
|
|
68
70
|
volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
|
|
@@ -368,6 +370,32 @@ async function worldCommand(verb, args) {
|
|
|
368
370
|
process.exitCode = r.exitCode;
|
|
369
371
|
return;
|
|
370
372
|
}
|
|
373
|
+
case 'mark': {
|
|
374
|
+
const m = await world.mark(pos[0]);
|
|
375
|
+
if (asJson) {
|
|
376
|
+
json(m);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
out(`mark ${m.id} ${m.ledgers.length} twin log${m.ledgers.length === 1 ? '' : 's'} clock ${m.clock ?? 'the machine\'s'} ${m.disk ? 'with the app\'s disk' : 'without the app\'s disk (no fault door)'}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
case 'play': {
|
|
383
|
+
const file = pos[0];
|
|
384
|
+
if (!file)
|
|
385
|
+
throw new Error('volter world play <scenario.json>');
|
|
386
|
+
const r = await world.play(file);
|
|
387
|
+
if (asJson) {
|
|
388
|
+
json(r);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
out(`played ${r.scenario} ${r.start} → ${r.end} ${r.calls.length} call${r.calls.length === 1 ? '' : 's'}`);
|
|
392
|
+
out(r.completeness.total === 0 ? 'complete the World was the whole internet this run reached' : `incomplete ${Object.entries(r.completeness.egress).map(([d, n]) => `${d} refused ${n.refused}${n.passed ? ` passed ${n.passed}` : ''}`).concat(Object.entries(r.completeness.unimplemented).flatMap(([v, ops]) => Object.entries(ops).map(([op, n]) => `${v} ${op} unimplemented ${n}`))).concat(Object.entries(r.completeness.machine).map(([f, n]) => `${f} fault refused ${n} (no fault door)`)).join('; ')}`);
|
|
393
|
+
for (const p of r.properties)
|
|
394
|
+
out(`${p.verdict.padEnd(8)} ${p.kind.padEnd(10)} ${p.name}${p.step !== undefined ? ` (step ${p.step}, ${p.at})` : ''}`);
|
|
395
|
+
if (r.properties.some((p) => p.verdict === 'broken'))
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
371
399
|
case 'reset': {
|
|
372
400
|
const r = await world.reset();
|
|
373
401
|
out(`Back to the default data on ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
|
|
@@ -383,13 +411,13 @@ async function worldCommand(verb, args) {
|
|
|
383
411
|
return;
|
|
384
412
|
}
|
|
385
413
|
const atText = value(args, '--at');
|
|
386
|
-
const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : (() => {
|
|
414
|
+
const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : !atText.includes('@') ? { mark: atText } : (() => {
|
|
387
415
|
const positions = {};
|
|
388
416
|
const views = {};
|
|
389
417
|
for (const p of atText.split(',')) {
|
|
390
418
|
const match = /^([^@]+)@(?:([a-f0-9]{64}):)?(\d+)$/.exec(p);
|
|
391
419
|
if (!match)
|
|
392
|
-
throw new Error('--at takes an instant, twin@position, or twin@view:position');
|
|
420
|
+
throw new Error('--at takes an instant, a mark, twin@position, or twin@view:position');
|
|
393
421
|
positions[match[1]] = Number(match[3]);
|
|
394
422
|
if (match[2])
|
|
395
423
|
views[match[1]] = match[2];
|
|
@@ -414,23 +442,23 @@ async function worldCommand(verb, args) {
|
|
|
414
442
|
if (action === 'set') {
|
|
415
443
|
if (!arg)
|
|
416
444
|
throw new Error('volter world clock set <iso-8601>');
|
|
417
|
-
out(world.setClock(arg));
|
|
445
|
+
out(await world.setClock(arg));
|
|
418
446
|
return;
|
|
419
447
|
}
|
|
420
448
|
if (action === 'advance') {
|
|
421
449
|
if (!arg)
|
|
422
450
|
throw new Error('volter world clock advance <N s|m|h|d>');
|
|
423
|
-
out(world.advanceClock(arg));
|
|
451
|
+
out(await world.advanceClock(arg));
|
|
424
452
|
return;
|
|
425
453
|
}
|
|
426
454
|
if (action === 'shift') {
|
|
427
455
|
if (!arg)
|
|
428
456
|
throw new Error('volter world clock shift <N s|m|h|d>');
|
|
429
|
-
out(world.shiftClock(arg));
|
|
457
|
+
out(await world.shiftClock(arg));
|
|
430
458
|
return;
|
|
431
459
|
}
|
|
432
460
|
if (action === 'clear') {
|
|
433
|
-
out(world.clearClock());
|
|
461
|
+
out(await world.clearClock());
|
|
434
462
|
return;
|
|
435
463
|
}
|
|
436
464
|
if (action === undefined || action === 'show') {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { World, parseOriginUrl, worldNameFor, type LogEntry, type WorldMode, type WorldRef, type WorldStatus } from './world.js';
|
|
2
2
|
export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from './locate.js';
|
|
3
3
|
export { credentialsPath, requireToken, storeToken, tokenFor } from './credentials.js';
|
|
4
|
+
export { always, sometimes, eventually, type Property, type RunReport } from '@volter/world-runtime';
|
package/dist/src/index.js
CHANGED
|
@@ -4,3 +4,5 @@
|
|
|
4
4
|
export { World, parseOriginUrl, worldNameFor } from "./world.js";
|
|
5
5
|
export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from "./locate.js";
|
|
6
6
|
export { credentialsPath, requireToken, storeToken, tokenFor } from "./credentials.js";
|
|
7
|
+
// a scenario's properties (ADR 0036 §7): `export default [always(...), sometimes(...), eventually(...)]`
|
|
8
|
+
export { always, sometimes, eventually } from '@volter/world-runtime';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
|
|
2
2
|
// (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
|
|
3
3
|
// the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
|
|
4
|
-
import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
4
|
+
import { clockFile, deriveWorldManifest, followWorld, overlayWorldPackFacts, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
5
|
+
import clockForm from '@volter/world-core/world-clock';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
7
|
export async function openWorldBrowser(world, name, url, cdp, say) {
|
|
6
8
|
const instance = statusWorld(world.name, world.root);
|
|
7
9
|
const record = worldBrowser(instance.dirs.data, name);
|
|
@@ -9,6 +11,7 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
|
|
|
9
11
|
const names = worldBrowsers(instance.dirs.data).map(row => row.name);
|
|
10
12
|
throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
|
|
11
13
|
}
|
|
14
|
+
overlayWorldPackFacts({ configPath: instance.configPath, root: world.root });
|
|
12
15
|
const manifest = deriveWorldManifest(world.name, instance.env ?? {});
|
|
13
16
|
if (!instance.running || !manifest.browser)
|
|
14
17
|
throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
|
|
@@ -20,10 +23,14 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
|
|
|
20
23
|
throw new Error('volter world browser needs playwright: npm i playwright');
|
|
21
24
|
}
|
|
22
25
|
const profile = record.state;
|
|
26
|
+
// the page follows the World (ADR 0036 §6): its clock as the World's stands, its randomness seeded by World and browser
|
|
27
|
+
const clock = existsSync(clockFile(world.root, world.name)) ? clockForm.parseClock(readFileSync(clockFile(world.root, world.name), 'utf8')) : null;
|
|
28
|
+
const follow = { now: new Date(clock ? clockForm.clockNowMs(clock, Date.now()) : Date.now()).toISOString(), frozen: clock?.kind === 'frozen', random: `${world.name}:${name}` };
|
|
23
29
|
if (cdp === undefined) {
|
|
24
30
|
const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
|
|
25
31
|
const closed = new Promise(resolve => browser.on('disconnected', () => resolve()));
|
|
26
32
|
const context = await browser.newContext({ ...profile.context, storageState: profile.storageState });
|
|
33
|
+
await followWorld(context, follow);
|
|
27
34
|
const page = await context.newPage();
|
|
28
35
|
await page.goto(url ?? 'about:blank');
|
|
29
36
|
await closed;
|
|
@@ -37,6 +44,7 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
|
|
|
37
44
|
if (Object.keys(profile.context).length)
|
|
38
45
|
say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
|
|
39
46
|
await context.addCookies(profile.storageState.cookies);
|
|
47
|
+
await followWorld(context, follow);
|
|
40
48
|
const page = await context.newPage();
|
|
41
49
|
await page.goto(url ?? 'about:blank');
|
|
42
50
|
await browser.close();
|
package/dist/src/world.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { renderDeploymentTemplates, type DeploymentTemplate } from '@volter/world-runtime/deployment-env';
|
|
2
2
|
import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core/runtime';
|
|
3
|
-
import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, statusWorld, writeGeneratedSeed, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
|
|
3
|
+
import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, playWorld, statusWorld, writeGeneratedSeed, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
|
|
4
4
|
import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
|
|
5
5
|
export type WorldRef = {
|
|
6
6
|
name?: string;
|
|
7
7
|
root?: string;
|
|
8
8
|
};
|
|
9
9
|
export type WorldMode = 'local' | 'share' | 'sealed';
|
|
10
|
+
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
11
|
+
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
10
12
|
export declare class TwinLog {
|
|
11
13
|
readonly service: string;
|
|
12
14
|
readonly stateService: string;
|
|
@@ -153,6 +155,8 @@ export declare class World {
|
|
|
153
155
|
hasSeed(): boolean;
|
|
154
156
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|
|
155
157
|
seedServed(base: string, token: string): ReturnType<typeof seedServedWorld>;
|
|
158
|
+
/** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
|
|
159
|
+
play(scenario: string): ReturnType<typeof playWorld>;
|
|
156
160
|
/** Back to the default data: forget this branch's state, boot, seed. */
|
|
157
161
|
reset(opts?: {
|
|
158
162
|
entry?: string;
|
|
@@ -168,6 +172,7 @@ export declare class World {
|
|
|
168
172
|
instant?: string;
|
|
169
173
|
positions?: Record<string, number>;
|
|
170
174
|
views?: Record<string, string>;
|
|
175
|
+
mark?: string;
|
|
171
176
|
};
|
|
172
177
|
}): Promise<World>;
|
|
173
178
|
/** Switch to a branch: its twins come back with their state; this branch's stop. */
|
|
@@ -187,15 +192,17 @@ export declare class World {
|
|
|
187
192
|
frozen: boolean;
|
|
188
193
|
running: boolean;
|
|
189
194
|
};
|
|
190
|
-
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
|
|
196
|
+
* clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
|
|
197
|
+
setClock(iso: string): Promise<string>;
|
|
198
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
|
|
199
|
+
advanceClock(by: string): Promise<string>;
|
|
194
200
|
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
195
|
-
* running clock further. */
|
|
196
|
-
shiftClock(by: string): string
|
|
197
|
-
/** Return the World to the machine's time. */
|
|
198
|
-
clearClock(): string
|
|
201
|
+
* running clock further. Answers as setClock does. */
|
|
202
|
+
shiftClock(by: string): Promise<string>;
|
|
203
|
+
/** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
|
|
204
|
+
clearClock(): Promise<string>;
|
|
205
|
+
private settled;
|
|
199
206
|
private writeClock;
|
|
200
207
|
/** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
|
|
201
208
|
rebaseBranch(): Array<{
|
|
@@ -291,7 +298,9 @@ export declare class World {
|
|
|
291
298
|
token?: string;
|
|
292
299
|
force?: boolean;
|
|
293
300
|
}): Promise<Array<Awaited<ReturnType<typeof pushWorldChangeset>>>>;
|
|
294
|
-
mark
|
|
301
|
+
/** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
|
|
302
|
+
* joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
|
|
303
|
+
mark(id?: string, now?: Date): Promise<WorldMarker>;
|
|
295
304
|
marks(): WorldMarker[];
|
|
296
305
|
verify(name: string, opts?: {
|
|
297
306
|
into?: string;
|
package/dist/src/world.js
CHANGED
|
@@ -7,21 +7,14 @@ import { platformPerson } from "./credentials.js";
|
|
|
7
7
|
import { rebaseChangeset, worldBootMarker, listActions, inWorldHistory, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core/runtime';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import clockForm from '@volter/world-core/world-clock';
|
|
10
|
-
import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, materializeBrowserFiles, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
10
|
+
import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, findWorldMark, machineDoor, materializeBrowserFiles, playWorld, settleLocalWorldTime, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { requireToken, storeToken } from "./credentials.js";
|
|
14
14
|
import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigPath, worldEnvPath, worldSeedPath } from "./locate.js";
|
|
15
|
-
import { destinationOf, parentEntries, rebaseBranch } from '@volter/world-core/runtime';
|
|
15
|
+
import { destinationOf, durationMs, parentEntries, rebaseBranch } from '@volter/world-core/runtime';
|
|
16
16
|
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
17
17
|
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
18
|
-
/** `30d`, `12h`, `5m`, `90s` in ms. */
|
|
19
|
-
function durationMs(by) {
|
|
20
|
-
const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
|
|
21
|
-
if (!m)
|
|
22
|
-
throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
|
|
23
|
-
return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
24
|
-
}
|
|
25
18
|
export class TwinLog {
|
|
26
19
|
service;
|
|
27
20
|
stateService;
|
|
@@ -255,6 +248,8 @@ export class World {
|
|
|
255
248
|
hasSeed() { return existsSync(worldSeedPath(this.root)); }
|
|
256
249
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|
|
257
250
|
seedServed(base, token) { return seedServedWorld(base, token, { root: this.root, config: this.configRef(), cwd: this.root }); }
|
|
251
|
+
/** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
|
|
252
|
+
play(scenario) { return playWorld(this.name, { root: this.root, scenario }); }
|
|
258
253
|
/** Back to the default data: forget this branch's state, boot, seed. */
|
|
259
254
|
reset(opts = {}) { return resetWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
|
|
260
255
|
// ── branches ────────────────────────────────────────────────────────────────────────────
|
|
@@ -264,6 +259,14 @@ export class World {
|
|
|
264
259
|
* one starts; `checkout` brings it back with its state.
|
|
265
260
|
*/
|
|
266
261
|
async branch(name, opts = {}) {
|
|
262
|
+
// at a mark: from the branch it was taken on, at each twin's view and position there, with its clock (and the app's
|
|
263
|
+
// disk through the machine's fault door, where the mark took it)
|
|
264
|
+
const mark = opts.at?.mark !== undefined ? findWorldMark(opts.at.mark, this.root) : undefined;
|
|
265
|
+
const door = mark?.disk ? machineDoor() : null;
|
|
266
|
+
if (mark?.disk && !door)
|
|
267
|
+
throw new Error(`mark "${mark.id}" holds the app's disk, and this host gives no fault door to restore it through (VOLTER_WORLD_MACHINE_DOOR)`);
|
|
268
|
+
const base = mark?.world ?? this.name;
|
|
269
|
+
const at = mark ? { positions: Object.fromEntries(mark.ledgers.flatMap((l) => (l.position !== undefined ? [[l.stateService, l.position]] : []))), views: Object.fromEntries(mark.ledgers.flatMap((l) => (l.view !== undefined ? [[l.stateService, l.view]] : []))) } : opts.at;
|
|
267
270
|
if (listWorlds(this.root).some((w) => w.name === name))
|
|
268
271
|
throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
|
|
269
272
|
this.refuseWhileServed('branch');
|
|
@@ -273,7 +276,7 @@ export class World {
|
|
|
273
276
|
if (wasRunning)
|
|
274
277
|
await downWorld(this.name, this.root);
|
|
275
278
|
try {
|
|
276
|
-
await branchWorld(
|
|
279
|
+
await branchWorld(base, name, { root: this.root, envFile: worldEnvPath(this.root), ...(at ? { at } : {}) });
|
|
277
280
|
}
|
|
278
281
|
catch (error) {
|
|
279
282
|
if (wasRunning)
|
|
@@ -283,7 +286,16 @@ export class World {
|
|
|
283
286
|
setCurrentBranch(this.root, name);
|
|
284
287
|
const selected = statusWorld(name, this.root);
|
|
285
288
|
materializeBrowserFiles(selected.dirs.data, this.root, name, selected.config);
|
|
286
|
-
|
|
289
|
+
const made = new World(name, this.root);
|
|
290
|
+
if (mark) {
|
|
291
|
+
if (mark.clock)
|
|
292
|
+
await made.setClock(mark.clock);
|
|
293
|
+
else if (mark.clock === null)
|
|
294
|
+
await made.clearClock();
|
|
295
|
+
if (door)
|
|
296
|
+
await door.restorePosition(mark.id);
|
|
297
|
+
}
|
|
298
|
+
return made;
|
|
287
299
|
}
|
|
288
300
|
/** Switch to a branch: its twins come back with their state; this branch's stop. */
|
|
289
301
|
async checkout(name) {
|
|
@@ -333,25 +345,26 @@ export class World {
|
|
|
333
345
|
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
334
346
|
return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
|
|
335
347
|
}
|
|
336
|
-
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves.
|
|
348
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
|
|
349
|
+
* clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
|
|
337
350
|
setClock(iso) {
|
|
338
351
|
const parsed = Date.parse(iso);
|
|
339
352
|
if (Number.isNaN(parsed))
|
|
340
353
|
throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
|
|
341
354
|
this.writeClock({ kind: 'frozen', at: parsed });
|
|
342
|
-
return new Date(parsed).toISOString();
|
|
355
|
+
return this.settled(new Date(parsed).toISOString());
|
|
343
356
|
}
|
|
344
|
-
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
|
|
357
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
|
|
345
358
|
advanceClock(by) {
|
|
346
359
|
const file = clockFile(this.root, this.name);
|
|
347
360
|
if (!existsSync(file))
|
|
348
361
|
throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
|
|
349
362
|
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
350
363
|
this.writeClock({ ...c, at: c.at + durationMs(by) });
|
|
351
|
-
return this.clock().at;
|
|
364
|
+
return this.settled(this.clock().at);
|
|
352
365
|
}
|
|
353
366
|
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
354
|
-
* running clock further. */
|
|
367
|
+
* running clock further. Answers as setClock does. */
|
|
355
368
|
shiftClock(by) {
|
|
356
369
|
const file = clockFile(this.root, this.name);
|
|
357
370
|
const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
|
|
@@ -359,14 +372,18 @@ export class World {
|
|
|
359
372
|
throw new Error('the clock is frozen — `volter world clock advance` moves it');
|
|
360
373
|
const wall = Date.now();
|
|
361
374
|
this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
|
|
362
|
-
return this.clock().at;
|
|
375
|
+
return this.settled(this.clock().at);
|
|
363
376
|
}
|
|
364
|
-
/** Return the World to the machine's time. */
|
|
377
|
+
/** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
|
|
365
378
|
clearClock() {
|
|
366
379
|
const file = clockFile(this.root, this.name);
|
|
367
380
|
if (existsSync(file))
|
|
368
381
|
rmSync(file);
|
|
369
|
-
return new Date().toISOString();
|
|
382
|
+
return this.settled(new Date().toISOString());
|
|
383
|
+
}
|
|
384
|
+
async settled(at) {
|
|
385
|
+
await settleLocalWorldTime(this.name, this.root);
|
|
386
|
+
return at;
|
|
370
387
|
}
|
|
371
388
|
writeClock(c) {
|
|
372
389
|
const file = clockFile(this.root, this.name);
|
|
@@ -562,7 +579,15 @@ export class World {
|
|
|
562
579
|
return outcomes;
|
|
563
580
|
}
|
|
564
581
|
// ── review (the operator's verbs, kept on the SDK for the remote and for CI) ────────────
|
|
565
|
-
mark
|
|
582
|
+
/** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
|
|
583
|
+
* joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
|
|
584
|
+
async mark(id, now) {
|
|
585
|
+
const door = machineDoor();
|
|
586
|
+
const marker = markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}), ...(door ? { disk: true } : {}) });
|
|
587
|
+
if (door)
|
|
588
|
+
await door.takePosition(marker.id);
|
|
589
|
+
return marker;
|
|
590
|
+
}
|
|
566
591
|
marks() { return listWorldMarks(this.name, this.root); }
|
|
567
592
|
verify(name, opts = { ephemeral: true }) { return verifyWorldChangeset(name, { root: this.root, world: this.name, ...opts }); }
|
|
568
593
|
approve(name, principal, note) { return approveWorldChangeset(name, { root: this.root, world: this.name, principal, ...(note ? { note } : {}) }); }
|
|
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
|
|
|
13
13
|
Copy this starter into a new folder:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.194 example customer-onboarding --dir onboarding
|
|
17
17
|
$ cd onboarding
|
|
18
18
|
$ pnpm install
|
|
19
19
|
```
|
|
@@ -7,22 +7,22 @@ try {
|
|
|
7
7
|
await initialize();
|
|
8
8
|
switch (process.argv[2]) {
|
|
9
9
|
case 'baseline': {
|
|
10
|
-
world.setClock(at);
|
|
10
|
+
await world.setClock(at);
|
|
11
11
|
const rows = await listSignups();
|
|
12
12
|
if (rows.length > 1 || rows.some(r => r.name !== 'Grace')) throw new Error('This demo contains customer work; reset both your disposable application database and demo World explicitly.');
|
|
13
13
|
const grace = rows.find(r => r.name === 'Grace') ?? await onboard({ name: 'Grace', email: 'owner@world.test' });
|
|
14
|
-
world.advanceClock('5s');
|
|
14
|
+
await world.advanceClock('5s');
|
|
15
15
|
console.log('Baseline', JSON.stringify(summarize((await listSignups()).find(r => r.customer_id === grace.customer_id)))); break;
|
|
16
16
|
}
|
|
17
17
|
case 'present': {
|
|
18
18
|
const rows = await listSignups();
|
|
19
19
|
if (rows.length !== 1 || rows[0].name !== 'Grace') throw new Error('Prepare the Grace-only baseline first.');
|
|
20
|
-
world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
|
|
20
|
+
await world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
|
|
21
21
|
const partial = await onboard({ name: 'Ada', email: 'owner@world.test', from: 'Example <welcome@unverified.world.test>' });
|
|
22
22
|
console.log('Email refusal; customer and SQL row retained', JSON.stringify(summarize(partial)));
|
|
23
23
|
const retry = await retryWelcome(partial.customer_id);
|
|
24
24
|
console.log('Retry accepted on the same customer', JSON.stringify(summarize(retry)));
|
|
25
|
-
world.advanceClock('5s');
|
|
25
|
+
await world.advanceClock('5s');
|
|
26
26
|
console.log('Delivered', JSON.stringify((await listSignups()).map(summarize))); break;
|
|
27
27
|
}
|
|
28
28
|
default: throw new Error('Use baseline or present');
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"pg": "8.16.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.194",
|
|
15
|
+
"@volter/world-core": "3.0.167",
|
|
16
|
+
"@volter/world-runtime": "3.0.192",
|
|
17
|
+
"@volter/world-console": "3.0.162",
|
|
18
18
|
"@volter/twin-stripe": "3.0.2",
|
|
19
19
|
"@volter/twin-resend": "1.0.1",
|
|
20
|
-
"@volter/world-ui": "1.0.
|
|
20
|
+
"@volter/world-ui": "1.0.166"
|
|
21
21
|
},
|
|
22
22
|
"packageManager": "pnpm@12.15.0",
|
|
23
23
|
"devEngines": {
|
|
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
|
|
|
13
13
|
Use an empty parent folder. Create the baseline starter there:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir baseline
|
|
17
17
|
$ cd baseline
|
|
18
18
|
$ pnpm install
|
|
19
19
|
$ ./node_modules/.bin/volter world init --name baseline --twins stripe
|
|
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
|
|
|
38
38
|
Create three more copies in the same parent folder:
|
|
39
39
|
|
|
40
40
|
```console
|
|
41
|
-
$ npx @volter/world@3.0.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
41
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir integration
|
|
42
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir worker-a
|
|
43
|
+
$ npx @volter/world@3.0.194 example integration-branches --dir worker-b
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"stripe": "17.7.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@volter/world": "3.0.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
12
|
+
"@volter/world": "3.0.194",
|
|
13
|
+
"@volter/world-core": "3.0.167",
|
|
14
|
+
"@volter/world-runtime": "3.0.192",
|
|
15
|
+
"@volter/world-console": "3.0.162",
|
|
16
16
|
"@volter/twin-stripe": "3.0.2",
|
|
17
|
-
"@volter/world-ui": "1.0.
|
|
17
|
+
"@volter/world-ui": "1.0.166"
|
|
18
18
|
},
|
|
19
19
|
"packageManager": "pnpm@12.15.0",
|
|
20
20
|
"devEngines": {
|
|
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
|
|
|
19
19
|
together; its recorded workflow does not establish behavior for a different release.
|
|
20
20
|
|
|
21
21
|
```console
|
|
22
|
-
$ npx @volter/world@3.0.
|
|
22
|
+
$ npx @volter/world@3.0.194 example workplace-agent --dir workplace
|
|
23
23
|
$ cd workplace
|
|
24
24
|
$ pnpm install
|
|
25
25
|
$ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"@slack/web-api": "7.19.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.194",
|
|
15
|
+
"@volter/world-core": "3.0.167",
|
|
16
|
+
"@volter/world-runtime": "3.0.192",
|
|
17
|
+
"@volter/world-console": "3.0.162",
|
|
18
18
|
"@volter/twin-github": "3.0.9",
|
|
19
19
|
"@volter/twin-linear": "3.0.5",
|
|
20
20
|
"@volter/twin-slack": "3.0.8",
|
|
21
|
-
"@volter/world-ui": "1.0.
|
|
21
|
+
"@volter/world-ui": "1.0.166"
|
|
22
22
|
},
|
|
23
23
|
"packageManager": "pnpm@12.15.0",
|
|
24
24
|
"devEngines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.194",
|
|
4
4
|
"description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"volter",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
34
|
-
"@volter/world-core": "3.0.
|
|
35
|
-
"@volter/world-runtime": "3.0.
|
|
34
|
+
"@volter/world-core": "3.0.167",
|
|
35
|
+
"@volter/world-runtime": "3.0.192",
|
|
36
36
|
"zod": "^4.6.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@volter/world-console": "3.0.
|
|
39
|
+
"@volter/world-console": "3.0.162",
|
|
40
40
|
"playwright": "^1.63.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@volter/twin-anthropic": "1.0.6",
|
|
52
|
-
"@volter/world-console": "3.0.
|
|
52
|
+
"@volter/world-console": "3.0.162"
|
|
53
53
|
},
|
|
54
54
|
"bin": {
|
|
55
55
|
"volter": "dist/src/cli.js"
|
|
@@ -117,10 +117,12 @@ Identity comes from credentials in the vendor's manner; inspect `GET /twin` for
|
|
|
117
117
|
identity and time rules. Use `clock set` and `clock advance` for backdated history. Without a
|
|
118
118
|
frozen World clock, the runtime uses wall time.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
Place a vendor's judgment (a decision) and failures (faults) on a scenario's timeline,
|
|
121
|
+
`scenarios/<name>.json` beside `world.json` (typically `.volter/scenarios/`), and run it with
|
|
122
|
+
`volter world play <file>`: it resets to the default data, sets the clock to the scenario's
|
|
123
|
+
`start` and makes the scenario the World's, which every twin reads at each request. A twin turns
|
|
124
|
+
each input into its own answer and records it; nothing in a scenario answers in its place.
|
|
125
|
+
`GET /twin/scenario` shows what is placed on that twin and what it consumed.
|
|
124
126
|
Model twins serve deterministic scripts or labeled stubs, so verify calls and state changes,
|
|
125
127
|
not the quality of prose from an unconfigured model.
|
|
126
128
|
|
package/src/cli.ts
CHANGED
|
@@ -51,9 +51,11 @@ world — a shared simulated internet (the world of the current directory)
|
|
|
51
51
|
volter world generate write seed.generated.ts from world.json's people, organizations and accounts
|
|
52
52
|
volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
|
|
53
53
|
volter world seed load the default data
|
|
54
|
+
volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties
|
|
55
|
+
volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)
|
|
54
56
|
volter world reset back to the default data
|
|
55
57
|
volter world down [--purge] stop the twins (--purge forgets this branch's state)
|
|
56
|
-
volter world branch [<name>]
|
|
58
|
+
volter world branch [<name>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) and check it out
|
|
57
59
|
volter world checkout <name> switch branches
|
|
58
60
|
volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
|
|
59
61
|
volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
|
|
@@ -302,6 +304,23 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
|
302
304
|
if (r.exitCode !== 0) process.exitCode = r.exitCode;
|
|
303
305
|
return;
|
|
304
306
|
}
|
|
307
|
+
case 'mark': {
|
|
308
|
+
const m = await world.mark(pos[0]);
|
|
309
|
+
if (asJson) { json(m); return; }
|
|
310
|
+
out(`mark ${m.id} ${m.ledgers.length} twin log${m.ledgers.length === 1 ? '' : 's'} clock ${m.clock ?? 'the machine\'s'} ${m.disk ? 'with the app\'s disk' : 'without the app\'s disk (no fault door)'}`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
case 'play': {
|
|
314
|
+
const file = pos[0];
|
|
315
|
+
if (!file) throw new Error('volter world play <scenario.json>');
|
|
316
|
+
const r = await world.play(file);
|
|
317
|
+
if (asJson) { json(r); return; }
|
|
318
|
+
out(`played ${r.scenario} ${r.start} → ${r.end} ${r.calls.length} call${r.calls.length === 1 ? '' : 's'}`);
|
|
319
|
+
out(r.completeness.total === 0 ? 'complete the World was the whole internet this run reached' : `incomplete ${Object.entries(r.completeness.egress).map(([d, n]) => `${d} refused ${n.refused}${n.passed ? ` passed ${n.passed}` : ''}`).concat(Object.entries(r.completeness.unimplemented).flatMap(([v, ops]) => Object.entries(ops).map(([op, n]) => `${v} ${op} unimplemented ${n}`))).concat(Object.entries(r.completeness.machine).map(([f, n]) => `${f} fault refused ${n} (no fault door)`)).join('; ')}`);
|
|
320
|
+
for (const p of r.properties) out(`${p.verdict.padEnd(8)} ${p.kind.padEnd(10)} ${p.name}${p.step !== undefined ? ` (step ${p.step}, ${p.at})` : ''}`);
|
|
321
|
+
if (r.properties.some((p) => p.verdict === 'broken')) process.exitCode = 1;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
305
324
|
case 'reset': {
|
|
306
325
|
const r = await world.reset();
|
|
307
326
|
out(`Back to the default data on ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
|
|
@@ -312,11 +331,11 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
|
312
331
|
const name = pos[0];
|
|
313
332
|
if (!name) { for (const b of world.branches()) out(`${b === world.name ? '* ' : ' '}${b}`); return; }
|
|
314
333
|
const atText = value(args, '--at');
|
|
315
|
-
const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : (() => {
|
|
334
|
+
const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : !atText.includes('@') ? { mark: atText } : (() => {
|
|
316
335
|
const positions: Record<string, number> = {}; const views: Record<string, string> = {};
|
|
317
336
|
for (const p of atText.split(',')) {
|
|
318
337
|
const match = /^([^@]+)@(?:([a-f0-9]{64}):)?(\d+)$/.exec(p);
|
|
319
|
-
if (!match) throw new Error('--at takes an instant, twin@position, or twin@view:position');
|
|
338
|
+
if (!match) throw new Error('--at takes an instant, a mark, twin@position, or twin@view:position');
|
|
320
339
|
positions[match[1]!] = Number(match[3]); if (match[2]) views[match[1]!] = match[2];
|
|
321
340
|
}
|
|
322
341
|
return { positions, views };
|
|
@@ -335,10 +354,10 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
|
335
354
|
}
|
|
336
355
|
case 'clock': {
|
|
337
356
|
const [action, arg] = pos;
|
|
338
|
-
if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(world.setClock(arg)); return; }
|
|
339
|
-
if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(world.advanceClock(arg)); return; }
|
|
340
|
-
if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(world.shiftClock(arg)); return; }
|
|
341
|
-
if (action === 'clear') { out(world.clearClock()); return; }
|
|
357
|
+
if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(await world.setClock(arg)); return; }
|
|
358
|
+
if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(await world.advanceClock(arg)); return; }
|
|
359
|
+
if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(await world.shiftClock(arg)); return; }
|
|
360
|
+
if (action === 'clear') { out(await world.clearClock()); return; }
|
|
342
361
|
if (action === undefined || action === 'show') { const c = world.clock(); out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`); return; }
|
|
343
362
|
throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
|
|
344
363
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,3 +4,5 @@
|
|
|
4
4
|
export { World, parseOriginUrl, worldNameFor, type LogEntry, type WorldMode, type WorldRef, type WorldStatus } from './world.ts';
|
|
5
5
|
export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from './locate.ts';
|
|
6
6
|
export { credentialsPath, requireToken, storeToken, tokenFor } from './credentials.ts';
|
|
7
|
+
// a scenario's properties (ADR 0036 §7): `export default [always(...), sometimes(...), eventually(...)]`
|
|
8
|
+
export { always, sometimes, eventually, type Property, type RunReport } from '@volter/world-runtime';
|
package/src/sdk.test.ts
CHANGED
|
@@ -68,8 +68,8 @@ describe('World — the twins an app needs, running together', () => {
|
|
|
68
68
|
await expect(back.checkout('nonesuch')).rejects.toThrow(/No branch "nonesuch"/);
|
|
69
69
|
|
|
70
70
|
// down stops compute; up resumes the branch WITH its state (reset is the way back to the defaults)
|
|
71
|
-
back.setClock(AT);
|
|
72
|
-
back.advanceClock('1h');
|
|
71
|
+
await back.setClock(AT);
|
|
72
|
+
await back.advanceClock('1h');
|
|
73
73
|
const retainedClock = back.clock();
|
|
74
74
|
await back.down();
|
|
75
75
|
expect(back.status().running).toBe(false);
|
package/src/world-browser.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
|
|
2
2
|
// (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
|
|
3
3
|
// the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
|
|
4
|
-
import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
4
|
+
import { clockFile, deriveWorldManifest, followWorld, overlayWorldPackFacts, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
5
|
+
import clockForm from '@volter/world-core/world-clock';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
7
|
|
|
6
8
|
export async function openWorldBrowser(world: { name: string; root: string }, name: string, url: string | undefined, cdp: string | undefined, say: (line: string) => void): Promise<void> {
|
|
7
9
|
const instance = statusWorld(world.name, world.root);
|
|
@@ -10,16 +12,21 @@ export async function openWorldBrowser(world: { name: string; root: string }, na
|
|
|
10
12
|
const names = worldBrowsers(instance.dirs.data).map(row => row.name);
|
|
11
13
|
throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
|
|
12
14
|
}
|
|
15
|
+
overlayWorldPackFacts({ configPath: instance.configPath, root: world.root });
|
|
13
16
|
const manifest = deriveWorldManifest(world.name, instance.env ?? {});
|
|
14
17
|
if (!instance.running || !manifest.browser) throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
|
|
15
18
|
let chromium: typeof import('playwright').chromium;
|
|
16
19
|
try { ({ chromium } = await import('playwright')); }
|
|
17
20
|
catch { throw new Error('volter world browser needs playwright: npm i playwright'); }
|
|
18
21
|
const profile = record.state;
|
|
22
|
+
// the page follows the World (ADR 0036 §6): its clock as the World's stands, its randomness seeded by World and browser
|
|
23
|
+
const clock = existsSync(clockFile(world.root, world.name)) ? clockForm.parseClock(readFileSync(clockFile(world.root, world.name), 'utf8')) : null;
|
|
24
|
+
const follow = { now: new Date(clock ? clockForm.clockNowMs(clock, Date.now()) : Date.now()).toISOString(), frozen: clock?.kind === 'frozen', random: `${world.name}:${name}` };
|
|
19
25
|
if (cdp === undefined) {
|
|
20
26
|
const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
|
|
21
27
|
const closed = new Promise<void>(resolve => browser.on('disconnected', () => resolve()));
|
|
22
28
|
const context = await browser.newContext({ ...profile.context, storageState: profile.storageState } as Parameters<typeof browser.newContext>[0]);
|
|
29
|
+
await followWorld(context, follow);
|
|
23
30
|
const page = await context.newPage();
|
|
24
31
|
await page.goto(url ?? 'about:blank');
|
|
25
32
|
await closed;
|
|
@@ -31,6 +38,7 @@ export async function openWorldBrowser(world: { name: string; root: string }, na
|
|
|
31
38
|
// An existing default context takes no context options (ADR 0016): its cookies are what is loaded.
|
|
32
39
|
if (Object.keys(profile.context).length) say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
|
|
33
40
|
await context.addCookies(profile.storageState.cookies as Parameters<typeof context.addCookies>[0]);
|
|
41
|
+
await followWorld(context, follow);
|
|
34
42
|
const page = await context.newPage();
|
|
35
43
|
await page.goto(url ?? 'about:blank');
|
|
36
44
|
await browser.close();
|
package/src/world.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import clockForm from '@volter/world-core/world-clock';
|
|
10
10
|
import {
|
|
11
11
|
activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld,
|
|
12
|
-
materializeBrowserFiles, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
|
|
12
|
+
findWorldMark, machineDoor, materializeBrowserFiles, playWorld, settleLocalWorldTime, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
|
|
13
13
|
bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, type ServedWorld, type ViewConsole, type WorldView, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn,
|
|
14
14
|
type DeployTwinOutcome, type MaterializedRoot,
|
|
15
15
|
type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
@@ -17,19 +17,13 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
|
|
|
17
17
|
import { dirname, join, resolve } from 'node:path';
|
|
18
18
|
import { requireToken, storeToken } from './credentials.ts';
|
|
19
19
|
import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigPath, worldEnvPath, worldSeedPath } from './locate.ts';
|
|
20
|
-
import { destinationOf, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
|
|
20
|
+
import { destinationOf, durationMs, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
|
|
21
21
|
|
|
22
22
|
export type WorldRef = { name?: string; root?: string };
|
|
23
23
|
export type WorldMode = 'local' | 'share' | 'sealed';
|
|
24
24
|
|
|
25
25
|
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
26
26
|
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
27
|
-
/** `30d`, `12h`, `5m`, `90s` in ms. */
|
|
28
|
-
function durationMs(by: string): number {
|
|
29
|
-
const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
|
|
30
|
-
if (!m) throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
|
|
31
|
-
return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
|
|
32
|
-
}
|
|
33
27
|
|
|
34
28
|
export class TwinLog {
|
|
35
29
|
constructor(readonly service: string, readonly stateService: string, readonly root: string) {}
|
|
@@ -248,6 +242,8 @@ export class World {
|
|
|
248
242
|
hasSeed(): boolean { return existsSync(worldSeedPath(this.root)); }
|
|
249
243
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|
|
250
244
|
seedServed(base: string, token: string): ReturnType<typeof seedServedWorld> { return seedServedWorld(base, token, { root: this.root, config: this.configRef(), cwd: this.root }); }
|
|
245
|
+
/** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
|
|
246
|
+
play(scenario: string): ReturnType<typeof playWorld> { return playWorld(this.name, { root: this.root, scenario }); }
|
|
251
247
|
/** Back to the default data: forget this branch's state, boot, seed. */
|
|
252
248
|
reset(opts: { entry?: string; cwd?: string } = {}): ReturnType<typeof resetWorld> { return resetWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
|
|
253
249
|
|
|
@@ -257,7 +253,14 @@ export class World {
|
|
|
257
253
|
* at a time in a world (the app's env names one set of twins), so this branch stops as the new
|
|
258
254
|
* one starts; `checkout` brings it back with its state.
|
|
259
255
|
*/
|
|
260
|
-
async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string
|
|
256
|
+
async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string>; mark?: string } } = {}): Promise<World> {
|
|
257
|
+
// at a mark: from the branch it was taken on, at each twin's view and position there, with its clock (and the app's
|
|
258
|
+
// disk through the machine's fault door, where the mark took it)
|
|
259
|
+
const mark = opts.at?.mark !== undefined ? findWorldMark(opts.at.mark, this.root) : undefined;
|
|
260
|
+
const door = mark?.disk ? machineDoor() : null;
|
|
261
|
+
if (mark?.disk && !door) throw new Error(`mark "${mark.id}" holds the app's disk, and this host gives no fault door to restore it through (VOLTER_WORLD_MACHINE_DOOR)`);
|
|
262
|
+
const base = mark?.world ?? this.name;
|
|
263
|
+
const at = mark ? { positions: Object.fromEntries(mark.ledgers.flatMap((l) => (l.position !== undefined ? [[l.stateService, l.position]] : []))), views: Object.fromEntries(mark.ledgers.flatMap((l) => (l.view !== undefined ? [[l.stateService, l.view]] : []))) } : opts.at;
|
|
261
264
|
if (listWorlds(this.root).some((w) => w.name === name)) throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
|
|
262
265
|
this.refuseWhileServed('branch');
|
|
263
266
|
// this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
|
|
@@ -265,7 +268,7 @@ export class World {
|
|
|
265
268
|
const wasRunning = !!this.instance()?.running;
|
|
266
269
|
if (wasRunning) await downWorld(this.name, this.root);
|
|
267
270
|
try {
|
|
268
|
-
await branchWorld(
|
|
271
|
+
await branchWorld(base, name, { root: this.root, envFile: worldEnvPath(this.root), ...(at ? { at } : {}) });
|
|
269
272
|
} catch (error) {
|
|
270
273
|
if (wasRunning) await checkoutWorld(this.name, { root: this.root });
|
|
271
274
|
throw error;
|
|
@@ -273,7 +276,12 @@ export class World {
|
|
|
273
276
|
setCurrentBranch(this.root, name);
|
|
274
277
|
const selected = statusWorld(name, this.root);
|
|
275
278
|
materializeBrowserFiles(selected.dirs.data, this.root, name, selected.config);
|
|
276
|
-
|
|
279
|
+
const made = new World(name, this.root);
|
|
280
|
+
if (mark) {
|
|
281
|
+
if (mark.clock) await made.setClock(mark.clock); else if (mark.clock === null) await made.clearClock();
|
|
282
|
+
if (door) await door.restorePosition(mark.id);
|
|
283
|
+
}
|
|
284
|
+
return made;
|
|
277
285
|
}
|
|
278
286
|
/** Switch to a branch: its twins come back with their state; this branch's stop. */
|
|
279
287
|
async checkout(name: string): Promise<World> {
|
|
@@ -315,36 +323,41 @@ export class World {
|
|
|
315
323
|
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
316
324
|
return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
|
|
317
325
|
}
|
|
318
|
-
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves.
|
|
319
|
-
|
|
326
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
|
|
327
|
+
* clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
|
|
328
|
+
setClock(iso: string): Promise<string> {
|
|
320
329
|
const parsed = Date.parse(iso);
|
|
321
330
|
if (Number.isNaN(parsed)) throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
|
|
322
331
|
this.writeClock({ kind: 'frozen', at: parsed });
|
|
323
|
-
return new Date(parsed).toISOString();
|
|
332
|
+
return this.settled(new Date(parsed).toISOString());
|
|
324
333
|
}
|
|
325
|
-
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
|
|
326
|
-
advanceClock(by: string): string {
|
|
334
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
|
|
335
|
+
advanceClock(by: string): Promise<string> {
|
|
327
336
|
const file = clockFile(this.root, this.name);
|
|
328
337
|
if (!existsSync(file)) throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
|
|
329
338
|
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
330
339
|
this.writeClock({ ...c, at: c.at + durationMs(by) });
|
|
331
|
-
return this.clock().at;
|
|
340
|
+
return this.settled(this.clock().at);
|
|
332
341
|
}
|
|
333
342
|
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
334
|
-
* running clock further. */
|
|
335
|
-
shiftClock(by: string): string {
|
|
343
|
+
* running clock further. Answers as setClock does. */
|
|
344
|
+
shiftClock(by: string): Promise<string> {
|
|
336
345
|
const file = clockFile(this.root, this.name);
|
|
337
346
|
const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
|
|
338
347
|
if (c?.kind === 'frozen') throw new Error('the clock is frozen — `volter world clock advance` moves it');
|
|
339
348
|
const wall = Date.now();
|
|
340
349
|
this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
|
|
341
|
-
return this.clock().at;
|
|
350
|
+
return this.settled(this.clock().at);
|
|
342
351
|
}
|
|
343
|
-
/** Return the World to the machine's time. */
|
|
344
|
-
clearClock(): string {
|
|
352
|
+
/** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
|
|
353
|
+
clearClock(): Promise<string> {
|
|
345
354
|
const file = clockFile(this.root, this.name);
|
|
346
355
|
if (existsSync(file)) rmSync(file);
|
|
347
|
-
return new Date().toISOString();
|
|
356
|
+
return this.settled(new Date().toISOString());
|
|
357
|
+
}
|
|
358
|
+
private async settled(at: string): Promise<string> {
|
|
359
|
+
await settleLocalWorldTime(this.name, this.root);
|
|
360
|
+
return at;
|
|
348
361
|
}
|
|
349
362
|
private writeClock(c: Parameters<typeof clockForm.formatClock>[0]): void {
|
|
350
363
|
const file = clockFile(this.root, this.name);
|
|
@@ -509,7 +522,14 @@ export class World {
|
|
|
509
522
|
}
|
|
510
523
|
|
|
511
524
|
// ── review (the operator's verbs, kept on the SDK for the remote and for CI) ────────────
|
|
512
|
-
|
|
525
|
+
/** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
|
|
526
|
+
* joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
|
|
527
|
+
async mark(id?: string, now?: Date): Promise<WorldMarker> {
|
|
528
|
+
const door = machineDoor();
|
|
529
|
+
const marker = markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}), ...(door ? { disk: true } : {}) });
|
|
530
|
+
if (door) await door.takePosition(marker.id);
|
|
531
|
+
return marker;
|
|
532
|
+
}
|
|
513
533
|
marks(): WorldMarker[] { return listWorldMarks(this.name, this.root); }
|
|
514
534
|
verify(name: string, opts: { into?: string; ephemeral?: boolean } = { ephemeral: true }): ReturnType<typeof verifyWorldChangeset> { return verifyWorldChangeset(name, { root: this.root, world: this.name, ...opts }); }
|
|
515
535
|
approve(name: string, principal: string, note?: string): ReturnType<typeof approveWorldChangeset> { return approveWorldChangeset(name, { root: this.root, world: this.name, principal, ...(note ? { note } : {}) }); }
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// A world in the app repo carries its handlers as ROOT-RELATIVE paths (docs/concepts/worlds.md
|
|
2
|
-
// #the-config-and-the-running-world): init copies a twin's starter handlers to .volter/handlers/<vendor>.json and
|
|
3
|
-
// names that path relative to the world root, so the committed config reads the same on every
|
|
4
|
-
// machine; `up` resolves it against the root on both the spawn path (--scenario) and the colocated
|
|
5
|
-
// host (scenarioPath). Proven against the real anthropic twin, its files linked from the pack repository: its starter
|
|
6
|
-
// handlers are the scripted turns of its own life (journeys/scenario.json).
|
|
7
|
-
import { afterAll, describe, expect, test } from 'bun:test';
|
|
8
|
-
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
import { installedPackageDir } from '@volter/world-core/runtime-state';
|
|
12
|
-
import { World } from './index.ts';
|
|
13
|
-
|
|
14
|
-
function app(): string {
|
|
15
|
-
const parent = mkdtempSync(join(tmpdir(), 'handlers-in-repo-'));
|
|
16
|
-
const dir = join(parent, 'agent');
|
|
17
|
-
mkdirSync(dir);
|
|
18
|
-
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'agent', dependencies: { '@anthropic-ai/sdk': '^0.30' } }));
|
|
19
|
-
writeFileSync(join(dir, '.env.example'), 'ANTHROPIC_API_KEY=\n');
|
|
20
|
-
const installed = join(parent, 'node_modules', '@volter', 'twin-anthropic');
|
|
21
|
-
mkdirSync(installed, { recursive: true });
|
|
22
|
-
// the published pack, this package's dev dependency (ADR 0034, decision 6)
|
|
23
|
-
const pack = installedPackageDir(import.meta.dir, '@volter/twin-anthropic')!;
|
|
24
|
-
for (const entry of readdirSync(pack)) {
|
|
25
|
-
if (entry === 'package.json') copyFileSync(join(pack, entry), join(installed, entry));
|
|
26
|
-
else symlinkSync(join(pack, entry), join(installed, entry));
|
|
27
|
-
}
|
|
28
|
-
return dir;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const root = app();
|
|
32
|
-
const world = World.init(root).world;
|
|
33
|
-
afterAll(async () => { await world.down().catch(() => undefined); });
|
|
34
|
-
|
|
35
|
-
describe('handlers in the app repo', () => {
|
|
36
|
-
test('init copies the starter handlers and names them relative to the root', () => {
|
|
37
|
-
expect(readFileSync(join(root, '.volter', 'handlers', 'anthropic.json'), 'utf8')).toContain('"handlers"');
|
|
38
|
-
const config = JSON.parse(readFileSync(join(root, '.volter', 'world.json'), 'utf8')) as { schemaVersion: number; services: Array<{ id: string; execution?: { process?: { args?: string[] }; colocate?: { scenarioPath?: string } } }> };
|
|
39
|
-
expect(config.schemaVersion).toBe(2);
|
|
40
|
-
const anthropic = config.services.find((s) => s.id === 'anthropic')!;
|
|
41
|
-
expect(anthropic.execution?.process?.args).toEqual(['--scenario', '.volter/handlers/anthropic.json']);
|
|
42
|
-
expect(anthropic.execution?.colocate?.scenarioPath).toBe('.volter/handlers/anthropic.json');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('up resolves them against the root, and the twin serves them', async () => {
|
|
46
|
-
const instance = await world.up({ seed: false });
|
|
47
|
-
const url = instance.services.anthropic!.url!;
|
|
48
|
-
const scenario = await fetch(`${url}/twin/scenario`).then((r) => r.json()) as { handlers?: Array<{ id: string }> };
|
|
49
|
-
expect((scenario.handlers ?? []).map((h) => h.id)).toContain('draft-result');
|
|
50
|
-
});
|
|
51
|
-
});
|