@vibecook/ice 0.3.0 → 0.6.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 (52) hide show
  1. package/README.md +4 -4
  2. package/dist/{chunk-2GUYATUM.js → chunk-BFT7YWS2.js} +44 -11
  3. package/dist/chunk-BFT7YWS2.js.map +1 -0
  4. package/dist/{chunk-PKK6GLSA.js → chunk-ELCBCTKY.js} +138 -13
  5. package/dist/chunk-ELCBCTKY.js.map +1 -0
  6. package/dist/{chunk-5QY7HK6G.js → chunk-JLXETATP.js} +5594 -2712
  7. package/dist/chunk-JLXETATP.js.map +1 -0
  8. package/dist/devtools.js +5 -2
  9. package/dist/devtools.js.map +1 -1
  10. package/dist/dom.js +16 -2
  11. package/dist/ground.js +1 -1
  12. package/dist/index.js +39 -1
  13. package/dist/r3f.js +3 -3
  14. package/dist/react.js +5 -3
  15. package/dist/types/packages/core/src/behavior/compile.d.ts +74 -0
  16. package/dist/types/packages/core/src/behavior/define-behavior.d.ts +55 -0
  17. package/dist/types/packages/core/src/behavior/differ.d.ts +91 -0
  18. package/dist/types/packages/core/src/behavior/harness.d.ts +65 -0
  19. package/dist/types/packages/core/src/behavior/migrate.d.ts +87 -0
  20. package/dist/types/packages/core/src/behavior/public-reads.d.ts +31 -0
  21. package/dist/types/packages/core/src/behavior/runtime.d.ts +88 -0
  22. package/dist/types/packages/core/src/behavior/types.d.ts +312 -0
  23. package/dist/types/packages/core/src/behavior/world-view.d.ts +36 -0
  24. package/dist/types/packages/core/src/catalog/pointer.d.ts +11 -0
  25. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +10 -0
  26. package/dist/types/packages/core/src/doc/rename-sweep.d.ts +3 -0
  27. package/dist/types/packages/core/src/doc/rename.d.ts +7 -0
  28. package/dist/types/packages/core/src/doc/version-gate.d.ts +14 -1
  29. package/dist/types/packages/core/src/engine/engine.d.ts +38 -1
  30. package/dist/types/packages/core/src/engine/guests.d.ts +196 -0
  31. package/dist/types/packages/core/src/engine/reflectors.d.ts +8 -0
  32. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +16 -0
  33. package/dist/types/packages/core/src/guards/guarded-tx.d.ts +57 -0
  34. package/dist/types/packages/core/src/index.d.ts +10 -3
  35. package/dist/types/packages/core/src/input/queue.d.ts +9 -0
  36. package/dist/types/packages/core/src/schema/meta.d.ts +12 -0
  37. package/dist/types/packages/core/src/widget/define-widget.d.ts +89 -0
  38. package/dist/types/packages/core/src/widget/props.d.ts +35 -1
  39. package/dist/types/packages/core/src/widget/spawn.d.ts +14 -0
  40. package/dist/types/packages/dom/src/index.d.ts +2 -0
  41. package/dist/types/packages/dom/src/input-ownership.d.ts +52 -0
  42. package/dist/types/packages/dom/src/loop.d.ts +9 -0
  43. package/dist/types/packages/dom/src/pointer-adapter.d.ts +16 -3
  44. package/dist/types/packages/dom/src/widget-focus.d.ts +58 -0
  45. package/dist/types/packages/react/src/hooks.d.ts +12 -1
  46. package/dist/types/packages/react/src/index.d.ts +1 -1
  47. package/dist/types/packages/react/src/infinite-canvas.d.ts +20 -2
  48. package/dist/types/packages/react/src/keymap.d.ts +23 -4
  49. package/package.json +5 -5
  50. package/dist/chunk-2GUYATUM.js.map +0 -1
  51. package/dist/chunk-5QY7HK6G.js.map +0 -1
  52. package/dist/chunk-PKK6GLSA.js.map +0 -1
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The guest runtime (petition I14 — the literal PA-28 "spine scheduler").
3
+ *
4
+ * A GUEST is frame-cadence work the engine did not write: a plugin's declared
5
+ * system, a host's derived-state job, and (from design-009) every compiled
6
+ * behavior. Guests get a NAMED slot in the frame contract — design-002 §1 as
7
+ * amended 2026-08-15:
8
+ *
9
+ * setFrameInfo → sync → tick → GUESTS → publish hooks → notify → reflect
10
+ *
11
+ * Guests run BEFORE the publish hooks so derived state has settled by the time
12
+ * presence I/O reads it, and before `notify()` so the same frame reflects their
13
+ * writes. Three properties the engine owes anything it hosts here:
14
+ *
15
+ * 1. **Fault isolation.** A throwing guest is caught, struck, and skipped — it
16
+ * never kills the loop, its neighbours, or the frame. (Engine systems and
17
+ * publish hooks keep propagating loudly by design; guests are the untrusted
18
+ * tier, and the breaker below is what makes hosting them honest.)
19
+ * 2. **The snapshot rule.** `runAll` iterates a snapshot with a liveness check,
20
+ * so a guest that adds or removes guests mid-run never skips a neighbour —
21
+ * the bug this same release fixed in the publish-hook and reflector loops.
22
+ * A guest added during a run starts NEXT frame.
23
+ * 3. **A circuit breaker, named honestly.** A main-thread body cannot be
24
+ * preempted: by the time we measure, the frame is already spent. What the
25
+ * breaker guarantees is that no guest hurts the canvas TWICE sustained.
26
+ *
27
+ * GENERATIONS. `make(gen)` builds a per-generation instance; the generation
28
+ * boundary in ICE is `world.reset()` (doc close/switch — the world is reset in
29
+ * place, so systems, observers and this registry all survive while every entity
30
+ * dies). On reset we mark the generation dirty and rebuild at the TOP of the
31
+ * next guest step — never inside the observer emit, where structural writes are
32
+ * illegal and user code has no business running. `gen.signal` aborts when the
33
+ * generation ends or the guest is removed.
34
+ *
35
+ * THE LEDGER IS HOST-INJECTABLE. Downstream (VibeField) an engine is per-doc, so
36
+ * a registry-local ledger would reset on every doc switch and hand chronic
37
+ * offenders a fresh probation forever. `add({ledger})` seeds prior state and
38
+ * `onLedgerChange` reports every transition, so the host persists across
39
+ * generations it owns.
40
+ */
41
+ import type { World } from "@vibecook/strata-ecs";
42
+ /** Frame facts a guest may read (FrameInfo, pre-read by the engine). */
43
+ export interface GuestFrame {
44
+ readonly dtMs: number;
45
+ readonly tick: number;
46
+ /** Accumulated CLAMPED dt — under-reports across freezes; never accumulate
47
+ * wall time from it (frame-info.ts). */
48
+ readonly clock: number;
49
+ }
50
+ export type GuestRun = (frame: GuestFrame) => void;
51
+ export interface GuestInstance {
52
+ run: GuestRun;
53
+ dispose?: () => void;
54
+ /**
55
+ * "I still owe visible work." Feeds the frame gate's settle walk, so a
56
+ * freeze taken mid-derive parks only AFTER the guest has caught up — the
57
+ * canvas is never photographed between a structural change and its reflow.
58
+ * A guest with no owed-work notion simply omits it (the frame gate then
59
+ * treats it as quiet, which is the honest default).
60
+ */
61
+ busy?: () => boolean;
62
+ }
63
+ export interface GuestGeneration {
64
+ readonly world: World;
65
+ /** Aborts at generation teardown (world reset), removal, or engine dispose. */
66
+ readonly signal: AbortSignal;
67
+ }
68
+ export type GuestFactory = (gen: GuestGeneration) => GuestInstance | GuestRun;
69
+ /** The persistable half of a guest's breaker state. */
70
+ export interface GuestLedgerRecord {
71
+ /** Cumulative over-budget/fault strikes (never reset by resume — honesty). */
72
+ readonly strikes: number;
73
+ readonly suspended: boolean;
74
+ }
75
+ export interface GuestSpec {
76
+ /** Stable id — the ledger key, the profiler lane name, the doctor's row. */
77
+ readonly id: string;
78
+ readonly make: GuestFactory;
79
+ /** Per-invocation budget request; the effective budget is
80
+ * `min(budgetMs, seamCapMs)` — no single guest may claim the whole seam. */
81
+ readonly budgetMs?: number;
82
+ /** RESERVED. v1 hosts the publish slot only; pipeline-group values arrive
83
+ * with the behavior framework (design-009 M13a), wrapped by this breaker. */
84
+ readonly phase?: "publish";
85
+ /** Seed prior breaker state (host-persisted across engine generations). */
86
+ readonly ledger?: GuestLedgerRecord;
87
+ }
88
+ export interface GuestStatus {
89
+ readonly id: string;
90
+ readonly status: "running" | "suspended" | "failed";
91
+ /** Last invocation, ms. */
92
+ readonly lastMs: number;
93
+ /** p95 over the rolling window, ms. */
94
+ readonly p95Ms: number;
95
+ readonly strikes: number;
96
+ readonly budgetMs: number;
97
+ }
98
+ /**
99
+ * A guest whose CADENCE its owner drives (design-009 §7): compiled behaviors
100
+ * run as ordinary pipeline systems, in their declared phase, inside the tick —
101
+ * so nothing here can call their bodies. What they still need is everything
102
+ * ELSE a guest gets: one breaker, one ledger, one doctor row, one seam total.
103
+ * That is what this handle is — the breaker, detached from the scheduler.
104
+ */
105
+ export interface DrivenGuest {
106
+ /** Breaker verdict. Cheap and never throws — poll it from a system `runIf`. */
107
+ suspended(): boolean;
108
+ /**
109
+ * Run `body` inside this guest's fault domain, ACCRUING its cost into the
110
+ * current frame's charge. Returns false when the guest is suspended (the
111
+ * body did not run) or the body threw (contained).
112
+ *
113
+ * Accrual, not per-call recording, is what makes the budget mean "per frame"
114
+ * for a CHUNKED body: a tick system's body runs once per matching chunk, and
115
+ * recording each chunk as its own invocation would compare a 2ms frame budget
116
+ * against a fraction of the work it was meant to bound. The accrued total is
117
+ * closed as ONE invocation at the guest slot, every frame.
118
+ */
119
+ run(body: () => void): boolean;
120
+ /**
121
+ * Report a fault the caller already CONTAINED — a per-instance hook throw
122
+ * that the delivery loop caught so one bad node cannot kill a 1000-node
123
+ * behavior (BF-D18). It takes the ordinary throw ladder.
124
+ */
125
+ fault(err: unknown): void;
126
+ /** Idempotent. */
127
+ remove(): void;
128
+ }
129
+ export interface DrivenGuestSpec {
130
+ readonly id: string;
131
+ readonly budgetMs?: number;
132
+ readonly ledger?: GuestLedgerRecord;
133
+ /** Owed-work reporter for the frame gate's settle walk. */
134
+ readonly busy?: () => boolean;
135
+ }
136
+ export interface GuestRegistry {
137
+ /** Register a guest. Returns a remover (idempotent; disposes the instance). */
138
+ add(spec: GuestSpec): () => void;
139
+ /**
140
+ * Register a guest the registry does NOT call (see {@link DrivenGuest}).
141
+ * Its cost still lands in this frame's seam total, its strikes in the same
142
+ * ledger, and its row in `list()`.
143
+ */
144
+ addDriven(spec: DrivenGuestSpec): DrivenGuest;
145
+ /** Live status for devtools/doctor — never throws, safe to poll. */
146
+ list(): readonly GuestStatus[];
147
+ /** Fires whenever a guest's persistable breaker state changes. */
148
+ onLedgerChange(cb: (id: string, record: GuestLedgerRecord) => void): () => void;
149
+ /** Clear a suspension (the doctor's explicit action). Cumulative strikes
150
+ * survive; the rolling window is cleared so probation starts fresh. */
151
+ resume(id: string): void;
152
+ }
153
+ /** The engine-facing half (step drives it; hosts never see these). */
154
+ export interface GuestRegistryInternal extends GuestRegistry {
155
+ runAll(frame: GuestFrame): void;
156
+ /** Any live guest reporting owed work — the engine's settle reporter. */
157
+ anyBusy(): boolean;
158
+ dispose(): void;
159
+ }
160
+ /**
161
+ * Breaker constants (design-009 §12 / door ED-D3). Exported so tests and the
162
+ * downstream doctor cite ONE source rather than re-deriving thresholds.
163
+ */
164
+ export declare const GUEST_BUDGET: {
165
+ /** Default per-invocation budget when a guest declares none. */
166
+ readonly defaultBudgetMs: 2;
167
+ /** The whole guest seam's sustained budget — also the per-guest ceiling. */
168
+ readonly seamCapMs: 8;
169
+ /** Rolling window length, in invocations. */
170
+ readonly window: 120;
171
+ /** Trip: more than this many of the last `window` invocations over budget. */
172
+ readonly overBudgetTrip: 30;
173
+ /** Trip: this many CONSECUTIVE invocations at ≥ `heavyFactor` × budget. */
174
+ readonly consecutiveHeavy: 3;
175
+ readonly heavyFactor: 4;
176
+ /** Trip: this many CONSECUTIVE invocations over `stallMs` (one GC pause or
177
+ * debugger breakpoint must never suspend — hence 2, not 1). */
178
+ readonly consecutiveStall: 2;
179
+ readonly stallMs: 50;
180
+ /** Trip: this many consecutive throws. */
181
+ readonly consecutiveThrows: 3;
182
+ /** Frames the whole seam may sit over `seamCapMs` before the worst offender
183
+ * is suspended. */
184
+ readonly seamOverFrames: 3;
185
+ };
186
+ export interface GuestRegistryOpts {
187
+ /**
188
+ * Dev leniency: while true, TIMING strikes log and never suspend (the throw
189
+ * ladder still bites). Wired to `engine.enableTelemetry()` — i.e. devtools
190
+ * attached — so stepping in a debugger cannot suspend a healthy guest.
191
+ */
192
+ readonly lenient?: () => boolean;
193
+ readonly onFault?: (id: string, err: unknown) => void;
194
+ readonly onNotice?: (message: string) => void;
195
+ }
196
+ export declare function createGuestRegistry(world: World, opts?: GuestRegistryOpts): GuestRegistryInternal;
@@ -50,6 +50,14 @@ export interface ReflectorRegistry {
50
50
  /** Every registered reflector, registration order (devtools enumeration —
51
51
  * independent of telemetry; a reflector idle since boot still lists). */
52
52
  names(): readonly string[];
53
+ /** Arm flush timing (idempotent). Costs one `performance.now()` pair per
54
+ * flushed reflector per frame, so it rides `engine.enableTelemetry()` and is
55
+ * off by default — the same opt-in posture as per-system timing. */
56
+ armTelemetry(): void;
57
+ /** Total µs of the most recent `flushAll` (0 until armed). */
58
+ lastFlushMicros(): number;
59
+ /** Per-reflector µs of the most recent `flushAll` (empty until armed). */
60
+ lastFlushDetail(): ReadonlyMap<string, number>;
53
61
  }
54
62
  export declare function createReflectorRegistry(world: World, opts?: {
55
63
  onFault?: (name: string, err: unknown) => void;
@@ -19,6 +19,8 @@
19
19
  * remain reviewed constants (RUNTIME_BUDGETS) in v1 — recorded.
20
20
  */
21
21
  import type { Entity, World } from "@vibecook/strata-ecs";
22
+ import { type BehaviorRuntime } from "../behavior/runtime";
23
+ import type { AnyBehaviorDef } from "../behavior/types";
22
24
  import { type Engine } from "../engine/engine";
23
25
  import type { FrameControl } from "../engine/frame-control";
24
26
  import { type InteractionStack } from "../interaction/install";
@@ -42,6 +44,13 @@ export interface CanvasEngineOpts {
42
44
  }[];
43
45
  /** Registered tools (definition happens at defineTool; validated). */
44
46
  readonly tools?: readonly Tool[];
47
+ /**
48
+ * Behaviors this engine runs (design-009). Definition is the import
49
+ * side-effect; listing them here COMPILES and installs them — a behavior that
50
+ * is defined but never registered costs nothing and does nothing, which is
51
+ * what makes "a plugin declared it" and "this engine runs it" separable.
52
+ */
53
+ readonly behaviors?: readonly AnyBehaviorDef[];
45
54
  readonly budgets?: {
46
55
  readonly keepMounted?: number;
47
56
  readonly fboBytes?: number;
@@ -166,6 +175,13 @@ export interface StageControl {
166
175
  export interface CanvasEngine {
167
176
  readonly world: World;
168
177
  readonly engine: Engine;
178
+ /**
179
+ * The behavior runtime (design-009 §6): attach/detach/has/read for
180
+ * runtime-class behaviors, plus `list()` for devtools and the doctor.
181
+ * DURABLE attachment is deliberately absent — it is a document op and goes
182
+ * through `tx.attach` inside a transaction, so it syncs and undoes.
183
+ */
184
+ readonly behaviors: BehaviorRuntime;
169
185
  readonly stack: InteractionStack;
170
186
  readonly runtime: WidgetRuntime & {
171
187
  uninstall(): void;
@@ -14,6 +14,7 @@
14
14
  import type { Component, Entity, OrderPlace, Relation, Resource, Tag, World } from "@vibecook/strata-ecs";
15
15
  import type { Mutator } from "@vibecook/strata-ecs/durable";
16
16
  import { type ComponentInit, type Prefab } from "../schema/prefab";
17
+ import type { LiveWriter } from "./live-writer";
17
18
  export interface GuardedTx {
18
19
  spawnPrefab(prefab: Prefab, overrides?: readonly ComponentInit[]): Entity;
19
20
  /** Raw spawn — entity is unknown to the guard (prefer spawnPrefab). */
@@ -32,6 +33,29 @@ export interface GuardedTx {
32
33
  /** Reorder within the current parent's sibling sequence — ordered relations only (petition 8). */
33
34
  moveRelation(e: Entity, r: Relation, place: OrderPlace): void;
34
35
  setResource<S>(res: Resource<S>, v: S): void;
36
+ /**
37
+ * Animation-integrated write (petition I15): ONE commit owns the capture, the
38
+ * durable final, and the glide. `from` is read AT CALL TIME (pre-commit by
39
+ * construction), the final lands in THIS transaction — inheriting its
40
+ * `undoable` — and after a successful seal the tail installs the verified
41
+ * public-surface glide: `TransformTween` (which GRANTS the divergence) then a
42
+ * live rewind to `from`. One commit, one undo entry, no double-write.
43
+ *
44
+ * `animateMs` absent/0 = snap (no tween attached at all). A `Grab`-held
45
+ * entity is SKIPPED — the gesture owns its divergence. An entity already
46
+ * tweening is RETARGETED (its `toX/toY` rewritten) rather than restarted, so
47
+ * a re-layout mid-glide re-aims instead of completing at a stale target.
48
+ *
49
+ * Requires `opts.live` (the session's LiveWriter) — without it the rewind
50
+ * could not be divergence-guarded, so the call refuses rather than writing
51
+ * around the guard.
52
+ */
53
+ move(e: Entity, to: {
54
+ x: number;
55
+ y: number;
56
+ }, opts?: {
57
+ animateMs?: number;
58
+ }): void;
35
59
  }
36
60
  interface GuardedEditor {
37
61
  set<S>(c: Component<S>, v: S): GuardedEditor;
@@ -39,6 +63,7 @@ interface GuardedEditor {
39
63
  interface TxDoc {
40
64
  transaction(fn: (tx: Mutator) => void, opts?: {
41
65
  undoable?: boolean;
66
+ meta?: Record<string, unknown>;
42
67
  }): void;
43
68
  }
44
69
  export interface GuardedTxOpts {
@@ -48,6 +73,38 @@ export interface GuardedTxOpts {
48
73
  * first-⌘Z-removes-a-seed quirk). Default true: one tx = one undo step.
49
74
  */
50
75
  readonly undoable?: boolean;
76
+ /**
77
+ * The session's live writer. Required by {@link GuardedTx.move} (its
78
+ * post-commit rewind is a live write to a doc cell, legal only under the
79
+ * tween grant) and by the post-seal retarget sweep below.
80
+ */
81
+ readonly live?: LiveWriter;
82
+ /** Optional entity → doc-key probe; when absent every entity is treated as
83
+ * durable-if-Position-eligible. Used to skip runtime-only movers' tx write. */
84
+ readonly keyOf?: (e: Entity) => string | undefined;
85
+ /**
86
+ * Per-commit PROVENANCE (strata 0.12.0, petition 9): a small record carried
87
+ * in-CRDT with the batch, so a receiving peer can say who wrote this and why.
88
+ * The behavior framework stamps `{ behavior, label }` on every `ctx.commit`
89
+ * (design-009 §4.4 — `label` is a stable machine id, never display text).
90
+ * Note that shallow saves erase provenance below the compaction boundary, so
91
+ * it is diagnostics, never a data dependency.
92
+ */
93
+ readonly meta?: Record<string, unknown>;
51
94
  }
52
95
  export declare function guardedTransaction(doc: TxDoc, world: World, run: (tx: GuardedTx) => void, opts?: GuardedTxOpts): void;
96
+ /**
97
+ * Retarget every live tween onto its DURABLE truth — the undo/redo chokepoint
98
+ * (petition I15). Undo does not pass through {@link guardedTransaction}: it is
99
+ * strata's own local batch, so a glide in flight when ⌘Z lands would otherwise
100
+ * complete at the pre-undo target and strand the cell exactly as above.
101
+ *
102
+ * `durablePositionOf` reads the committed value (the doc's, not the runtime's).
103
+ * A tween whose entity has no durable Position is left alone — nothing to
104
+ * reconcile against.
105
+ */
106
+ export declare function retargetTweensToDoc(world: World, entities: Iterable<Entity>, durablePositionOf: (e: Entity) => {
107
+ x: number;
108
+ y: number;
109
+ } | undefined): void;
53
110
  export {};
@@ -3,7 +3,7 @@
3
3
  * scene systems · doc kit · publish step · engine facade · reflector registry.
4
4
  * Import wall: strata-ecs + @ice/kernel ONLY — never react/dom/three (enforced).
5
5
  */
6
- export declare const CORE_VERSION = "0.1.0";
6
+ export declare const CORE_VERSION = "0.6.0";
7
7
  export { defineSchemaOnce, hmrInvalidateOnSchemaChange } from "./boot/hmr";
8
8
  export { All, Any, Not, Related, createWorld, defineQuery, defineSystem, defineTickSystem, enumOf, field, Local, } from "@vibecook/strata-ecs";
9
9
  export type { Batch, Component, Condition, Entity, OrderPlace, Pipeline, Query, Relation, Resource, System, SystemCtx, Tag, TickSystem, World, } from "@vibecook/strata-ecs";
@@ -15,6 +15,12 @@ export { createPipelineRegistry, PHASE_GROUPS, type AnySystem, type PhaseGroup,
15
15
  export { createReflectorRegistry, type ReflectorDef, type ReflectorObserve, type ReflectorQuerySpec, type ReflectorRegistry, } from "./engine/reflectors";
16
16
  export { createEngine, type Engine, type EngineOpts, type FrameTelemetry, type PublishHook, type SystemRunRecord, } from "./engine/engine";
17
17
  export { createFrameControl, SETTLE_CAP, type FrameControl } from "./engine/frame-control";
18
+ export { createGuestRegistry, GUEST_BUDGET, type GuestFactory, type GuestFrame, type GuestGeneration, type GuestInstance, type GuestLedgerRecord, type GuestRegistry, type GuestRegistryOpts, type GuestRun, type GuestSpec, type GuestStatus, } from "./engine/guests";
19
+ export { behaviors, defineBehavior } from "./behavior/define-behavior";
20
+ export { BEHAVIOR_DEFAULT_PHASE, BEHAVIOR_PHASES, type AnyBehaviorAttachSpec, type AnyBehaviorDef, type BehaviorAttachSpec, type BehaviorChanges, type BehaviorCtxBase, type BehaviorCtxFor, type BehaviorFrame, type BehaviorHandle, type BehaviorHooks, type BehaviorPhase, type BehaviorQuery, type BehaviorQuerySpec, type BehaviorRead, type BehaviorSchema, type BehaviorSpec, type BehaviorStore, type BehaviorTx, type BehaviorWorld, type DataOf, type DurableBehaviorCtx, type EphemeralBehaviorCtx, type PeerFacet, type RuntimeBehaviorCtx, } from "./behavior/types";
21
+ export { compileBehavior, isDurableEligible, type CompiledBehavior } from "./behavior/compile";
22
+ export { behaviorMarkerKey, classifyBehaviorVersion, docBehaviorVersion, stampBehaviorMarker, BEHAVIOR_MARKER_PREFIX, type BehaviorMeta, type BehaviorVerdict, } from "./behavior/migrate";
23
+ export { createBehaviorHarness, type BehaviorHarness, type BehaviorHarnessOpts, type HarnessCommit, } from "./behavior/harness";
18
24
  export { devGuardsEnabled, setDevGuards } from "./guards/dev";
19
25
  export { createLiveWriter, type LiveWriter, type LiveWriterOpts } from "./guards/live-writer";
20
26
  export { guardedTransaction, type GuardedTx, type GuardedTxOpts } from "./guards/guarded-tx";
@@ -22,6 +28,7 @@ export { writeRuntimeResource } from "./guards/resource-writer";
22
28
  export * from "./catalog";
23
29
  export * from "./helpers/phase-set";
24
30
  export * from "./helpers/version-stamps";
31
+ export * from "./helpers/churn-guard";
25
32
  export * from "./ops/arrange";
26
33
  export * from "./ops/selection";
27
34
  export * from "./ops/cascade";
@@ -48,9 +55,9 @@ export type { DurableUndoStatusValue } from "@vibecook/strata-ecs/durable";
48
55
  export { createDocSession, openDocSession, type DocSession, type DocSessionOpts, type OpenDocResult, } from "./doc/doc-kit";
49
56
  export { runMigrations, type MigrationCtx, type MigrationOutcome } from "./doc/migrate";
50
57
  export { p, type JsonShape, type PropSpec, type PropsDecl, type StandardSchemaV1 } from "./widget/props";
51
- export { WidgetEquipped, defineWidget, widgets, type SizeMode, type WidgetDef, type WidgetGroup, type WidgetInteraction, type WidgetPortDecl, type WidgetSurface, type WidgetType, } from "./widget/define-widget";
58
+ export { WidgetEquipped, defineWidget, renames, widgets, type SizeMode, type WidgetDef, type WidgetGroup, type WidgetInteraction, type WidgetPortDecl, type WidgetBehaviorDecl, type WidgetBehaviorEntry, type WidgetRename, type WidgetRenameEntry, type WidgetSurface, type WidgetType, } from "./widget/define-widget";
52
59
  export { createWidgetEquipSystem } from "./widget/equip";
53
- export { attachSpawnParent, spawnWidget, widgetSpawnInits, type SpawnWidgetOpts } from "./widget/spawn";
60
+ export { attachSpawnBehaviors, attachSpawnParent, spawnWidget, widgetSpawnInits, type SpawnWidgetOpts } from "./widget/spawn";
54
61
  export { setWidgetProps } from "./widget/set-props";
55
62
  export { createWidgetRuntime, installWidgetRuntime, type MountEntry, type WidgetMountStore, type WidgetRuntime, } from "./widget/mount-store";
56
63
  export * from "./settings/defaults";
@@ -40,6 +40,15 @@ export interface InputEvent {
40
40
  };
41
41
  /** GL-island opt-out (pinned widget-event contract, design-002 §8): fact still lands, recognizers skip. */
42
42
  readonly surfaceHandled?: boolean;
43
+ /**
44
+ * Wheel-only opt-out (design-007 §3.5, petition I4): this wheel's deltas were
45
+ * ceded to scrollable widget content — the adapter skipped `preventDefault`
46
+ * and native scroll consumed them. The fact still lands (one input path);
47
+ * ingest stamps the one-tick `WheelHandled` tag so the wheel recognizers skip
48
+ * the tick. DISTINCT from `surfaceHandled` on purpose: the shared latch on
49
+ * the persistent mouse pointer would collide with a same-tick canvas down.
50
+ */
51
+ readonly wheelHandled?: boolean;
43
52
  /**
44
53
  * Hover-time widget-boundary fact (design-002 §8 amendment, 2026-07-18): the
45
54
  * pointer is currently over content that would opt a down out — a native
@@ -28,6 +28,18 @@ export interface ResourceMeta {
28
28
  durable: boolean;
29
29
  }
30
30
  export declare function defineComponent<const Sch extends RawSchema>(name: string, schema: Sch): ReturnType<typeof strataDefineComponent<Sch>>;
31
+ /**
32
+ * Reuse-or-define by NAME (design-008 §3.1): strata's schema registry throws
33
+ * on duplicate identifiers (its registry.ts:38), and rename legacy components
34
+ * (`<oldType>:<group>`) can legitimately re-enter the process — a test that
35
+ * defined the old widget before resetting ice registries, or a hot-reload
36
+ * double-eval — while strata's global registry survives. An existing
37
+ * SAME-SHAPED component is the same component: reuse its handle. A mismatched
38
+ * shape under the same name is a real collision: throw. The shape compare is
39
+ * JSON-stringify over the recorded raw schema — sound here because both sides
40
+ * are built by the same `strataFieldOf` constructors in the same field order.
41
+ */
42
+ export declare function ensureComponent<const Sch extends RawSchema>(name: string, schema: Sch): Component;
31
43
  export declare function defineTag(name: string): Tag;
32
44
  export declare function defineRelation(name: string, opts?: {
33
45
  arity?: "one" | "many";
@@ -1,4 +1,5 @@
1
1
  import type { Component, Tag } from "@vibecook/strata-ecs";
2
+ import type { AnyBehaviorAttachSpec, AnyBehaviorDef } from "../behavior/types";
2
3
  import { type Prefab } from "../schema/prefab";
3
4
  import type { PropSpec, PropsDecl } from "./props";
4
5
  export type WidgetSurface = "dom" | "gl";
@@ -11,6 +12,42 @@ export interface WidgetPortDecl {
11
12
  /** Compatibility keys (empty = connects to anything). */
12
13
  readonly accepts?: readonly string[];
13
14
  }
15
+ /**
16
+ * A prior durable id of this widget type (design-008 §3, petition I5). Docs
17
+ * written under the old id fold to this type at open (doc/rename.ts) and via
18
+ * the live zombie sweep (doc/rename-sweep.ts) — replicas converge through
19
+ * ordinary CRDT delivery, retiring offline byte surgery. Declares group-NAME
20
+ * continuity at the rename boundary (the pack runner's existing fence).
21
+ */
22
+ export interface WidgetRename {
23
+ /** The old durable type id (`PrefabId.id` in pre-rename docs). */
24
+ readonly type: string;
25
+ /**
26
+ * The pack version old-build writers of `type` were at (default 1). Only
27
+ * the SWEEP consumes it — a late old-shape delivery carries no marker, so
28
+ * its values chain-fold from here; the open-path runner reads the version
29
+ * off the doc's own markers instead (design-008 §6.3).
30
+ */
31
+ readonly atVersion?: number;
32
+ }
33
+ /** A rename registry entry (doc/rename runner + sweep input). */
34
+ export interface WidgetRenameEntry {
35
+ readonly widget: WidgetType;
36
+ readonly oldType: string;
37
+ readonly atVersion: number;
38
+ /** Legacy `<oldType>:<group>` components, index-aligned with `widget.groups`. */
39
+ readonly legacyGroups: readonly Component[];
40
+ }
41
+ /**
42
+ * A behavior pre-attachment on a widget type: the bare handle for defaults, or
43
+ * `Behavior.with({...})` for a starting value (design-009 §6).
44
+ */
45
+ export type WidgetBehaviorDecl = AnyBehaviorDef | AnyBehaviorAttachSpec;
46
+ /** Normalized pre-attachment (registry truth for the spawn + equip paths). */
47
+ export interface WidgetBehaviorEntry {
48
+ readonly behavior: AnyBehaviorDef;
49
+ readonly data: Readonly<Record<string, unknown>>;
50
+ }
14
51
  export interface WidgetInteraction {
15
52
  readonly selectable?: boolean;
16
53
  readonly movable?: boolean;
@@ -26,6 +63,25 @@ export interface WidgetInteraction {
26
63
  * membership — never reparenting). Default false.
27
64
  */
28
65
  readonly sweepContained?: boolean;
66
+ /**
67
+ * Keyboard claim (design-007 §3.1, petitions I1/I4). `"exclusive"`: while a
68
+ * node inside this widget holds browser focus, the engine keymap and the
69
+ * adapter's Space pan modifier stand down — keys flow to the widget's own
70
+ * handlers (the engine stops competing; it never delivers behavior). The
71
+ * dom-widgets reflector marks the host `data-canvas-keyboard` + `tabindex=-1`
72
+ * so a click anywhere in the widget acquires focus, and plain wheel over the
73
+ * widget's scrollable content cedes to native scroll (ctrl/pinch stays
74
+ * canvas zoom always). Default `"shared"` — today's behavior, byte-identical.
75
+ */
76
+ readonly keyboard?: "shared" | "exclusive";
77
+ /**
78
+ * Escape ownership under an exclusive claim (design-007 §3.3 / §7-Q1).
79
+ * `"release"` (default): Escape is the engine-reserved release gesture — it
80
+ * blurs the widget; the NEXT Escape, with focus gone, cancels gestures.
81
+ * `"widget"`: even Escape flows to the widget (vim-grade terminals); release
82
+ * is click-away or `blurFocus()`. Ignored unless `keyboard: "exclusive"`.
83
+ */
84
+ readonly keyboardEscape?: "release" | "widget";
29
85
  }
30
86
  export interface WidgetDef {
31
87
  readonly type: string;
@@ -101,6 +157,28 @@ export interface WidgetDef {
101
157
  * (its `provides` wins).
102
158
  */
103
159
  readonly provides?: readonly string[];
160
+ /**
161
+ * Prior durable ids folded to this type (design-008; petition I5). Additive:
162
+ * registers read-only LEGACY group components under the old names so
163
+ * pre-rename cells project, gates pre-rename docs "migrate" instead of
164
+ * read-only, and arms the live zombie sweep on every writable session.
165
+ */
166
+ readonly renamedFrom?: readonly WidgetRename[];
167
+ /**
168
+ * Behaviors every instance of this type carries (design-009 §6). The store
169
+ * class routes HOW, exactly as it routes everything else:
170
+ * - `durable` behaviors are attached as post-spawn `addComponent` calls in
171
+ * the SAME spawn transaction — document truth, so they sync and undo with
172
+ * the widget. (Not spawn-init overrides: those hit the prefab's
173
+ * all-builds eligibility throw in `instantiate.ts`.)
174
+ * - `runtime` behaviors are stamped at PROJECTION by the equip system,
175
+ * beside the capability tags. That is the only mechanism that also equips
176
+ * a widget arriving from a remote peer or a restored file, which a rider
177
+ * attached at spawn time would miss on every peer but the author's.
178
+ * Ephemeral behaviors are refused: an ephemeral instance IS the local
179
+ * presence peer, so it cannot ride a widget at all.
180
+ */
181
+ readonly behaviors?: readonly WidgetBehaviorDecl[];
104
182
  /**
105
183
  * fromVersion → idempotent absolute transform (M9's `runMigrations` runs the
106
184
  * chain at open; `prev`/return are flat prop records spanning every group).
@@ -146,7 +224,13 @@ export interface WidgetType {
146
224
  readonly ports: readonly WidgetPortDecl[];
147
225
  /** Runtime capability tags the equip system stamps at projection. */
148
226
  readonly capabilityTags: readonly Tag[];
227
+ /** Keyboard claim (design-007): the dom layer reads THIS (registry truth), not the tag. */
228
+ readonly keyboard: "shared" | "exclusive";
229
+ /** Escape ownership under an exclusive claim ("release" = engine-reserved). */
230
+ readonly keyboardEscape: "release" | "widget";
149
231
  readonly migrate: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
232
+ /** Pre-attached behaviors, normalized (spawn tx for durable, equip for runtime). */
233
+ readonly behaviors: readonly WidgetBehaviorEntry[];
150
234
  /** Normalized author preview component (opaque; react narrows). null = none declared. */
151
235
  readonly previewComponent: unknown;
152
236
  /** Prop overrides for the default real-component preview mount (validated names). */
@@ -158,6 +242,11 @@ export declare const widgets: {
158
242
  get(type: string): WidgetType | undefined;
159
243
  all(): WidgetType[];
160
244
  };
245
+ /** The rename registry (design-008 §3): old durable id → fold target. */
246
+ export declare const renames: {
247
+ get(oldType: string): WidgetRenameEntry | undefined;
248
+ all(): WidgetRenameEntry[];
249
+ };
161
250
  export declare function defineWidget(def: WidgetDef): WidgetType;
162
251
  /** TEST-ONLY wipe (mirrors __resetPrefabsForTests; not on the barrel). */
163
252
  export declare function __resetWidgetsForTests(): void;
@@ -25,7 +25,7 @@ export interface StandardSchemaV1<Input = unknown, Output = Input> {
25
25
  };
26
26
  };
27
27
  }
28
- export type PropSpec = StringSpec | NumberSpec | BooleanSpec | EnumSpec | JsonSpec;
28
+ export type PropSpec = StringSpec | NumberSpec | BooleanSpec | EnumSpec | JsonSpec | EntityKeySpec;
29
29
  export interface StringSpec extends StandardSchemaV1<unknown, string> {
30
30
  readonly kind: "string";
31
31
  readonly default?: string;
@@ -52,6 +52,19 @@ export interface JsonSpec extends StandardSchemaV1<unknown, unknown> {
52
52
  /** Serialized default (derived from the inner root when not given). */
53
53
  readonly default?: string;
54
54
  }
55
+ /**
56
+ * A DURABLE entity reference (design-009 §4.2): the doc key of another entity,
57
+ * stored as a plain string cell. This is the ONLY legal cross-entity reference
58
+ * in behavior data — Law 7 bans `eid` from durable cells because a runtime
59
+ * handle means nothing to the peer that receives it, while a doc key survives
60
+ * the wire, the file, and a reload. Empty string = no reference; resolution is
61
+ * `ctx.entityFor(key)`, which may legitimately return undefined while the
62
+ * target is not yet projected.
63
+ */
64
+ export interface EntityKeySpec extends StandardSchemaV1<unknown, string> {
65
+ readonly kind: "entityKey";
66
+ readonly default?: string;
67
+ }
55
68
  /** Shapes legal only INSIDE p.json (validation-only, never components). */
56
69
  export type JsonShape = {
57
70
  readonly kind: "string";
@@ -91,8 +104,29 @@ export declare const p: {
91
104
  json(inner: JsonShape, opts?: {
92
105
  default?: unknown;
93
106
  }): JsonSpec;
107
+ /**
108
+ * A durable reference to another entity by DOC KEY (design-009 §4.2).
109
+ * Behavior-facing: `defineWidget` props have no use for it (a widget's
110
+ * references are relations), but nothing rejects it there either — it is an
111
+ * ordinary string cell with a declared meaning.
112
+ */
113
+ entityKey(opts?: {
114
+ default?: string;
115
+ }): EntityKeySpec;
94
116
  array(item: JsonShape): JsonShape;
95
117
  object(fields: Readonly<Record<string, JsonShape>>): JsonShape;
96
118
  };
97
119
  /** A widget props declaration: top-level named specs (the compiler's input). */
98
120
  export type PropsDecl = Readonly<Record<string, PropSpec>>;
121
+ /**
122
+ * The CELL default for a spec — the value a whole-component write fills in for
123
+ * an untouched field, and the value a generated strata field declares.
124
+ *
125
+ * One function, three callers (widget spawn defaults, `defineWidget`'s
126
+ * essential set, `defineBehavior`'s attach defaults): each used to carry its
127
+ * own else-if chain over `spec.kind`, and each chain had to be found and
128
+ * amended when `p.entityKey` was added — one of them by inspection rather than
129
+ * by a type error, since the chains end in a catch-all `else`. It lives here,
130
+ * beside the specs whose shape it reads.
131
+ */
132
+ export declare function defaultValueOf(spec: PropSpec): string | number | boolean;
@@ -8,7 +8,9 @@
8
8
  */
9
9
  import type { Entity, OrderPlace, Relation, World } from "@vibecook/strata-ecs";
10
10
  import type { DurableStore } from "@vibecook/strata-ecs/durable";
11
+ import { type GuardedTx } from "../guards/guarded-tx";
11
12
  import { type ComponentInit } from "../schema/prefab";
13
+ import { type WidgetType } from "./define-widget";
12
14
  export interface SpawnWidgetOpts {
13
15
  readonly x: number;
14
16
  readonly y: number;
@@ -47,4 +49,16 @@ export declare function widgetSpawnInits(type: string, opts: SpawnWidgetOpts): {
47
49
  prefab: import("../schema/prefab").Prefab;
48
50
  overrides: ComponentInit[];
49
51
  };
52
+ /**
53
+ * Attach a widget type's DURABLE pre-attached behaviors (design-009 §6), as
54
+ * post-spawn `addComponent` calls inside the spawn transaction.
55
+ *
56
+ * Post-spawn rather than spawn-init overrides: `instantiate` checks every
57
+ * override against the prefab's eligible set, and a behavior is eligible on
58
+ * every entity by definition (BF-D6) — it is not in any prefab's set and never
59
+ * will be. Runtime pre-attachments are NOT here: they ride projection (the
60
+ * equip system), which is the only path that also equips a widget arriving
61
+ * from a peer or a restored file.
62
+ */
63
+ export declare function attachSpawnBehaviors(tx: GuardedTx, widget: WidgetType, e: Entity): void;
50
64
  export declare function spawnWidget(store: DurableStore, world: World, type: string, opts: SpawnWidgetOpts): Entity;
@@ -10,6 +10,8 @@ export { createGrayboxReflector } from "./reflectors/graybox";
10
10
  export { createDomWidgetsReflector, type DomWidgetsHost, type DomWidgetsReflector, } from "./reflectors/dom-widgets";
11
11
  export { startRafLoop } from "./loop";
12
12
  export { attachPointerAdapter, type GLRoute, type GLRouteVerdict, type PointerAdapterOpts } from "./pointer-adapter";
13
+ export { CLAIM_OWNS_ESCAPE, KEYBOARD_CLAIM_ATTR, isEditableTarget, keyboardClaimOf, wheelCede, type KeyboardClaim, } from "./input-ownership";
14
+ export { FOCUS_PROXY_ATTR, attachWidgetFocus, type FocusHostLookup, type WidgetFocusHandle, } from "./widget-focus";
13
15
  export { createCursorReflector } from "./reflectors/cursor";
14
16
  export { createRemoteCursorsReflector, type RemoteCursorsReflector } from "./reflectors/remote-cursors";
15
17
  export { createChromeReflector } from "./reflectors/chrome";