@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.
- package/LICENSE +202 -0
- package/README.md +68 -0
- package/dist/src/actions.d.ts +138 -0
- package/dist/src/actions.js +201 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +425 -0
- package/dist/src/connector.d.ts +106 -0
- package/dist/src/connector.js +129 -0
- package/dist/src/control-plane.d.ts +21 -0
- package/dist/src/control-plane.js +40 -0
- package/dist/src/egress.d.ts +93 -0
- package/dist/src/egress.js +264 -0
- package/dist/src/fork.d.ts +126 -0
- package/dist/src/fork.js +206 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/index.js +52 -0
- package/dist/src/lease.d.ts +50 -0
- package/dist/src/lease.js +80 -0
- package/dist/src/packRegistry.d.ts +34 -0
- package/dist/src/packRegistry.js +22 -0
- package/dist/src/plan.d.ts +97 -0
- package/dist/src/plan.js +151 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +152 -0
- package/dist/src/pushLedger.d.ts +81 -0
- package/dist/src/pushLedger.js +130 -0
- package/dist/src/queueLifecycle.d.ts +62 -0
- package/dist/src/queueLifecycle.js +95 -0
- package/dist/src/reconcile.d.ts +58 -0
- package/dist/src/reconcile.js +137 -0
- package/dist/src/refs.d.ts +29 -0
- package/dist/src/refs.js +68 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve.d.ts +44 -0
- package/dist/src/serve.js +93 -0
- package/dist/src/shadow.d.ts +77 -0
- package/dist/src/shadow.js +138 -0
- package/dist/src/status.d.ts +31 -0
- package/dist/src/status.js +42 -0
- package/dist/src/storage.d.ts +119 -0
- package/dist/src/storage.js +535 -0
- package/dist/src/sync.d.ts +91 -0
- package/dist/src/sync.js +121 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validate.d.ts +27 -0
- package/dist/src/validate.js +68 -0
- package/dist/src/visualizer.d.ts +13 -0
- package/dist/src/visualizer.js +133 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +16 -0
- package/inject.cjs +429 -0
- package/package.json +81 -0
- package/src/actions.ts +285 -0
- package/src/args.ts +14 -0
- package/src/cli.ts +443 -0
- package/src/connector.ts +220 -0
- package/src/control-plane.ts +66 -0
- package/src/egress.ts +355 -0
- package/src/fork.ts +256 -0
- package/src/index.ts +222 -0
- package/src/lease.ts +97 -0
- package/src/packRegistry.ts +60 -0
- package/src/plan.ts +190 -0
- package/src/proxy.ts +180 -0
- package/src/pushLedger.ts +189 -0
- package/src/queueLifecycle.ts +130 -0
- package/src/reconcile.ts +192 -0
- package/src/refs.ts +91 -0
- package/src/schemas.ts +56 -0
- package/src/serve.ts +120 -0
- package/src/shadow.ts +192 -0
- package/src/status.ts +58 -0
- package/src/storage.ts +632 -0
- package/src/sync.ts +160 -0
- package/src/types.ts +50 -0
- package/src/validate.ts +95 -0
- package/src/visualizer.ts +142 -0
- package/src/worldConfig.ts +26 -0
package/dist/src/fork.js
ADDED
|
@@ -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>;
|