@volter/world-core 2.0.0 → 2.0.1

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/src/storage.ts CHANGED
@@ -59,13 +59,25 @@ function assertServiceName(service: string): string {
59
59
  */
60
60
  let worldPathsObserver: ((service: string, root: string | undefined) => void) | undefined;
61
61
 
62
- export function observeWorldPaths(observer: (service: string, root: string | undefined) => void): void {
62
+ /** Install the observer; the one it replaces is returned (a test puts it back). */
63
+ export function observeWorldPaths(observer: ((service: string, root: string | undefined) => void) | undefined): ((service: string, root: string | undefined) => void) | undefined {
64
+ const previous = worldPathsObserver;
63
65
  worldPathsObserver = observer;
66
+ return previous;
67
+ }
68
+
69
+ /** While > 0, a read of ANOTHER pack's store is in progress (`projectOwnerResources`): that is not the request's twin
70
+ * saying who it is, so it claims no journal identity. */
71
+ let identityQuiet = 0;
72
+ /** Run `fn` without its worldPaths calls reaching the identity observer. */
73
+ export function withoutIdentityClaim<T>(fn: () => T): T {
74
+ identityQuiet++;
75
+ try { return fn(); } finally { identityQuiet--; }
64
76
  }
65
77
 
66
78
  export function worldPaths(service: string, root?: string): WorldPaths {
67
79
  const safeService = assertServiceName(service);
68
- worldPathsObserver?.(safeService, root);
80
+ if (identityQuiet === 0) worldPathsObserver?.(safeService, root);
69
81
  const resolvedRoot = projectRoot(root);
70
82
  const dir = join(worldStateRoot(root), safeService);
71
83
  return {
@@ -81,6 +93,44 @@ export function worldPaths(service: string, root?: string): WorldPaths {
81
93
  };
82
94
  }
83
95
 
96
+ /**
97
+ * THE WORLD'S DATA DIRECTORY, as the World runtime hands it to every service it starts. Each twin of a World runs on
98
+ * its own root, `<data>/<service id>` (world-runtime's startService and startColocatedServices), so one twin's
99
+ * `.volter/world/<service>` is not another's; `ownerStoreRoots` finds a store across them.
100
+ */
101
+ export const WORLD_DATA_ENV = 'VOLTER_WORLD_DATA';
102
+
103
+ /** Does `root` hold `service`'s store: its log or its branch record (what a write or a fork leaves; a read leaves
104
+ * neither)? The paths are built without worldPaths, so a probe claims no journal identity. */
105
+ function holdsStore(service: string, root: string): boolean {
106
+ const dir = join(worldStateRoot(root), service);
107
+ const store = getActiveWorldStore();
108
+ return ['events.jsonl', 'actions.jsonl', 'branch.json'].some((file) => store.exists(join(dir, file)));
109
+ }
110
+
111
+ /**
112
+ * The roots that hold the store of `owner` (ANOTHER pack's, read by contract: architecture A3), for a twin on `root`:
113
+ * - `[root]` when `root` itself holds that store (both packs on one root: `volter world serve`, a test, a pack serving
114
+ * several services on its root, as aws does);
115
+ * - else, when `root` is one of a World's service roots (a directory directly under the World's data directory), every
116
+ * other service root there that holds it (usually one; two when the World runs the owning pack twice);
117
+ * - else `[]`: nothing is issued yet, or the twin runs outside a World.
118
+ * Only a declared cross-pack read resolves through here (`projectOwnerResources`); a pack's reads of its OWN store never
119
+ * leave its root, so a fresh twin never reads a sibling that happens to run the same pack. Not memoized: it is a few
120
+ * stats per service root, and a store that appears after the first read (the owner's first write) must be found.
121
+ */
122
+ export function ownerStoreRoots(owner: string, root?: string): string[] {
123
+ const safeOwner = assertServiceName(owner);
124
+ const own = projectRoot(root);
125
+ if (holdsStore(safeOwner, own)) return [own];
126
+ const data = typeof process === 'undefined' ? undefined : process.env[WORLD_DATA_ENV];
127
+ if (!data || dirname(own) !== resolve(data)) return [];
128
+ return getActiveWorldStore().list(dirname(own))
129
+ .map((name) => join(dirname(own), name))
130
+ .filter((candidate) => candidate !== own && holdsStore(safeOwner, candidate))
131
+ .sort();
132
+ }
133
+
84
134
  function readJsonl<T>(path: string): T[] {
85
135
  const rows: T[] = [];
86
136
  for (const [index, line] of getActiveWorldStore().readLines(path).entries()) {
@@ -6,28 +6,31 @@
6
6
  // is the operator door (set / advance / show). No file, or no env → real wall-clock time,
7
7
  // exactly the pre-clock behavior; a world only has scripted time when its operator says so.
8
8
  //
9
- // File format: a single ISO-8601 line (a FROZEN instant). Every read returns exactly that
10
- // instant — deterministic by construction. Advancing time is an explicit operator act, never
11
- // a background drift.
12
- // Read through the ACTIVE WorldStore, not node:fs: the clock file is world state, and a
13
- // hydrated serverless namespace must carry its frozen instant with it (runtime contract R11–R14
14
- // — a MemoryWorldStore world that read the clock off the host filesystem would silently
15
- // serve the HOST's time, the exact two-services-disagreeing-about-now defect below).
9
+ // File format (world-clock.cjs, the one home of it, shared with the injector that gives an application process the
10
+ // same time): a FROZEN instant, one ISO-8601 line, every read returning exactly that instant; or a RUNNING clock,
11
+ // `<instant> from <wall instant>`, the World's time at a moment of the machine's, running on at the machine's pace (a
12
+ // World serving an application). Moving time is an explicit operator act either way, never a background drift.
16
13
  import { getActiveWorldStore } from './world-store.ts';
14
+ import clockForm from '../world-clock.cjs';
17
15
 
18
16
  export const WORLD_CLOCK_ENV = 'TWIN_WORLD_CLOCK_FILE';
19
17
 
20
- /** The world's "now": the frozen instant in the clock file when one is configured and
21
- * parseable; real wall-clock time otherwise. */
18
+ /** The machine's own time: the real Date.now even where the injector gives the process the World's time (inject.cjs
19
+ * leaves the original under this symbol), so a running clock's offset is applied once. */
20
+ const machineNow = (): number => {
21
+ const real = (globalThis as Record<symbol, unknown>)[Symbol.for('volter.machineDateNow')];
22
+ return typeof real === 'function' ? (real as () => number)() : Date.now();
23
+ };
24
+
25
+ /** The world's "now": the clock file's time when one is configured (its frozen instant, or a running clock's time
26
+ * now); real wall-clock time otherwise. */
22
27
  export function worldNow(): string {
23
28
  const file = process.env[WORLD_CLOCK_ENV];
24
29
  if (file && getActiveWorldStore().exists(file)) {
25
- const raw = (getActiveWorldStore().read(file) ?? '').trim();
26
- const parsed = Date.parse(raw);
27
- if (!Number.isNaN(parsed)) return new Date(parsed).toISOString();
28
- // A configured-but-corrupt clock is a WORLD defect — fail loudly, never silently drift
29
- // back to wall-clock (two services disagreeing about now breaks the world).
30
- throw new Error(`world clock: ${file} does not contain a parseable ISO-8601 instant (got ${JSON.stringify(raw.slice(0, 40))})`);
30
+ // A configured-but-corrupt clock is a WORLD defect — parseClock fails loudly, never silently drifting back to
31
+ // wall-clock (two services disagreeing about now breaks the world).
32
+ const clock = clockForm.parseClock(getActiveWorldStore().read(file) ?? '');
33
+ return new Date(clockForm.clockNowMs(clock, machineNow())).toISOString();
31
34
  }
32
- return new Date().toISOString();
35
+ return new Date(machineNow()).toISOString();
33
36
  }
package/vendor-hosts.cjs CHANGED
@@ -22,7 +22,7 @@
22
22
  //
23
23
  // THE SHARED-HOST RULE (Cal.com incident, 2026-08): a shared-host predicate must claim ONLY the
24
24
  // paths its twin actually serves — never "the rest of the host". Claiming the remainder is how
25
- // every `@googleapis/calendar` call (37 methods, all defaulting to www.googleapis.com) was routed
25
+ // every `@googleapis/calendar` call (all of its methods default to www.googleapis.com) was routed
26
26
  // into the googleauth/gemini twin and answered with a plausible Google-shaped 404: a mis-route
27
27
  // that fails OPEN and PLAUSIBLE, strictly worse than a coverage gap. An UNCLAIMED path on a
28
28
  // claimed host is refused LOUDLY instead (see unclaimedTwinnedHostPathMessage below): the error
@@ -32,7 +32,7 @@
32
32
  * Exactly the routes gemini-twin.ts §googleauth models: `POST /token` (the oauth2.googleapis.com
33
33
  * path, also answered on the legacy host) and `POST /oauth2/v4/token` (the legacy
34
34
  * www.googleapis.com token path older google-auth-library versions default to). NOTHING else:
35
- * /calendar/v3/*, /drive/v3/*, … are other Google products with no twin in this repo, and
35
+ * /calendar/v3/* (the googlecalendar pack's), /drive/v3/*, … are other Google products, and
36
36
  * claiming them would mis-route those SDKs into an auth twin that answers with vendor-shaped
37
37
  * 404s (the Cal.com incident).
38
38
  */
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ // THE WORLD CLOCK'S FORM — the one home of how a clock file reads (dependency-free, side-effect free), shared by the
3
+ // twins' worldNow() (src/world-clock.ts, reading through the active WorldStore) and the injector, which gives an
4
+ // application process the same time (inject.cjs, WORLD TIME).
5
+ //
6
+ // A clock file holds one line in one of two forms:
7
+ // · FROZEN: `<ISO-8601 instant>` — every read answers exactly that instant (scripted time: lives, seeds, a story);
8
+ // · RUNNING: `<ISO-8601 instant> from <ISO-8601 wall instant>` — the World's time was <instant> when the machine's was
9
+ // <wall instant>, and runs on at the machine's pace from there: a World serving an application moves time this way
10
+ // (`volter-world clock <world> shift <N s|m|h|d>`), since a frozen instant would stop the application's time.
11
+ // No file: the machine's time.
12
+
13
+ /** The clock a file's text states, or throws on text that states neither form. */
14
+ function parseClock(raw) {
15
+ const text = String(raw ?? '').trim();
16
+ const m = /^(\S+)\s+from\s+(\S+)$/.exec(text);
17
+ if (m) {
18
+ const at = Date.parse(m[1]);
19
+ const since = Date.parse(m[2]);
20
+ if (!Number.isNaN(at) && !Number.isNaN(since)) return { kind: 'running', at, since };
21
+ } else {
22
+ const at = Date.parse(text);
23
+ if (!Number.isNaN(at)) return { kind: 'frozen', at };
24
+ }
25
+ throw new Error(`world clock: expected an ISO-8601 instant, or "<instant> from <instant>", got ${JSON.stringify(text.slice(0, 60))}`);
26
+ }
27
+
28
+ /** The World's time in ms for a parsed clock, at the machine's time `wallMs`. */
29
+ function clockNowMs(clock, wallMs) {
30
+ return clock.kind === 'running' ? clock.at + (wallMs - clock.since) : clock.at;
31
+ }
32
+
33
+ /** A clock's text: frozen at `atMs`, or running from `atMs` as of the machine's `sinceMs`. */
34
+ function formatClock(clock) {
35
+ return clock.kind === 'running'
36
+ ? `${new Date(clock.at).toISOString()} from ${new Date(clock.since).toISOString()}`
37
+ : new Date(clock.at).toISOString();
38
+ }
39
+
40
+ module.exports = { parseClock, clockNowMs, formatClock };
@@ -0,0 +1,4 @@
1
+ export type WorldClock = { kind: 'frozen'; at: number } | { kind: 'running'; at: number; since: number };
2
+ export function parseClock(raw: string | null | undefined): WorldClock;
3
+ export function clockNowMs(clock: WorldClock, wallMs: number): number;
4
+ export function formatClock(clock: WorldClock): string;