@amalgm/live 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 (159) hide show
  1. package/PURPOSE.md +103 -0
  2. package/README.md +45 -0
  3. package/dist/contracts/binding.d.ts +49 -0
  4. package/dist/contracts/binding.d.ts.map +1 -0
  5. package/dist/contracts/binding.js +23 -0
  6. package/dist/contracts/binding.js.map +1 -0
  7. package/dist/contracts/content.d.ts +42 -0
  8. package/dist/contracts/content.d.ts.map +1 -0
  9. package/dist/contracts/content.js +72 -0
  10. package/dist/contracts/content.js.map +1 -0
  11. package/dist/contracts/docs.d.ts +64 -0
  12. package/dist/contracts/docs.d.ts.map +1 -0
  13. package/dist/contracts/docs.js +51 -0
  14. package/dist/contracts/docs.js.map +1 -0
  15. package/dist/contracts/events.d.ts +45 -0
  16. package/dist/contracts/events.d.ts.map +1 -0
  17. package/dist/contracts/events.js +38 -0
  18. package/dist/contracts/events.js.map +1 -0
  19. package/dist/contracts/frames/gateway.d.ts +83 -0
  20. package/dist/contracts/frames/gateway.d.ts.map +1 -0
  21. package/dist/contracts/frames/gateway.js +35 -0
  22. package/dist/contracts/frames/gateway.js.map +1 -0
  23. package/dist/contracts/frames/stream.d.ts +59 -0
  24. package/dist/contracts/frames/stream.d.ts.map +1 -0
  25. package/dist/contracts/frames/stream.js +17 -0
  26. package/dist/contracts/frames/stream.js.map +1 -0
  27. package/dist/contracts/frames/tunnel.d.ts +78 -0
  28. package/dist/contracts/frames/tunnel.d.ts.map +1 -0
  29. package/dist/contracts/frames/tunnel.js +55 -0
  30. package/dist/contracts/frames/tunnel.js.map +1 -0
  31. package/dist/contracts/index.d.ts +19 -0
  32. package/dist/contracts/index.d.ts.map +1 -0
  33. package/dist/contracts/index.js +19 -0
  34. package/dist/contracts/index.js.map +1 -0
  35. package/dist/contracts/mutation.d.ts +51 -0
  36. package/dist/contracts/mutation.d.ts.map +1 -0
  37. package/dist/contracts/mutation.js +55 -0
  38. package/dist/contracts/mutation.js.map +1 -0
  39. package/dist/contracts/presence.d.ts +46 -0
  40. package/dist/contracts/presence.d.ts.map +1 -0
  41. package/dist/contracts/presence.js +34 -0
  42. package/dist/contracts/presence.js.map +1 -0
  43. package/dist/contracts/resources.d.ts +52 -0
  44. package/dist/contracts/resources.d.ts.map +1 -0
  45. package/dist/contracts/resources.js +105 -0
  46. package/dist/contracts/resources.js.map +1 -0
  47. package/dist/contracts/snapshot.d.ts +24 -0
  48. package/dist/contracts/snapshot.d.ts.map +1 -0
  49. package/dist/contracts/snapshot.js +28 -0
  50. package/dist/contracts/snapshot.js.map +1 -0
  51. package/dist/contracts/validate.d.ts +32 -0
  52. package/dist/contracts/validate.d.ts.map +1 -0
  53. package/dist/contracts/validate.js +25 -0
  54. package/dist/contracts/validate.js.map +1 -0
  55. package/dist/detection/adapter-contract.d.ts +134 -0
  56. package/dist/detection/adapter-contract.d.ts.map +1 -0
  57. package/dist/detection/adapter-contract.js +162 -0
  58. package/dist/detection/adapter-contract.js.map +1 -0
  59. package/dist/detection/classify.d.ts +31 -0
  60. package/dist/detection/classify.d.ts.map +1 -0
  61. package/dist/detection/classify.js +28 -0
  62. package/dist/detection/classify.js.map +1 -0
  63. package/dist/detection/continuity.d.ts +54 -0
  64. package/dist/detection/continuity.d.ts.map +1 -0
  65. package/dist/detection/continuity.js +85 -0
  66. package/dist/detection/continuity.js.map +1 -0
  67. package/dist/detection/coverage.d.ts +43 -0
  68. package/dist/detection/coverage.d.ts.map +1 -0
  69. package/dist/detection/coverage.js +60 -0
  70. package/dist/detection/coverage.js.map +1 -0
  71. package/dist/detection/diff.d.ts +144 -0
  72. package/dist/detection/diff.d.ts.map +1 -0
  73. package/dist/detection/diff.js +187 -0
  74. package/dist/detection/diff.js.map +1 -0
  75. package/dist/detection/enrollment.d.ts +16 -0
  76. package/dist/detection/enrollment.d.ts.map +1 -0
  77. package/dist/detection/enrollment.js +70 -0
  78. package/dist/detection/enrollment.js.map +1 -0
  79. package/dist/detection/index.d.ts +29 -0
  80. package/dist/detection/index.d.ts.map +1 -0
  81. package/dist/detection/index.js +29 -0
  82. package/dist/detection/index.js.map +1 -0
  83. package/dist/detection/refusal.d.ts +33 -0
  84. package/dist/detection/refusal.d.ts.map +1 -0
  85. package/dist/detection/refusal.js +40 -0
  86. package/dist/detection/refusal.js.map +1 -0
  87. package/dist/entities/bytes.d.ts +18 -0
  88. package/dist/entities/bytes.d.ts.map +1 -0
  89. package/dist/entities/bytes.js +61 -0
  90. package/dist/entities/bytes.js.map +1 -0
  91. package/dist/entities/cloud.d.ts +82 -0
  92. package/dist/entities/cloud.d.ts.map +1 -0
  93. package/dist/entities/cloud.js +136 -0
  94. package/dist/entities/cloud.js.map +1 -0
  95. package/dist/entities/index.d.ts +23 -0
  96. package/dist/entities/index.d.ts.map +1 -0
  97. package/dist/entities/index.js +23 -0
  98. package/dist/entities/index.js.map +1 -0
  99. package/dist/entities/moves.d.ts +42 -0
  100. package/dist/entities/moves.d.ts.map +1 -0
  101. package/dist/entities/moves.js +82 -0
  102. package/dist/entities/moves.js.map +1 -0
  103. package/dist/entities/repo-states.d.ts +121 -0
  104. package/dist/entities/repo-states.d.ts.map +1 -0
  105. package/dist/entities/repo-states.js +127 -0
  106. package/dist/entities/repo-states.js.map +1 -0
  107. package/dist/entities/rules.d.ts +104 -0
  108. package/dist/entities/rules.d.ts.map +1 -0
  109. package/dist/entities/rules.js +200 -0
  110. package/dist/entities/rules.js.map +1 -0
  111. package/dist/entities/types.d.ts +94 -0
  112. package/dist/entities/types.d.ts.map +1 -0
  113. package/dist/entities/types.js +40 -0
  114. package/dist/entities/types.js.map +1 -0
  115. package/dist/index.d.ts +17 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +17 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/machines/arbitration.d.ts +36 -0
  120. package/dist/machines/arbitration.d.ts.map +1 -0
  121. package/dist/machines/arbitration.js +37 -0
  122. package/dist/machines/arbitration.js.map +1 -0
  123. package/dist/machines/backoff.d.ts +36 -0
  124. package/dist/machines/backoff.d.ts.map +1 -0
  125. package/dist/machines/backoff.js +34 -0
  126. package/dist/machines/backoff.js.map +1 -0
  127. package/dist/machines/bootstrap.d.ts +65 -0
  128. package/dist/machines/bootstrap.d.ts.map +1 -0
  129. package/dist/machines/bootstrap.js +105 -0
  130. package/dist/machines/bootstrap.js.map +1 -0
  131. package/dist/machines/chunks.d.ts +51 -0
  132. package/dist/machines/chunks.d.ts.map +1 -0
  133. package/dist/machines/chunks.js +67 -0
  134. package/dist/machines/chunks.js.map +1 -0
  135. package/dist/machines/index.d.ts +14 -0
  136. package/dist/machines/index.d.ts.map +1 -0
  137. package/dist/machines/index.js +14 -0
  138. package/dist/machines/index.js.map +1 -0
  139. package/dist/machines/journal.d.ts +63 -0
  140. package/dist/machines/journal.d.ts.map +1 -0
  141. package/dist/machines/journal.js +71 -0
  142. package/dist/machines/journal.js.map +1 -0
  143. package/dist/machines/ordering.d.ts +53 -0
  144. package/dist/machines/ordering.d.ts.map +1 -0
  145. package/dist/machines/ordering.js +87 -0
  146. package/dist/machines/ordering.js.map +1 -0
  147. package/dist/machines/presence.d.ts +26 -0
  148. package/dist/machines/presence.d.ts.map +1 -0
  149. package/dist/machines/presence.js +49 -0
  150. package/dist/machines/presence.js.map +1 -0
  151. package/dist/merge/index.d.ts +9 -0
  152. package/dist/merge/index.d.ts.map +1 -0
  153. package/dist/merge/index.js +9 -0
  154. package/dist/merge/index.js.map +1 -0
  155. package/dist/merge/merge3.d.ts +39 -0
  156. package/dist/merge/merge3.d.ts.map +1 -0
  157. package/dist/merge/merge3.js +188 -0
  158. package/dist/merge/merge3.js.map +1 -0
  159. package/package.json +48 -0
package/PURPOSE.md ADDED
@@ -0,0 +1,103 @@
1
+ # @amalgm/live
2
+
3
+ ## Purpose
4
+
5
+ `@amalgm/live` is the portable realtime SDK for the Amalgm platform: one
6
+ library that owns every **wire contract** and every **delivery state machine**
7
+ for synchronizing entities, documents, and presence between a machine and the
8
+ cloud authority. A CLI, desktop app, gateway, service, or third-party
9
+ JavaScript host consumes it without inheriting a filesystem watcher, a SQLite
10
+ database, or a user-interface dependency.
11
+
12
+ The public developer model is `live.bind(surface, channel)` and is specified
13
+ in `docs/realtime-sdk.md`. The shipped wire behavior this package encodes is
14
+ `docs/local-live-protocol.md`. The journal-first write discipline is
15
+ `docs/cloud-mutation-discipline.md`. This repository is where those documents
16
+ become executable and testable; the engine repo keeps read-only copies until
17
+ its cutover.
18
+
19
+ ## Axioms
20
+
21
+ 1. **The SDK owns portable live contracts, not a machine.** Entity-record
22
+ shapes, resource identifiers, content manifests, checksums, frame shapes,
23
+ mutation envelopes, and ordering rules belong here; paths, inodes,
24
+ watchers, services, and local database files do not.
25
+ 2. **A host supplies its own effects.** The SDK receives transport, durable
26
+ journal, content-store, clock, and cryptography adapters. It never reaches
27
+ into Node globals, user homes, or a product-specific HTTP server to find
28
+ them. Nothing in `src/` may import `node:fs`, `node:net`, or spawn a
29
+ process.
30
+ 3. **There is one definition of every wire rule.** Client, runtime, and
31
+ gateway must share the same versioned schemas and integrity checks. A
32
+ second copy of a protocol or manifest validator is a bug, not a
33
+ compatibility layer.
34
+ 4. **Protocol evolution is additive only.** Every peer treats missing fields
35
+ and unknown frame types as "the other end is older" and keeps the legacy
36
+ behavior. A change that breaks a deployed v1 peer is rejected in review.
37
+ 5. **One edit, one mutation identity.** A surface edit creates exactly one
38
+ mutation id, stable across every retry, deduplicated by
39
+ `(resourceId, mutationId)` at every door. Transport packing never merges
40
+ mutation identities.
41
+ 6. **Journal first, materialize second.** A durable mutation is journaled
42
+ before local state changes, and that same journal row is the cloud outbox
43
+ record. There is no second outgoing table and no contract-specific retry
44
+ loop.
45
+ 7. **Lifecycle state is honest.** A socket being open is never `live`.
46
+ `live` means snapshot applied and ordered delivery confirmed; degraded and
47
+ reconnecting states are reported as themselves.
48
+ 8. **Never a silent winner.** Any merge path that durably discards text
49
+ preserves the losing side and says so on the event that carried the
50
+ decision.
51
+ 9. **The current runtime remains the single production source until moved.**
52
+ This package extracts contracts and machines with their tests; it does not
53
+ create a parallel execution path. The runtime and gateway adopt it module
54
+ by module, deleting their duplicated copy in the same change.
55
+ 10. **Amalgm's own surfaces use this SDK.** A private shortcut in the Amalgm
56
+ UI or runtime means the SDK is incomplete — fix the SDK, not the caller.
57
+
58
+ ## Five surfaces, one behavior
59
+
60
+ ```
61
+ src/ the SDK — the behavior. Contracts + machines + adapter ports.
62
+ surfaces/api REST router factory a host mounts (diagnose/validate routes).
63
+ surfaces/cli `amalgm-live` — inspect frames, derive ids, simulate merges.
64
+ surfaces/mcp MCP server exposing the same actions to agents.
65
+ skills/live the skill: teaches an agent this protocol via the CLI/MCP.
66
+ ```
67
+
68
+ Every surface is a thin adapter over `src/`. A surface that implements
69
+ behavior instead of delegating to the SDK is a bug (axiom 3).
70
+
71
+ ## Layout
72
+
73
+ ```
74
+ src/contracts/ value types + validators: events, snapshots, resource ids,
75
+ mutation envelopes, content manifests, frames (SSE /
76
+ gateway socket / tunnel), doc-write merge outcomes,
77
+ presence.
78
+ src/machines/ transport-neutral state machines: sequence ordering + gap
79
+ recovery, bootstrap barrier, mutation journal, presence
80
+ TTL, slot arbitration, chunk reassembly, backoff.
81
+ src/entities/ the entity model's rule surface: the six-field record,
82
+ derived versions, lifecycle laws, move planning, the
83
+ cloud and repo-state codecs — behind injected ports.
84
+ src/detection/ the Detect/Watch pure cores: the adapter contract, the
85
+ three-channel compare (diff) core, coverage, continuity
86
+ verdicts, classification, refusals, enrollment policy.
87
+ src/adapters/ the ports a host injects: transport, journal store,
88
+ content store, clock, crypto, logger.
89
+ tests/ unit tests per module, in the engine wall's style: each
90
+ exam states a law; mutants convict.
91
+ tests/parity/ golden-vector harness: the same inputs run against this
92
+ package and the shipped runtime implementation must produce
93
+ identical outputs.
94
+ tests/conformance/ the 10-rule suite from cloud-mutation-discipline.md,
95
+ written against the public SDK API only.
96
+ ```
97
+
98
+ ## Status
99
+
100
+ Extraction stage 1–2 (contracts and machines, per
101
+ `docs/extraction-boundary.md`). The runtime and gateway do not consume this
102
+ package yet; parity tests pin behavior until they do. Until both hosts
103
+ consume it, the engine runtime remains the single production source.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # amalgm-live
2
+
3
+ The portable realtime SDK for the Amalgm platform.
4
+
5
+ One library owns every wire contract and every delivery state machine for
6
+ synchronizing entities, documents, and presence between a machine and the
7
+ cloud authority — behind adapters the host injects. No filesystem, no
8
+ SQLite, no UI dependency.
9
+
10
+ ```ts
11
+ const binding = live.bind(surface, channel)
12
+ ```
13
+
14
+ Start with [PURPOSE.md](PURPOSE.md) — purpose, axioms, and layout — and
15
+ [ROADMAP.md](ROADMAP.md) for what moves here next. The
16
+ normative specifications live in [docs/](docs/):
17
+
18
+ | document | owns |
19
+ | --- | --- |
20
+ | [realtime-sdk.md](docs/realtime-sdk.md) | the public `bind(surface, channel)` model |
21
+ | [local-live-protocol.md](docs/local-live-protocol.md) | the shipped local wire protocol (v1/v2) |
22
+ | [cloud-mutation-discipline.md](docs/cloud-mutation-discipline.md) | the journal-first write discipline and conformance rules |
23
+ | [shared-realtime-wave-3.md](docs/shared-realtime-wave-3.md) | the multi-user cloud-authority design |
24
+ | [extraction-boundary.md](docs/extraction-boundary.md) | what moves here from the engine runtime, and in what order |
25
+
26
+ ## Surfaces
27
+
28
+ | surface | entry | what it is |
29
+ | --- | --- | --- |
30
+ | SDK | `src/` → `@amalgm/live` | the behavior: contracts, machines, adapter ports |
31
+ | API | `surfaces/api/` | REST router factory a host mounts |
32
+ | CLI | `surfaces/cli/` | `amalgm-live` — inspect, derive, validate, simulate |
33
+ | MCP | `surfaces/mcp/` | the same actions, exposed to agents |
34
+ | Skill | `skills/live/` | teaches an agent this protocol via the CLI/MCP |
35
+
36
+ Every surface delegates to `src/`. One behavior, five doors.
37
+
38
+ ## Develop
39
+
40
+ ```bash
41
+ npm install
42
+ npm test # build + unit tests
43
+ npm run test:parity # golden vectors vs the shipped engine runtime
44
+ npm run test:conformance
45
+ ```
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The binding — `live.bind(surface, channel)` (realtime-sdk.md).
3
+ *
4
+ * The surface knows what the user is looking at and what counts as one
5
+ * edit. The channel identifies the shared thing and its delivery class.
6
+ * The binding moves changes both ways and exposes honest lifecycle state:
7
+ * a socket being open is never `live` (axiom 7).
8
+ */
9
+ /** Delivery classes (realtime-sdk.md §6). Media setup shares the binding; media packets never ride the durable log. */
10
+ export type DeliveryClass = 'durable' | 'ephemeral' | 'media';
11
+ /** A channel is a stable reference, not a socket. Authority and permissions come from the authenticated resource record — never from client fields. */
12
+ export interface LiveChannelRef {
13
+ readonly id: string;
14
+ /** Must match the surface contract, e.g. "text-yjs@1". */
15
+ readonly contract: string;
16
+ readonly delivery: DeliveryClass;
17
+ }
18
+ /**
19
+ * Connection lifecycle vocabulary (realtime-sdk.md §7). `live` means the
20
+ * snapshot is applied AND ordered delivery is confirmed; `degraded` means
21
+ * delivery works through a slower fallback (e.g. gateway poll).
22
+ */
23
+ export declare const BINDING_STATES: readonly ["connecting", "hydrating", "live", "degraded", "reconnecting", "blocked", "revoked"];
24
+ export type BindingState = (typeof BINDING_STATES)[number];
25
+ export interface ApplyContext {
26
+ /** True while a snapshot install is in progress; surfaces must not echo. */
27
+ readonly hydrating: boolean;
28
+ }
29
+ export interface SnapshotContext {
30
+ /** Why the snapshot is being installed: first hydration or gap recovery. */
31
+ readonly reason: 'hydration' | 'recovery';
32
+ }
33
+ /**
34
+ * A surface is an adapter over a real interactive UI — not a React
35
+ * component type, not a transport client (realtime-sdk.md §1). It defines
36
+ * the edit boundary, emits exactly one operation per edit, applies remote
37
+ * operations in place, and never echoes a remotely applied operation back
38
+ * as a new local edit.
39
+ */
40
+ export interface LiveSurface<Snapshot, Operation, Presence = never> {
41
+ readonly contract: string;
42
+ observeEdits(emit: (operation: Operation) => void): () => void;
43
+ applyRemote(operation: Operation, context: ApplyContext): void;
44
+ /** Initial hydration or recovery only — never the normal response to a remote edit. */
45
+ applySnapshot(snapshot: Snapshot, context: SnapshotContext): void;
46
+ observePresence?(emit: (presence: Presence | null) => void): () => void;
47
+ applyPresence?(participantId: string, presence: Presence | null): void;
48
+ }
49
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/contracts/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uHAAuH;AACvH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;AAE9D,uJAAuJ;AACvJ,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,gGAQjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAAG,KAAK;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,YAAY,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC/D,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/D,uFAAuF;IACvF,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAElE,eAAe,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,aAAa,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;CACxE"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The binding — `live.bind(surface, channel)` (realtime-sdk.md).
3
+ *
4
+ * The surface knows what the user is looking at and what counts as one
5
+ * edit. The channel identifies the shared thing and its delivery class.
6
+ * The binding moves changes both ways and exposes honest lifecycle state:
7
+ * a socket being open is never `live` (axiom 7).
8
+ */
9
+ /**
10
+ * Connection lifecycle vocabulary (realtime-sdk.md §7). `live` means the
11
+ * snapshot is applied AND ordered delivery is confirmed; `degraded` means
12
+ * delivery works through a slower fallback (e.g. gateway poll).
13
+ */
14
+ export const BINDING_STATES = [
15
+ 'connecting',
16
+ 'hydrating',
17
+ 'live',
18
+ 'degraded',
19
+ 'reconnecting',
20
+ 'blocked',
21
+ 'revoked',
22
+ ];
23
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/contracts/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,YAAY;IACZ,WAAW;IACX,MAAM;IACN,UAAU;IACV,cAAc;IACd,SAAS;IACT,SAAS;CACD,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Content manifests — the wire shape of immutable entity bytes.
3
+ *
4
+ * A file's content travels as bounded, content-addressed chunks named by a
5
+ * manifest (engine registration/entity-content.js, contract
6
+ * `amalgm-entity-content@1`). Every writer derives the same manifest from
7
+ * the same bytes, so manifests are idempotent facts, never negotiations.
8
+ *
9
+ * The chunk rule is `bytes <= CHUNK_BYTES`, never an exact size — older,
10
+ * smaller chunks remain valid forever. Capture, storage, and upload are
11
+ * host effects; this file owns the shape and its laws.
12
+ */
13
+ import { type Checked } from './validate.js';
14
+ export declare const CONTENT_CONTRACT = "amalgm-entity-content@1";
15
+ /** One chunk is one WAN round trip; the gateway accepts parts up to this same bound. */
16
+ export declare const CHUNK_BYTES: number;
17
+ export interface ContentChunk {
18
+ readonly sha256: string;
19
+ readonly bytes: number;
20
+ }
21
+ export interface ContentManifest {
22
+ readonly contract: typeof CONTENT_CONTRACT;
23
+ readonly contentHash: string;
24
+ readonly bytes: number;
25
+ readonly chunks: readonly ContentChunk[];
26
+ }
27
+ /**
28
+ * Validate and normalize a manifest. The laws, in conviction order:
29
+ * well-formed hashes; every chunk within the bound; chunk bytes sum to the
30
+ * total; empty content has no chunks. Error phrases match the engine's —
31
+ * they are the ratified refusal text.
32
+ */
33
+ export declare function checkContentManifest(input: unknown, expectedHash?: string | null): Checked<ContentManifest>;
34
+ /**
35
+ * Derive the manifest for already-chunked bytes. The host reads and hashes
36
+ * (sha256 hex of each part and of the whole); this seals the shape so a
37
+ * hand-built manifest cannot drift from the derivation rule.
38
+ */
39
+ export declare function manifestFromChunks(contentHash: string, chunks: readonly ContentChunk[]): Checked<ContentManifest>;
40
+ /** Split a byte count into the chunk sizes capture will produce: full chunks, then the remainder. Zero bytes → zero chunks. */
41
+ export declare function chunkSizes(totalBytes: number): number[];
42
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/contracts/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,eAAe,CAAC;AAEnE,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,wFAAwF;AACxF,eAAO,MAAM,WAAW,QAAkB,CAAC;AAI3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,gBAAgB,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAkCjH;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,YAAY,EAAE,GAC9B,OAAO,CAAC,eAAe,CAAC,CAK1B;AAED,+HAA+H;AAC/H,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvD"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Content manifests — the wire shape of immutable entity bytes.
3
+ *
4
+ * A file's content travels as bounded, content-addressed chunks named by a
5
+ * manifest (engine registration/entity-content.js, contract
6
+ * `amalgm-entity-content@1`). Every writer derives the same manifest from
7
+ * the same bytes, so manifests are idempotent facts, never negotiations.
8
+ *
9
+ * The chunk rule is `bytes <= CHUNK_BYTES`, never an exact size — older,
10
+ * smaller chunks remain valid forever. Capture, storage, and upload are
11
+ * host effects; this file owns the shape and its laws.
12
+ */
13
+ import { pass, fail, isRecord } from './validate.js';
14
+ export const CONTENT_CONTRACT = 'amalgm-entity-content@1';
15
+ /** One chunk is one WAN round trip; the gateway accepts parts up to this same bound. */
16
+ export const CHUNK_BYTES = 4 * 1024 * 1024;
17
+ const HASH = /^[0-9a-f]{64}$/;
18
+ /**
19
+ * Validate and normalize a manifest. The laws, in conviction order:
20
+ * well-formed hashes; every chunk within the bound; chunk bytes sum to the
21
+ * total; empty content has no chunks. Error phrases match the engine's —
22
+ * they are the ratified refusal text.
23
+ */
24
+ export function checkContentManifest(input, expectedHash = null) {
25
+ if (!isRecord(input) ||
26
+ typeof input.contentHash !== 'string' ||
27
+ !HASH.test(input.contentHash) ||
28
+ (expectedHash !== null && input.contentHash !== expectedHash) ||
29
+ !Number.isSafeInteger(input.bytes) ||
30
+ input.bytes < 0 ||
31
+ !Array.isArray(input.chunks) ||
32
+ input.chunks.some((chunk) => !isRecord(chunk) ||
33
+ !HASH.test(String(chunk.sha256 ?? '')) ||
34
+ !Number.isSafeInteger(chunk.bytes) ||
35
+ chunk.bytes < 1 ||
36
+ chunk.bytes > CHUNK_BYTES)) {
37
+ return fail('entity content manifest is invalid');
38
+ }
39
+ const bytes = input.bytes;
40
+ const chunks = input.chunks;
41
+ if (chunks.reduce((total, chunk) => total + chunk.bytes, 0) !== bytes) {
42
+ return fail('entity content manifest byte count is invalid');
43
+ }
44
+ if (bytes === 0 && chunks.length !== 0) {
45
+ return fail('empty entity content must have no chunks');
46
+ }
47
+ return pass({
48
+ contract: CONTENT_CONTRACT,
49
+ contentHash: input.contentHash,
50
+ bytes,
51
+ chunks: chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
52
+ });
53
+ }
54
+ /**
55
+ * Derive the manifest for already-chunked bytes. The host reads and hashes
56
+ * (sha256 hex of each part and of the whole); this seals the shape so a
57
+ * hand-built manifest cannot drift from the derivation rule.
58
+ */
59
+ export function manifestFromChunks(contentHash, chunks) {
60
+ return checkContentManifest({ contract: CONTENT_CONTRACT, contentHash, bytes: chunks.reduce((t, c) => t + c.bytes, 0), chunks }, contentHash);
61
+ }
62
+ /** Split a byte count into the chunk sizes capture will produce: full chunks, then the remainder. Zero bytes → zero chunks. */
63
+ export function chunkSizes(totalBytes) {
64
+ if (!Number.isSafeInteger(totalBytes) || totalBytes < 0)
65
+ return [];
66
+ const sizes = [];
67
+ for (let remaining = totalBytes; remaining > 0; remaining -= CHUNK_BYTES) {
68
+ sizes.push(Math.min(remaining, CHUNK_BYTES));
69
+ }
70
+ return sizes;
71
+ }
72
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/contracts/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Versioned document writes — the contract for whole-text writers.
3
+ *
4
+ * `POST /state/docs/write` (local-live-protocol.md §8) exists so a writer
5
+ * that read a file, thought, and wrote back does not clobber live edits
6
+ * that landed in between. `baseHash` is the lowercase sha256 hex of the
7
+ * EXACT text the writer read; every response's `sha256` chains into the
8
+ * next write.
9
+ *
10
+ * The governing invariant — never a silent winner: any path that durably
11
+ * discards text preserves it in a sidecar first and surfaces the decision
12
+ * on the doc event.
13
+ */
14
+ import { type Checked } from './validate.js';
15
+ /** Text over this limit is refused with 413. */
16
+ export declare const MAX_DOC_BYTES: number;
17
+ /** Bases resolve against a ring of recent disk-sync states, per doc. */
18
+ export declare const RETENTION_RING: {
19
+ readonly states: 8;
20
+ readonly maxTotalBytes: number;
21
+ readonly maxAgeMs: number;
22
+ };
23
+ export interface DocWriteRequest {
24
+ readonly path: string;
25
+ /** Full new file contents — this is a whole-text contract. */
26
+ readonly text: string;
27
+ /** sha256 hex of the exact text the writer read. Prefer this over baseSeq. */
28
+ readonly baseHash?: string;
29
+ /** Exact-match lookup hint, consulted only when no baseHash was sent. */
30
+ readonly baseSeq?: number;
31
+ }
32
+ /**
33
+ * Every outcome states what happens to the losing text in the same breath.
34
+ *
35
+ * | outcome | when | losing text |
36
+ * |----------------|----------------------------------------|------------------------|
37
+ * | `noop` | incoming === live | none exists |
38
+ * | `clean` | base === live | none exists |
39
+ * | `merged` | base in retention, no overlapping hunks| both sides land |
40
+ * | `conflict` | overlapping hunks | full incoming → sidecar|
41
+ * | `base-unknown` | base outside the retention window | live text → sidecar |
42
+ * | `overwrite` | no base sent | replaced text → sidecar unless nothing was deleted |
43
+ */
44
+ export declare const MERGE_OUTCOMES: readonly ["noop", "clean", "merged", "conflict", "base-unknown", "overwrite"];
45
+ export type MergeOutcome = (typeof MERGE_OUTCOMES)[number];
46
+ export declare const CONFLICT_REASONS: readonly ["overlapping-hunks", "unversioned-overwrite", "base-unknown"];
47
+ export type ConflictReason = (typeof CONFLICT_REASONS)[number];
48
+ /** Travels on the write response and on the doc event (`patch.conflict`, source `doc:write`). */
49
+ export interface DocConflict {
50
+ readonly reason: ConflictReason;
51
+ readonly sidecarPath: string;
52
+ readonly hunks?: number;
53
+ readonly at: number;
54
+ }
55
+ export interface DocWriteResponse {
56
+ readonly merge: MergeOutcome;
57
+ readonly seq: number;
58
+ /** Hash of the resulting text — hold it as the next write's baseHash. */
59
+ readonly sha256: string;
60
+ readonly conflict: DocConflict | null;
61
+ }
62
+ export declare const isSha256Hex: (value: unknown) => value is string;
63
+ export declare function checkDocWriteRequest(input: unknown): Checked<DocWriteRequest>;
64
+ //# sourceMappingURL=docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/contracts/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAsD,MAAM,eAAe,CAAC;AAEjG,gDAAgD;AAChD,eAAO,MAAM,aAAa,QAAkB,CAAC;AAE7C,wEAAwE;AACxE,eAAO,MAAM,cAAc;;;;CAAgF,CAAC;AAE5G,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,+EAAgF,CAAC;AAC5G,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AACxG,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;CACvC;AAID,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACK,CAAC;AAE5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7E"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Versioned document writes — the contract for whole-text writers.
3
+ *
4
+ * `POST /state/docs/write` (local-live-protocol.md §8) exists so a writer
5
+ * that read a file, thought, and wrote back does not clobber live edits
6
+ * that landed in between. `baseHash` is the lowercase sha256 hex of the
7
+ * EXACT text the writer read; every response's `sha256` chains into the
8
+ * next write.
9
+ *
10
+ * The governing invariant — never a silent winner: any path that durably
11
+ * discards text preserves it in a sidecar first and surfaces the decision
12
+ * on the doc event.
13
+ */
14
+ import { pass, failShape, isRecord, isNonEmptyString, isSeq } from './validate.js';
15
+ /** Text over this limit is refused with 413. */
16
+ export const MAX_DOC_BYTES = 2 * 1024 * 1024;
17
+ /** Bases resolve against a ring of recent disk-sync states, per doc. */
18
+ export const RETENTION_RING = { states: 8, maxTotalBytes: 4 * 1024 * 1024, maxAgeMs: 10 * 60_000 };
19
+ /**
20
+ * Every outcome states what happens to the losing text in the same breath.
21
+ *
22
+ * | outcome | when | losing text |
23
+ * |----------------|----------------------------------------|------------------------|
24
+ * | `noop` | incoming === live | none exists |
25
+ * | `clean` | base === live | none exists |
26
+ * | `merged` | base in retention, no overlapping hunks| both sides land |
27
+ * | `conflict` | overlapping hunks | full incoming → sidecar|
28
+ * | `base-unknown` | base outside the retention window | live text → sidecar |
29
+ * | `overwrite` | no base sent | replaced text → sidecar unless nothing was deleted |
30
+ */
31
+ export const MERGE_OUTCOMES = ['noop', 'clean', 'merged', 'conflict', 'base-unknown', 'overwrite'];
32
+ export const CONFLICT_REASONS = ['overlapping-hunks', 'unversioned-overwrite', 'base-unknown'];
33
+ const REQUEST_EXAMPLE = '{ path: "notes.md", text: "full new contents", baseHash: "9f86d0…" }';
34
+ export const isSha256Hex = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
35
+ export function checkDocWriteRequest(input) {
36
+ if (!isRecord(input))
37
+ return failShape(REQUEST_EXAMPLE, `got ${input === null ? 'null' : typeof input}`);
38
+ if (!isNonEmptyString(input.path)) {
39
+ return failShape(REQUEST_EXAMPLE, `path was ${JSON.stringify(input.path)}, not a non-empty string`);
40
+ }
41
+ if (typeof input.text !== 'string')
42
+ return failShape(REQUEST_EXAMPLE, 'text was missing — this is a whole-text write');
43
+ if (input.baseHash !== undefined && !isSha256Hex(input.baseHash)) {
44
+ return failShape(REQUEST_EXAMPLE, `baseHash was ${JSON.stringify(input.baseHash)}, not lowercase sha256 hex`);
45
+ }
46
+ if (input.baseSeq !== undefined && !isSeq(input.baseSeq)) {
47
+ return failShape(REQUEST_EXAMPLE, `baseSeq was ${JSON.stringify(input.baseSeq)}, not a non-negative integer`);
48
+ }
49
+ return pass(input);
50
+ }
51
+ //# sourceMappingURL=docs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/contracts/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAgB,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEjG,gDAAgD;AAChD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE7C,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,EAAW,CAAC;AAY5G;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,CAAU,CAAC;AAG5G,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,mBAAmB,EAAE,uBAAuB,EAAE,cAAc,CAAU,CAAC;AAmBxG,MAAM,eAAe,GAAG,sEAAsE,CAAC;AAE/F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAc,EAAmB,EAAE,CAC7D,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAE5D,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC,eAAe,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;IACzG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAC,eAAe,EAAE,YAAY,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACtG,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,eAAe,EAAE,+CAA+C,CAAC,CAAC;IACvH,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjE,OAAO,SAAS,CAAC,eAAe,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,4BAA4B,CAAC,CAAC;IAChH,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,OAAO,SAAS,CAAC,eAAe,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAChH,CAAC;IACD,OAAO,IAAI,CAAC,KAAmC,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * State events — the unit of change on the local wire.
3
+ *
4
+ * Every change is an event (local-live-protocol.md §1). Tables and disk are
5
+ * truth; the event log is a bounded (~15 min) delivery buffer, so a client
6
+ * that falls off the buffer re-snapshots rather than asking for history.
7
+ *
8
+ * `seq` is a strictly increasing integer per runtime. The two optional
9
+ * Wave 3 fields (`mutationId`, `sharedResourceId`) are additive: legacy
10
+ * rows omit both keys exactly as before, and validation here accepts both
11
+ * generations (axiom: additive-only evolution).
12
+ */
13
+ import { type Checked } from './validate.js';
14
+ import type { ResourceName } from './resources.js';
15
+ /** The shipped wire operations. `move`/`delta` (realtime-sdk.md §3) ride as contract-specific patches until a wire rev adds them. */
16
+ export declare const WIRE_OPS: readonly ["insert", "update", "delete", "replace", "invalidate"];
17
+ export type WireOp = (typeof WIRE_OPS)[number];
18
+ export interface StateEvent {
19
+ readonly seq: number;
20
+ readonly ts: number;
21
+ readonly resource: ResourceName;
22
+ readonly op: WireOp;
23
+ /** Addressed item within the resource, when the op targets one. */
24
+ readonly id?: string;
25
+ readonly value?: unknown;
26
+ readonly patch?: unknown;
27
+ /** Provenance, e.g. 'doc:write'. */
28
+ readonly source?: string;
29
+ /** Echo of the client-supplied mutation id, for optimistic reconciliation. */
30
+ readonly clientMutationId?: string;
31
+ readonly version?: number;
32
+ /** Wave 3, additive: durable mutation identity. */
33
+ readonly mutationId?: string;
34
+ /** Wave 3, additive: the cloud id of the shared resource this event belongs to. */
35
+ readonly sharedResourceId?: string;
36
+ }
37
+ export declare const isWireOp: (value: unknown) => value is WireOp;
38
+ /**
39
+ * Validate one wire event. Unknown extra keys are preserved untouched —
40
+ * a newer peer may be speaking an additive extension we do not know yet.
41
+ */
42
+ export declare function checkStateEvent(input: unknown): Checked<StateEvent>;
43
+ /** Events apply in seq order; equal or older seq means already applied (client obligation, protocol §6). */
44
+ export declare const isNewerThan: (event: StateEvent, appliedSeq: number) => boolean;
45
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/contracts/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAsD,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,qIAAqI;AACrI,eAAO,MAAM,QAAQ,kEAAmE,CAAC;AACzF,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAID,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAC2B,CAAC;AAE/E;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CASnE;AAED,4GAA4G;AAC5G,eAAO,MAAM,WAAW,GAAI,OAAO,UAAU,EAAE,YAAY,MAAM,KAAG,OAAiC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * State events — the unit of change on the local wire.
3
+ *
4
+ * Every change is an event (local-live-protocol.md §1). Tables and disk are
5
+ * truth; the event log is a bounded (~15 min) delivery buffer, so a client
6
+ * that falls off the buffer re-snapshots rather than asking for history.
7
+ *
8
+ * `seq` is a strictly increasing integer per runtime. The two optional
9
+ * Wave 3 fields (`mutationId`, `sharedResourceId`) are additive: legacy
10
+ * rows omit both keys exactly as before, and validation here accepts both
11
+ * generations (axiom: additive-only evolution).
12
+ */
13
+ import { pass, failShape, isRecord, isNonEmptyString, isSeq } from './validate.js';
14
+ /** The shipped wire operations. `move`/`delta` (realtime-sdk.md §3) ride as contract-specific patches until a wire rev adds them. */
15
+ export const WIRE_OPS = ['insert', 'update', 'delete', 'replace', 'invalidate'];
16
+ const EXAMPLE = '{ seq: 42, ts: 1719000000000, resource: "tasks", op: "update", id: "t1", patch: {...} }';
17
+ export const isWireOp = (value) => typeof value === 'string' && WIRE_OPS.includes(value);
18
+ /**
19
+ * Validate one wire event. Unknown extra keys are preserved untouched —
20
+ * a newer peer may be speaking an additive extension we do not know yet.
21
+ */
22
+ export function checkStateEvent(input) {
23
+ if (!isRecord(input))
24
+ return failShape(EXAMPLE, `got ${input === null ? 'null' : typeof input}`);
25
+ if (!isSeq(input.seq))
26
+ return failShape(EXAMPLE, `seq was ${JSON.stringify(input.seq)}, not a non-negative integer`);
27
+ if (typeof input.ts !== 'number')
28
+ return failShape(EXAMPLE, `ts was ${JSON.stringify(input.ts)}, not a number`);
29
+ if (!isNonEmptyString(input.resource))
30
+ return failShape(EXAMPLE, `resource was ${JSON.stringify(input.resource)}, not a name`);
31
+ if (!isWireOp(input.op)) {
32
+ return failShape(EXAMPLE, `op was ${JSON.stringify(input.op)}, not one of ${WIRE_OPS.join('|')}`);
33
+ }
34
+ return pass(input);
35
+ }
36
+ /** Events apply in seq order; equal or older seq means already applied (client obligation, protocol §6). */
37
+ export const isNewerThan = (event, appliedSeq) => event.seq > appliedSeq;
38
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/contracts/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgB,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGjG,qIAAqI;AACrI,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAuBzF,MAAM,OAAO,GAAG,yFAAyF,CAAC;AAE1G,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAmB,EAAE,CAC1D,OAAO,KAAK,KAAK,QAAQ,IAAK,QAA8B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAE/E;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;IACjG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IACrH,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAChH,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC/H,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,IAAI,CAAC,KAA8B,CAAC,CAAC;AAC9C,CAAC;AAED,4GAA4G;AAC5G,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAiB,EAAE,UAAkB,EAAW,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC"}