@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,77 @@
1
+ import type { AppendEventResult, WorldServiceEvent } from './types.js';
2
+ export declare const DELTA_TYPE_SUFFIX = ".delta";
3
+ export type SubjectFields = Record<string, unknown>;
4
+ export type SubjectShadow = {
5
+ subject: {
6
+ type: string;
7
+ id: string;
8
+ };
9
+ fields: SubjectFields;
10
+ fieldHashes: Record<string, string>;
11
+ latestEventId: string;
12
+ updatedAt: string;
13
+ };
14
+ export type ShadowState = {
15
+ version: 1;
16
+ service: string;
17
+ rebuiltAt: string;
18
+ subjects: Record<string, SubjectShadow>;
19
+ };
20
+ /**
21
+ * Maps a world event to the subject fields it observes, or null when the
22
+ * event says nothing about remote subject state (comments, egress records, …).
23
+ * Extractors are provider-specific; the shadow engine is not.
24
+ */
25
+ export type SubjectFieldExtractor = (event: WorldServiceEvent) => SubjectFields | null;
26
+ export type FieldChange = {
27
+ before: unknown;
28
+ after: unknown;
29
+ };
30
+ export declare function hashFieldValue(value: unknown): string;
31
+ /**
32
+ * Fold the service event log into per-subject materialized remote state.
33
+ * Delta events apply their `after` values directly; every other event goes
34
+ * through the extractor. The log is the source of truth — the shadow is
35
+ * always reproducible from a replay.
36
+ */
37
+ export declare function buildShadowState(service: string, extractor: SubjectFieldExtractor, root?: string): ShadowState;
38
+ /** Field-level diff of freshly observed subject fields against the shadow. */
39
+ export declare function diffSubjectFields(shadow: SubjectShadow | undefined, observed: SubjectFields): Record<string, FieldChange>;
40
+ export type DeltaObservation = {
41
+ service: string;
42
+ subject: {
43
+ type: string;
44
+ id: string;
45
+ };
46
+ /** Freshly fetched provider-side fields for the subject. */
47
+ observed: SubjectFields;
48
+ /** Provider timestamp of the observed state when available. */
49
+ occurredAt?: string;
50
+ external?: {
51
+ provider: string;
52
+ id?: string;
53
+ url?: string;
54
+ cursor?: string;
55
+ };
56
+ actor?: {
57
+ kind: 'human' | 'agent' | 'bot' | 'system';
58
+ id?: string;
59
+ name?: string;
60
+ };
61
+ };
62
+ export type DeltaResult = {
63
+ changed: false;
64
+ } | {
65
+ changed: true;
66
+ event: WorldServiceEvent;
67
+ changes: Record<string, FieldChange>;
68
+ append: AppendEventResult;
69
+ };
70
+ /**
71
+ * Compare an observed provider snapshot against the shadow and append a
72
+ * single delta event carrying only the changed fields. An unchanged
73
+ * re-observation appends nothing — this is the fix for re-snapshot noise.
74
+ * The delta idempotencyKey is derived from the after-state hashes, so the
75
+ * same observed change never appends twice.
76
+ */
77
+ export declare function recordObservedDelta(state: ShadowState, observation: DeltaObservation, root?: string): DeltaResult;
@@ -0,0 +1,138 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isEgressEventType } from "./egress.js";
3
+ import { appendEvent, createEvent, listEvents } from "./storage.js";
4
+ export const DELTA_TYPE_SUFFIX = '.delta';
5
+ function canonicalJson(value) {
6
+ if (Array.isArray(value))
7
+ return `[${value.map(canonicalJson).join(',')}]`;
8
+ if (value && typeof value === 'object') {
9
+ const entries = Object.entries(value)
10
+ .filter(([, item]) => item !== undefined)
11
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
12
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
13
+ return `{${entries.join(',')}}`;
14
+ }
15
+ return JSON.stringify(value) ?? 'null';
16
+ }
17
+ export function hashFieldValue(value) {
18
+ return createHash('sha256').update(canonicalJson(value)).digest('hex').slice(0, 16);
19
+ }
20
+ function subjectKey(subject) {
21
+ return `${subject.type}:${subject.id}`;
22
+ }
23
+ function isDeltaEvent(event) {
24
+ return event.type.endsWith(DELTA_TYPE_SUFFIX) && typeof event.data.changed === 'object';
25
+ }
26
+ function deltaAfterFields(event) {
27
+ const changed = event.data.changed;
28
+ const fields = {};
29
+ for (const [field, change] of Object.entries(changed))
30
+ fields[field] = change.after;
31
+ return fields;
32
+ }
33
+ function applyFields(state, event, fields) {
34
+ const key = subjectKey(event.subject);
35
+ const existing = state.subjects[key];
36
+ // A late-arriving observation of OLDER provider state must not regress the
37
+ // shadow. Comparable occurredAt timestamps win over log append order.
38
+ if (existing) {
39
+ const incoming = Date.parse(event.occurredAt);
40
+ const current = Date.parse(existing.updatedAt);
41
+ if (Number.isFinite(incoming) && Number.isFinite(current) && incoming < current)
42
+ return;
43
+ }
44
+ const target = existing ?? {
45
+ subject: { ...event.subject },
46
+ fields: {},
47
+ fieldHashes: {},
48
+ latestEventId: event.id,
49
+ updatedAt: event.occurredAt,
50
+ };
51
+ for (const [field, value] of Object.entries(fields)) {
52
+ if (value === undefined)
53
+ continue;
54
+ target.fields[field] = value;
55
+ target.fieldHashes[field] = hashFieldValue(value);
56
+ }
57
+ target.latestEventId = event.id;
58
+ target.updatedAt = event.occurredAt || target.updatedAt;
59
+ state.subjects[key] = target;
60
+ }
61
+ /**
62
+ * Fold the service event log into per-subject materialized remote state.
63
+ * Delta events apply their `after` values directly; every other event goes
64
+ * through the extractor. The log is the source of truth — the shadow is
65
+ * always reproducible from a replay.
66
+ */
67
+ export function buildShadowState(service, extractor, root) {
68
+ const state = { version: 1, service, rebuiltAt: new Date().toISOString(), subjects: {} };
69
+ for (const event of listEvents(service, root)) {
70
+ if (isEgressEventType(event.type))
71
+ continue;
72
+ if (isDeltaEvent(event)) {
73
+ applyFields(state, event, deltaAfterFields(event));
74
+ continue;
75
+ }
76
+ const fields = extractor(event);
77
+ if (fields)
78
+ applyFields(state, event, fields);
79
+ }
80
+ return state;
81
+ }
82
+ /** Field-level diff of freshly observed subject fields against the shadow. */
83
+ export function diffSubjectFields(shadow, observed) {
84
+ const changed = {};
85
+ for (const [field, after] of Object.entries(observed)) {
86
+ if (after === undefined)
87
+ continue;
88
+ const beforeHash = shadow?.fieldHashes[field];
89
+ if (beforeHash !== undefined && beforeHash === hashFieldValue(after))
90
+ continue;
91
+ changed[field] = { before: shadow?.fields[field], after };
92
+ }
93
+ return changed;
94
+ }
95
+ /**
96
+ * Compare an observed provider snapshot against the shadow and append a
97
+ * single delta event carrying only the changed fields. An unchanged
98
+ * re-observation appends nothing — this is the fix for re-snapshot noise.
99
+ * The delta idempotencyKey is derived from the after-state hashes, so the
100
+ * same observed change never appends twice.
101
+ */
102
+ export function recordObservedDelta(state, observation, root) {
103
+ const shadow = state.subjects[subjectKey(observation.subject)];
104
+ const changes = diffSubjectFields(shadow, observation.observed);
105
+ if (Object.keys(changes).length === 0)
106
+ return { changed: false };
107
+ // The hash covers the provider timestamp as well as the after-state:
108
+ // A→B→A→B transitions revisit an after-state, and an id derived from
109
+ // content alone would collide with the earlier delta and make appendEvent
110
+ // throw (same id, different occurredAt). Re-observing the SAME provider
111
+ // state still dedupes — occurredAt comes from the provider, not the poll.
112
+ const afterHash = createHash('sha256')
113
+ .update(observation.occurredAt ?? '')
114
+ .update('\u001f')
115
+ .update(canonicalJson(Object.fromEntries(Object.entries(changes).map(([field, change]) => [field, change.after]))))
116
+ .digest('hex')
117
+ .slice(0, 16);
118
+ const type = `${observation.service}.${observation.subject.type}${DELTA_TYPE_SUFFIX}`;
119
+ const event = createEvent({
120
+ id: `${type}:${observation.subject.id}:${afterHash}`,
121
+ service: observation.service,
122
+ type,
123
+ idempotencyKey: `${observation.service}:delta:${observation.subject.type}:${observation.subject.id}:${afterHash}`,
124
+ occurredAt: observation.occurredAt ?? new Date().toISOString(),
125
+ origin: 'connector',
126
+ ...(observation.actor ? { actor: observation.actor } : {}),
127
+ subject: observation.subject,
128
+ ...(observation.external ? { external: observation.external } : {}),
129
+ data: {
130
+ changedFields: Object.keys(changes),
131
+ changed: changes,
132
+ },
133
+ });
134
+ const append = appendEvent(event, root);
135
+ if (append.appended)
136
+ applyFields(state, append.event, deltaAfterFields(append.event));
137
+ return { changed: true, event: append.event, changes, append };
138
+ }
@@ -0,0 +1,31 @@
1
+ import type { QueueCounts } from './queueLifecycle.js';
2
+ import type { WorldRemoteRef } from './refs.js';
3
+ export type WorldStatus = {
4
+ service: string;
5
+ remote: WorldRemoteRef | null;
6
+ queue: QueueCounts;
7
+ local: {
8
+ unpushed: number;
9
+ reverted: number;
10
+ };
11
+ push: {
12
+ unconfirmed: number;
13
+ };
14
+ drift: {
15
+ created: number;
16
+ changed: number;
17
+ } | null;
18
+ conflicts: Array<{
19
+ actionId: string;
20
+ reason: string;
21
+ }>;
22
+ staleBase: boolean | null;
23
+ };
24
+ export declare function worldStatus(service: string, opts?: {
25
+ root?: string;
26
+ forkId?: string;
27
+ provider?: string;
28
+ refName?: string;
29
+ }): WorldStatus;
30
+ /** Render the status as the operator text block the doc shows. */
31
+ export declare function formatStatus(s: WorldStatus): string;
@@ -0,0 +1,42 @@
1
+ // world status (the twins architecture notes → "Status"). The operator safety
2
+ // dashboard: it makes the four state classes impossible to confuse — queued
3
+ // deliveries, canonical observed events, local transaction commits, and push/apply
4
+ // records — plus drift, conflicts, and stale bases. Pure read over the kernel
5
+ // ledgers; deterministic.
6
+ import { listActions, pendingActions } from "./actions.js";
7
+ import { isFork, forkDivergence } from "./fork.js";
8
+ import { pendingConflicts } from "./plan.js";
9
+ import { unconfirmedPushes } from "./pushLedger.js";
10
+ import { queueCounts } from "./queueLifecycle.js";
11
+ import { isBaseStale, readLocalRef, readRemoteRef } from "./refs.js";
12
+ export function worldStatus(service, opts = {}) {
13
+ const { root, forkId, provider, refName = 'main' } = opts;
14
+ const actions = listActions(service, root);
15
+ const remote = provider ? readRemoteRef(service, provider, refName, root) : null;
16
+ const local = forkId ? readLocalRef(service, forkId, root) : null;
17
+ const drift = isFork(service, root ?? '') ? (() => { const d = forkDivergence(service, root ?? ''); return { created: d.created.length, changed: d.changed.length }; })() : null;
18
+ return {
19
+ service,
20
+ remote,
21
+ queue: queueCounts(service, root),
22
+ local: { unpushed: pendingActions(service, root).length, reverted: actions.filter((a) => a.op === 'revert').length },
23
+ push: { unconfirmed: unconfirmedPushes(service, root).length },
24
+ drift,
25
+ conflicts: pendingConflicts(service, root),
26
+ staleBase: local ? isBaseStale(local, root) : null,
27
+ };
28
+ }
29
+ /** Render the status as the operator text block the doc shows. */
30
+ export function formatStatus(s) {
31
+ const lines = [];
32
+ lines.push(s.remote ? `remote/${s.remote.provider}/${s.remote.name} at ${s.remote.cursor ?? s.remote.eventId ?? '(no checkpoint)'}` : 'remote: (no ref recorded)');
33
+ lines.push(`queue: ${s.queue.queued} queued, ${s.queue.committed} committed, ${s.queue.ignored} ignored, ${s.queue.superseded} superseded, ${s.queue.poisoned} poisoned`);
34
+ lines.push(`local: ${s.local.unpushed} unpushed transactions, ${s.local.reverted} reverted`);
35
+ lines.push(`push: ${s.push.unconfirmed} provider_accepted/attempted awaiting observed confirmation`);
36
+ if (s.drift)
37
+ lines.push(`drift: ${s.drift.created} created, ${s.drift.changed} changed since fork base`);
38
+ lines.push(`conflicts: ${s.conflicts.length}${s.conflicts.length ? ` (${s.conflicts.map((c) => c.actionId).join(', ')})` : ''}`);
39
+ if (s.staleBase !== null)
40
+ lines.push(`base: ${s.staleBase ? 'STALE — rebase before push' : 'current'}`);
41
+ return lines.join('\n');
42
+ }
@@ -0,0 +1,119 @@
1
+ import type { AppendEventResult, CommitQueuedEventsResult, EnqueueEventResult, GenericWorldState, QueuedWorldServiceEvent, WorldPaths, WorldReducer, WorldServiceEvent } from './types.js';
2
+ /**
3
+ * Name of the per-project state directory holding world data
4
+ * (`<root>/<stateDir>/world/...`). Defaults to `.volter`; hosts that need a
5
+ * different directory set VOLTER_STATE_DIR. Every path in this package must
6
+ * go through worldStateRoot — never the literal.
7
+ */
8
+ export declare function stateDirName(): string;
9
+ export declare function worldStateRoot(root?: string): string;
10
+ export declare function worldPaths(service: string, root?: string): WorldPaths;
11
+ /** Read + parse a whole-file JSON sidecar, citing the path on a parse failure (mirrors
12
+ * readJsonl's `${path}:...` error shape). Callers own existence semantics — this parses a
13
+ * file that is expected to exist; guard with existsSync first when absence is allowed. */
14
+ export declare function readJsonFile<T>(path: string): T;
15
+ /**
16
+ * Append `data` to `path`. When VOLTER_DURABLE=1, `fsyncSync` the file after the write so
17
+ * the appended record survives an OS crash / power loss — not just a process crash.
18
+ *
19
+ * Default (VOLTER_DURABLE unset) is OFF: `appendFileSync` is a completed write syscall, so a
20
+ * *process* crash after it returns still leaves the record on the log (the OS owns the page
21
+ * cache). The window this leaves open is a **kernel-panic / power loss** between the write
22
+ * landing in the page cache and the fs flushing it to stable storage — a torn or lost tail
23
+ * record. That crash window matters more once real pulled staging data lives in these files
24
+ * (purpose 2), so hosts that need durability opt in with VOLTER_DURABLE=1 at the cost of an
25
+ * fsync per append. See ARCHITECTURE.md D1.
26
+ */
27
+ export declare function appendDurable(path: string, data: string): void;
28
+ /**
29
+ * Opt-in structured stderr logging for the audit trail (ARCHITECTURE-REVIEW-TODOS D3):
30
+ * one line per action-log append and per push-ledger row, so "who reviewed the change
31
+ * that caused this real write" is mechanically greppable from a single log stream via
32
+ * `correlationId`. Off by default — mirrors the VOLTER_DURABLE opt-in policy above: no
33
+ * always-on I/O, never on the default path. Set VOLTER_TWIN_LOG=1 to enable.
34
+ */
35
+ export declare function twinLog(kind: string, details: Record<string, unknown>): void;
36
+ /** Run `fn` holding an exclusive cross-process file lock (the same primitive the event log
37
+ * uses). Used to make read-then-append critical sections atomic across processes. The
38
+ * lockfile records `{pid, hostname, at}`; a contender that finds a stale lock (dead pid or
39
+ * age > LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
40
+ * can't wedge the world forever. Reclaim is race-safe: only the process that wins the
41
+ * rename clears the stale inode, and the exclusive `wx` create still decides the winner. */
42
+ export declare function withFileLock<T>(lockPath: string, fn: () => T): T;
43
+ export declare function listEvents(service: string, root?: string): WorldServiceEvent[];
44
+ /** Exported so other modules that share a service's events log (e.g. egress.ts,
45
+ * TWIN-58) can serialize their own critical sections on the SAME lock appendEvent
46
+ * itself uses, instead of inventing a second, uncoordinated lockfile. */
47
+ export declare function eventsLockPath(paths: WorldPaths): string;
48
+ /**
49
+ * The append body, assuming the caller already holds `eventsLockPath(paths)`. Split
50
+ * out so `commitQueuedEvents` can run a whole batch of appends (and the rebuild that
51
+ * follows them) under ONE lock acquisition instead of nesting a fresh `withFileLock`
52
+ * per row — `withFileLock` is not reentrant, so re-acquiring it from inside an
53
+ * already-held lock in the same process would just spin to its own timeout. Also
54
+ * exported for egress.ts (TWIN-58): performExternalWrite's ledger-check +
55
+ * intent-append span holds `eventsLockPath` itself, so it must append through this
56
+ * already-locked path rather than the public `appendEvent` (which would try to
57
+ * re-acquire the same lock and spin to its own timeout).
58
+ */
59
+ export declare function appendEventLocked(parsed: WorldServiceEvent, paths: WorldPaths): AppendEventResult;
60
+ export declare function appendEvent(event: WorldServiceEvent, root?: string): AppendEventResult;
61
+ export declare function listQueuedEvents(service: string, root?: string): QueuedWorldServiceEvent[];
62
+ export declare function enqueueEvent(event: WorldServiceEvent, options?: {
63
+ root?: string;
64
+ source?: QueuedWorldServiceEvent['source'];
65
+ receivedAt?: string;
66
+ id?: string;
67
+ }): EnqueueEventResult;
68
+ /**
69
+ * Commit queued (webhook/listener) events into the canonical log, then rebuild the
70
+ * generic projection — spanning BOTH under the SAME events-lock acquisition (D7).
71
+ * Appending each event and rebuilding state.json are two separate durable writes;
72
+ * without a shared lock, a concurrent commit/append on another process could
73
+ * interleave between "this commit's last append" and "this commit's rebuild",
74
+ * so the rebuilt state.json would not correspond to exactly this commit's view of
75
+ * the log. Holding one lock across the whole sequence rules that out: no other
76
+ * appendEvent/commitQueuedEvents call for this service can run until both the
77
+ * appends AND the rebuild here have completed. (A single-process crash between the
78
+ * last durable append and the rebuild can still leave state.json one rebuild behind
79
+ * — that window is inherent to a two-file update with no WAL, and is unaffected by
80
+ * locking; it is closed by the next successful commit/rebuild, which always starts
81
+ * from the durably-appended log, so no event is ever lost, only state.json's
82
+ * projection is briefly stale.)
83
+ */
84
+ export declare function commitQueuedEvents(service: string, options?: {
85
+ root?: string;
86
+ limit?: number;
87
+ }): CommitQueuedEventsResult;
88
+ export declare function genericWorldReducer(state: GenericWorldState, event: WorldServiceEvent): GenericWorldState;
89
+ export declare function emptyGenericState(service: string): GenericWorldState;
90
+ export declare function rebuildState<State>(service: string, initialState: State, reducer: WorldReducer<State>, root?: string): State;
91
+ export declare function rebuildGenericState(service: string, root?: string): GenericWorldState;
92
+ export declare function loadState<T = unknown>(service: string, root?: string): T | null;
93
+ export declare function createEvent(input: Omit<WorldServiceEvent, 'schemaVersion' | 'observedAt'> & {
94
+ schemaVersion?: number;
95
+ observedAt?: string;
96
+ }): WorldServiceEvent;
97
+ export type ScrubResult = {
98
+ /** The directory scrub targeted (may not have existed). */
99
+ target: string;
100
+ /** Every file actually removed, path relative to `target`, in no particular order. */
101
+ removed: string[];
102
+ /** One-line human summary, safe to print as-is. */
103
+ message: string;
104
+ };
105
+ /** Delete one service's pulled data at rest: its event log, queued events,
106
+ * rebuilt state.json, and any resources/cursors/ingests sidecars — everything
107
+ * `worldPaths(service)` points at. Refuses (unless `force`) when the dir holds
108
+ * anything storage.ts didn't put there. */
109
+ export declare function scrubService(service: string, options?: {
110
+ root?: string;
111
+ force?: boolean;
112
+ }): ScrubResult;
113
+ /** Delete the ENTIRE world state dir (`<root>/<stateDir>/world`) — every
114
+ * service's pulled data at once. Refuses (unless `force`) when any entry in it
115
+ * isn't itself a recognizable per-service state dir. */
116
+ export declare function scrubWorld(options?: {
117
+ root?: string;
118
+ force?: boolean;
119
+ }): ScrubResult;