@vibecook/ice 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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/chunk-5S7GSXJG.js +6003 -0
  4. package/dist/chunk-5S7GSXJG.js.map +1 -0
  5. package/dist/chunk-BCJ47SX7.js +550 -0
  6. package/dist/chunk-BCJ47SX7.js.map +1 -0
  7. package/dist/chunk-FIAXPNNY.js +1446 -0
  8. package/dist/chunk-FIAXPNNY.js.map +1 -0
  9. package/dist/devtools.js +417 -0
  10. package/dist/devtools.js.map +1 -0
  11. package/dist/dom.js +41 -0
  12. package/dist/dom.js.map +1 -0
  13. package/dist/index.js +450 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/kernel.js +63 -0
  16. package/dist/kernel.js.map +1 -0
  17. package/dist/r3f.js +1101 -0
  18. package/dist/r3f.js.map +1 -0
  19. package/dist/react.js +440 -0
  20. package/dist/react.js.map +1 -0
  21. package/dist/types/packages/core/src/boot/hmr.d.ts +12 -0
  22. package/dist/types/packages/core/src/catalog/camera-derived.d.ts +144 -0
  23. package/dist/types/packages/core/src/catalog/gesture.d.ts +254 -0
  24. package/dist/types/packages/core/src/catalog/graph.d.ts +55 -0
  25. package/dist/types/packages/core/src/catalog/index.d.ts +32 -0
  26. package/dist/types/packages/core/src/catalog/pointer.d.ts +111 -0
  27. package/dist/types/packages/core/src/catalog/scene.d.ts +34 -0
  28. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +53 -0
  29. package/dist/types/packages/core/src/catalog/settings-resources.d.ts +46 -0
  30. package/dist/types/packages/core/src/doc/autosave.d.ts +69 -0
  31. package/dist/types/packages/core/src/doc/bootstrap.d.ts +74 -0
  32. package/dist/types/packages/core/src/doc/broadcast-relay.d.ts +52 -0
  33. package/dist/types/packages/core/src/doc/channels.d.ts +49 -0
  34. package/dist/types/packages/core/src/doc/doc-commit-sink.d.ts +22 -0
  35. package/dist/types/packages/core/src/doc/doc-kit.d.ts +50 -0
  36. package/dist/types/packages/core/src/doc/envelope.d.ts +36 -0
  37. package/dist/types/packages/core/src/doc/migrate.d.ts +69 -0
  38. package/dist/types/packages/core/src/doc/version-gate.d.ts +49 -0
  39. package/dist/types/packages/core/src/engine/commit-sink.d.ts +61 -0
  40. package/dist/types/packages/core/src/engine/engine.d.ts +62 -0
  41. package/dist/types/packages/core/src/engine/frame-info.d.ts +16 -0
  42. package/dist/types/packages/core/src/engine/instantiate.d.ts +45 -0
  43. package/dist/types/packages/core/src/engine/pipeline.d.ts +17 -0
  44. package/dist/types/packages/core/src/engine/reflectors.d.ts +56 -0
  45. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +120 -0
  46. package/dist/types/packages/core/src/guards/dev.d.ts +2 -0
  47. package/dist/types/packages/core/src/guards/guarded-tx.d.ts +51 -0
  48. package/dist/types/packages/core/src/guards/live-writer.d.ts +33 -0
  49. package/dist/types/packages/core/src/guards/resource-writer.d.ts +17 -0
  50. package/dist/types/packages/core/src/helpers/phase-set.d.ts +70 -0
  51. package/dist/types/packages/core/src/helpers/version-stamps.d.ts +55 -0
  52. package/dist/types/packages/core/src/index.d.ts +63 -0
  53. package/dist/types/packages/core/src/input/measure-queue.d.ts +30 -0
  54. package/dist/types/packages/core/src/input/queue.d.ts +60 -0
  55. package/dist/types/packages/core/src/interaction/install.d.ts +58 -0
  56. package/dist/types/packages/core/src/nav/nested-canvas.d.ts +62 -0
  57. package/dist/types/packages/core/src/ops/cascade.d.ts +18 -0
  58. package/dist/types/packages/core/src/ops/claims.d.ts +15 -0
  59. package/dist/types/packages/core/src/ops/gestures.d.ts +10 -0
  60. package/dist/types/packages/core/src/ops/point-pick.d.ts +42 -0
  61. package/dist/types/packages/core/src/ops/port-geometry.d.ts +17 -0
  62. package/dist/types/packages/core/src/ops/selection.d.ts +14 -0
  63. package/dist/types/packages/core/src/presence/index.d.ts +7 -0
  64. package/dist/types/packages/core/src/presence/presence-kit.d.ts +39 -0
  65. package/dist/types/packages/core/src/presence/publish.d.ts +14 -0
  66. package/dist/types/packages/core/src/presence/remote-cursors.d.ts +18 -0
  67. package/dist/types/packages/core/src/schema/meta.d.ts +43 -0
  68. package/dist/types/packages/core/src/schema/prefab.d.ts +69 -0
  69. package/dist/types/packages/core/src/settings/defaults.d.ts +56 -0
  70. package/dist/types/packages/core/src/systems/camera-sim.d.ts +19 -0
  71. package/dist/types/packages/core/src/systems/chrome.d.ts +48 -0
  72. package/dist/types/packages/core/src/systems/cleanup.d.ts +19 -0
  73. package/dist/types/packages/core/src/systems/l0-input.d.ts +31 -0
  74. package/dist/types/packages/core/src/systems/l1-pick.d.ts +44 -0
  75. package/dist/types/packages/core/src/systems/l1-wires.d.ts +30 -0
  76. package/dist/types/packages/core/src/systems/l2-arbitrate.d.ts +40 -0
  77. package/dist/types/packages/core/src/systems/l2-recognize.d.ts +45 -0
  78. package/dist/types/packages/core/src/systems/l3-behave.d.ts +24 -0
  79. package/dist/types/packages/core/src/systems/l3-claim.d.ts +26 -0
  80. package/dist/types/packages/core/src/systems/l3-connect.d.ts +57 -0
  81. package/dist/types/packages/core/src/systems/l3-draw.d.ts +21 -0
  82. package/dist/types/packages/core/src/systems/l3-drop.d.ts +21 -0
  83. package/dist/types/packages/core/src/systems/l3-marquee.d.ts +35 -0
  84. package/dist/types/packages/core/src/systems/l3-ports.d.ts +30 -0
  85. package/dist/types/packages/core/src/systems/l3-resize.d.ts +17 -0
  86. package/dist/types/packages/core/src/systems/l3-snap.d.ts +19 -0
  87. package/dist/types/packages/core/src/systems/l4-cursor.d.ts +24 -0
  88. package/dist/types/packages/core/src/systems/measure-ingest.d.ts +26 -0
  89. package/dist/types/packages/core/src/tools/define-tool.d.ts +79 -0
  90. package/dist/types/packages/core/src/widget/define-widget.d.ts +124 -0
  91. package/dist/types/packages/core/src/widget/equip.d.ts +14 -0
  92. package/dist/types/packages/core/src/widget/mount-store.d.ts +47 -0
  93. package/dist/types/packages/core/src/widget/props.d.ts +98 -0
  94. package/dist/types/packages/core/src/widget/spawn.d.ts +30 -0
  95. package/dist/types/packages/devtools/src/index.d.ts +8 -0
  96. package/dist/types/packages/devtools/src/panel.d.ts +48 -0
  97. package/dist/types/packages/dom/src/host.d.ts +24 -0
  98. package/dist/types/packages/dom/src/index.d.ts +19 -0
  99. package/dist/types/packages/dom/src/loop.d.ts +11 -0
  100. package/dist/types/packages/dom/src/measure-adapter.d.ts +31 -0
  101. package/dist/types/packages/dom/src/measure-wiring.d.ts +49 -0
  102. package/dist/types/packages/dom/src/planes.d.ts +33 -0
  103. package/dist/types/packages/dom/src/pointer-adapter.d.ts +48 -0
  104. package/dist/types/packages/dom/src/reflectors/chrome.d.ts +33 -0
  105. package/dist/types/packages/dom/src/reflectors/cursor.d.ts +21 -0
  106. package/dist/types/packages/dom/src/reflectors/dom-widgets.d.ts +54 -0
  107. package/dist/types/packages/dom/src/reflectors/graybox.d.ts +31 -0
  108. package/dist/types/packages/dom/src/reflectors/grid.d.ts +63 -0
  109. package/dist/types/packages/dom/src/reflectors/plane-transform.d.ts +38 -0
  110. package/dist/types/packages/dom/src/reflectors/remote-cursors.d.ts +20 -0
  111. package/dist/types/packages/dom/src/reflectors/wires.d.ts +88 -0
  112. package/dist/types/packages/ice/src/devtools.d.ts +1 -0
  113. package/dist/types/packages/ice/src/dom.d.ts +1 -0
  114. package/dist/types/packages/ice/src/index.d.ts +1 -0
  115. package/dist/types/packages/ice/src/kernel.d.ts +1 -0
  116. package/dist/types/packages/ice/src/r3f.d.ts +1 -0
  117. package/dist/types/packages/ice/src/react.d.ts +1 -0
  118. package/dist/types/packages/kernel/src/anchors.d.ts +18 -0
  119. package/dist/types/packages/kernel/src/bezier.d.ts +38 -0
  120. package/dist/types/packages/kernel/src/coords.d.ts +67 -0
  121. package/dist/types/packages/kernel/src/eviction.d.ts +23 -0
  122. package/dist/types/packages/kernel/src/index.d.ts +13 -0
  123. package/dist/types/packages/kernel/src/shapes.d.ts +22 -0
  124. package/dist/types/packages/kernel/src/snap.d.ts +44 -0
  125. package/dist/types/packages/kernel/src/spatial-index.d.ts +16 -0
  126. package/dist/types/packages/kernel/src/zoom-bands.d.ts +19 -0
  127. package/dist/types/packages/r3f/src/bridge.d.ts +68 -0
  128. package/dist/types/packages/r3f/src/composite-material.d.ts +17 -0
  129. package/dist/types/packages/r3f/src/compositor-pass.d.ts +83 -0
  130. package/dist/types/packages/r3f/src/dev-write-trap.d.ts +30 -0
  131. package/dist/types/packages/r3f/src/gl-root.d.ts +21 -0
  132. package/dist/types/packages/r3f/src/gl-router.d.ts +60 -0
  133. package/dist/types/packages/r3f/src/index.d.ts +16 -0
  134. package/dist/types/packages/r3f/src/island-state.d.ts +68 -0
  135. package/dist/types/packages/r3f/src/island.d.ts +13 -0
  136. package/dist/types/packages/r3f/src/pool.d.ts +56 -0
  137. package/dist/types/packages/r3f/src/resource-registry.d.ts +30 -0
  138. package/dist/types/packages/r3f/src/use-island-frame.d.ts +24 -0
  139. package/dist/types/packages/react/src/engine-context.d.ts +31 -0
  140. package/dist/types/packages/react/src/hooks.d.ts +40 -0
  141. package/dist/types/packages/react/src/index.d.ts +14 -0
  142. package/dist/types/packages/react/src/infinite-canvas.d.ts +71 -0
  143. package/dist/types/packages/react/src/keymap.d.ts +42 -0
  144. package/dist/types/packages/react/src/use-commit.d.ts +35 -0
  145. package/dist/types/packages/react/src/use-presence.d.ts +23 -0
  146. package/dist/types/packages/react/src/use-tool.d.ts +4 -0
  147. package/dist/types/packages/react/src/widget-root.d.ts +33 -0
  148. package/package.json +112 -0
@@ -0,0 +1,53 @@
1
+ /** Runtime selection rider on widgets. */
2
+ export declare const Selected: import("@vibecook/strata-ecs").Tag;
3
+ export declare const Selectable: import("@vibecook/strata-ecs").Tag;
4
+ export declare const Movable: import("@vibecook/strata-ecs").Tag;
5
+ export declare const Resizable: import("@vibecook/strata-ecs").Tag;
6
+ export declare const SnapSource: import("@vibecook/strata-ecs").Tag;
7
+ export declare const SnapTarget: import("@vibecook/strata-ecs").Tag;
8
+ /**
9
+ * Marks an ephemeral presence-peer entity (design-001 §5.6). Rides the presence
10
+ * blob so remote peers project it — a peer query is `[PresencePeer, Not(Local)]`
11
+ * (remote peers) or `[PresencePeer, Local]` (self). `Local` is strata-owned and
12
+ * auto-applied by the ephemeral store on spawn (never transmitted); this is the
13
+ * ONLY presence tag the engine mints, so pack differences can't corrupt it.
14
+ */
15
+ export declare const PresencePeer: import("@vibecook/strata-ecs").Tag;
16
+ /** Peer display identity. */
17
+ export declare const PresenceInfo: import("@vibecook/strata-ecs").Component<import("@vibecook/strata-ecs").ValueOf<{
18
+ readonly name: "string";
19
+ readonly color: "string";
20
+ }>, {
21
+ readonly name: "string";
22
+ readonly color: "string";
23
+ }>;
24
+ /** Peer cursor in world coords. */
25
+ export declare const PresenceCursor: import("@vibecook/strata-ecs").Component<import("@vibecook/strata-ecs").ValueOf<{
26
+ readonly x: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
27
+ readonly y: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
28
+ readonly device: import("@vibecook/strata-ecs").EnumType<"mouse" | "touch" | "pen">;
29
+ }>, {
30
+ readonly x: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
31
+ readonly y: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
32
+ readonly device: import("@vibecook/strata-ecs").EnumType<"mouse" | "touch" | "pen">;
33
+ }>;
34
+ /**
35
+ * Selection broadcast: count + combined bbox + a JSON key list capped at 32 (peers outline the
36
+ * listed widgets; beyond the cap they render the bbox only). Facet semantics: present only while the
37
+ * selection is non-empty.
38
+ */
39
+ export declare const SelectionSummary: import("@vibecook/strata-ecs").Component<import("@vibecook/strata-ecs").ValueOf<{
40
+ readonly count: import("@vibecook/strata-ecs").FieldSpec<"u16", true>;
41
+ readonly x: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
42
+ readonly y: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
43
+ readonly w: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
44
+ readonly h: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
45
+ readonly keys: import("@vibecook/strata-ecs").FieldSpec<"string", true>;
46
+ }>, {
47
+ readonly count: import("@vibecook/strata-ecs").FieldSpec<"u16", true>;
48
+ readonly x: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
49
+ readonly y: import("@vibecook/strata-ecs").FieldSpec<"f64", true>;
50
+ readonly w: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
51
+ readonly h: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
52
+ readonly keys: import("@vibecook/strata-ecs").FieldSpec<"string", true>;
53
+ }>;
@@ -0,0 +1,46 @@
1
+ /** Recognizer timing/slop live mirror (design-003 §4.2 kind table; inertia constants §5 item 9). */
2
+ export declare const GestureSettings: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
3
+ readonly tapMaxMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
4
+ readonly tapSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
5
+ readonly longPressMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
6
+ readonly longPressSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
7
+ readonly dragSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
8
+ readonly pinchSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
9
+ readonly wheelEndSilenceMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
10
+ readonly inertiaMinVelocityPxPerS: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
11
+ readonly inertiaDecayMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
12
+ readonly multiTapWindowMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
13
+ readonly multiTapSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
14
+ }>, {
15
+ readonly tapMaxMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
16
+ readonly tapSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
17
+ readonly longPressMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
18
+ readonly longPressSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
19
+ readonly dragSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
20
+ readonly pinchSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
21
+ readonly wheelEndSilenceMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
22
+ readonly inertiaMinVelocityPxPerS: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
23
+ readonly inertiaDecayMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
24
+ readonly multiTapWindowMs: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
25
+ readonly multiTapSlopPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
26
+ }>;
27
+ /** Pick radii + retarget dead-band live mirror (design-003 §3 L1 targeting). */
28
+ export declare const PointerSettings: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
29
+ readonly radiusMousePx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
30
+ readonly radiusTouchPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
31
+ readonly radiusPenPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
32
+ readonly hoverReleaseDeadBandPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
33
+ }>, {
34
+ readonly radiusMousePx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
35
+ readonly radiusTouchPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
36
+ readonly radiusPenPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
37
+ readonly hoverReleaseDeadBandPx: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
38
+ }>;
39
+ /** Zoom clamp live mirror (design-005 §4 `settings.zoom`; camera-sim consumes). */
40
+ export declare const CameraLimits: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
41
+ readonly minZoom: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
42
+ readonly maxZoom: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
43
+ }>, {
44
+ readonly minZoom: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
45
+ readonly maxZoom: import("@vibecook/strata-ecs").FieldSpec<"f32", true>;
46
+ }>;
@@ -0,0 +1,69 @@
1
+ import type { World } from "@vibecook/strata-ecs";
2
+ import { type DocSession, type DocSessionOpts } from "./doc-kit";
3
+ import type { DocVersionReport } from "./version-gate";
4
+ /** The app-owned write side of storage (localStorage, IndexedDB, fs, cloud — WHERE is app-owned). */
5
+ export interface AutosaveStorageWrite {
6
+ put(bytes: Uint8Array): Promise<void> | void;
7
+ }
8
+ /** The app-owned read side of storage (restore path). */
9
+ export interface AutosaveStorageRead {
10
+ get(): Promise<Uint8Array | undefined> | Uint8Array | undefined;
11
+ }
12
+ /** Cancels a scheduled callback (the return of an injected `schedule`). */
13
+ export type CancelScheduled = () => void;
14
+ export interface AutosaveOpts {
15
+ readonly storage: AutosaveStorageWrite;
16
+ /** The session's world — read for the gesture-deferral gate (never written). */
17
+ readonly world: World;
18
+ /** Quiet-period before a save (default 800 ms). */
19
+ readonly debounceMs?: number;
20
+ /** Hard ceiling from the first unsaved change to a forced save (default 10 s). */
21
+ readonly maxWaitMs?: number;
22
+ /** Wall-clock source; the engine never reads a clock itself (default `Date.now`). */
23
+ readonly now?: () => number;
24
+ /** Timer factory; return a canceller (default `setTimeout`/`clearTimeout`). */
25
+ readonly schedule?: (fn: () => void, ms: number) => CancelScheduled;
26
+ }
27
+ export type AutosaveStatus = "idle" | "pending" | "deferred" | "saving" | "saved" | "error";
28
+ export interface AutosaveState {
29
+ readonly status: AutosaveStatus;
30
+ /** Wall-clock ms of the last successful save (via the injected `now`), or undefined. */
31
+ readonly lastSavedAt: number | undefined;
32
+ /** True while there are unsaved changes (a timer is armed or deferred). */
33
+ readonly pending: boolean;
34
+ }
35
+ export interface Autosave {
36
+ /** Force a save of the current document now and await it (beforeunload / explicit save). */
37
+ flush(): Promise<void>;
38
+ /** Unsubscribe and cancel any pending timer. Idempotent. */
39
+ stop(): void;
40
+ /** A pull-based status read for a per-frame HUD (design demo). */
41
+ state(): AutosaveState;
42
+ }
43
+ /**
44
+ * Begin autosaving `session` to `opts.storage`. Debounced with a max-wait
45
+ * ceiling, deferred while a gesture (or `Camera.gesturing`) is live. Returns the
46
+ * control surface; call `stop()` at teardown.
47
+ */
48
+ export declare function startAutosave(session: DocSession, opts: AutosaveOpts): Autosave;
49
+ /** The outcome of a restore attempt — a tagged union; nothing here ever throws. */
50
+ export type RestoreResult = {
51
+ readonly status: "restored";
52
+ readonly session: DocSession;
53
+ } | {
54
+ readonly status: "empty";
55
+ } | {
56
+ readonly status: "quarantined";
57
+ readonly reason: string;
58
+ readonly bytes: Uint8Array;
59
+ readonly report?: DocVersionReport;
60
+ };
61
+ /**
62
+ * Load the autosaved document into `world`. Missing/empty storage ⇒
63
+ * `{ status: "empty" }` (the app creates fresh). A successful open ⇒
64
+ * `{ status: "restored", session }` (may be read-only per the version gate).
65
+ * Corrupt or incompatible bytes ⇒ `{ status: "quarantined", reason, bytes }` —
66
+ * NEVER a throw, NEVER a bricked boot (M5 exit criterion): the app stashes the
67
+ * bytes aside and starts fresh.
68
+ */
69
+ export declare function restoreAutosave(world: World, storage: AutosaveStorageRead, opts?: DocSessionOpts): Promise<RestoreResult>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The transport/bootstrap kit (design-005 §6.5) — the engine-provided join state
3
+ * machine; the app provides the byte channel (channels.ts).
4
+ *
5
+ * `joinDoc` runs the LOCKED §6.5 protocol:
6
+ * join = broadcast hello → buffer inbound → import addressed snapshot as causal
7
+ * base → re-broadcast own base → drain buffer; ~800 ms silence ⇒ first peer ⇒
8
+ * seed. Reconnect = the caller calls `joinDoc` again (streams are never
9
+ * resumed). A strata `PendingImportError` on a live update ⇒ automatic internal
10
+ * re-bootstrap (one retry, then surfaced via `onError`).
11
+ *
12
+ * ── THE FRAME PROTOCOL (1-byte kind + payload) ──
13
+ * HELLO(1) payload = joinerId (UTF-8). "I'm joining; send me a base."
14
+ * SNAPSHOT_OFFER(2) payload = an ENVELOPE-framed doc snapshot (header + loro
15
+ * bytes). An UNaddressed base broadcast — the post-import
16
+ * "re-broadcast own base" step; a buffering peer OPENS it (the
17
+ * gate runs), an active peer unwraps + merges it (dedupes).
18
+ * SNAPSHOT(3) payload = idLen(u16 LE) ++ targetId(UTF-8) ++ ENVELOPE. An
19
+ * addressed answer to a HELLO — only the peer whose id ==
20
+ * targetId adopts it as its causal base (others unwrap/merge).
21
+ * UPDATE(4) payload = a sealed local commit's incremental (raw) loro bytes.
22
+ * PRESENCE(5) payload = ephemeral presence bytes — rides the same channel,
23
+ * applied immediately (independent of the doc handshake).
24
+ *
25
+ * ── STATES ──
26
+ * BUFFERING: helloed, no base yet. UPDATE frames are BUFFERED; the first snapshot
27
+ * (offer, or addressed to us) triggers `becomeJoiner`; the hello-timeout triggers
28
+ * `becomeSeeder`. PRESENCE flows immediately; HELLOs are ignored (nothing to offer).
29
+ * ACTIVE: session live (either role). Answers HELLOs with an addressed snapshot,
30
+ * merges snapshots/offers, applies updates, relays presence. Local commits ride
31
+ * `store.subscribeOutbound` → UPDATE frames (remote-origin imports never re-fire it,
32
+ * so an imported change is never rebroadcast — the origin IS the feedback guard).
33
+ *
34
+ * Two cold joiners with no existing doc BOTH time out and seed (split-brain) — the
35
+ * design's "first peer seeds" assumes a joiner arrives after a seeder exists; a
36
+ * simultaneous-cold-start tie-break is out of scope for M9 and noted here.
37
+ */
38
+ import type { World } from "@vibecook/strata-ecs";
39
+ import type { ByteChannel } from "./channels";
40
+ import { type DocSession, type DocSessionOpts } from "./doc-kit";
41
+ import type { PresenceSession } from "../presence/presence-kit";
42
+ /** Injectable timer surface (autosave's clock precedent) so the hello-timeout is deterministic in tests. */
43
+ export interface BootstrapClock {
44
+ setTimeout(fn: () => void, ms: number): unknown;
45
+ clearTimeout(handle: unknown): void;
46
+ }
47
+ export interface JoinDocOpts {
48
+ /** Passed to create/open (maxUndoSteps, etc.). */
49
+ readonly docOpts?: DocSessionOpts;
50
+ /** Version-gate override, forwarded to `openDocSession` (a non-"ok" verdict ⇒ a read-only joiner). */
51
+ readonly onGate?: DocSessionOpts["onGate"];
52
+ /** Injected timer surface (tests). Default: global setTimeout/clearTimeout. */
53
+ readonly clock?: BootstrapClock;
54
+ /** Silence window before the lone/first peer seeds (design §6.5, default 800 ms). */
55
+ readonly helloTimeoutMs?: number;
56
+ /** Populate the fresh document — runs on the SEEDER path only, before it goes active. */
57
+ readonly seed?: (session: DocSession) => void;
58
+ /** Presence session to relay over the same channel (kind PRESENCE). Optional. */
59
+ readonly presence?: PresenceSession;
60
+ /** Surfaced when a re-bootstrap (after a PendingImportError) itself fails, or a base import fails post-join. */
61
+ readonly onError?: (err: unknown) => void;
62
+ }
63
+ export interface JoinResult {
64
+ /** "seeder" = we created the doc (timed out); "joiner" = we imported a peer's base. */
65
+ readonly role: "seeder" | "joiner";
66
+ readonly session: DocSession;
67
+ /** Stop relaying, unsubscribe the channel, and close the session. Idempotent. */
68
+ leave(): void;
69
+ }
70
+ /**
71
+ * Join (or found) the room's document over `channel`. Resolves once we go active —
72
+ * as a "joiner" (imported a peer's base) or a "seeder" (timed out and created one).
73
+ */
74
+ export declare function joinDoc(world: World, channel: ByteChannel, opts?: JoinDocOpts): Promise<JoinResult>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The dumb snapshot relay (design-005 §6.5 M5 stopgap; §6, decision "relay is
3
+ * dumb — inventory §1.3).
4
+ *
5
+ * M5 ONLY: broadcasts the WHOLE converged document on every local change and
6
+ * imports whole documents it receives. Loro imports dedupe, so a full snapshot
7
+ * is a valid (if wasteful) relay — this is deliberately the simplest thing that
8
+ * converges two tabs. M9 REPLACES it with the transport/bootstrap protocol
9
+ * (hello → addressed-snapshot base → increment stream); do not grow this into
10
+ * that — it is the throwaway.
11
+ *
12
+ * CHANGE-SUBSCRIPTION SURFACE + FEEDBACK GUARD: rides the same
13
+ * `DurableStore.subscribeOutbound` as the autosave kit, which fires ONLY on
14
+ * sealed LOCAL commits (durable-store.ts `onLocalBatch` early-returns on
15
+ * `origin !== "local"`). That origin filter IS the feedback-loop guard: applying
16
+ * a peer's snapshot via `applyRemote` produces remote-origin batches, which do
17
+ * NOT re-fire `subscribeOutbound`, so an imported change is never rebroadcast.
18
+ * No `applying` flag is needed — the subscription tells us the origin.
19
+ *
20
+ * LATE-JOINER SEED: on attach we post our snapshot (best-effort seed) AND a
21
+ * `hello`; peers answer a `hello` with their snapshot, so a tab opened second
22
+ * converges up to the first. BroadcastChannel never delivers a message to the
23
+ * instance that posted it, so hello/answer never self-loops.
24
+ */
25
+ import type { DocSession } from "./doc-kit";
26
+ /**
27
+ * The minimal channel surface the relay needs — structurally satisfied by the
28
+ * global `BroadcastChannel` and by test fakes. Anything carrying a `Uint8Array`
29
+ * through `postMessage` works (design-005 §6.5).
30
+ */
31
+ export interface RelayChannel {
32
+ postMessage(message: unknown): void;
33
+ addEventListener(type: "message", listener: (ev: {
34
+ data: unknown;
35
+ }) => void): void;
36
+ removeEventListener(type: "message", listener: (ev: {
37
+ data: unknown;
38
+ }) => void): void;
39
+ close?(): void;
40
+ }
41
+ export interface BroadcastRelayOpts {
42
+ /** Channel name; used to construct a `BroadcastChannel` when `channel` is absent. */
43
+ readonly channelName: string;
44
+ /** Injected channel (tests). When omitted, a `new BroadcastChannel(channelName)` is created and owned. */
45
+ readonly channel?: RelayChannel;
46
+ }
47
+ export interface BroadcastRelay {
48
+ /** Unsubscribe from the store, detach the listener, and close an owned channel. Idempotent. */
49
+ stop(): void;
50
+ }
51
+ /** Attach a dumb snapshot relay to `session` over a `BroadcastChannel` (or injected channel). */
52
+ export declare function attachBroadcastRelay(session: DocSession, opts: BroadcastRelayOpts): BroadcastRelay;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Byte-channel adapters for the bootstrap kit (design-005 §6.5: "provided
3
+ * adapters for BroadcastChannel and WebSocket byte relays; anything carrying a
4
+ * Uint8Array works").
5
+ *
6
+ * A `ByteChannel` is the whole transport contract the bootstrap state machine
7
+ * needs: `send(bytes)` out, `subscribe(fn)` in. The engine stays transport-free —
8
+ * the app owns the socket lifecycle and hands the instance in.
9
+ *
10
+ * Both adapters interact through STRUCTURAL types (no DOM lib dependency beyond
11
+ * what core already uses — the broadcast-relay precedent), so a test double or an
12
+ * alternative runtime satisfies them without the platform's exact event types.
13
+ */
14
+ /** The transport contract: bytes out, bytes in. Structurally satisfied by both adapters + test fakes. */
15
+ export interface ByteChannel {
16
+ send(bytes: Uint8Array): void;
17
+ /** Deliver every inbound frame to `fn`. Returns an unsubscribe. */
18
+ subscribe(fn: (bytes: Uint8Array) => void): () => void;
19
+ }
20
+ /** A `ByteChannel` that owns a resource (a BroadcastChannel) and can release it. */
21
+ export interface OwnedByteChannel extends ByteChannel {
22
+ close(): void;
23
+ }
24
+ /** The slice of `WebSocket` we use (structural — the app supplies the instance). */
25
+ export interface WebSocketLike {
26
+ binaryType: string;
27
+ readyState: number;
28
+ send(data: ArrayBufferLike | ArrayBufferView): void;
29
+ addEventListener(type: string, listener: (ev: {
30
+ data: unknown;
31
+ }) => void): void;
32
+ removeEventListener(type: string, listener: (ev: {
33
+ data: unknown;
34
+ }) => void): void;
35
+ }
36
+ /**
37
+ * A `ByteChannel` over a `BroadcastChannel` (same-origin tab-to-tab). Uint8Array
38
+ * is structured-cloneable, so it rides `postMessage` directly. A BroadcastChannel
39
+ * never delivers a message to the instance that posted it, so a peer never hears
40
+ * its own frames. `close()` releases the owned channel.
41
+ */
42
+ export declare function broadcastChannelByteChannel(name: string): OwnedByteChannel;
43
+ /**
44
+ * A `ByteChannel` over a caller-supplied WebSocket. Sets `binaryType =
45
+ * "arraybuffer"` (so inbound frames arrive as `ArrayBuffer`), and QUEUES outbound
46
+ * frames until the socket is OPEN, flushing on the `open` event. The app owns the
47
+ * socket (construction, reconnection, close).
48
+ */
49
+ export declare function webSocketByteChannel(ws: WebSocketLike): ByteChannel;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The doc-backed CommitSink — the M5 half of the commit seam (design-001 §3
3
+ * step 3a; design-002 decision 4).
4
+ *
5
+ * One intent = ONE `guardedTransaction` = one undo step. Behaviors already
6
+ * filtered their write sets to live `Drags` edges; the sink re-guards anyway —
7
+ * the doc is the authority (design-001 §3: `keyOf(w) === undefined ⇒ skip` —
8
+ * one dead widget must not roll back the gesture). Value writes go through
9
+ * `tx.edit().set` on PRE-EXISTING cells (the synchronous agreement point);
10
+ * consume's reparent is a relation write in the SAME transaction.
11
+ *
12
+ * Read-only sessions (version gate) swap in `createReadOnlyCommitSink`: the
13
+ * gesture still runs (runtime feels live) but nothing commits — the runtime
14
+ * cells reconverge with the baseline at the next remote sweep, exactly like a
15
+ * cancel, and a DEV warn says why.
16
+ */
17
+ import type { DurableStore } from "@vibecook/strata-ecs/durable";
18
+ import type { World } from "@vibecook/strata-ecs";
19
+ import type { CommitSink } from "../engine/commit-sink";
20
+ export declare function createDocCommitSink(store: DurableStore, world: World): CommitSink;
21
+ /** The version-gate's read-only posture: gestures run, nothing commits. */
22
+ export declare function createReadOnlyCommitSink(): CommitSink;
@@ -0,0 +1,50 @@
1
+ import type { World } from "@vibecook/strata-ecs";
2
+ import type { DurableStore } from "@vibecook/strata-ecs/durable";
3
+ import type { CommitSink } from "../engine/commit-sink";
4
+ import { type LiveWriter } from "../guards/live-writer";
5
+ import { type DocVersionReport, type GateVerdict } from "./version-gate";
6
+ export interface DocSessionOpts {
7
+ readonly maxUndoSteps?: number;
8
+ /** Override the default gate policy (design-005 §6.3 facade hook). */
9
+ readonly onGate?: (report: DocVersionReport, verdict: GateVerdict) => GateVerdict;
10
+ /**
11
+ * Run the M9 read-repair migrator on a "migrate" verdict (design-005 §6.4).
12
+ * Default TRUE: the doc attaches writable, the migrator upgrades older packs
13
+ * in place, and the session re-gates. Set false to keep M5 behavior (a
14
+ * "migrate" verdict attaches read-only, no upgrade).
15
+ */
16
+ readonly migrate?: boolean;
17
+ }
18
+ export interface DocSession {
19
+ readonly store: DurableStore;
20
+ readonly sink: CommitSink;
21
+ readonly readOnly: boolean;
22
+ /**
23
+ * The design-001 §3 step-2 DEV guard, wired to THIS doc: userland live
24
+ * writes to doc cells go through it (throws without a claim/tween;
25
+ * cellInDoc is exact via store.getComponent). Engine behaviors stay on
26
+ * ctx — they hold claims by construction.
27
+ */
28
+ readonly liveWriter: LiveWriter;
29
+ /** Present on the open path (the gate's evidence). */
30
+ readonly report?: DocVersionReport;
31
+ /** Envelope-framed snapshot for storage (autosave kit consumes this). */
32
+ exportEnvelope(savedAt?: number): Uint8Array;
33
+ /** Relay passthroughs (M5 dumb relay; M9 brings the bootstrap protocol). */
34
+ exportSnapshot(): Uint8Array;
35
+ applyRemote(bytes: Uint8Array): void;
36
+ /** cancel gestures → detach → world reset in place → respawn the anchor. */
37
+ close(): void;
38
+ }
39
+ export type OpenDocResult = {
40
+ readonly ok: true;
41
+ readonly session: DocSession;
42
+ } | {
43
+ readonly ok: false;
44
+ readonly reason: string;
45
+ readonly report?: DocVersionReport;
46
+ };
47
+ /** Create + attach a fresh document (local-first by construction, design-001 §6.3). */
48
+ export declare function createDocSession(world: World, opts?: DocSessionOpts): DocSession;
49
+ /** Open an envelope: gate BEFORE attach; quarantine is a return value, never a throw. */
50
+ export declare function openDocSession(world: World, bytes: Uint8Array, opts?: DocSessionOpts): OpenDocResult;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The file envelope — the engine-owned at-rest format (design-001 §6.5,
3
+ * design-005 §6.2).
4
+ *
5
+ * A document at rest is NEVER raw Loro bytes: it is an engine-versioned frame
6
+ * read BEFORE any import, so the version gate and the autosave quarantine have
7
+ * a hook point that cannot brick boot. Layout (little-endian):
8
+ *
9
+ * bytes 0..3 magic "ICE1"
10
+ * byte 4 envelope version (1)
11
+ * bytes 5..8 u32 header length
12
+ * ... header JSON: { engineSchema, prefabVersions, savedAt? }
13
+ * ... payload: Loro snapshot bytes (opaque here)
14
+ *
15
+ * WHERE bytes go (fs/IndexedDB/cloud) is app-owned; WHAT they are is this file.
16
+ */
17
+ export declare const ENVELOPE_MAGIC = "ICE1";
18
+ export declare const ENVELOPE_VERSION = 1;
19
+ /** The document-format version this build writes (gate compares against it). */
20
+ export declare const ENGINE_SCHEMA_VERSION = 1;
21
+ export interface EnvelopeHeader {
22
+ readonly engineSchema: number;
23
+ /** Per-prefab pack versions at save time ({ [prefabId]: version }). */
24
+ readonly prefabVersions: Readonly<Record<string, number>>;
25
+ /** App-supplied wall-clock ms (the engine never reads a clock). */
26
+ readonly savedAt?: number;
27
+ }
28
+ export declare class EnvelopeError extends Error {
29
+ readonly reason: "magic" | "version" | "truncated" | "header";
30
+ constructor(message: string, reason: "magic" | "version" | "truncated" | "header");
31
+ }
32
+ export declare function encodeEnvelope(header: EnvelopeHeader, payload: Uint8Array): Uint8Array;
33
+ export declare function decodeEnvelope(bytes: Uint8Array): {
34
+ header: EnvelopeHeader;
35
+ payload: Uint8Array;
36
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The M9 migration runner — read-repair at open (design-005 §6.4).
3
+ *
4
+ * A migration is NOT an offline byte transform: the document is already
5
+ * attached, its old cells projected into the runtime, and we upgrade them in
6
+ * place. For each durable type whose doc-side pack marker is OLDER than the
7
+ * local registry (`report.olderInDoc`) and whose widget carries a `migrate`
8
+ * chain covering the gap, `runMigrations`:
9
+ * 1. reads each entity's current group values from the runtime (already
10
+ * canonicalised to the LOCAL schema — see the field-tolerance note below),
11
+ * 2. folds the chain `docV → docV+1 → … → localV-1` over a flat prop record,
12
+ * 3. writes the absolute per-group result back in ONE
13
+ * `store.transaction(fn, { undoable: false })` — the commit never enters
14
+ * the user's undo stack, history hooks skip it, a pending redo survives,
15
+ * 4. stamps the new `engine.pack.<id>.<localV>` write-once marker via
16
+ * `store.metaTransaction` (excluded from undo, invisible to observers).
17
+ *
18
+ * WHY NO LEGACY-SCHEMA REGISTRATION (the as-built finding — design-005 §6.4 is
19
+ * conservative here): design-005 says "the compiler keeps legacy group
20
+ * components registered for every version covered by the migrate chain" because
21
+ * "strata skips unknown inbound components at projection." That is TRUE at the
22
+ * COMPONENT-NAME granularity — an unknown component name is never projected
23
+ * (strata-ecs binding.ts:390-392, 804; "unknown component — never projected").
24
+ * But it is field-TOLERANT within a known name: `canon(C, v)` iterates the
25
+ * LOCAL schema's `C.fields` (strata-ecs substrate/canon.ts:57-64), so a stored
26
+ * cell that carries EXTRA fields (a v2 that dropped a field) has them dropped,
27
+ * and one MISSING fields (a v2 that added a field) gets them default-filled by
28
+ * `encodeComponentValue`. Because `defineWidget` gives EVERY generated field a
29
+ * default (define-widget.ts strataFieldOf), a v1 cell projects cleanly into a
30
+ * v2 group component of the SAME name with no legacy registration — the
31
+ * transform reads the already-v2-shaped runtime value. Legacy registration
32
+ * would only be needed to migrate data OUT of a group REMOVED wholesale in v2
33
+ * (its component name unregistered → never projected); that coincides with the
34
+ * cross-schema data movement design-005 §6.4 already defers to vNext, so the
35
+ * `migrate` API stays the doc's `{ fromVersion: (prev) => next }` shape with no
36
+ * parallel legacy field. The `migrate.test.ts` "field tolerance" case proves
37
+ * the v1→v2 projection empirically (a v1 cell missing a v2-added field).
38
+ *
39
+ * Idempotence / convergence: writes are ABSOLUTE (whole-group values computed
40
+ * from v1), so two peers that migrate the same v1 doc independently produce
41
+ * identical cells that merge by last-writer-wins, and the marker is a
42
+ * first-writer-wins ADD. Re-opening an already-migrated doc reads the v2 marker
43
+ * → the type is no longer `olderInDoc` → the runner skips it. The runner must
44
+ * therefore run once per open against the open-time report (the caller re-gates
45
+ * after it, never re-invokes it against the same report).
46
+ */
47
+ import type { World } from "@vibecook/strata-ecs";
48
+ import type { DurableStore } from "@vibecook/strata-ecs/durable";
49
+ import type { DocVersionReport } from "./version-gate";
50
+ /** What the runner needs: the doc store to write through, the world to read from. */
51
+ export interface MigrationCtx {
52
+ readonly store: DurableStore;
53
+ readonly world: World;
54
+ }
55
+ export interface MigrationOutcome {
56
+ /** Type ids whose entities were upgraded and whose v-local marker was stamped. */
57
+ readonly migrated: string[];
58
+ /** Type ids left as-is (no widget, no/gappy chain) — they stay read-only per the re-gate. */
59
+ readonly skipped: string[];
60
+ }
61
+ /**
62
+ * Run every applicable per-type migration against an ATTACHED doc. Reads happen
63
+ * before the write transaction opens (a clean read snapshot → compute → one
64
+ * absolute write pass); each type stamps its new marker in its own
65
+ * `metaTransaction` after its data transaction commits. Never throws for a
66
+ * merely-un-migratable type (missing widget / gappy chain → skipped); a genuine
67
+ * fault (a throwing transform) propagates so the caller can fall back read-only.
68
+ */
69
+ export declare function runMigrations(ctx: MigrationCtx, report: DocVersionReport): MigrationOutcome;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The document version gate (design-001 §6.1/§5.2, design-005 §6.3).
3
+ *
4
+ * Version state lives in the doc's reserved `meta` root map as WRITE-ONCE
5
+ * MARKER KEYS — a monotone key SET, never a mutated cell (sidestepping
6
+ * first-writer-wins): `engine.schema.<n>` and `engine.pack.<type>.<v>` are
7
+ * presence markers. Reading needs no strata API: the gate reads
8
+ * `doc.getMap("meta")` on the RAW LoroDoc after import, BEFORE
9
+ * `createDurableStore`/`attachDurable` — an incompatible doc never projects.
10
+ *
11
+ * Stamps go through `store.metaTransaction` (strata 0.4.0 — petition 3b
12
+ * LANDED): the commit is properly tagged (peers no longer warn about an
13
+ * untagged foreign writer), excluded from undo/redo, and invisible to
14
+ * observers. Same underlying `meta` root map and raw keys as the pre-0.4.0
15
+ * raw-doc stamps, so docs stamped by either path read identically here —
16
+ * and importers also stopped warning about metadata-only commits from
17
+ * before the API, so legacy docs are quiet too.
18
+ *
19
+ * M5 gate policy: exact schema + no doc-side pack NEWER than local ⇒ ok;
20
+ * anything else ⇒ readOnly (or reject via policy). The migrate verdict (older
21
+ * doc packs with a migrate chain) lands at M9 — the gate's verdict surface
22
+ * already carries it so the facade policy hook is stable.
23
+ */
24
+ import type { LoroDoc } from "loro-crdt";
25
+ import type { DurableStore } from "@vibecook/strata-ecs/durable";
26
+ export interface DocVersionReport {
27
+ /** Highest engine.schema.<n> marker present (0 = unstamped/pre-engine doc). */
28
+ readonly docSchema: number;
29
+ readonly localSchema: number;
30
+ /** Highest engine.pack.<id>.<v> per prefab id present in the doc. */
31
+ readonly docPacks: Readonly<Record<string, number>>;
32
+ /** Local registry versions for DURABLE prefabs. */
33
+ readonly localPacks: Readonly<Record<string, number>>;
34
+ /** Prefab ids whose doc marker is NEWER than local (built by a newer app). */
35
+ readonly newerInDoc: readonly string[];
36
+ /** Prefab ids whose doc marker is OLDER than local (M9 migration input). */
37
+ readonly olderInDoc: readonly string[];
38
+ }
39
+ export type GateVerdict = "ok" | "readOnly" | "migrate" | "reject";
40
+ /**
41
+ * Stamp the engine markers through the store's sanctioned meta path (create
42
+ * path; idempotent write-once; callable BEFORE attach — the order is
43
+ * `createDurableStore(doc)` first, then stamp, then `attachDurable`).
44
+ */
45
+ export declare function stampEngineMeta(store: DurableStore): void;
46
+ /** Read the marker keys off a raw (imported, unattached) doc and compare. */
47
+ export declare function readDocVersionReport(doc: LoroDoc): DocVersionReport;
48
+ /** The default M5 policy. A facade policy hook may override (design-005 §6.3). */
49
+ export declare function gateVerdict(report: DocVersionReport): GateVerdict;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The commit seam (design-001 §3 gesture protocol × implementation-plan M4/M5).
3
+ *
4
+ * Behaviors end a gesture with ONE commit (design-002 decision 4). Until the
5
+ * durable layer lands (M5), commits are recorded as INTENTS through this seam:
6
+ * the M4 frame traces assert runtime state + commit intents, and M5 swaps in a
7
+ * doc-backed sink that turns an intent into one `guardedTransaction` per
8
+ * gesture (liveness guards re-checked inside the tx via live edges + keyOf —
9
+ * the intent carries the already-liveness-filtered write set, but the doc sink
10
+ * re-guards because the doc is the authority).
11
+ *
12
+ * The seam is deliberately dumb data: no callbacks into behaviors, no partial
13
+ * application — a whole gesture outcome in one value.
14
+ */
15
+ import type { Component, Entity } from "@vibecook/strata-ecs";
16
+ /** One absolute component write inside a commit (whole-value, design-001 §2). */
17
+ export interface CommitWrite {
18
+ readonly entity: Entity;
19
+ readonly component: Component<any>;
20
+ readonly value: Record<string, unknown>;
21
+ }
22
+ /** A structural reparent riding a consume outcome (design-003 §5.5). */
23
+ export interface CommitReparent {
24
+ readonly entity: Entity;
25
+ readonly container: Entity;
26
+ }
27
+ /**
28
+ * A wire creation riding a connect outcome (design-003 §5.8; design-001
29
+ * §5.3): wires bind WIDGET entities + port IDS — never port entities.
30
+ */
31
+ export interface CommitWireSpawn {
32
+ readonly from: Entity;
33
+ readonly to: Entity;
34
+ readonly fromPort: string;
35
+ readonly toPort: string;
36
+ }
37
+ /** A widget creation riding a create outcome (draw tool, design-005 §3). */
38
+ export interface CommitCreate {
39
+ readonly type: string;
40
+ readonly x: number;
41
+ readonly y: number;
42
+ readonly w: number;
43
+ readonly h: number;
44
+ }
45
+ export interface CommitIntent {
46
+ /** Outcome kind — traces assert on it. */
47
+ readonly kind: "move" | "consume" | "resize" | "connect" | "create" | "delete";
48
+ /** The recognizer (or op source) that owned the gesture. */
49
+ readonly gesture: Entity;
50
+ readonly writes: readonly CommitWrite[];
51
+ readonly reparents?: readonly CommitReparent[];
52
+ readonly wires?: readonly CommitWireSpawn[];
53
+ readonly creates?: readonly CommitCreate[];
54
+ }
55
+ export interface CommitSink {
56
+ commit(intent: CommitIntent): void;
57
+ }
58
+ /** The M4 default: record intents for traces/devtools; commit nothing. */
59
+ export declare function createRecordingCommitSink(): CommitSink & {
60
+ readonly intents: CommitIntent[];
61
+ };