@volter/twin 0.1.0 → 0.1.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/README.md +16 -2
- package/inject.cjs +453 -59
- package/package.json +12 -22
- package/src/actions.ts +234 -49
- package/src/blob-store.ts +136 -0
- package/src/changeset.ts +807 -0
- package/src/cli.ts +60 -10
- package/src/connector.ts +30 -7
- package/src/control-plane.ts +17 -1
- package/src/emit.ts +242 -0
- package/src/fork.ts +19 -7
- package/src/index.ts +139 -6
- package/src/lease.ts +4 -6
- package/src/lifecycle.ts +8 -0
- package/src/packRegistry.ts +248 -2
- package/src/plan.ts +131 -23
- package/src/proxy.ts +5 -2
- package/src/pushLedger.ts +116 -11
- package/src/queueLifecycle.ts +3 -4
- package/src/rateBudget.ts +1115 -0
- package/src/refs.ts +9 -10
- package/src/remote-execute.ts +16 -0
- package/src/scenario.ts +387 -0
- package/src/serve.ts +397 -15
- package/src/shadow.ts +86 -7
- package/src/storage.ts +76 -147
- package/src/sync.ts +63 -17
- package/src/twin-fetch.ts +115 -0
- package/src/validate.ts +6 -5
- package/src/world-clock.ts +33 -0
- package/src/world-store.ts +482 -0
- package/src/worldConfig.ts +4 -3
- package/dist/src/actions.d.ts +0 -138
- package/dist/src/actions.js +0 -201
- package/dist/src/args.d.ts +0 -3
- package/dist/src/args.js +0 -12
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -425
- package/dist/src/connector.d.ts +0 -106
- package/dist/src/connector.js +0 -129
- package/dist/src/control-plane.d.ts +0 -21
- package/dist/src/control-plane.js +0 -40
- package/dist/src/egress.d.ts +0 -93
- package/dist/src/egress.js +0 -264
- package/dist/src/fork.d.ts +0 -126
- package/dist/src/fork.js +0 -206
- package/dist/src/index.d.ts +0 -42
- package/dist/src/index.js +0 -52
- package/dist/src/lease.d.ts +0 -50
- package/dist/src/lease.js +0 -80
- package/dist/src/packRegistry.d.ts +0 -34
- package/dist/src/packRegistry.js +0 -22
- package/dist/src/plan.d.ts +0 -97
- package/dist/src/plan.js +0 -151
- package/dist/src/proxy.d.ts +0 -25
- package/dist/src/proxy.js +0 -152
- package/dist/src/pushLedger.d.ts +0 -81
- package/dist/src/pushLedger.js +0 -130
- package/dist/src/queueLifecycle.d.ts +0 -62
- package/dist/src/queueLifecycle.js +0 -95
- package/dist/src/reconcile.d.ts +0 -58
- package/dist/src/reconcile.js +0 -137
- package/dist/src/refs.d.ts +0 -29
- package/dist/src/refs.js +0 -68
- package/dist/src/schemas.d.ts +0 -78
- package/dist/src/schemas.js +0 -50
- package/dist/src/serve.d.ts +0 -44
- package/dist/src/serve.js +0 -93
- package/dist/src/shadow.d.ts +0 -77
- package/dist/src/shadow.js +0 -138
- package/dist/src/status.d.ts +0 -31
- package/dist/src/status.js +0 -42
- package/dist/src/storage.d.ts +0 -119
- package/dist/src/storage.js +0 -535
- package/dist/src/sync.d.ts +0 -91
- package/dist/src/sync.js +0 -121
- package/dist/src/types.d.ts +0 -40
- package/dist/src/types.js +0 -1
- package/dist/src/validate.d.ts +0 -27
- package/dist/src/validate.js +0 -68
- package/dist/src/visualizer.d.ts +0 -13
- package/dist/src/visualizer.js +0 -133
- package/dist/src/worldConfig.d.ts +0 -9
- package/dist/src/worldConfig.js +0 -16
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { QueuedWorldServiceEvent } from './types.js';
|
|
2
|
-
export type QueueRowStatus = 'queued' | 'committed' | 'ignored' | 'superseded' | 'poisoned';
|
|
3
|
-
export type QueueStatusTransition = {
|
|
4
|
-
queueId: string;
|
|
5
|
-
status: QueueRowStatus;
|
|
6
|
-
at: string;
|
|
7
|
-
reason?: string;
|
|
8
|
-
/** for committed rows: the canonical event id it produced. */
|
|
9
|
-
eventId?: string;
|
|
10
|
-
/** for superseded rows: the row that replaced it. */
|
|
11
|
-
supersededBy?: string;
|
|
12
|
-
};
|
|
13
|
-
export type QueueRowWithStatus = {
|
|
14
|
-
row: QueuedWorldServiceEvent;
|
|
15
|
-
status: QueueRowStatus;
|
|
16
|
-
transition?: QueueStatusTransition;
|
|
17
|
-
};
|
|
18
|
-
export declare function setQueueRowStatus(service: string, queueId: string, status: QueueRowStatus, opts?: {
|
|
19
|
-
root?: string;
|
|
20
|
-
at?: string;
|
|
21
|
-
reason?: string;
|
|
22
|
-
eventId?: string;
|
|
23
|
-
supersededBy?: string;
|
|
24
|
-
}): QueueStatusTransition;
|
|
25
|
-
export declare function queueRowStatus(service: string, queueId: string, root?: string): QueueRowStatus;
|
|
26
|
-
/** Every queue row with its current lifecycle status. */
|
|
27
|
-
export declare function listQueueWithStatus(service: string, root?: string): QueueRowWithStatus[];
|
|
28
|
-
/** Rows still awaiting a decision (status === 'queued'). */
|
|
29
|
-
export declare function pendingQueueRows(service: string, root?: string): QueuedWorldServiceEvent[];
|
|
30
|
-
export type QueueCounts = Record<QueueRowStatus, number>;
|
|
31
|
-
export declare function queueCounts(service: string, root?: string): QueueCounts;
|
|
32
|
-
/** Commit a single queued row into the canonical event log + stamp `committed`. */
|
|
33
|
-
export declare function commitQueueRow(service: string, queueId: string, opts?: {
|
|
34
|
-
root?: string;
|
|
35
|
-
at?: string;
|
|
36
|
-
}): {
|
|
37
|
-
appended: boolean;
|
|
38
|
-
eventId?: string;
|
|
39
|
-
};
|
|
40
|
-
export declare function ignoreQueueRow(service: string, queueId: string, opts?: {
|
|
41
|
-
root?: string;
|
|
42
|
-
at?: string;
|
|
43
|
-
reason?: string;
|
|
44
|
-
}): QueueStatusTransition;
|
|
45
|
-
export declare function supersedeQueueRow(service: string, queueId: string, supersededBy: string, opts?: {
|
|
46
|
-
root?: string;
|
|
47
|
-
at?: string;
|
|
48
|
-
reason?: string;
|
|
49
|
-
}): QueueStatusTransition;
|
|
50
|
-
export declare function poisonQueueRow(service: string, queueId: string, opts?: {
|
|
51
|
-
root?: string;
|
|
52
|
-
at?: string;
|
|
53
|
-
reason?: string;
|
|
54
|
-
}): QueueStatusTransition;
|
|
55
|
-
/** Commit all rows currently `queued`, in deterministic provider-time order; stamps committed. */
|
|
56
|
-
export declare function commitPendingQueue(service: string, opts?: {
|
|
57
|
-
root?: string;
|
|
58
|
-
at?: string;
|
|
59
|
-
}): {
|
|
60
|
-
committed: number;
|
|
61
|
-
eventIds: string[];
|
|
62
|
-
};
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// Event queue lifecycle (the twins architecture notes → "Event Intake Queue").
|
|
2
|
-
// Listener/webhook/broker deliveries land in the durable queue (events stay
|
|
3
|
-
// NON-canonical until committed). This module gives queue rows the named
|
|
4
|
-
// lifecycle the doc specifies — queued | committed | ignored | superseded |
|
|
5
|
-
// poisoned — as an append-only status ledger over the existing queue, so the
|
|
6
|
-
// queue file itself stays immutable delivery history. Current status = the latest
|
|
7
|
-
// transition (default `queued`). Deterministic: caller may supply `at`.
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { appendDurable, appendEvent, listQueuedEvents, rebuildGenericState, worldPaths } from "./storage.js";
|
|
11
|
-
function statusPath(service, root) {
|
|
12
|
-
return join(worldPaths(service, root).dir, 'event-queue-status.jsonl');
|
|
13
|
-
}
|
|
14
|
-
function readTransitions(service, root) {
|
|
15
|
-
const path = statusPath(service, root);
|
|
16
|
-
if (!existsSync(path))
|
|
17
|
-
return [];
|
|
18
|
-
return readFileSync(path, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
19
|
-
}
|
|
20
|
-
/** Latest transition per queue row (append order is the tiebreak — last wins). */
|
|
21
|
-
function latestByRow(service, root) {
|
|
22
|
-
const latest = new Map();
|
|
23
|
-
for (const t of readTransitions(service, root))
|
|
24
|
-
latest.set(t.queueId, t); // later lines overwrite
|
|
25
|
-
return latest;
|
|
26
|
-
}
|
|
27
|
-
export function setQueueRowStatus(service, queueId, status, opts = {}) {
|
|
28
|
-
const path = statusPath(service, opts.root);
|
|
29
|
-
mkdirSync(join(path, '..'), { recursive: true });
|
|
30
|
-
const transition = {
|
|
31
|
-
queueId,
|
|
32
|
-
status,
|
|
33
|
-
at: opts.at ?? new Date().toISOString(),
|
|
34
|
-
...(opts.reason ? { reason: opts.reason } : {}),
|
|
35
|
-
...(opts.eventId ? { eventId: opts.eventId } : {}),
|
|
36
|
-
...(opts.supersededBy ? { supersededBy: opts.supersededBy } : {}),
|
|
37
|
-
};
|
|
38
|
-
appendDurable(path, `${JSON.stringify(transition)}\n`);
|
|
39
|
-
return transition;
|
|
40
|
-
}
|
|
41
|
-
export function queueRowStatus(service, queueId, root) {
|
|
42
|
-
return latestByRow(service, root).get(queueId)?.status ?? 'queued';
|
|
43
|
-
}
|
|
44
|
-
/** Every queue row with its current lifecycle status. */
|
|
45
|
-
export function listQueueWithStatus(service, root) {
|
|
46
|
-
const latest = latestByRow(service, root);
|
|
47
|
-
return listQueuedEvents(service, root).map((row) => {
|
|
48
|
-
const transition = latest.get(row.id);
|
|
49
|
-
return { row, status: transition?.status ?? 'queued', ...(transition ? { transition } : {}) };
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
/** Rows still awaiting a decision (status === 'queued'). */
|
|
53
|
-
export function pendingQueueRows(service, root) {
|
|
54
|
-
return listQueueWithStatus(service, root).filter((r) => r.status === 'queued').map((r) => r.row);
|
|
55
|
-
}
|
|
56
|
-
export function queueCounts(service, root) {
|
|
57
|
-
const counts = { queued: 0, committed: 0, ignored: 0, superseded: 0, poisoned: 0 };
|
|
58
|
-
for (const r of listQueueWithStatus(service, root))
|
|
59
|
-
counts[r.status] += 1;
|
|
60
|
-
return counts;
|
|
61
|
-
}
|
|
62
|
-
/** Commit a single queued row into the canonical event log + stamp `committed`. */
|
|
63
|
-
export function commitQueueRow(service, queueId, opts = {}) {
|
|
64
|
-
const status = queueRowStatus(service, queueId, opts.root);
|
|
65
|
-
if (status !== 'queued')
|
|
66
|
-
throw new Error(`queue row ${queueId} is ${status}, not committable`);
|
|
67
|
-
const row = listQueuedEvents(service, opts.root).find((r) => r.id === queueId);
|
|
68
|
-
if (!row)
|
|
69
|
-
throw new Error(`queue row not found: ${queueId}`);
|
|
70
|
-
const result = appendEvent(row.event, opts.root);
|
|
71
|
-
rebuildGenericState(service, opts.root);
|
|
72
|
-
setQueueRowStatus(service, queueId, 'committed', { ...opts, eventId: result.event.id });
|
|
73
|
-
return { appended: result.appended, eventId: result.event.id };
|
|
74
|
-
}
|
|
75
|
-
export function ignoreQueueRow(service, queueId, opts = {}) {
|
|
76
|
-
return setQueueRowStatus(service, queueId, 'ignored', opts);
|
|
77
|
-
}
|
|
78
|
-
export function supersedeQueueRow(service, queueId, supersededBy, opts = {}) {
|
|
79
|
-
return setQueueRowStatus(service, queueId, 'superseded', { ...opts, supersededBy });
|
|
80
|
-
}
|
|
81
|
-
export function poisonQueueRow(service, queueId, opts = {}) {
|
|
82
|
-
return setQueueRowStatus(service, queueId, 'poisoned', opts);
|
|
83
|
-
}
|
|
84
|
-
/** Commit all rows currently `queued`, in deterministic provider-time order; stamps committed. */
|
|
85
|
-
export function commitPendingQueue(service, opts = {}) {
|
|
86
|
-
const pending = pendingQueueRows(service, opts.root)
|
|
87
|
-
.sort((a, b) => a.event.occurredAt.localeCompare(b.event.occurredAt) || a.receivedAt.localeCompare(b.receivedAt) || a.id.localeCompare(b.id));
|
|
88
|
-
const eventIds = [];
|
|
89
|
-
for (const row of pending) {
|
|
90
|
-
const { eventId } = commitQueueRow(service, row.id, opts);
|
|
91
|
-
if (eventId)
|
|
92
|
-
eventIds.push(eventId);
|
|
93
|
-
}
|
|
94
|
-
return { committed: eventIds.length, eventIds };
|
|
95
|
-
}
|
package/dist/src/reconcile.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { TwinResource } from './serve.js';
|
|
2
|
-
export type ReconcilePolicy = 'hub-wins' | 'twin-wins' | 'merge';
|
|
3
|
-
export type FieldDecision = {
|
|
4
|
-
field: string;
|
|
5
|
-
base: unknown;
|
|
6
|
-
fork: unknown;
|
|
7
|
-
real: unknown;
|
|
8
|
-
forkChanged: boolean;
|
|
9
|
-
realChanged: boolean;
|
|
10
|
-
resolution: 'unchanged' | 'take-fork' | 'take-real' | 'conflict';
|
|
11
|
-
value: unknown;
|
|
12
|
-
};
|
|
13
|
-
export type SubjectReconcile = {
|
|
14
|
-
id: string;
|
|
15
|
-
type: string;
|
|
16
|
-
inFork: boolean;
|
|
17
|
-
inReal: boolean;
|
|
18
|
-
existence: 'both' | 'fork-only' | 'real-only';
|
|
19
|
-
fields: FieldDecision[];
|
|
20
|
-
conflicts: FieldDecision[];
|
|
21
|
-
};
|
|
22
|
-
export type ReconcilePlan = {
|
|
23
|
-
policy: ReconcilePolicy;
|
|
24
|
-
subjects: SubjectReconcile[];
|
|
25
|
-
toPush: Array<{
|
|
26
|
-
id: string;
|
|
27
|
-
type: string;
|
|
28
|
-
fields: Record<string, unknown>;
|
|
29
|
-
}>;
|
|
30
|
-
toPull: Array<{
|
|
31
|
-
id: string;
|
|
32
|
-
type: string;
|
|
33
|
-
fields: Record<string, unknown>;
|
|
34
|
-
}>;
|
|
35
|
-
conflictCount: number;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Compute a three-way reconciliation plan. Inputs are resource snapshots keyed
|
|
39
|
-
* by id (base = at fork time, fork = twin now, real = fresh pull). Pure.
|
|
40
|
-
*/
|
|
41
|
-
export declare function reconcile(opts: {
|
|
42
|
-
policy: ReconcilePolicy;
|
|
43
|
-
base: TwinResource[];
|
|
44
|
-
fork: TwinResource[];
|
|
45
|
-
real: TwinResource[];
|
|
46
|
-
}): ReconcilePlan;
|
|
47
|
-
/** True when the plan can be enacted without a human decision. */
|
|
48
|
-
export declare function isCleanlyReconcilable(plan: ReconcilePlan): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Whether enacting this plan's `toPush` requires explicit approval before
|
|
51
|
-
* `syncPush` may perform it. Nothing pushed → nothing to gate. Otherwise: any
|
|
52
|
-
* unresolved conflict anywhere in the plan requires approval (never silently
|
|
53
|
-
* applied), and so does any pushed field whose resolution is 'take-fork' while
|
|
54
|
-
* `realChanged` is true — twin-wins discarding a change that also happened on the
|
|
55
|
-
* real side, the "destructive twin-wins reconcile push" case that must not go out
|
|
56
|
-
* silently.
|
|
57
|
-
*/
|
|
58
|
-
export declare function reconcileRequiresApproval(plan: ReconcilePlan): boolean;
|
package/dist/src/reconcile.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
const META_FIELDS = new Set(['id', 'type', 'updatedAt']);
|
|
2
|
-
function eq(a, b) {
|
|
3
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
4
|
-
}
|
|
5
|
-
function dataFields(r) {
|
|
6
|
-
if (!r)
|
|
7
|
-
return {};
|
|
8
|
-
const out = {};
|
|
9
|
-
for (const [k, v] of Object.entries(r))
|
|
10
|
-
if (!META_FIELDS.has(k))
|
|
11
|
-
out[k] = v;
|
|
12
|
-
return out;
|
|
13
|
-
}
|
|
14
|
-
function decideField(field, base, fork, real, policy) {
|
|
15
|
-
const forkChanged = !eq(base, fork);
|
|
16
|
-
const realChanged = !eq(base, real);
|
|
17
|
-
let resolution;
|
|
18
|
-
let value;
|
|
19
|
-
if (!forkChanged && !realChanged) {
|
|
20
|
-
resolution = 'unchanged';
|
|
21
|
-
value = base;
|
|
22
|
-
}
|
|
23
|
-
else if (forkChanged && !realChanged) {
|
|
24
|
-
// only the twin changed it
|
|
25
|
-
resolution = policy === 'hub-wins' ? 'take-real' : 'take-fork';
|
|
26
|
-
value = resolution === 'take-fork' ? fork : real;
|
|
27
|
-
}
|
|
28
|
-
else if (!forkChanged && realChanged) {
|
|
29
|
-
// only the real service changed it
|
|
30
|
-
resolution = policy === 'twin-wins' ? 'take-fork' : 'take-real';
|
|
31
|
-
value = resolution === 'take-fork' ? fork : real;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
// BOTH changed (relative to base) — the genuine conflict case
|
|
35
|
-
if (eq(fork, real)) {
|
|
36
|
-
resolution = 'take-fork'; // converged independently to the same value
|
|
37
|
-
value = fork;
|
|
38
|
-
}
|
|
39
|
-
else if (policy === 'hub-wins') {
|
|
40
|
-
resolution = 'take-real';
|
|
41
|
-
value = real;
|
|
42
|
-
}
|
|
43
|
-
else if (policy === 'twin-wins') {
|
|
44
|
-
resolution = 'take-fork';
|
|
45
|
-
value = fork;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
resolution = 'conflict'; // merge policy surfaces it; no silent pick
|
|
49
|
-
value = real; // safe default: do NOT overwrite real on an unresolved conflict
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return { field, base, fork, real, forkChanged, realChanged, resolution, value };
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Compute a three-way reconciliation plan. Inputs are resource snapshots keyed
|
|
56
|
-
* by id (base = at fork time, fork = twin now, real = fresh pull). Pure.
|
|
57
|
-
*/
|
|
58
|
-
export function reconcile(opts) {
|
|
59
|
-
const { policy } = opts;
|
|
60
|
-
const baseById = new Map(opts.base.map((r) => [r.id, r]));
|
|
61
|
-
const forkById = new Map(opts.fork.map((r) => [r.id, r]));
|
|
62
|
-
const realById = new Map(opts.real.map((r) => [r.id, r]));
|
|
63
|
-
const allIds = [...new Set([...forkById.keys(), ...realById.keys()])].sort();
|
|
64
|
-
const subjects = [];
|
|
65
|
-
const toPush = [];
|
|
66
|
-
const toPull = [];
|
|
67
|
-
let conflictCount = 0;
|
|
68
|
-
for (const id of allIds) {
|
|
69
|
-
const baseR = baseById.get(id);
|
|
70
|
-
const forkR = forkById.get(id);
|
|
71
|
-
const realR = realById.get(id);
|
|
72
|
-
const inFork = !!forkR;
|
|
73
|
-
const inReal = !!realR;
|
|
74
|
-
const existence = inFork && inReal ? 'both' : inFork ? 'fork-only' : 'real-only';
|
|
75
|
-
const type = (forkR?.type ?? realR?.type ?? baseR?.type ?? 'unknown');
|
|
76
|
-
const baseF = dataFields(baseR);
|
|
77
|
-
const forkF = dataFields(forkR);
|
|
78
|
-
const realF = dataFields(realR);
|
|
79
|
-
const fieldNames = [...new Set([...Object.keys(baseF), ...Object.keys(forkF), ...Object.keys(realF)])].sort();
|
|
80
|
-
const fields = fieldNames.map((f) => decideField(f, baseF[f], forkF[f], realF[f], policy));
|
|
81
|
-
const conflicts = fields.filter((d) => d.resolution === 'conflict');
|
|
82
|
-
conflictCount += conflicts.length;
|
|
83
|
-
subjects.push({ id, type, inFork, inReal, existence, fields, conflicts });
|
|
84
|
-
// Enactment summaries (gated; not applied here):
|
|
85
|
-
const pushFields = {};
|
|
86
|
-
const pullFields = {};
|
|
87
|
-
if (existence === 'both') {
|
|
88
|
-
// field-level convergence only makes sense when the subject is on both sides:
|
|
89
|
-
// - fields resolved 'take-fork' that differ from real → push to real.
|
|
90
|
-
// - fields resolved 'take-real' that differ from fork → pull into twin.
|
|
91
|
-
for (const d of fields) {
|
|
92
|
-
if (d.resolution === 'take-fork' && !eq(d.value, d.real))
|
|
93
|
-
pushFields[d.field] = d.value;
|
|
94
|
-
if (d.resolution === 'take-real' && !eq(d.value, d.fork))
|
|
95
|
-
pullFields[d.field] = d.value;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else if (existence === 'fork-only') {
|
|
99
|
-
// present in the twin, absent in real. Under twin-wins/merge that's a
|
|
100
|
-
// create-on-real candidate; under hub-wins (real authoritative) the fork's
|
|
101
|
-
// creation is discarded — neither pushed nor pulled.
|
|
102
|
-
if (policy !== 'hub-wins')
|
|
103
|
-
for (const [k, v] of Object.entries(forkF))
|
|
104
|
-
pushFields[k] = v;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
// real-only: a create-in-twin candidate (pull) under every policy.
|
|
108
|
-
for (const [k, v] of Object.entries(realF))
|
|
109
|
-
pullFields[k] = v;
|
|
110
|
-
}
|
|
111
|
-
if (Object.keys(pushFields).length > 0)
|
|
112
|
-
toPush.push({ id, type, fields: pushFields });
|
|
113
|
-
if (Object.keys(pullFields).length > 0)
|
|
114
|
-
toPull.push({ id, type, fields: pullFields });
|
|
115
|
-
}
|
|
116
|
-
return { policy, subjects, toPush, toPull, conflictCount };
|
|
117
|
-
}
|
|
118
|
-
/** True when the plan can be enacted without a human decision. */
|
|
119
|
-
export function isCleanlyReconcilable(plan) {
|
|
120
|
-
return plan.conflictCount === 0;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Whether enacting this plan's `toPush` requires explicit approval before
|
|
124
|
-
* `syncPush` may perform it. Nothing pushed → nothing to gate. Otherwise: any
|
|
125
|
-
* unresolved conflict anywhere in the plan requires approval (never silently
|
|
126
|
-
* applied), and so does any pushed field whose resolution is 'take-fork' while
|
|
127
|
-
* `realChanged` is true — twin-wins discarding a change that also happened on the
|
|
128
|
-
* real side, the "destructive twin-wins reconcile push" case that must not go out
|
|
129
|
-
* silently.
|
|
130
|
-
*/
|
|
131
|
-
export function reconcileRequiresApproval(plan) {
|
|
132
|
-
if (plan.toPush.length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
if (plan.conflictCount > 0)
|
|
135
|
-
return true;
|
|
136
|
-
return plan.subjects.some((s) => s.fields.some((f) => f.resolution === 'take-fork' && f.realChanged));
|
|
137
|
-
}
|
package/dist/src/refs.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export type WorldRemoteRef = {
|
|
2
|
-
service: string;
|
|
3
|
-
provider: string;
|
|
4
|
-
/** "main" by default; a named checkpoint otherwise. */
|
|
5
|
-
name: string;
|
|
6
|
-
eventId?: string;
|
|
7
|
-
cursor?: string;
|
|
8
|
-
providerVersion?: string;
|
|
9
|
-
observedAt: string;
|
|
10
|
-
};
|
|
11
|
-
export type WorldLocalRef = {
|
|
12
|
-
service: string;
|
|
13
|
-
/** the fork this ref belongs to. */
|
|
14
|
-
forkId: string;
|
|
15
|
-
/** the remote ref the fork is based on (provider + name + checkpoint). */
|
|
16
|
-
baseRemoteRef: WorldRemoteRef;
|
|
17
|
-
recordedAt: string;
|
|
18
|
-
};
|
|
19
|
-
/** Record/advance a provider checkpoint (e.g. after a confirmed pull). */
|
|
20
|
-
export declare function writeRemoteRef(ref: WorldRemoteRef, root?: string): WorldRemoteRef;
|
|
21
|
-
export declare function readRemoteRef(service: string, provider: string, name?: string, root?: string): WorldRemoteRef | null;
|
|
22
|
-
export declare function listRemoteRefs(service: string, root?: string): WorldRemoteRef[];
|
|
23
|
-
export declare function writeLocalRef(ref: WorldLocalRef, root?: string): WorldLocalRef;
|
|
24
|
-
export declare function readLocalRef(service: string, forkId: string, root?: string): WorldLocalRef | null;
|
|
25
|
-
/**
|
|
26
|
-
* Is a fork's base ref still current against the live remote ref? A push against a
|
|
27
|
-
* stale base must be blocked or explicitly reconciled (rebase) first.
|
|
28
|
-
*/
|
|
29
|
-
export declare function isBaseStale(local: WorldLocalRef, root?: string): boolean;
|
package/dist/src/refs.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Remote refs (the twins architecture notes → "Remote Refs"): the provider-side
|
|
2
|
-
// baselines a fork or push is based on. `remote/<provider>/<name>` records the last
|
|
3
|
-
// CONFIRMED provider checkpoint (cursor/version/event id/observed time); a local
|
|
4
|
-
// ref records which remote ref a fork was based on, so a stale-base push can be
|
|
5
|
-
// rejected before any provider call. Pure file I/O over the world dir; deterministic
|
|
6
|
-
// (caller supplies observedAt).
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { readJsonFile, worldPaths } from "./storage.js";
|
|
10
|
-
function refsDir(service, root) {
|
|
11
|
-
return join(worldPaths(service, root).dir, 'refs');
|
|
12
|
-
}
|
|
13
|
-
function remoteRefPath(service, provider, name, root) {
|
|
14
|
-
return join(refsDir(service, root), 'remote', provider, `${name}.json`);
|
|
15
|
-
}
|
|
16
|
-
function localRefPath(service, forkId, root) {
|
|
17
|
-
return join(refsDir(service, root), 'local', `${forkId}.json`);
|
|
18
|
-
}
|
|
19
|
-
function safe(part) {
|
|
20
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(part))
|
|
21
|
-
throw new Error(`invalid ref segment: ${part}`);
|
|
22
|
-
return part;
|
|
23
|
-
}
|
|
24
|
-
/** Record/advance a provider checkpoint (e.g. after a confirmed pull). */
|
|
25
|
-
export function writeRemoteRef(ref, root) {
|
|
26
|
-
const path = remoteRefPath(ref.service, safe(ref.provider), safe(ref.name), root);
|
|
27
|
-
mkdirSync(join(path, '..'), { recursive: true });
|
|
28
|
-
writeFileSync(path, `${JSON.stringify(ref, null, 2)}\n`);
|
|
29
|
-
return ref;
|
|
30
|
-
}
|
|
31
|
-
export function readRemoteRef(service, provider, name = 'main', root) {
|
|
32
|
-
const path = remoteRefPath(service, safe(provider), safe(name), root);
|
|
33
|
-
return existsSync(path) ? readJsonFile(path) : null;
|
|
34
|
-
}
|
|
35
|
-
export function listRemoteRefs(service, root) {
|
|
36
|
-
const base = join(refsDir(service, root), 'remote');
|
|
37
|
-
if (!existsSync(base))
|
|
38
|
-
return [];
|
|
39
|
-
const out = [];
|
|
40
|
-
for (const provider of readdirSync(base)) {
|
|
41
|
-
const providerDir = join(base, provider);
|
|
42
|
-
for (const file of readdirSync(providerDir)) {
|
|
43
|
-
if (file.endsWith('.json'))
|
|
44
|
-
out.push(readJsonFile(join(providerDir, file)));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return out.sort((a, b) => (a.provider === b.provider ? a.name.localeCompare(b.name) : a.provider.localeCompare(b.provider)));
|
|
48
|
-
}
|
|
49
|
-
export function writeLocalRef(ref, root) {
|
|
50
|
-
const path = localRefPath(ref.service, safe(ref.forkId), root);
|
|
51
|
-
mkdirSync(join(path, '..'), { recursive: true });
|
|
52
|
-
writeFileSync(path, `${JSON.stringify(ref, null, 2)}\n`);
|
|
53
|
-
return ref;
|
|
54
|
-
}
|
|
55
|
-
export function readLocalRef(service, forkId, root) {
|
|
56
|
-
const path = localRefPath(service, safe(forkId), root);
|
|
57
|
-
return existsSync(path) ? readJsonFile(path) : null;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Is a fork's base ref still current against the live remote ref? A push against a
|
|
61
|
-
* stale base must be blocked or explicitly reconciled (rebase) first.
|
|
62
|
-
*/
|
|
63
|
-
export function isBaseStale(local, root) {
|
|
64
|
-
const current = readRemoteRef(local.service, local.baseRemoteRef.provider, local.baseRemoteRef.name, root);
|
|
65
|
-
if (!current)
|
|
66
|
-
return false; // no live checkpoint recorded yet → cannot be stale
|
|
67
|
-
return (current.eventId ?? current.cursor ?? '') !== (local.baseRemoteRef.eventId ?? local.baseRemoteRef.cursor ?? '');
|
|
68
|
-
}
|
package/dist/src/schemas.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export declare const WorldActorSchema: z.ZodObject<{
|
|
3
|
-
id: z.ZodOptional<z.ZodString>;
|
|
4
|
-
name: z.ZodOptional<z.ZodString>;
|
|
5
|
-
kind: z.ZodEnum<{
|
|
6
|
-
human: "human";
|
|
7
|
-
agent: "agent";
|
|
8
|
-
bot: "bot";
|
|
9
|
-
system: "system";
|
|
10
|
-
}>;
|
|
11
|
-
}, z.core.$strip>;
|
|
12
|
-
export declare const WorldSubjectSchema: z.ZodObject<{
|
|
13
|
-
type: z.ZodString;
|
|
14
|
-
id: z.ZodString;
|
|
15
|
-
}, z.core.$strip>;
|
|
16
|
-
export declare const WorldExternalRefSchema: z.ZodObject<{
|
|
17
|
-
provider: z.ZodString;
|
|
18
|
-
id: z.ZodOptional<z.ZodString>;
|
|
19
|
-
url: z.ZodOptional<z.ZodString>;
|
|
20
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
21
|
-
rawRef: z.ZodOptional<z.ZodString>;
|
|
22
|
-
}, z.core.$strip>;
|
|
23
|
-
export declare const WorldServiceEventSchema: z.ZodObject<{
|
|
24
|
-
id: z.ZodString;
|
|
25
|
-
service: z.ZodString;
|
|
26
|
-
type: z.ZodString;
|
|
27
|
-
schemaVersion: z.ZodNumber;
|
|
28
|
-
idempotencyKey: z.ZodString;
|
|
29
|
-
occurredAt: z.ZodString;
|
|
30
|
-
observedAt: z.ZodString;
|
|
31
|
-
origin: z.ZodEnum<{
|
|
32
|
-
agent: "agent";
|
|
33
|
-
virtual: "virtual";
|
|
34
|
-
external: "external";
|
|
35
|
-
connector: "connector";
|
|
36
|
-
replay: "replay";
|
|
37
|
-
migration: "migration";
|
|
38
|
-
}>;
|
|
39
|
-
actor: z.ZodOptional<z.ZodObject<{
|
|
40
|
-
id: z.ZodOptional<z.ZodString>;
|
|
41
|
-
name: z.ZodOptional<z.ZodString>;
|
|
42
|
-
kind: z.ZodEnum<{
|
|
43
|
-
human: "human";
|
|
44
|
-
agent: "agent";
|
|
45
|
-
bot: "bot";
|
|
46
|
-
system: "system";
|
|
47
|
-
}>;
|
|
48
|
-
}, z.core.$strip>>;
|
|
49
|
-
subject: z.ZodObject<{
|
|
50
|
-
type: z.ZodString;
|
|
51
|
-
id: z.ZodString;
|
|
52
|
-
}, z.core.$strip>;
|
|
53
|
-
causationId: z.ZodOptional<z.ZodString>;
|
|
54
|
-
correlationId: z.ZodOptional<z.ZodString>;
|
|
55
|
-
external: z.ZodOptional<z.ZodObject<{
|
|
56
|
-
provider: z.ZodString;
|
|
57
|
-
id: z.ZodOptional<z.ZodString>;
|
|
58
|
-
url: z.ZodOptional<z.ZodString>;
|
|
59
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
60
|
-
rawRef: z.ZodOptional<z.ZodString>;
|
|
61
|
-
}, z.core.$strip>>;
|
|
62
|
-
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
63
|
-
raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
|
-
}, z.core.$strip>;
|
|
65
|
-
export declare const GenericWorldStateSchema: z.ZodObject<{
|
|
66
|
-
version: z.ZodLiteral<1>;
|
|
67
|
-
service: z.ZodString;
|
|
68
|
-
rebuiltAt: z.ZodString;
|
|
69
|
-
eventCount: z.ZodNumber;
|
|
70
|
-
latestEventId: z.ZodOptional<z.ZodString>;
|
|
71
|
-
subjects: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
72
|
-
type: z.ZodString;
|
|
73
|
-
id: z.ZodString;
|
|
74
|
-
latestEventId: z.ZodString;
|
|
75
|
-
latestType: z.ZodString;
|
|
76
|
-
updatedAt: z.ZodString;
|
|
77
|
-
}, z.core.$strip>>;
|
|
78
|
-
}, z.core.$strip>;
|
package/dist/src/schemas.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export const WorldActorSchema = z.object({
|
|
3
|
-
id: z.string().min(1).optional(),
|
|
4
|
-
name: z.string().min(1).optional(),
|
|
5
|
-
kind: z.enum(['human', 'agent', 'bot', 'system']),
|
|
6
|
-
});
|
|
7
|
-
export const WorldSubjectSchema = z.object({
|
|
8
|
-
type: z.string().min(1),
|
|
9
|
-
id: z.string().min(1),
|
|
10
|
-
});
|
|
11
|
-
export const WorldExternalRefSchema = z.object({
|
|
12
|
-
provider: z.string().min(1),
|
|
13
|
-
id: z.string().min(1).optional(),
|
|
14
|
-
url: z.string().min(1).optional(),
|
|
15
|
-
cursor: z.string().min(1).optional(),
|
|
16
|
-
rawRef: z.string().min(1).optional(),
|
|
17
|
-
});
|
|
18
|
-
export const WorldServiceEventSchema = z.object({
|
|
19
|
-
id: z.string().min(1),
|
|
20
|
-
service: z.string().min(1),
|
|
21
|
-
type: z.string().min(1),
|
|
22
|
-
schemaVersion: z.number().int().positive(),
|
|
23
|
-
idempotencyKey: z.string().min(1),
|
|
24
|
-
occurredAt: z.string().min(1),
|
|
25
|
-
observedAt: z.string().min(1),
|
|
26
|
-
origin: z.enum(['virtual', 'external', 'agent', 'connector', 'replay', 'migration']),
|
|
27
|
-
actor: WorldActorSchema.optional(),
|
|
28
|
-
subject: WorldSubjectSchema,
|
|
29
|
-
causationId: z.string().min(1).optional(),
|
|
30
|
-
correlationId: z.string().min(1).optional(),
|
|
31
|
-
external: WorldExternalRefSchema.optional(),
|
|
32
|
-
data: z.record(z.string(), z.unknown()),
|
|
33
|
-
raw: z.record(z.string(), z.unknown()).optional(),
|
|
34
|
-
});
|
|
35
|
-
// NOTE: WorldAnnotationSchema moved to @volter/tracker/world-annotations — annotating
|
|
36
|
-
// world events as sources/noise is a tracker (verification) concern, not the twin's.
|
|
37
|
-
export const GenericWorldStateSchema = z.object({
|
|
38
|
-
version: z.literal(1),
|
|
39
|
-
service: z.string().min(1),
|
|
40
|
-
rebuiltAt: z.string().min(1),
|
|
41
|
-
eventCount: z.number().int().nonnegative(),
|
|
42
|
-
latestEventId: z.string().min(1).optional(),
|
|
43
|
-
subjects: z.record(z.string(), z.object({
|
|
44
|
-
type: z.string().min(1),
|
|
45
|
-
id: z.string().min(1),
|
|
46
|
-
latestEventId: z.string().min(1),
|
|
47
|
-
latestType: z.string().min(1),
|
|
48
|
-
updatedAt: z.string().min(1),
|
|
49
|
-
})),
|
|
50
|
-
});
|
package/dist/src/serve.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { SubjectFields } from './shadow.js';
|
|
2
|
-
import type { TwinActionPrecondition } from './actions.js';
|
|
3
|
-
export type TwinResource = {
|
|
4
|
-
id: string;
|
|
5
|
-
type: string;
|
|
6
|
-
updatedAt: string;
|
|
7
|
-
} & Record<string, unknown>;
|
|
8
|
-
export declare function twinResources(service: string, root?: string): TwinResource[];
|
|
9
|
-
export type TwinWriteResult = {
|
|
10
|
-
status: 'performed' | 'replayed';
|
|
11
|
-
actionId: string;
|
|
12
|
-
};
|
|
13
|
-
export declare function resolveTwinRead(service: string, pathname: string, opts?: {
|
|
14
|
-
root?: string;
|
|
15
|
-
}): {
|
|
16
|
-
status: number;
|
|
17
|
-
body: unknown;
|
|
18
|
-
};
|
|
19
|
-
export declare function applyTwinWrite(service: string, write: {
|
|
20
|
-
operation: string;
|
|
21
|
-
provider?: string;
|
|
22
|
-
subjectType: string;
|
|
23
|
-
subjectId: string;
|
|
24
|
-
fields: SubjectFields;
|
|
25
|
-
occurredAt?: string;
|
|
26
|
-
actor?: {
|
|
27
|
-
kind: 'agent' | 'human' | 'bot' | 'system';
|
|
28
|
-
id?: string;
|
|
29
|
-
};
|
|
30
|
-
preconditions?: TwinActionPrecondition[];
|
|
31
|
-
correlationId?: string;
|
|
32
|
-
}, root?: string): Promise<{
|
|
33
|
-
result: TwinWriteResult;
|
|
34
|
-
resource: TwinResource;
|
|
35
|
-
}>;
|
|
36
|
-
export declare function createTwinServer(options: {
|
|
37
|
-
service: string;
|
|
38
|
-
root?: string;
|
|
39
|
-
port?: number;
|
|
40
|
-
readOnly?: boolean;
|
|
41
|
-
}): {
|
|
42
|
-
port: number;
|
|
43
|
-
stop: () => void;
|
|
44
|
-
};
|