@volter/twin 0.1.0

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.
Files changed (82) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +68 -0
  3. package/dist/src/actions.d.ts +138 -0
  4. package/dist/src/actions.js +201 -0
  5. package/dist/src/args.d.ts +3 -0
  6. package/dist/src/args.js +12 -0
  7. package/dist/src/cli.d.ts +2 -0
  8. package/dist/src/cli.js +425 -0
  9. package/dist/src/connector.d.ts +106 -0
  10. package/dist/src/connector.js +129 -0
  11. package/dist/src/control-plane.d.ts +21 -0
  12. package/dist/src/control-plane.js +40 -0
  13. package/dist/src/egress.d.ts +93 -0
  14. package/dist/src/egress.js +264 -0
  15. package/dist/src/fork.d.ts +126 -0
  16. package/dist/src/fork.js +206 -0
  17. package/dist/src/index.d.ts +42 -0
  18. package/dist/src/index.js +52 -0
  19. package/dist/src/lease.d.ts +50 -0
  20. package/dist/src/lease.js +80 -0
  21. package/dist/src/packRegistry.d.ts +34 -0
  22. package/dist/src/packRegistry.js +22 -0
  23. package/dist/src/plan.d.ts +97 -0
  24. package/dist/src/plan.js +151 -0
  25. package/dist/src/proxy.d.ts +25 -0
  26. package/dist/src/proxy.js +152 -0
  27. package/dist/src/pushLedger.d.ts +81 -0
  28. package/dist/src/pushLedger.js +130 -0
  29. package/dist/src/queueLifecycle.d.ts +62 -0
  30. package/dist/src/queueLifecycle.js +95 -0
  31. package/dist/src/reconcile.d.ts +58 -0
  32. package/dist/src/reconcile.js +137 -0
  33. package/dist/src/refs.d.ts +29 -0
  34. package/dist/src/refs.js +68 -0
  35. package/dist/src/schemas.d.ts +78 -0
  36. package/dist/src/schemas.js +50 -0
  37. package/dist/src/serve.d.ts +44 -0
  38. package/dist/src/serve.js +93 -0
  39. package/dist/src/shadow.d.ts +77 -0
  40. package/dist/src/shadow.js +138 -0
  41. package/dist/src/status.d.ts +31 -0
  42. package/dist/src/status.js +42 -0
  43. package/dist/src/storage.d.ts +119 -0
  44. package/dist/src/storage.js +535 -0
  45. package/dist/src/sync.d.ts +91 -0
  46. package/dist/src/sync.js +121 -0
  47. package/dist/src/types.d.ts +40 -0
  48. package/dist/src/types.js +1 -0
  49. package/dist/src/validate.d.ts +27 -0
  50. package/dist/src/validate.js +68 -0
  51. package/dist/src/visualizer.d.ts +13 -0
  52. package/dist/src/visualizer.js +133 -0
  53. package/dist/src/worldConfig.d.ts +9 -0
  54. package/dist/src/worldConfig.js +16 -0
  55. package/inject.cjs +429 -0
  56. package/package.json +81 -0
  57. package/src/actions.ts +285 -0
  58. package/src/args.ts +14 -0
  59. package/src/cli.ts +443 -0
  60. package/src/connector.ts +220 -0
  61. package/src/control-plane.ts +66 -0
  62. package/src/egress.ts +355 -0
  63. package/src/fork.ts +256 -0
  64. package/src/index.ts +222 -0
  65. package/src/lease.ts +97 -0
  66. package/src/packRegistry.ts +60 -0
  67. package/src/plan.ts +190 -0
  68. package/src/proxy.ts +180 -0
  69. package/src/pushLedger.ts +189 -0
  70. package/src/queueLifecycle.ts +130 -0
  71. package/src/reconcile.ts +192 -0
  72. package/src/refs.ts +91 -0
  73. package/src/schemas.ts +56 -0
  74. package/src/serve.ts +120 -0
  75. package/src/shadow.ts +192 -0
  76. package/src/status.ts +58 -0
  77. package/src/storage.ts +632 -0
  78. package/src/sync.ts +160 -0
  79. package/src/types.ts +50 -0
  80. package/src/validate.ts +95 -0
  81. package/src/visualizer.ts +142 -0
  82. package/src/worldConfig.ts +26 -0
@@ -0,0 +1,206 @@
1
+ // Fork mode for twins (the twins architecture notes): a deliberate,
2
+ // auditable divergence from a known base.
3
+ //
4
+ // simulator already accepts local writes; fork ADDS three things on top:
5
+ // 1. a KNOWN BASE — snapshot the source twin's state at fork time, into an
6
+ // isolated root, so the fork starts byte-identical to its base (copy-on-write).
7
+ // 2. DIVERGENCE — diff the fork's current state against that recorded base
8
+ // (what an agent changed while working in the fork).
9
+ // 3. a CHECKABLE REAL-WRITE AUDIT: local writes in a fork stay in the action
10
+ // log and never touch the vendor — but a fork is NOT forbidden from pushing
11
+ // for real (that's the whole "fork it... push selected changes back" arc,
12
+ // via `applyPlan`/`pushTransaction` or `syncPush`); the fork boundary is a
13
+ // sandbox for local work, not a guarantee no real write can ever happen from
14
+ // it. `auditForkNoRealWrites` is therefore a checkable REPORT — has any real
15
+ // write happened from this fork SO FAR? — not a structural impossibility
16
+ // proof, and it has to look at every channel a real write can travel
17
+ // through: the egress ledger (direct `performExternalWrite`/`syncPush`
18
+ // calls) AND the push ledger (`pushTransaction`/`applyPlan`). Missing either
19
+ // channel makes `ok: true` a fabricated green on a fork that pushed.
20
+ //
21
+ // Convergence (rebase onto a fresh pull / push the divergence to real) is the
22
+ // separate `reconcile` concern and is not implemented here.
23
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { dirname, join } from 'node:path';
25
+ import { listEgressLedger } from "./egress.js";
26
+ import { appendAction, listActions, pendingActions, TwinActionPreconditionError } from "./actions.js";
27
+ import { pendingConflicts } from "./plan.js";
28
+ import { listPushLedger } from "./pushLedger.js";
29
+ import { twinResources } from "./serve.js";
30
+ import { appendEvent, listEvents, readJsonFile, worldPaths } from "./storage.js";
31
+ // Fields that are bookkeeping, not vendor data — excluded from divergence diffs.
32
+ const META_FIELDS = new Set(['id', 'type', 'updatedAt']);
33
+ function forkMetaPath(service, root) {
34
+ return join(dirname(worldPaths(service, root).events), 'fork-meta.json');
35
+ }
36
+ /**
37
+ * Create a fork of `service` from `fromRoot` into the fresh isolated `toRoot`.
38
+ * Copies the base event log so the fork folds to identical state, then records
39
+ * the baseline. `occurredAt` is supplied by the caller (deterministic; no clock).
40
+ */
41
+ export function forkTwin(opts) {
42
+ const { service, toRoot, fromRoot } = opts;
43
+ if (existsSync(forkMetaPath(service, toRoot))) {
44
+ throw new Error(`fork target already initialized for ${service}: ${toRoot}`);
45
+ }
46
+ const baseEvents = listEvents(service, fromRoot);
47
+ // Copy the base log into the fork root (copy-on-write: the fork owns its log).
48
+ for (const event of baseEvents)
49
+ appendEvent(event, toRoot);
50
+ const baseline = twinResources(service, toRoot).sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
51
+ const meta = {
52
+ service,
53
+ forkedAt: opts.occurredAt,
54
+ baseRoot: fromRoot ?? '',
55
+ baseEventCount: baseEvents.length,
56
+ baseLatestEventId: baseEvents.length ? baseEvents[baseEvents.length - 1].id : null,
57
+ baseline,
58
+ };
59
+ writeFileSync(forkMetaPath(service, toRoot), `${JSON.stringify(meta, null, 2)}\n`);
60
+ return meta;
61
+ }
62
+ export function readForkMeta(service, root) {
63
+ const path = forkMetaPath(service, root);
64
+ if (!existsSync(path))
65
+ throw new Error(`not a fork (no fork-meta.json) for ${service}: ${root}`);
66
+ return readJsonFile(path);
67
+ }
68
+ export function isFork(service, root) {
69
+ return existsSync(forkMetaPath(service, root));
70
+ }
71
+ /** What changed in the fork relative to its recorded base. */
72
+ export function forkDivergence(service, forkRoot) {
73
+ const meta = readForkMeta(service, forkRoot);
74
+ const baseById = new Map(meta.baseline.map((r) => [r.id, r]));
75
+ const current = twinResources(service, forkRoot);
76
+ const created = [];
77
+ const changed = [];
78
+ for (const cur of current) {
79
+ const base = baseById.get(cur.id);
80
+ if (!base) {
81
+ created.push(cur);
82
+ continue;
83
+ }
84
+ const fieldChanges = {};
85
+ const keys = new Set([...Object.keys(base), ...Object.keys(cur)].filter((k) => !META_FIELDS.has(k)));
86
+ for (const key of keys) {
87
+ const before = base[key];
88
+ const after = cur[key];
89
+ if (JSON.stringify(before) !== JSON.stringify(after))
90
+ fieldChanges[key] = { before, after };
91
+ }
92
+ if (Object.keys(fieldChanges).length > 0)
93
+ changed.push({ id: cur.id, type: cur.type, changed: fieldChanges });
94
+ }
95
+ created.sort((a, b) => (a.id < b.id ? -1 : 1));
96
+ changed.sort((a, b) => (a.id < b.id ? -1 : 1));
97
+ return { service, baseCount: meta.baseline.length, currentCount: current.length, created, changed };
98
+ }
99
+ // A push-ledger row at any of these statuses means writeFn was actually invoked
100
+ // (or its outcome recorded) — a real provider call, not just a locally-queued
101
+ // intent. 'attempted' is written unconditionally right before writeFn runs, so
102
+ // its presence alone already implies every later status is also a real write;
103
+ // listed explicitly (rather than "anything past intent") so the set reads as a
104
+ // deliberate audit policy, not an artifact of the phase ordering. 'abandoned'
105
+ // and bare 'intent' are excluded: those mean writeFn was never called.
106
+ const PUSH_REAL_WRITE_STATUSES = new Set([
107
+ 'attempted', 'provider_accepted', 'observed_confirmed', 'projection_suppressed', 'confirmed', 'succeeded', 'failed',
108
+ ]);
109
+ /**
110
+ * Report whether the fork has made any real write SO FAR (R18 model + TWIN-52):
111
+ * local fork work lives in the ACTION log and never reaches the vendor on its
112
+ * own, so a real write can only have happened through one of the two channels
113
+ * that touch the vendor —
114
+ * - the egress ledger (`performExternalWrite`, and `syncPush` which is built
115
+ * on it): any entry there is a completed or attempted direct write.
116
+ * - the push ledger (`pushTransaction`, and `applyPlan` which drives it): any
117
+ * row whose status reached `attempted` or further is a real provider call
118
+ * that was made (or is unreconciled — see UnreconciledPushError — which is
119
+ * itself "may have happened", so it counts as a breach too).
120
+ * Reading only the egress ledger (the old implementation) missed the push
121
+ * channel entirely: a fork that pushed via `applyPlan`/`pushTransaction` (the
122
+ * canonical arc in examples/lifecycle.ts) audited as `{ok:true, realWrites:0}`,
123
+ * a fabricated green. This is a checkable AUDIT of what happened, not a
124
+ * guarantee that nothing can — pushing for real from a fork is a supported
125
+ * operation, not a violation.
126
+ */
127
+ export function auditForkNoRealWrites(service, forkRoot) {
128
+ const egressLedger = listEgressLedger(service, forkRoot);
129
+ const pushBreachActionIds = new Set();
130
+ for (const row of listPushLedger(service, forkRoot)) {
131
+ if (PUSH_REAL_WRITE_STATUSES.has(row.status))
132
+ pushBreachActionIds.add(row.actionId);
133
+ }
134
+ const breaches = [...egressLedger.map((e) => e.intent.id), ...pushBreachActionIds];
135
+ return {
136
+ ok: egressLedger.length === 0 && pushBreachActionIds.size === 0,
137
+ localActions: pendingActions(service, forkRoot).length,
138
+ realWrites: egressLedger.length + pushBreachActionIds.size,
139
+ breaches,
140
+ };
141
+ }
142
+ /** Remove a fork root entirely (the fork is throwaway by design). */
143
+ export function discardFork(forkRoot) {
144
+ rmSync(forkRoot, { recursive: true, force: true });
145
+ }
146
+ /**
147
+ * cherry-pick: copy selected `set` transaction commits from one fork into another.
148
+ * A commit whose precondition fails in the target is reported as a conflict, not
149
+ * applied (the rest still copy).
150
+ */
151
+ export function cherryPickActions(opts) {
152
+ const wanted = new Set(opts.actionIds);
153
+ const src = listActions(opts.service, opts.fromRoot).filter((a) => a.op === 'set' && wanted.has(a.id));
154
+ const copied = [];
155
+ const conflicts = [];
156
+ for (const a of src) {
157
+ try {
158
+ copied.push(appendAction({ ...a, occurredAt: opts.occurredAt }, opts.toRoot));
159
+ }
160
+ catch (e) {
161
+ if (e instanceof TwinActionPreconditionError)
162
+ conflicts.push({ actionId: a.id, reason: e.message });
163
+ else
164
+ throw e;
165
+ }
166
+ }
167
+ return { copied, conflicts };
168
+ }
169
+ /**
170
+ * merge: combine another fork's ACTIVE (pending) transaction history into this one.
171
+ * Requires compatible bases (same base latest-event id) unless `force`. Failed
172
+ * preconditions surface as conflicts.
173
+ */
174
+ export function mergeForks(opts) {
175
+ const into = readForkMeta(opts.service, opts.intoRoot);
176
+ const from = readForkMeta(opts.service, opts.fromRoot);
177
+ if (!opts.force && into.baseLatestEventId !== from.baseLatestEventId) {
178
+ throw new Error(`merge: incompatible fork bases (${into.baseLatestEventId} vs ${from.baseLatestEventId}); rebase first or pass force`);
179
+ }
180
+ const ids = pendingActions(opts.service, opts.fromRoot).map((a) => a.id);
181
+ return cherryPickActions({ service: opts.service, fromRoot: opts.fromRoot, toRoot: opts.intoRoot, actionIds: ids, occurredAt: opts.occurredAt });
182
+ }
183
+ /**
184
+ * reset: suppress all local projections non-destructively by appending a revert for
185
+ * every pending transaction (history is retained — unlike discardFork, which deletes
186
+ * the root). Returns how many transactions were reset.
187
+ */
188
+ export function resetFork(opts) {
189
+ const pending = pendingActions(opts.service, opts.root);
190
+ for (const a of pending) {
191
+ appendAction({ id: `revert:${a.id}`, service: opts.service, op: 'revert', subject: a.subject, occurredAt: opts.occurredAt, revertsActionId: a.id }, opts.root);
192
+ }
193
+ return pending.length;
194
+ }
195
+ /**
196
+ * rebase: commit fresh remote observations onto the fork's base, then replay active
197
+ * local transactions over the new base. Transactions whose preconditions now fail
198
+ * are returned as conflicts (the doc's "failed preconditions become conflicts").
199
+ */
200
+ export function rebaseFork(opts) {
201
+ let applied = 0;
202
+ for (const ev of opts.freshEvents)
203
+ if (appendEvent(ev, opts.root).appended)
204
+ applied += 1;
205
+ return { applied, conflicts: pendingConflicts(opts.service, opts.root) };
206
+ }
@@ -0,0 +1,42 @@
1
+ export * as controlPlane from './control-plane.js';
2
+ export { clearRegistry, getPack, hasPack, listPacks, registerPack } from './packRegistry.js';
3
+ export type { PackTransport, TwinPack } from './packRegistry.js';
4
+ export { GenericWorldStateSchema, WorldActorSchema, WorldExternalRefSchema, WorldServiceEventSchema, WorldSubjectSchema, } from './schemas.js';
5
+ export { findWriteResultByExternal, isEgressEventType, listEgressLedger, listUnreconciledWriteIntents, performExternalWrite, recordWriteIntent, UnreconciledWriteIntentError, WRITE_INTENT_SUFFIX, WRITE_RESULT_SUFFIX, } from './egress.js';
6
+ export type { EgressActor, EgressLedgerEntry, EgressWriteOutcome, EgressWriteRequest, EgressWriteResult, } from './egress.js';
7
+ export { loadWorldConfig, } from './worldConfig.js';
8
+ export type { WorldConfig, WorldServiceConfig, } from './worldConfig.js';
9
+ export { buildShadowState, DELTA_TYPE_SUFFIX, diffSubjectFields, hashFieldValue, recordObservedDelta, } from './shadow.js';
10
+ export type { DeltaObservation, DeltaResult, FieldChange, ShadowState, SubjectFieldExtractor, SubjectFields, SubjectShadow, } from './shadow.js';
11
+ export { appendDurable, appendEvent, commitQueuedEvents, createEvent, emptyGenericState, enqueueEvent, genericWorldReducer, listEvents, listQueuedEvents, loadState, readJsonFile, rebuildGenericState, rebuildState, scrubService, scrubWorld, stateDirName, withFileLock, worldPaths, worldStateRoot, } from './storage.js';
12
+ export type { AppendEventResult, CommitQueuedEventsResult, GenericWorldState, EnqueueEventResult, QueuedWorldServiceEvent, WorldPaths, WorldReducer, WorldServiceEvent, } from './types.js';
13
+ export type { ScrubResult } from './storage.js';
14
+ export { loadPollCursor, pollCursorPath, runConnectorPoll, runConnectorSweep, savePollCursor, } from './connector.js';
15
+ export type { ConnectorObservation, ConnectorPollResult, ConnectorSweepResult, SweepConnector, WorldConnector, } from './connector.js';
16
+ export { discoverWorldServices, summarizeWorldFindings, validateWorld, validateWorldService, } from './validate.js';
17
+ export type { WorldValidationFinding, WorldValidationReport, WorldValidationSummary, } from './validate.js';
18
+ export { applyTwinWrite, createTwinServer, resolveTwinRead, twinResources, } from './serve.js';
19
+ export type { TwinResource, } from './serve.js';
20
+ export { createTwinProxy } from './proxy.js';
21
+ export type { TwinProxy, TwinProxyOptions, VendorRoute } from './proxy.js';
22
+ export { auditForkNoRealWrites, cherryPickActions, discardFork, forkDivergence, forkTwin, isFork, mergeForks, rebaseFork, readForkMeta, resetFork, } from './fork.js';
23
+ export type { CopyResult, ForkAudit, ForkDivergence, ForkMeta, SubjectDivergence } from './fork.js';
24
+ export { isCleanlyReconcilable, reconcile, reconcileRequiresApproval } from './reconcile.js';
25
+ export type { FieldDecision, ReconcilePlan, ReconcilePolicy, SubjectReconcile, } from './reconcile.js';
26
+ export { currentResources, syncPull, syncPush } from './sync.js';
27
+ export type { PullResult, PushItemResult, PushResult, SyncResource } from './sync.js';
28
+ export { createVisualizerServer, renderTwinHtml } from './visualizer.js';
29
+ export { appendAction, appendTransactionCommit, confirmAction, listActions, listTransactionCommits, pendingActions, pendingEmits, pendingTransactionCommits, projectResources, TwinActionPreconditionError, } from './actions.js';
30
+ export type { ActionProjection, ProjectedDelivery, ProjectedResource, ProjectedResourcePatch, ProjectedResourceRef, TwinAction, TwinActionOp, TwinActionPrecondition, TwinActionPreconditionOp, TwinActionRevertSpec, TwinTransactionCommit, TwinTransactionCommitOp, TwinTransactionPrecondition, TwinTransactionRevertSpec, } from './actions.js';
31
+ export { isBaseStale, listRemoteRefs, readLocalRef, readRemoteRef, writeLocalRef, writeRemoteRef } from './refs.js';
32
+ export type { WorldLocalRef, WorldRemoteRef } from './refs.js';
33
+ export { commitPendingQueue, commitQueueRow, ignoreQueueRow, listQueueWithStatus, pendingQueueRows, poisonQueueRow, queueCounts, queueRowStatus, setQueueRowStatus, supersedeQueueRow, } from './queueLifecycle.js';
34
+ export type { QueueCounts, QueueRowStatus, QueueRowWithStatus, QueueStatusTransition } from './queueLifecycle.js';
35
+ export { abandonPush, appendPushRecord, latestPushByActionId, listPushLedger, pushTransaction, unconfirmedPushes, UnreconciledPushError } from './pushLedger.js';
36
+ export type { PushOutcome, PushStatus, WorldPushRecord } from './pushLedger.js';
37
+ export { acquireLease, activeLease, isLeaseActive, LeaseHeldError, listLeases, releaseLease } from './lease.js';
38
+ export type { WorldApplyLease } from './lease.js';
39
+ export { applyPlan, buildApplyPlan, listPlans, pendingConflicts, planRequiresApproval, readPlan, writePlan } from './plan.js';
40
+ export type { ActionMapper, ApplyResult, ProviderCall, WorldApplyPlan } from './plan.js';
41
+ export { formatStatus, worldStatus } from './status.js';
42
+ export type { WorldStatus } from './status.js';
@@ -0,0 +1,52 @@
1
+ // @volter/twin — SHARED LIBRARIES a twin is built FROM. This is a library, NOT a
2
+ // framework/"kernel": it does not own the request lifecycle and a pack is not required to
3
+ // implement any contract here — a pack COMPOSES the vendor-INDEPENDENT mechanics it needs and
4
+ // presents its own vendor's exact API + semantics (see ARCHITECTURE.md; "share the
5
+ // vendor-independent mechanics, not the implementation — no false cross-vendor uniformity").
6
+ // Two groupings, kept distinct on purpose:
7
+ // 1. the OPERATOR CONTROL PLANE — what you DRIVE A TWIN'S STATE WITH (status/plan/push+phases/
8
+ // lease/refs/queue-lifecycle/fork-ops). Operator-facing (the `volter-twin` CLI), NOT a
9
+ // contract packs implement. Available cohesively as the `controlPlane` namespace below.
10
+ // 2. the DATA-PLANE LIBRARIES a twin reuses (event log, projection, shadow/delta, egress,
11
+ // connectors). Vendor-independent mechanics; reuse them rather than reimplement (the
12
+ // durable log + projection ARE the product's state core, the same for every vendor).
13
+ // (Annotations + source books are tracker concerns — @volter/tracker, not here.)
14
+ // The flat exports below remain for back-compat; new code can lean on the split.
15
+ export * as controlPlane from "./control-plane.js";
16
+ // Conformance/validation tooling (capability + spec + recorded-diff + UI harnesses and
17
+ // the spec derivers) is NOT part of the runtime kernel — it lives in @volter/twin-tooling,
18
+ // a dev dependency. A twin runs without it; only tests and the conformance scripts use it.
19
+ // Pack registry — vendor twins self-describe (TwinPack) so tooling discovers them.
20
+ export { clearRegistry, getPack, hasPack, listPacks, registerPack } from "./packRegistry.js";
21
+ export { GenericWorldStateSchema, WorldActorSchema, WorldExternalRefSchema, WorldServiceEventSchema, WorldSubjectSchema, } from "./schemas.js";
22
+ // NOTE: source books (event→tracker-source mapping) moved OUT of the world — it was
23
+ // the last tracker coupling. It now lives at `@volter/tracker/world-source-books`,
24
+ // so `@volter/twin` is a pure twin runtime.
25
+ export { findWriteResultByExternal, isEgressEventType, listEgressLedger, listUnreconciledWriteIntents, performExternalWrite, recordWriteIntent, UnreconciledWriteIntentError, WRITE_INTENT_SUFFIX, WRITE_RESULT_SUFFIX, } from "./egress.js";
26
+ export { loadWorldConfig, } from "./worldConfig.js";
27
+ export { buildShadowState, DELTA_TYPE_SUFFIX, diffSubjectFields, hashFieldValue, recordObservedDelta, } from "./shadow.js";
28
+ export { appendDurable, appendEvent, commitQueuedEvents, createEvent, emptyGenericState, enqueueEvent, genericWorldReducer, listEvents, listQueuedEvents, loadState, readJsonFile, rebuildGenericState, rebuildState,
29
+ // Scrub: delete pulled data at rest (TWIN-45) — the honest, plain-`rm` counterpart to
30
+ // sync pull's fold-into-the-log. See docs/DATA_AT_REST.md for the full data-at-rest story.
31
+ scrubService, scrubWorld, stateDirName,
32
+ // The kernel's cross-process mutual-exclusion primitive (exclusive-create lockfile with
33
+ // stale-holder reclaim). Public so world-runtime can guard concurrent `upWorld` claims of
34
+ // one instance dir with the SAME lock semantics the event log uses (TWIN-36).
35
+ withFileLock, worldPaths, worldStateRoot, } from "./storage.js";
36
+ export { loadPollCursor, pollCursorPath, runConnectorPoll, runConnectorSweep, savePollCursor, } from "./connector.js";
37
+ export { discoverWorldServices, summarizeWorldFindings, validateWorld, validateWorldService, } from "./validate.js";
38
+ export { applyTwinWrite, createTwinServer, resolveTwinRead, twinResources, } from "./serve.js";
39
+ export { createTwinProxy } from "./proxy.js";
40
+ export { auditForkNoRealWrites, cherryPickActions, discardFork, forkDivergence, forkTwin, isFork, mergeForks, rebaseFork, readForkMeta, resetFork, } from "./fork.js";
41
+ export { isCleanlyReconcilable, reconcile, reconcileRequiresApproval } from "./reconcile.js";
42
+ export { currentResources, syncPull, syncPush } from "./sync.js";
43
+ export { createVisualizerServer, renderTwinHtml } from "./visualizer.js";
44
+ export { appendAction, appendTransactionCommit, confirmAction, listActions, listTransactionCommits, pendingActions, pendingEmits, pendingTransactionCommits, projectResources, TwinActionPreconditionError, } from "./actions.js";
45
+ // ── Operator control plane (R19/R20): remote refs, queue lifecycle, push ledger,
46
+ // apply leases, plan, status. (the twins architecture notes)
47
+ export { isBaseStale, listRemoteRefs, readLocalRef, readRemoteRef, writeLocalRef, writeRemoteRef } from "./refs.js";
48
+ export { commitPendingQueue, commitQueueRow, ignoreQueueRow, listQueueWithStatus, pendingQueueRows, poisonQueueRow, queueCounts, queueRowStatus, setQueueRowStatus, supersedeQueueRow, } from "./queueLifecycle.js";
49
+ export { abandonPush, appendPushRecord, latestPushByActionId, listPushLedger, pushTransaction, unconfirmedPushes, UnreconciledPushError } from "./pushLedger.js";
50
+ export { acquireLease, activeLease, isLeaseActive, LeaseHeldError, listLeases, releaseLease } from "./lease.js";
51
+ export { applyPlan, buildApplyPlan, listPlans, pendingConflicts, planRequiresApproval, readPlan, writePlan } from "./plan.js";
52
+ export { formatStatus, worldStatus } from "./status.js";
@@ -0,0 +1,50 @@
1
+ import type { WorldRemoteRef } from './refs.js';
2
+ export type WorldApplyLease = {
3
+ id: string;
4
+ service: string;
5
+ provider: string;
6
+ remoteRef: WorldRemoteRef;
7
+ planId: string;
8
+ holder: {
9
+ kind: 'agent' | 'human' | 'system';
10
+ id: string;
11
+ };
12
+ acquiredAt: string;
13
+ expiresAt: string;
14
+ releasedAt?: string;
15
+ };
16
+ export declare class LeaseHeldError extends Error {
17
+ readonly held: WorldApplyLease;
18
+ constructor(held: WorldApplyLease);
19
+ }
20
+ export declare function listLeases(service: string, root?: string): WorldApplyLease[];
21
+ export declare function isLeaseActive(lease: WorldApplyLease, now: string): boolean;
22
+ /** The active lease (if any) for a provider/checkpoint, given the current time. */
23
+ export declare function activeLease(service: string, provider: string, refName: string, now: string, root?: string): WorldApplyLease | null;
24
+ /**
25
+ * Acquire an apply lease. Throws LeaseHeldError if another active lease already
26
+ * targets the same provider/checkpoint (single-writer). `now` is used only for the
27
+ * active-conflict check; `acquiredAt`/`expiresAt` are caller-supplied.
28
+ *
29
+ * The active-check + write is a single critical section run under `withFileLock`
30
+ * (the kernel's cross-process mutual-exclusion primitive — see storage.ts) so two
31
+ * concurrent acquirers targeting the same (service, provider, refName) cannot both
32
+ * observe "no active lease" and both write: only one holds the lock at a time, and
33
+ * by the time the second gets it, the first's lease (if it won) is already on disk.
34
+ */
35
+ export declare function acquireLease(opts: {
36
+ service: string;
37
+ provider: string;
38
+ remoteRef: WorldRemoteRef;
39
+ planId: string;
40
+ holder: WorldApplyLease['holder'];
41
+ id: string;
42
+ acquiredAt: string;
43
+ expiresAt: string;
44
+ now?: string;
45
+ root?: string;
46
+ }): WorldApplyLease;
47
+ export declare function releaseLease(service: string, id: string, opts?: {
48
+ root?: string;
49
+ at?: string;
50
+ }): WorldApplyLease;
@@ -0,0 +1,80 @@
1
+ // Apply leases (the twins architecture notes → "Leases"). Before a push/apply, an
2
+ // operator acquires a lease against a provider/checkpoint so two agents cannot push
3
+ // divergent histories onto the same remote baseline. Invariant: at most one active
4
+ // (unreleased, unexpired) lease per (provider, remoteRef.name). Deterministic:
5
+ // caller supplies id/acquiredAt/expiresAt and `now` for expiry checks (no clock).
6
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { readJsonFile, withFileLock, worldPaths } from "./storage.js";
9
+ export class LeaseHeldError extends Error {
10
+ held;
11
+ constructor(held) {
12
+ super(`apply lease already held for ${held.provider}/${held.remoteRef.name} by ${held.holder.kind}:${held.holder.id} (lease ${held.id})`);
13
+ this.held = held;
14
+ this.name = 'LeaseHeldError';
15
+ }
16
+ }
17
+ function leasesDir(service, root) {
18
+ return join(worldPaths(service, root).dir, 'leases');
19
+ }
20
+ function leasePath(service, id, root) {
21
+ if (!/^[A-Za-z0-9_.-]+$/.test(id))
22
+ throw new Error(`invalid lease id: ${id}`);
23
+ return join(leasesDir(service, root), `${id}.json`);
24
+ }
25
+ /** The cross-process mutual-exclusion lockfile guarding acquireLease's whole
26
+ * active-check + write critical section (see acquireLease). One lock per
27
+ * service's leases dir — sufficient because the active-lease check itself is
28
+ * scoped to (service, provider, refName) and different services never share a dir. */
29
+ function acquireLockPath(service, root) {
30
+ return join(leasesDir(service, root), 'acquire.lock');
31
+ }
32
+ export function listLeases(service, root) {
33
+ const dir = leasesDir(service, root);
34
+ if (!existsSync(dir))
35
+ return [];
36
+ return readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => readJsonFile(join(dir, f)));
37
+ }
38
+ export function isLeaseActive(lease, now) {
39
+ return !lease.releasedAt && lease.expiresAt > now;
40
+ }
41
+ /** The active lease (if any) for a provider/checkpoint, given the current time. */
42
+ export function activeLease(service, provider, refName, now, root) {
43
+ return listLeases(service, root).find((l) => l.provider === provider && l.remoteRef.name === refName && isLeaseActive(l, now)) ?? null;
44
+ }
45
+ function writeLease(lease, root) {
46
+ const path = leasePath(lease.service, lease.id, root);
47
+ mkdirSync(join(path, '..'), { recursive: true });
48
+ writeFileSync(path, `${JSON.stringify(lease, null, 2)}\n`);
49
+ return lease;
50
+ }
51
+ /**
52
+ * Acquire an apply lease. Throws LeaseHeldError if another active lease already
53
+ * targets the same provider/checkpoint (single-writer). `now` is used only for the
54
+ * active-conflict check; `acquiredAt`/`expiresAt` are caller-supplied.
55
+ *
56
+ * The active-check + write is a single critical section run under `withFileLock`
57
+ * (the kernel's cross-process mutual-exclusion primitive — see storage.ts) so two
58
+ * concurrent acquirers targeting the same (service, provider, refName) cannot both
59
+ * observe "no active lease" and both write: only one holds the lock at a time, and
60
+ * by the time the second gets it, the first's lease (if it won) is already on disk.
61
+ */
62
+ export function acquireLease(opts) {
63
+ const now = opts.now ?? opts.acquiredAt;
64
+ return withFileLock(acquireLockPath(opts.service, opts.root), () => {
65
+ const held = activeLease(opts.service, opts.provider, opts.remoteRef.name, now, opts.root);
66
+ if (held && held.id !== opts.id)
67
+ throw new LeaseHeldError(held);
68
+ return writeLease({
69
+ id: opts.id, service: opts.service, provider: opts.provider, remoteRef: opts.remoteRef,
70
+ planId: opts.planId, holder: opts.holder, acquiredAt: opts.acquiredAt, expiresAt: opts.expiresAt,
71
+ }, opts.root);
72
+ });
73
+ }
74
+ export function releaseLease(service, id, opts = {}) {
75
+ const path = leasePath(service, id, opts.root);
76
+ if (!existsSync(path))
77
+ throw new Error(`no such lease: ${id}`);
78
+ const lease = readJsonFile(path);
79
+ return writeLease({ ...lease, releasedAt: opts.at ?? new Date().toISOString() }, opts.root);
80
+ }
@@ -0,0 +1,34 @@
1
+ export type PackTransport = 'rest' | 'graphql' | 'web-api';
2
+ export type TwinPack = {
3
+ /** vendor id / service, e.g. 'stripe'. */
4
+ vendor: string;
5
+ transport: PackTransport;
6
+ /** subject types the twin serves, e.g. ['customer','charge','payment_intent']. */
7
+ resources: string[];
8
+ /** the `world-<vendor>` operator bin, if any. */
9
+ bin?: string;
10
+ /** conformance field map { object: { field: type } } — vendored or derived from the spec. */
11
+ conformanceFields?: Record<string, Record<string, string>>;
12
+ /** where the exact surface came from (a spec path) — provenance for re-derivation. */
13
+ specSource?: string;
14
+ /** one-line human description. */
15
+ description?: string;
16
+ /** How this vendor's BROWSER SDK addresses its API — used by the zero-edit dev proxy so
17
+ * the kernel proxy stays vendor-agnostic (it forwards/rewrites by these values, never by a
18
+ * hardcoded vendor table). `apiPathPrefix`: the same-origin path the browser SDK calls
19
+ * (e.g. Stripe.js → '/v1/'). `loaderHost`: the absolute API host to strip from the loaded
20
+ * SDK so its calls become same-origin (e.g. 'https://api.stripe.com'). Omit for vendors
21
+ * with no browser SDK. */
22
+ browserRouting?: {
23
+ apiPathPrefix: string;
24
+ loaderHost?: string;
25
+ };
26
+ };
27
+ /** Register (or replace) a pack descriptor. Returns it. */
28
+ export declare function registerPack(pack: TwinPack): TwinPack;
29
+ export declare function getPack(vendor: string): TwinPack | undefined;
30
+ /** All registered packs, sorted by vendor (deterministic). */
31
+ export declare function listPacks(): TwinPack[];
32
+ export declare function hasPack(vendor: string): boolean;
33
+ /** Clear the registry (tests). */
34
+ export declare function clearRegistry(): void;
@@ -0,0 +1,22 @@
1
+ const registry = new Map();
2
+ /** Register (or replace) a pack descriptor. Returns it. */
3
+ export function registerPack(pack) {
4
+ if (!/^[a-z0-9-]+$/.test(pack.vendor))
5
+ throw new Error(`invalid pack vendor id: ${pack.vendor}`);
6
+ registry.set(pack.vendor, pack);
7
+ return pack;
8
+ }
9
+ export function getPack(vendor) {
10
+ return registry.get(vendor);
11
+ }
12
+ /** All registered packs, sorted by vendor (deterministic). */
13
+ export function listPacks() {
14
+ return [...registry.values()].sort((a, b) => a.vendor.localeCompare(b.vendor));
15
+ }
16
+ export function hasPack(vendor) {
17
+ return registry.has(vendor);
18
+ }
19
+ /** Clear the registry (tests). */
20
+ export function clearRegistry() {
21
+ registry.clear();
22
+ }
@@ -0,0 +1,97 @@
1
+ import type { TwinAction } from './actions.js';
2
+ import type { WorldRemoteRef } from './refs.js';
3
+ import type { PushOutcome, WorldPushRecord } from './pushLedger.js';
4
+ export type ProviderCall = {
5
+ actionId: string;
6
+ provider: string;
7
+ operation: string;
8
+ input: Record<string, unknown>;
9
+ idempotencyKey: string;
10
+ destructive: boolean;
11
+ expectedConfirmation: {
12
+ subject: {
13
+ type: string;
14
+ id: string;
15
+ };
16
+ eventType: string;
17
+ matcher: Record<string, unknown>;
18
+ };
19
+ };
20
+ export type WorldApplyPlan = {
21
+ id: string;
22
+ service: string;
23
+ forkId: string;
24
+ baseRemoteRef: WorldRemoteRef;
25
+ transactions: string[];
26
+ providerCalls: ProviderCall[];
27
+ conflicts: Array<{
28
+ actionId: string;
29
+ reason: string;
30
+ }>;
31
+ requiresApproval: boolean;
32
+ createdAt: string;
33
+ };
34
+ /** Map a local transaction commit → the provider call that materializes it (vendor-specific). Return null to skip. */
35
+ export type ActionMapper = (action: TwinAction) => Omit<ProviderCall, 'actionId'> | null;
36
+ /** Pending transactions whose preconditions would fail against current projected state. */
37
+ export declare function pendingConflicts(service: string, root?: string): Array<{
38
+ actionId: string;
39
+ reason: string;
40
+ }>;
41
+ /**
42
+ * Build a reviewable apply plan from a fork's pending transactions. `mapper` is the
43
+ * vendor-specific action→provider-call mapping; actions it returns null for are
44
+ * omitted. Conflicts are pending actions whose preconditions fail against current
45
+ * projected state. Approval is required if any call is destructive or any conflict exists.
46
+ */
47
+ export declare function buildApplyPlan(opts: {
48
+ service: string;
49
+ forkId: string;
50
+ baseRemoteRef: WorldRemoteRef;
51
+ mapper: ActionMapper;
52
+ id: string;
53
+ createdAt: string;
54
+ root?: string;
55
+ }): WorldApplyPlan;
56
+ export declare function writePlan(plan: WorldApplyPlan, root?: string): WorldApplyPlan;
57
+ export declare function readPlan(service: string, id: string, root?: string): WorldApplyPlan | null;
58
+ export declare function listPlans(service: string, root?: string): WorldApplyPlan[];
59
+ export type ApplyResult = {
60
+ planId: string;
61
+ leaseId: string;
62
+ pushed: WorldPushRecord[];
63
+ skipped: string[];
64
+ };
65
+ /**
66
+ * Whether a plan requires approval before it may be applied — recomputed from the
67
+ * plan's OWN `providerCalls`/`conflicts`, the exact rule `buildApplyPlan` uses to set
68
+ * the field in the first place. `applyPlan` calls this instead of trusting
69
+ * `plan.requiresApproval`: that field lives on plan JSON that is neither signed nor
70
+ * otherwise authenticated, so a hand-edited file (or a hand-built plan object) that
71
+ * flips the boolean to `false` must not be able to skip the approval gate — only
72
+ * removing the destructive calls/conflicts themselves does.
73
+ */
74
+ export declare function planRequiresApproval(plan: WorldApplyPlan): boolean;
75
+ /**
76
+ * Enact a plan against the real provider — the gated push path. Refuses an
77
+ * unapproved plan (approval requirement recomputed from the plan's own data, never
78
+ * trusted off the stored bit) and a push whose base has gone stale (the live
79
+ * remote ref moved since `plan.baseRemoteRef` was recorded) before any provider
80
+ * call or lease acquisition — zero side effects either way. Then acquires an apply
81
+ * lease for the plan's baseRemoteRef (single-writer), drives every provider call
82
+ * through the push phases via the injected writeFn (the only real I/O), then
83
+ * releases the lease.
84
+ */
85
+ export declare function applyPlan(plan: WorldApplyPlan, writeFn: (call: ProviderCall) => Promise<PushOutcome>, opts: {
86
+ holder: {
87
+ kind: 'agent' | 'human' | 'system';
88
+ id: string;
89
+ };
90
+ leaseId: string;
91
+ acquiredAt: string;
92
+ expiresAt: string;
93
+ occurredAt: string;
94
+ approve?: boolean;
95
+ overrideStaleBase?: boolean;
96
+ root?: string;
97
+ }): Promise<ApplyResult>;