@fundamental-engine/core 0.9.1 → 0.9.3

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 (124) hide show
  1. package/README.md +12 -11
  2. package/dist/agents/event-agent.d.ts.map +1 -1
  3. package/dist/agents/event-agent.js +13 -9
  4. package/dist/agents/event-agent.js.map +1 -1
  5. package/dist/config/forces.config.js +2 -2
  6. package/dist/config/forces.config.js.map +1 -1
  7. package/dist/conformance/run.d.ts +2 -2
  8. package/dist/conformance/run.d.ts.map +1 -1
  9. package/dist/conformance/run.js +30 -27
  10. package/dist/conformance/run.js.map +1 -1
  11. package/dist/conformance/types.d.ts +3 -0
  12. package/dist/conformance/types.d.ts.map +1 -1
  13. package/dist/contracts/guards.d.ts +3 -1
  14. package/dist/contracts/guards.d.ts.map +1 -1
  15. package/dist/contracts/guards.js.map +1 -1
  16. package/dist/contracts/passport.d.ts +6 -0
  17. package/dist/contracts/passport.d.ts.map +1 -1
  18. package/dist/contracts/passport.js +12 -3
  19. package/dist/contracts/passport.js.map +1 -1
  20. package/dist/core/accretion.d.ts +1 -1
  21. package/dist/core/accretion.d.ts.map +1 -1
  22. package/dist/core/accretion.js +1 -1
  23. package/dist/core/accretion.js.map +1 -1
  24. package/dist/core/events.d.ts +3 -3
  25. package/dist/core/events.d.ts.map +1 -1
  26. package/dist/core/events.js +1 -1
  27. package/dist/core/feedback-sink.d.ts +1 -1
  28. package/dist/core/feedback-sink.d.ts.map +1 -1
  29. package/dist/core/feedback-sink.js +1 -2
  30. package/dist/core/feedback-sink.js.map +1 -1
  31. package/dist/core/field-snapshot.d.ts +21 -0
  32. package/dist/core/field-snapshot.d.ts.map +1 -0
  33. package/dist/core/field-snapshot.js +0 -0
  34. package/dist/core/field-snapshot.js.map +1 -0
  35. package/dist/core/field.d.ts.map +1 -1
  36. package/dist/core/field.js +1006 -58
  37. package/dist/core/field.js.map +1 -1
  38. package/dist/core/frame-harness.d.ts +2 -2
  39. package/dist/core/frame-harness.d.ts.map +1 -1
  40. package/dist/core/frame-harness.js +2 -2
  41. package/dist/core/frame-harness.js.map +1 -1
  42. package/dist/core/geometry.d.ts +9 -0
  43. package/dist/core/geometry.d.ts.map +1 -1
  44. package/dist/core/geometry.js +16 -0
  45. package/dist/core/geometry.js.map +1 -1
  46. package/dist/core/governance.d.ts +20 -0
  47. package/dist/core/governance.d.ts.map +1 -0
  48. package/dist/core/governance.js +77 -0
  49. package/dist/core/governance.js.map +1 -0
  50. package/dist/core/host.d.ts +95 -21
  51. package/dist/core/host.d.ts.map +1 -1
  52. package/dist/core/host.js +53 -1
  53. package/dist/core/host.js.map +1 -1
  54. package/dist/core/integrator.d.ts +42 -1
  55. package/dist/core/integrator.d.ts.map +1 -1
  56. package/dist/core/integrator.js +229 -27
  57. package/dist/core/integrator.js.map +1 -1
  58. package/dist/core/lane-registry.d.ts +14 -0
  59. package/dist/core/lane-registry.d.ts.map +1 -0
  60. package/dist/core/lane-registry.js +64 -0
  61. package/dist/core/lane-registry.js.map +1 -0
  62. package/dist/core/projection-agent-json.d.ts +12 -0
  63. package/dist/core/projection-agent-json.d.ts.map +1 -0
  64. package/dist/core/projection-agent-json.js +37 -0
  65. package/dist/core/projection-agent-json.js.map +1 -0
  66. package/dist/core/query-lens.d.ts +6 -0
  67. package/dist/core/query-lens.d.ts.map +1 -0
  68. package/dist/core/query-lens.js +24 -0
  69. package/dist/core/query-lens.js.map +1 -0
  70. package/dist/core/reactions.d.ts +7 -0
  71. package/dist/core/reactions.d.ts.map +1 -1
  72. package/dist/core/reactions.js +7 -0
  73. package/dist/core/reactions.js.map +1 -1
  74. package/dist/core/render-modes.d.ts +64 -0
  75. package/dist/core/render-modes.d.ts.map +1 -1
  76. package/dist/core/render-modes.js +165 -0
  77. package/dist/core/render-modes.js.map +1 -1
  78. package/dist/core/scanner.d.ts +29 -3
  79. package/dist/core/scanner.d.ts.map +1 -1
  80. package/dist/core/scanner.js +46 -3
  81. package/dist/core/scanner.js.map +1 -1
  82. package/dist/core/spatial-hash.d.ts +8 -0
  83. package/dist/core/spatial-hash.d.ts.map +1 -1
  84. package/dist/core/spatial-hash.js +26 -3
  85. package/dist/core/spatial-hash.js.map +1 -1
  86. package/dist/core/types.d.ts +762 -8
  87. package/dist/core/types.d.ts.map +1 -1
  88. package/dist/diagnostics/probes.d.ts +11 -1
  89. package/dist/diagnostics/probes.d.ts.map +1 -1
  90. package/dist/diagnostics/probes.js +48 -8
  91. package/dist/diagnostics/probes.js.map +1 -1
  92. package/dist/forces/extended.d.ts.map +1 -1
  93. package/dist/forces/extended.js +8 -2
  94. package/dist/forces/extended.js.map +1 -1
  95. package/dist/forces/natural.d.ts.map +1 -1
  96. package/dist/forces/natural.js +11 -12
  97. package/dist/forces/natural.js.map +1 -1
  98. package/dist/index.d.ts +6 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +6 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/recipes/compile.d.ts.map +1 -1
  103. package/dist/recipes/compile.js +3 -1
  104. package/dist/recipes/compile.js.map +1 -1
  105. package/dist/recipes/schema.d.ts +1 -1
  106. package/dist/recipes/schema.d.ts.map +1 -1
  107. package/dist/recipes/schema.js +1 -0
  108. package/dist/recipes/schema.js.map +1 -1
  109. package/dist/record/record.d.ts +1 -1
  110. package/dist/record/record.d.ts.map +1 -1
  111. package/dist/semantic/layers.d.ts +16 -2
  112. package/dist/semantic/layers.d.ts.map +1 -1
  113. package/dist/semantic/layers.js +20 -3
  114. package/dist/semantic/layers.js.map +1 -1
  115. package/dist/semantic/materials.js +3 -3
  116. package/dist/semantic/materials.js.map +1 -1
  117. package/dist/semantic/states.js +1 -1
  118. package/dist/semantic/states.js.map +1 -1
  119. package/dist/version.d.ts +1 -1
  120. package/dist/version.js +1 -1
  121. package/dist/visual/visualization.d.ts.map +1 -1
  122. package/dist/visual/visualization.js +4 -0
  123. package/dist/visual/visualization.js.map +1 -1
  124. package/package.json +3 -2
@@ -27,6 +27,21 @@ export interface Vec2 {
27
27
  x: number;
28
28
  y: number;
29
29
  }
30
+ /** A 3D vector — the optional z lane (a `depth > 0` field) and the accumulator's linear channel. */
31
+ export interface Vec3 {
32
+ x: number;
33
+ y: number;
34
+ z: number;
35
+ }
36
+ /** An axis-aligned rectangle in field coordinates, `DOMRect`-shaped (x/y/width/height) so a caller
37
+ * can pass `el.getBoundingClientRect()` straight into a {@link FieldQuery}'s `at`. (Distinct from the
38
+ * engine-internal {@link Rect} in geometry, which is centre + half-extents.) */
39
+ export interface FieldRect {
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ }
30
45
  /** A force id. Open string so the registry can be extended (§20), but the
31
46
  * canonical set is enumerated in `config/forces.config.ts`. */
32
47
  export type Token = string;
@@ -60,6 +75,25 @@ export interface Particle {
60
75
  */
61
76
  z?: number;
62
77
  vz?: number;
78
+ /**
79
+ * OPTIONAL ORIENTATION LANE (substrate doc 04 §Step 6): `orient` is the particle's angle (radians,
80
+ * about the z axis) and `spin` its angular velocity. Undefined ⇒ no orientation ⇒ byte-identical to
81
+ * the spin-less engine — exactly the z-lane discipline. Only a `torque`-style force ever writes
82
+ * `spin`; the integrator advances `orient += spin · dt` (and damps) only when `spin` is defined.
83
+ * Renderers may ignore it; nothing in the base field depends on a particle facing a direction.
84
+ */
85
+ orient?: number;
86
+ spin?: number;
87
+ /**
88
+ * OPTIONAL ACCELERATION LANE (velocity-Verlet, #659): the previous step's acceleration
89
+ * a(t) = Δv/dt, stored so the next position full-step can take `x += v·dt + ½·a·dt²` and the
90
+ * velocity half-step can average `½·(a + a′)·dt`. Only the `'velocity-verlet'` integrator ever
91
+ * writes these; undefined ⇒ 0 ⇒ the default engine never materializes them (the z-lane
92
+ * discipline — byte-identical when unused).
93
+ */
94
+ ax?: number;
95
+ ay?: number;
96
+ az?: number;
63
97
  /** inertial mass — 1 = nominal (§21). */
64
98
  m: number;
65
99
  /** ∈ [0,1]; drives color (toward accent), size, and glow (§2.2). */
@@ -102,14 +136,64 @@ export interface AtomPayload {
102
136
  readonly weight?: number;
103
137
  readonly [key: string]: unknown;
104
138
  }
139
+ /**
140
+ * Who owns a body's position (substrate doc 04 §body-authority). `anchored` (default) — the DOM/host
141
+ * rect is authoritative, re-measured each frame (today's behavior for all bodies). `kinematic` — the
142
+ * engine writes the body's visual transform while the DOM stays the rendered object (the shipped
143
+ * `data-move` / transform pattern). `dynamic` — the engine owns position/velocity: the body integrates
144
+ * under the net field each frame and moves (recoil / field-to-body coupling, doc 04 §Step 5). Anchored
145
+ * and kinematic behave exactly as before. (Literal momentum-recoil from the body's own emission, torque,
146
+ * and conservation are later refinements.)
147
+ */
148
+ export type BodyAuthority = 'anchored' | 'kinematic' | 'dynamic';
149
+ /**
150
+ * A body's FIRST-CLASS IDENTITY (substrate critical path). A stable, structured handle for referring to
151
+ * a body across frames, snapshots, diffs, and relationships — decoupled from object reference and from
152
+ * display text. `id` is the stable primary key: it MUST be unique within a field and MUST NOT change for
153
+ * the life of the body. The rest is optional metadata that lets a consumer group / route bodies (e.g. a
154
+ * bridge keying host meshes off `host`, or an agent filtering by `kind`).
155
+ *
156
+ * DOCTRINE — identity is NOT:
157
+ * · display text (a heading's words are not its identity — they can change while identity holds);
158
+ * · necessarily a DOM `id` (a DOM id is one *source* of a stable id, not the concept);
159
+ * · an object reference (references don't survive a rescan or a serialize/replay round-trip).
160
+ * Snapshots, diffs, and relationships key on `identity.id`. When a body carries no supplied identity, the
161
+ * engine DERIVES a stable one deterministically (the element's DOM id, else a monotonic `body-N` counter —
162
+ * never `Math.random`, which is banned on the reproducible paths) so identity is always present and stable.
163
+ */
164
+ export interface FieldBodyIdentity {
165
+ /** the stable primary key — unique within the field, constant for the body's life. Equals the reading's
166
+ * top-level `id` (back-compat). Snapshot/diff/replay/relationships key on this. */
167
+ id: string;
168
+ /** optional grouping namespace (e.g. an app/module the body belongs to). Free-form; opaque to the engine. */
169
+ namespace?: string;
170
+ /** optional kind/type tag (e.g. `'card'`, `'heading'`, `'agent'`). Free-form; opaque to the engine. */
171
+ kind?: string;
172
+ /** optional host/owner tag (e.g. a renderer or view that owns the body's rendered object). Free-form. */
173
+ host?: string;
174
+ }
105
175
  /**
106
176
  * A registered DOM element acting as a force source (§3.1). Parsed from
107
177
  * `data-*` attributes; the runtime fields are refreshed each scan/frame.
108
178
  */
109
179
  export interface Body {
110
180
  el: HTMLElement;
181
+ /** FIRST-CLASS IDENTITY (see {@link FieldBodyIdentity}). Supplied via `addBody({ identity })` or the
182
+ * `identify` field option, else lazily DERIVED and cached the first time the body is keyed. Once
183
+ * resolved it is stable for the body's life; snapshots/diff/replay/relationships key on `identity.id`. */
184
+ identity?: FieldBodyIdentity;
111
185
  /** space-joined force ids from `data-body` (they compose, §4). */
112
186
  tokens: Token[];
187
+ /** who owns this body's position (`data-authority`); default `'anchored'`. See {@link BodyAuthority}. */
188
+ authority?: BodyAuthority;
189
+ /** engine-owned position + velocity for a `dynamic` body (substrate doc 04 §Step 5 — recoil). Lazily
190
+ * initialized from the body's first measured centre, then integrated under the net field each frame
191
+ * and written back to `cx`/`cy` (so `measureBodies`' per-frame rect overwrite doesn't reset it).
192
+ * Untouched for anchored/kinematic bodies. */
193
+ bx?: number;
194
+ by?: number;
195
+ bvx?: number;
196
+ bvy?: number;
113
197
  /** `tokens` split into `{ modifiers, forces, sources }` per the modifier contract
114
198
  * (workover v0.3). The scanner fills it at parse time; the integrator memoizes it
115
199
  * lazily for bodies built elsewhere (conformance, tests). Modifiers carry the
@@ -139,6 +223,10 @@ export interface Body {
139
223
  * box, not its centre, so matter gathers in a shell around the shape (field-systems
140
224
  * Stage C). Undefined ⇒ point source (the default). */
141
225
  shaped?: boolean;
226
+ /** `data-charge-gated` (opt-in, #711) — restrict `fieldflow` to *charged* matter (`charge ≠ 0`),
227
+ * modelling magnetized plasma tied to the field line. Undefined/false ⇒ the default neutral-medium
228
+ * advection (fieldflow transports ALL matter). Only read by the `fieldflow` force. */
229
+ chargeGated?: boolean;
142
230
  /** `data-species` — the species tag this body stamps on matter it *emits* (a `spawn` source),
143
231
  * so multiple ecologies (pollen vs seeds vs spores) can share one field. Undefined ⇒ 0. */
144
232
  species?: number;
@@ -177,6 +265,10 @@ export interface Body {
177
265
  warpHas?: boolean;
178
266
  /** source mass M for `gravity`/`charge` (§20.10/§21). */
179
267
  M: number;
268
+ /** INERTIAL mass (substrate momentum, #872) — how hard the body is to *move*, distinct from `M` (how
269
+ * strongly it *emits*). Undefined ⇒ nominal 1 ⇒ byte-identical. Populated (∝ rendered area, clamped)
270
+ * only under `mass: 'area'`; the dynamic-body recoil integrator divides by `inertia ?? M`. */
271
+ inertia?: number;
180
272
  cx: number;
181
273
  cy: number;
182
274
  hw: number;
@@ -315,6 +407,89 @@ export interface Env {
315
407
  * follows under `fieldflow`. Set by the integrator each step from the live bodies; absent
316
408
  * in bare/probe envs, where a field-following force simply no-ops. */
317
409
  fieldAt?(x: number, y: number): Vec2;
410
+ /**
411
+ * OPT-IN impulse accumulator (substrate critical path, doc 04). When present, the
412
+ * integrator's central `applyForce` records each force's per-particle contribution here
413
+ * (net + per-force attribution) WITHOUT changing the integration math — the force still
414
+ * updates velocity as before. Absent on the default hot path (zero overhead, byte-identical
415
+ * behavior); a diagnostic / Field-Query probe sets it to read structured attribution. The
416
+ * shape is dimension-aware from day one so orientation/time/semantic channels are not painted
417
+ * into a corner, even though only `linear` is populated today. Read-only contract: setting
418
+ * `accum` never alters how matter moves. */
419
+ accum?: FieldImpulseAccumulator;
420
+ /** The integration scheme (substrate doc 04 §Step 3). `undefined`/`'legacy'` is the shipped
421
+ * semi-implicit Euler with per-frame decay (the default — unchanged). `'fixed'` is the opt-in
422
+ * fixed-timestep integrator: additive force impulses and the `FRICTION`/`HEAT_DECAY` decays scale
423
+ * with `dt`, so motion is frame-rate independent. At `dt === 1` (the reference rate, and every
424
+ * golden/conformance run) the two are byte-identical, so opting in never moves the golden.
425
+ * `'velocity-verlet'` (#659) is the opt-in second-order scheme: the position full-step uses the
426
+ * previous step's stored acceleration, the force pass evaluates a′ at the updated position, and
427
+ * the velocity takes the half-step average — see the integrator for the exact math and the
428
+ * velocity-dependence / kinematic-force approximations. It changes trajectories BY DESIGN once
429
+ * opted into; the default path never engages it. */
430
+ integrator?: IntegratorMode;
431
+ /** INTERNAL (velocity-Verlet only): set by the central `applyForce` when a *kinematic*
432
+ * (velocity-REPLACING) force actually changed the current particle's velocity, and reset by the
433
+ * integrator at the top of each particle's force pass. A reflection / relaunch / teleport is a
434
+ * discontinuity, not an acceleration — the Verlet half-step average is skipped for that particle
435
+ * that step. Never touched on the `'legacy'`/`'fixed'` paths. */
436
+ kinTouch?: boolean;
437
+ }
438
+ /** The integration scheme for the field (see {@link Env.integrator}). */
439
+ export type IntegratorMode = 'legacy' | 'fixed' | 'velocity-verlet';
440
+ /**
441
+ * A single force's contribution to one agent in one step, in one channel (substrate doc 04).
442
+ * The unit the diagnostics (`causality`/`prediction`), Field Query, and Causal Replay consume:
443
+ * "this matter moved 0.42 in linear x because of `attract`."
444
+ */
445
+ export interface ForceAttribution {
446
+ /** the contributing force token (`Force.token`). */
447
+ force: Token;
448
+ /** which channel the contribution lands in. Only `linear` is populated today. */
449
+ channel: 'linear' | 'angular' | 'thermal' | 'temporal' | 'semantic' | 'constraint';
450
+ /** the contribution value — a `{x,y,z}` Δv for `linear`; a scalar for `thermal`. */
451
+ contribution: {
452
+ x: number;
453
+ y: number;
454
+ z: number;
455
+ } | number;
456
+ /** dimensions this force couples, if any (the coupling passport, doc 04 / dimensional-coupling). */
457
+ couplesDimensions?: string[];
458
+ }
459
+ /**
460
+ * A dimension-aware impulse accumulator (substrate doc 04). Collects per-force contributions for
461
+ * one agent so cause can be attributed before/independent of integration. `linear` is the running
462
+ * net Δv; `attribution` is the per-force breakdown. The optional channels (`angular`/`thermal`/
463
+ * `temporal`/`semantic`) are declared now so the contract does not assume all force is `vx/vy` —
464
+ * they are not populated until those dimensions are restored.
465
+ */
466
+ export interface FieldImpulseAccumulator {
467
+ /** running net linear Δv (x/y, plus z when the lane is engaged). */
468
+ linear: {
469
+ x: number;
470
+ y: number;
471
+ z: number;
472
+ };
473
+ /** angular Δω contribution (θx/θy/θz) — populated when a force writes `Particle.spin` (doc 04 §Step 6). */
474
+ angular?: {
475
+ x: number;
476
+ y: number;
477
+ z: number;
478
+ };
479
+ /** thermal (heat) contribution — populated when a force changes `Particle.heat` (doc 04 §Step 6). */
480
+ thermal?: number;
481
+ /** temporal contribution (delay/decay/phase) — `decay` populated when a force changes mortal matter's
482
+ * `Particle.age` (frames-to-live); delay/phase reserved (doc 04 §Step 6). */
483
+ temporal?: {
484
+ delay?: number;
485
+ decay?: number;
486
+ phase?: number;
487
+ };
488
+ /** semantic-channel contributions (attention/confidence/memory) — `attention` populated with the body's
489
+ * conserved-attention multiplier (`Body.attn`) when active; confidence/memory reserved (doc 04 §Step 6). */
490
+ semantic?: Record<string, number>;
491
+ /** per-force breakdown — preserves explainability (Paper 31 §6). */
492
+ attribution: ForceAttribution[];
318
493
  }
319
494
  /**
320
495
  * A force module (§4). The engine owns the loop and everything conserved; a
@@ -410,6 +585,53 @@ export type ConditionRegistry = Record<string, Condition>;
410
585
  export type OverlayMode = 'off' | 'streamlines' | 'force-vectors' | 'field-lines' | 'grid' | 'temperature' | 'energy' | 'path' | 'data';
411
586
  /** One reading, or an additive stack of readings, for `setOverlay` / `FieldOptions.overlay`. */
412
587
  export type OverlayInput = OverlayMode | readonly OverlayMode[];
588
+ /**
589
+ * Consumable field-resource budgets — upper bounds a host/session/user/app sets on what the field is
590
+ * PERMITTED to spend, distinct from what doctrine *allows* (that is governance — static lint). Each is
591
+ * optional; an unset budget means "unbounded / engine default". Values are normalized `0..1` unless the
592
+ * one-line note says otherwise. Carried on {@link FieldPolicy.budgets}.
593
+ *
594
+ * WIRED today: `motion` (folds into the effective motion allowance alongside reduced-motion + perf
595
+ * pressure) and `privacy` (gates body `data` in snapshots). The rest are DECLARED-not-yet-enforced —
596
+ * accepted and carried on the policy for host/tooling introspection, wired as their consumers land.
597
+ */
598
+ export interface FieldBudgets {
599
+ /** WIRED. `0..1` cap on how much motion the field may express; `0` behaves as reduced-motion (frozen). */
600
+ motion?: number;
601
+ /** DECLARED. `0..1` cap on applied force magnitude — the share of the impulse budget matter may absorb. */
602
+ force?: number;
603
+ /** DECLARED. `0..1` cap on conserved-attention spend (§2.4) — the finite focus budget. */
604
+ attention?: number;
605
+ /** DECLARED. `0..1` cap on thermal/heat accumulation the field may carry. */
606
+ thermal?: number;
607
+ /** DECLARED. `0..1` cap on render cost the field may spend (draw layers / fill). */
608
+ render?: number;
609
+ /** WIRED. `0..1` privacy budget; below the `PRIVACY_DATA_THRESHOLD` (0.5) snapshots withhold body `data`. */
610
+ privacy?: number;
611
+ /** DECLARED. `0..1` accessibility floor — the minimum non-motion legibility the field must preserve. */
612
+ accessibility?: number;
613
+ /** DECLARED. `0..1` cap on how much field state agent readers (query/snapshot/agent-json) may consume. */
614
+ agentRead?: number;
615
+ }
616
+ /**
617
+ * Runtime FIELD POLICY — what THIS host / session / user / app PERMITS, evaluated live. Distinct lane
618
+ * from GOVERNANCE (what doctrine allows — static lint): policy can only tighten, never loosen, the
619
+ * accessibility floor (reduced-motion always wins; a policy can lower motion but never raise it above
620
+ * what the host/user reduced-motion state allows). Set at creation via {@link FieldOptions.policy} and
621
+ * live via {@link FieldHandle.setPolicy}; read via {@link FieldHandle.policy}. Purely additive — a field
622
+ * with no policy behaves exactly as before.
623
+ */
624
+ export interface FieldPolicy {
625
+ /** permit body `data` to appear in snapshots (default: fall through to `FieldSnapshotOptions.includeData`). */
626
+ allowBodyDataInSnapshots?: boolean;
627
+ /** permit motion-expressing projections/animation at all; `false` pins the effective motion budget to 0. */
628
+ allowMotionProjection?: boolean;
629
+ /** `0..1` host/session cap on motion; folded (via `min`) with reduced-motion + perf pressure into the
630
+ * effective motion allowance the integrator/easing path reads. Reduced-motion can only lower it. */
631
+ maxMotionBudget?: number;
632
+ /** consumable-resource budgets (see {@link FieldBudgets}). */
633
+ budgets?: Partial<FieldBudgets>;
634
+ }
413
635
  export interface FieldOptions {
414
636
  /** travelling accent color (§9). */
415
637
  accent?: string;
@@ -423,8 +645,15 @@ export interface FieldOptions {
423
645
  * render projects z as a size/alpha recession. Purely additive: no API requires z.
424
646
  */
425
647
  depth?: number;
426
- /** draw the background Currents (§24); default true. Set false for the bare
427
- * free-particle field with no carrier waves. */
648
+ /** the integration scheme (substrate doc 04 §Step 3); default `'legacy'`. `'fixed'` opts into the
649
+ * frame-rate-independent fixed-timestep integrator (additive impulses and decay scale with `dt`);
650
+ * identical to legacy at the reference frame rate. `'velocity-verlet'` (#659) opts into the
651
+ * second-order velocity-Verlet scheme (higher positional accuracy; trajectories differ by
652
+ * design). See {@link Env.integrator}. */
653
+ integrator?: IntegratorMode;
654
+ /** draw the background Currents (§24); default **false** (opt-in, #979 — the signals-first
655
+ * companion to `render: 'none'`). A bare field has no carrier waves; set true for the ambient
656
+ * resting structure + the bound shimmer reservoir. */
428
657
  waves?: boolean;
429
658
  /** wave layout style: `'linear'` (default horizontal lines) or `'circular'` (concentric orbits around a center). */
430
659
  waveStyle?: 'linear' | 'circular';
@@ -451,13 +680,87 @@ export interface FieldOptions {
451
680
  * dots), 'streamlines' (draw the force field itself — diagnostic, REPLACES the dots),
452
681
  * 'flow' (the dots AND the streamlines drawn together in the one underlay canvas —
453
682
  * particles drifting along the visible flow, with no separate front surface and no
454
- * `mix-blend`, so it stays a single cheap layer). */
455
- render?: 'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none';
456
- /** first-class mass (§21.3): when true, particle mass ∝ size and body forces
457
- * accelerate by `a = F/m` (heavier matter moves less). Default false (unit mass). */
683
+ * `mix-blend`, so it stays a single cheap layer), 'knockout' (figure-ground inversion:
684
+ * an accent field wash with matter punched out as negative space — clip the canvas to
685
+ * type with a host CSS mask for the field-inside-letters treatment, #667), 'redshift'
686
+ * (dots tinted by spectral shift — Doppler from radial velocity + gravitational red
687
+ * near body wells, #668), 'blackbody' (dots tinted by energy on a thermal ramp, ember
688
+ * → white → blue-white, #669), 'depth' (the z lane made visible: far-to-near painter's
689
+ * sorting, perspective parallax, defocus with distance — pairs with `depth > 0`, #670). */
690
+ render?: 'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'knockout' | 'redshift' | 'blackbody' | 'depth' | 'none';
691
+ /**
692
+ * DECLARED render reference point (Wallpaper Rule, #975): the center of the cool→warm heat
693
+ * vignette the `dots`/`depth` swarm is tinted against — a body far from this point reads warm,
694
+ * a body near it reads cool. Given as viewport FRACTIONS `{ x, y }` ∈ [0,1] (resolved to pixels
695
+ * against the live canvas each frame, so it tracks resize). Formerly a hardcoded `(W/2, H·0.4)`
696
+ * painted into the draw path (a content-independent "gray debt"): the default `{ x: 0.5, y: 0.4 }`
697
+ * reproduces it exactly, so the default render is byte-identical. Only affects the `dots`/`depth`
698
+ * render modes. **Experimental.** */
699
+ heatCenter?: {
700
+ x: number;
701
+ y: number;
702
+ };
703
+ /**
704
+ * DECLARED render reference point (Wallpaper Rule, #975): the OBSERVER the `redshift` mode
705
+ * measures each particle's radial velocity against — receding matter reddens, approaching matter
706
+ * blues, at-rest at the observer. Given as viewport FRACTIONS `{ x, y }` ∈ [0,1] (resolved to
707
+ * pixels against the live canvas each frame). Formerly a hardcoded `(W/2, H/2)` (a "gray debt"):
708
+ * the default `{ x: 0.5, y: 0.5 }` reproduces it exactly, so the `redshift` render is byte-identical.
709
+ * Only affects the `redshift` render mode. **Experimental.** */
710
+ redshiftObserver?: {
711
+ x: number;
712
+ y: number;
713
+ };
714
+ /**
715
+ * DECLARED render reference point (Wallpaper Rule, #975): the perspective focal length (in CSS px)
716
+ * of the `depth` mode's camera — the size/parallax recession the z lane is projected through. A
717
+ * larger focal flattens the perspective (a longer lens); a smaller one exaggerates it. Formerly a
718
+ * hardcoded `FOCAL = 480` (a "gray debt"): the default `480` reproduces it exactly, so the `depth`
719
+ * render is byte-identical. Only affects the `depth` render mode (pairs with `depth > 0`).
720
+ * **Experimental.** */
721
+ depthFocal?: number;
722
+ /**
723
+ * DECLARED page-layout reference (Wallpaper Rule, #975): how the density `heatmap` glow fades out
724
+ * as the page scrolls past the hero. `start` is the scroll position (in VIEWPORTS, `scrollY / H`)
725
+ * at which the fade begins, `span` is how many viewports it takes to reach fully transparent — the
726
+ * layer is full above `start·H` and gone by `(start + span)·H`. Formerly a hardcoded
727
+ * `(1.15 - scrollY/H)/0.85` baked into a core draw path — a "content = first viewport" assumption
728
+ * (a "gray debt"): the default `{ start: 0.3, span: 0.85 }` reproduces it exactly (the historical
729
+ * curve is full through `1.15 - 0.85 = 0.3` viewports and gone by `1.15`), so the heatmap glow is
730
+ * byte-identical. Set a large `span` to disable the scroll fade. Only affects the density `heatmap`
731
+ * layer. **Experimental.** */
732
+ heatmapFade?: {
733
+ start: number;
734
+ span: number;
735
+ };
736
+ /** first-class mass (§21.3): when true, particle mass ∝ size and body forces accelerate by `a = F/m`
737
+ * (heavier matter moves less). Also gives DYNAMIC bodies inertial mass ∝ rendered area (#872), so a
738
+ * big heading recoils slowly and a small tag snaps. Default false (unit mass, byte-identical). */
458
739
  mass?: boolean;
740
+ /** Newtonian own-emission reaction (substrate momentum, #873): when true, a DYNAMIC body feels the
741
+ * equal-and-opposite of the net impulse it imparts to nearby matter (a directional emitter recoils
742
+ * like a rocket; reciprocity closes through *motion*, not just feedback). Best paired with `mass`
743
+ * (recoil ÷ inertial mass). Default false ⇒ byte-identical. **Experimental.** */
744
+ reaction?: boolean;
459
745
  /** strength of particle-to-particle separation/repulsion force (0 to 1, default 0). */
460
746
  separation?: number;
747
+ /**
748
+ * DECLARED ambient bias (Wallpaper Rule, #978): the resting `ambient` formation's tangential
749
+ * swirl injected into `attract` bodies (`Formation.orbit`) — the gentle spiral free matter
750
+ * traces around a well at rest. Formerly a hardcoded `0.1` painted into the default formation
751
+ * preset (a "gray debt": a content-independent constant inside an honest feature). Now a
752
+ * documented, opt-in dial with the historical value as its default, so behavior is unchanged;
753
+ * set `0` for a purely radial resting attract (no spiral), or raise it for a stronger orbit.
754
+ * Applies to the `ambient` formation only (the section formations keep their authored presets);
755
+ * `<field-root ambient-orbit>`. Default `0.1`. */
756
+ ambientOrbit?: number;
757
+ /**
758
+ * DECLARED ambient bias (Wallpaper Rule, #978): the resting `ambient` formation's `wander`
759
+ * term — the per-particle drift that keeps resting matter alive rather than frozen. Formerly a
760
+ * hardcoded `1.0` in the default formation preset. Now a documented, opt-in dial defaulting to
761
+ * the historical value (behavior unchanged); lower it for a calmer rest, `0` to still the drift.
762
+ * Applies to the `ambient` formation only; `<field-root ambient-wander>`. Default `1.0`. */
763
+ ambientWander?: number;
461
764
  /** color template for the travelling accent (§9): a built-in name
462
765
  * (`'ours'` · `'heatmap'` · `'infrared'` · `'spectrum'`) or custom hex stops. */
463
766
  palette?: string | readonly string[];
@@ -493,6 +796,17 @@ export interface FieldOptions {
493
796
  * provides the canvas, core only draws. Default unset → no overlay surface.
494
797
  */
495
798
  overlayCanvas?: HTMLCanvasElement;
799
+ /**
800
+ * Field Surfaces (overlay placement, #676): a lazy alternative to `overlayCanvas`. When no
801
+ * `overlayCanvas` is bound, core calls this provider ONCE — the first time an overlay reading actually
802
+ * becomes active (a non-`off` `setOverlay`, or `setRender` leaving `'none'` with a reading already
803
+ * set) — to obtain the surface. This lets a host (e.g. `<field-root>`) defer creating its
804
+ * full-viewport, mix-blend light-DOM canvas until an overlay is switched on, so the common
805
+ * `overlay: off` path never puts a canvas into the compositing tree at boot. Return `null` to decline
806
+ * (stays surface-less). Ignored when `overlayCanvas` is set. Keeps core DOM-free — the host still owns
807
+ * the element and its CSS placement.
808
+ */
809
+ overlayCanvasProvider?: () => HTMLCanvasElement | null;
496
810
  /** initial overlay visualization mode (Field Surfaces); default `'off'`. */
497
811
  overlay?: OverlayInput;
498
812
  /**
@@ -538,6 +852,21 @@ export interface FieldOptions {
538
852
  * you; inject a custom host for a headless renderer / different document / tests.
539
853
  */
540
854
  host?: FieldHost;
855
+ /**
856
+ * Initial runtime {@link FieldPolicy} — what this host/session/user/app PERMITS (runtime rules),
857
+ * distinct from governance (what doctrine allows). Change it live with {@link FieldHandle.setPolicy}.
858
+ * Purely additive — omit for the unbounded default. */
859
+ policy?: FieldPolicy;
860
+ /**
861
+ * FIRST-CLASS IDENTITY resolver (substrate critical path): derive a {@link FieldBodyIdentity} for a
862
+ * DOM-scanned body from its element. Called once per body, the first time the body is keyed; the
863
+ * returned identity is cached and used for query/snapshot/diff/replay/relationship keying. Return
864
+ * `undefined` (or omit the option) to fall back to the default derivation (the element's DOM `id`,
865
+ * else a monotonic `body-N`). The `id` a resolver returns MUST be unique within the field and stable
866
+ * for the body's life. Programmatic `addBody({ identity })` overrides this. Purely additive — a field
867
+ * with no `identify` behaves exactly as before.
868
+ */
869
+ identify?: (el: HTMLElement) => FieldBodyIdentity | undefined;
541
870
  }
542
871
  /** Per-element feedback values the engine produces each frame (Phase D3 seam). */
543
872
  export interface FeedbackChannels {
@@ -545,7 +874,7 @@ export interface FeedbackChannels {
545
874
  density?: number;
546
875
  /** the ambient heatmap density at the body → `--field-heatmap-density`. */
547
876
  heatmapDensity?: number;
548
- /** sink accretion fill ∈ [0,1] → `--load` / `--mass`. */
877
+ /** sink accretion fill ∈ [0,1] → `--load`. */
549
878
  load?: number;
550
879
  /** cross-boundary lit signal ∈ [0,1] → `--lit` + thresholded `field:lit` / `field:dim`. */
551
880
  lit?: number;
@@ -588,6 +917,13 @@ export interface AgentHandle {
588
917
  export interface BodySpec {
589
918
  /** the force ids this body emits (space-joined string or array), e.g. `'attract swirl'`. */
590
919
  tokens: string | readonly string[];
920
+ /** FIRST-CLASS IDENTITY for this programmatic body (see {@link FieldBodyIdentity}). Supply a stable
921
+ * `id` (unique in the field) plus optional `namespace`/`kind`/`host`, so snapshots/diff/replay and a
922
+ * bridge can reference this body by identity rather than the returned handle. A bare string is shorthand
923
+ * for `{ id }`. Omitted ⇒ the engine derives a stable synthetic `body-N`. */
924
+ identity?: FieldBodyIdentity | string;
925
+ /** who owns this body's position; default `'anchored'`. See {@link BodyAuthority}. */
926
+ authority?: BodyAuthority;
591
927
  /** overall force magnitude (scales every token). */
592
928
  strength?: number;
593
929
  /** radius of influence, in field px. */
@@ -659,6 +995,387 @@ export interface EdgeView {
659
995
  /** whether the relationship was exercised this tick (its source body was salient). */
660
996
  active: boolean;
661
997
  }
998
+ /** What a {@link FieldQuery} should return. Omitted ⇒ a sensible default (`bodies`, `metrics`,
999
+ * `relationships`, plus `influences` when the query targets a point/region). */
1000
+ export type FieldQueryInclude = 'bodies' | 'metrics' | 'relationships' | 'influences';
1001
+ /** A user-defined interpretation lens over a query/snapshot reading (substrate query phase 2) — a
1002
+ * declarative scope, NOT an opinionated preset catalog: the caller supplies the lens, the field/`applyLens`
1003
+ * filters by it. Each clause is an allow-list; omitting a clause keeps everything in that dimension.
1004
+ * Pure metadata — a lens never changes field state. **EXPERIMENTAL.** */
1005
+ export interface FieldLens {
1006
+ /** lens id, echoed onto the result as `FieldQueryResult.lens`. */
1007
+ id: string;
1008
+ label?: string;
1009
+ /** keep only these metric keys (applied to global metrics + each body's metrics/dimensions). */
1010
+ metrics?: readonly string[];
1011
+ /** keep only influences in these accumulator channels (a missing `channel` counts as `'linear'`). */
1012
+ channels?: readonly ForceAttribution['channel'][];
1013
+ /** keep only bodies carrying at least one of these tokens. */
1014
+ tokens?: readonly Token[];
1015
+ }
1016
+ /** A structured question put to the live field (read-only; never mutates state). */
1017
+ export interface FieldQuery {
1018
+ /** where to look: a point (`{x, y}`) or a rectangle (`{x, y, width, height}` — `DOMRect`-shaped).
1019
+ * Omitted ⇒ a global query over the whole field. */
1020
+ at?: Vec2 | FieldRect;
1021
+ /** for a point `at`, the query radius in field px (default 240). Ignored for a rect or global query. */
1022
+ radius?: number;
1023
+ /** which sections to include; omitted ⇒ the default set (see {@link FieldQueryInclude}). */
1024
+ include?: readonly FieldQueryInclude[];
1025
+ /** interpret the result through a lens (substrate query phase 2) — scopes metrics/influences/bodies.
1026
+ * Equivalent to calling `applyLens(result, lens)` on the answer. **EXPERIMENTAL.** */
1027
+ lens?: FieldLens;
1028
+ }
1029
+ /** A body as seen by a query — identity, box, active tokens, and its measured metrics/dimensions. */
1030
+ export interface FieldBodyReading {
1031
+ /** stable id: the element's `id` when present, else a per-field synthetic (`body-N`). Equals
1032
+ * `identity.id`. Kept as the top-level field for back-compat; new consumers may read `identity`. */
1033
+ id: string;
1034
+ /** the body's resolved FIRST-CLASS IDENTITY (see {@link FieldBodyIdentity}). `identity.id === id`;
1035
+ * `namespace`/`kind`/`host` carry any supplied structured metadata. Always present. */
1036
+ identity: FieldBodyIdentity;
1037
+ /** the body's box in field coordinates, when measured. */
1038
+ rect?: FieldRect;
1039
+ /** the composed force ids (the `data-body` tokens). */
1040
+ tokens: Token[];
1041
+ /** scalar readings (lane: metric) — e.g. `density`, `load`, `attention`, `engaged`. */
1042
+ metrics: Record<string, number>;
1043
+ /** measured field dimensions (lane: metric) — e.g. `entropy`, `coherence`, `temperature`. */
1044
+ dimensions: Record<string, number>;
1045
+ /** the Field Formation(s) biasing this body right now (the field's active formation). */
1046
+ activeFormations?: string[];
1047
+ /** who owns this body's position (see {@link BodyAuthority}); `'anchored'` by default. */
1048
+ authority?: BodyAuthority;
1049
+ }
1050
+ /** A relationship (edge) as seen by a query. */
1051
+ export interface FieldRelationshipReading {
1052
+ /** source / target body ids (see {@link FieldBodyReading.id}). */
1053
+ from: string;
1054
+ to: string;
1055
+ /** the relationship kind (`'related'` by default). */
1056
+ type: string;
1057
+ /** active coupling ∈ [0,1]. */
1058
+ strength: number;
1059
+ /** slow accumulated familiarity ∈ [0,1]. */
1060
+ memory?: number;
1061
+ /** exercised this tick. */
1062
+ active: boolean;
1063
+ /** whether the edge carried causal influence this frame (today: equal to `active`). */
1064
+ causal: boolean;
1065
+ }
1066
+ /** A single force's influence at the query point/region — which body's which force contributed how
1067
+ * much (from the impulse accumulator; lane: force). */
1068
+ export interface FieldInfluenceReading {
1069
+ /** the body whose force exerted the influence. */
1070
+ source: string;
1071
+ /** the influenced target, when the query has one (the query point/region). */
1072
+ target?: string;
1073
+ /** the contributing force token. */
1074
+ force: Token;
1075
+ /** which accumulator channel this contribution is in (`'linear'` Δv, `'thermal'` heat, …). Default
1076
+ * `'linear'` for back-compat with readers written before the thermal channel (doc 04 §Step 6). */
1077
+ channel?: ForceAttribution['channel'];
1078
+ /** the contribution — a Δv vector for `'linear'`, a scalar heat delta for `'thermal'`. */
1079
+ contribution: number | Vec2 | Vec3;
1080
+ /** optional human-readable note (lane: diagnostic). */
1081
+ reason?: string;
1082
+ }
1083
+ /** The structured answer to a {@link FieldQuery}. Plain data; safe to serialize. */
1084
+ export interface FieldQueryResult {
1085
+ /** the query that produced this reading (echoed back). */
1086
+ query: FieldQuery;
1087
+ /** the frame this reading was taken on. */
1088
+ frame: number;
1089
+ /** the field clock at read time. */
1090
+ time: number;
1091
+ /** the resolved region (for a point/rect query). */
1092
+ region?: FieldRect;
1093
+ bodies: FieldBodyReading[];
1094
+ metrics: Record<string, number>;
1095
+ relationships: FieldRelationshipReading[];
1096
+ influences: FieldInfluenceReading[];
1097
+ /** the projections registered on the field (substrate 05) — metadata only; read-only. */
1098
+ projections: FieldProjectionInfo[];
1099
+ /** the lens id this reading was scoped through, when a `lens` was supplied (substrate query phase 2). */
1100
+ lens?: string;
1101
+ }
1102
+ /**
1103
+ * A named **snapshot profile** — a concrete inclusion preset for {@link FieldHandle.snapshot}, resolved
1104
+ * to the TIGHTEST (most private) combination of its base inclusions, any explicit `include*` flags, and
1105
+ * the runtime {@link FieldPolicy} privacy budget. A profile can only tighten a call; it never widens past
1106
+ * what policy allows.
1107
+ *
1108
+ * - `'debug'` — everything: particles, relationships, influences, and body `data` (still gated by policy).
1109
+ * - `'agent'` — the Software-Agent read: stable ids + metrics + relationships + influence attribution +
1110
+ * projections, but NO opaque body `data` (raw/user-identifying payloads withheld regardless of `includeData`).
1111
+ * - `'bug-report'` — structural + versions (relationships + influences), no user data.
1112
+ * - `'public'` — minimal: ids + shape (bodies/metrics/projections), no relationships, influences, or data.
1113
+ */
1114
+ export type SnapshotProfile = 'debug' | 'agent' | 'bug-report' | 'public';
1115
+ /** Options for {@link FieldHandle.snapshot}. */
1116
+ export interface FieldSnapshotOptions {
1117
+ /** include the raw particle pool (heavier; off by default for lightweight exports). */
1118
+ includeParticles?: boolean;
1119
+ /** include the relationship (edge) graph (default true). */
1120
+ includeRelationships?: boolean;
1121
+ /** include each body's opaque `data` record (default false — privacy-preserving). */
1122
+ includeData?: boolean;
1123
+ /** include per-body force attribution (each body's own forces at its centre, via the impulse
1124
+ * accumulator) so a later `replay()` can derive `cause: 'force'` steps. Off by default. */
1125
+ includeInfluences?: boolean;
1126
+ /** apply a named {@link SnapshotProfile} preset. Composes with the explicit `include*` flags and the
1127
+ * {@link FieldPolicy} privacy budget, always resolving to the TIGHTEST (most private) result — a
1128
+ * profile can never widen past what policy or an explicit deny allows. */
1129
+ profile?: SnapshotProfile;
1130
+ }
1131
+ /**
1132
+ * A scoped read CAPABILITY an {@link AgentFieldView} grants. Each names one dimension of the field's
1133
+ * read surface; a capability set is an allow-list — a dimension the caps don't include is stripped from
1134
+ * every reading (it tightens, never widens). Read-only throughout — there is no write capability, because
1135
+ * *agent-readable is not agent-writable* (see `docs/canonical/agent-consumption-model.md`).
1136
+ */
1137
+ export type AgentCapability = 'read:metrics' | 'read:relationships' | 'read:influences' | 'read:snapshots' | 'read:body-data' | 'read:projections' | 'read:diagnostics' | 'read:replay';
1138
+ /** Options for {@link FieldHandle.forAgent} — the capability grant + optional redaction list. */
1139
+ export interface AgentViewOptions {
1140
+ /** the capabilities this agent view grants. An allow-list: any dimension not listed is stripped from
1141
+ * every reading. An empty set yields the most-restricted view (ids + shape only). */
1142
+ capabilities: AgentCapability[];
1143
+ /** dotted paths stripped from every reading AFTER capability scoping (e.g. `'body.data'`, `'host.user'`,
1144
+ * `'metrics.temperature'`). `body.*` / `relationship.*` / `influence.*` / `projection.*` prefixes address
1145
+ * the per-entry shape of each list; a bare top-level key addresses the result/snapshot itself. */
1146
+ redactions?: string[];
1147
+ }
1148
+ /**
1149
+ * A READ-ONLY facade over a field, scoped to a set of {@link AgentCapability}s — the surface a Software
1150
+ * Agent uses to read the field safely. It exposes ONLY scoped `query()` / `snapshot()` (and `replay()`
1151
+ * when `read:replay` is granted). It has NO mutation methods — no `applyForce`, no `addBody`, no
1152
+ * `setPolicy` — enforced by the facade's very shape: *agent-readable is not agent-writable*. Every
1153
+ * reading is tightened to the granted capabilities, then any `redactions` paths are stripped, and the
1154
+ * result can never widen past what the field's {@link FieldPolicy} already permits.
1155
+ */
1156
+ export interface AgentFieldView {
1157
+ /** the granted capabilities (a frozen copy). */
1158
+ readonly capabilities: readonly AgentCapability[];
1159
+ /** the redaction paths (a frozen copy). */
1160
+ readonly redactions: readonly string[];
1161
+ /** a capability-scoped, redacted {@link FieldQueryResult}. Dimensions the caps don't grant are absent
1162
+ * (no `read:influences` → no influences; no `read:relationships` → no relationships; etc.). */
1163
+ query(q?: FieldQuery): FieldQueryResult;
1164
+ /** a capability-scoped, redacted {@link FieldSnapshot}. Body `data` is withheld unless `read:body-data`
1165
+ * is granted (and policy permits it); a `profile`/`include*` request can only tighten from here. */
1166
+ snapshot(opts?: FieldSnapshotOptions): FieldSnapshot;
1167
+ /** narrate how the field changed between two snapshots — present ONLY when `read:replay` is granted
1168
+ * (otherwise `undefined`, so the facade's shape reflects the grant). */
1169
+ replay?(a: FieldSnapshot, b: FieldSnapshot, opts?: ReplayOptions): CausalReplay;
1170
+ }
1171
+ /** A body captured in a {@link FieldSnapshot}. */
1172
+ export interface FieldBodySnapshot {
1173
+ /** stable id — equals `identity.id`; snapshot/diff/replay key on it. */
1174
+ id: string;
1175
+ /** the body's resolved FIRST-CLASS IDENTITY (see {@link FieldBodyIdentity}). Always present. */
1176
+ identity: FieldBodyIdentity;
1177
+ /** who owns this body's position (see {@link BodyAuthority}); `'anchored'` by default. */
1178
+ authority?: BodyAuthority;
1179
+ /** the body's box in field coordinates (anchored bodies). */
1180
+ rect?: FieldRect;
1181
+ /** the body's centre in field coordinates (z = 0 for an anchored body). */
1182
+ position?: Vec3;
1183
+ tokens: Token[];
1184
+ metrics: Record<string, number>;
1185
+ dimensions: Record<string, number>;
1186
+ /** the body's opaque record, only when `includeData` was set. */
1187
+ data?: unknown;
1188
+ }
1189
+ /** One particle captured in a {@link FieldSnapshot} (only when `includeParticles`). */
1190
+ export interface FieldParticleSnapshot {
1191
+ x: number;
1192
+ y: number;
1193
+ z: number;
1194
+ heat: number;
1195
+ size: number;
1196
+ }
1197
+ /** A portable capture of field state at a moment in time — inspect, compare, test, export, or hand to
1198
+ * an agent. Plain data; safe to serialize. Format is versioned via {@link FieldSnapshot.version}. */
1199
+ export interface FieldSnapshot {
1200
+ /** a per-field unique id (`snap-<frame>-<n>`). */
1201
+ id: string;
1202
+ /** the field clock at capture (`env.t`). */
1203
+ createdAt: number;
1204
+ /** the frame captured. */
1205
+ frame: number;
1206
+ /** the engine build (`FIELD_VERSION`) — the snapshot-format version. */
1207
+ version: string;
1208
+ /** the active Field Formation id(s) at capture. */
1209
+ formations: string[];
1210
+ bodies: FieldBodySnapshot[];
1211
+ relationships: FieldRelationshipReading[];
1212
+ metrics: Record<string, number>;
1213
+ /** per-body force attribution at capture (only when `includeInfluences`) — each body's own forces at
1214
+ * its centre, by channel; lets `replay()` derive `cause: 'force'` steps. */
1215
+ influences?: FieldInfluenceReading[];
1216
+ /** the projections registered on the field at capture (substrate 05) — metadata only. */
1217
+ projections: FieldProjectionInfo[];
1218
+ particles?: FieldParticleSnapshot[];
1219
+ }
1220
+ /** A change to one body between two snapshots. */
1221
+ export interface BodyChange {
1222
+ id: string;
1223
+ kind: 'added' | 'removed' | 'changed';
1224
+ /** per-metric `{ from, to }` for the metrics that changed (kind `'changed'`). */
1225
+ metrics?: Record<string, {
1226
+ from: number;
1227
+ to: number;
1228
+ }>;
1229
+ }
1230
+ /** A change to one relationship between two snapshots. */
1231
+ export interface RelationshipChange {
1232
+ from: string;
1233
+ to: string;
1234
+ type: string;
1235
+ kind: 'added' | 'removed' | 'changed';
1236
+ strength?: {
1237
+ from: number;
1238
+ to: number;
1239
+ };
1240
+ active?: {
1241
+ from: boolean;
1242
+ to: boolean;
1243
+ };
1244
+ }
1245
+ /** A change to one field-level metric between two snapshots. */
1246
+ export interface MetricChange {
1247
+ key: string;
1248
+ from: number;
1249
+ to: number;
1250
+ }
1251
+ /** A Field Formation that activated or deactivated between two snapshots. */
1252
+ export interface FormationChange {
1253
+ id: string;
1254
+ kind: 'activated' | 'deactivated';
1255
+ }
1256
+ /** The structured comparison of two {@link FieldSnapshot}s — what changed in the field, by lane. */
1257
+ export interface FieldDiff {
1258
+ /** the `id`s of the two snapshots compared. */
1259
+ from: string;
1260
+ to: string;
1261
+ bodyChanges: BodyChange[];
1262
+ relationshipChanges: RelationshipChange[];
1263
+ metricChanges: MetricChange[];
1264
+ formationChanges: FormationChange[];
1265
+ }
1266
+ /** The kinds of output surface a {@link FieldProjection} can target. */
1267
+ export type FieldProjectionSurface = 'css' | 'dom-attribute' | 'svg' | 'canvas' | 'typography' | 'annotation' | 'sound' | 'haptic' | 'native' | 'spatial' | 'agent-json';
1268
+ /** Where a projection writes — a minimal, DOM-shaped sink (an element's style / attributes), but open
1269
+ * so non-DOM surfaces (native, agent-json) can pass their own target. */
1270
+ export interface FieldProjectionTarget {
1271
+ style?: {
1272
+ setProperty(key: string, value: string): void;
1273
+ };
1274
+ setAttribute?(key: string, value: string): void;
1275
+ [key: string]: unknown;
1276
+ }
1277
+ /** A named mapping from field state to an output surface. `apply` is the (optional) writer; the rest is
1278
+ * declarative metadata governance + tooling read. A projection must NOT change field state. */
1279
+ export interface FieldProjection {
1280
+ id: string;
1281
+ label: string;
1282
+ /** the field channels this projection reads (e.g. `['density','confidence']`). */
1283
+ channels: string[];
1284
+ /** the surface(s) it writes to. */
1285
+ surfaces: FieldProjectionSurface[];
1286
+ /** the non-motion equivalent, for `prefers-reduced-motion` (governance: motion must translate). */
1287
+ reducedMotionEquivalent?: string;
1288
+ /** the accessibility equivalent — an alternate projection of the same state, not a fallback. */
1289
+ accessibilityEquivalent?: string;
1290
+ /** write the reading onto the target (read-only w.r.t. the field). */
1291
+ apply?(reading: Record<string, number>, target: FieldProjectionTarget): void;
1292
+ }
1293
+ /** A live reading source for an auto-applied projection — called once per write phase to produce the
1294
+ * reading handed to the projection's `apply`. The field never reads it for simulation. */
1295
+ export type ProjectionSource = () => Record<string, number>;
1296
+ /** A {@link FieldProjectionTarget} for the `agent-json` surface: it captures the last reading written to
1297
+ * it as a plain object, serializable for agent / tooling consumption. Build one with `agentJsonTarget()`
1298
+ * and pair it with `agentJsonProjection()`. **EXPERIMENTAL.** */
1299
+ export interface AgentJsonTarget extends FieldProjectionTarget {
1300
+ /** receive a reading (called by the projection's `apply`). */
1301
+ receive(reading: Record<string, number>): void;
1302
+ /** the last received reading, or null before the first write. */
1303
+ value(): Record<string, number> | null;
1304
+ /** the last received reading serialized as JSON (`"null"` before the first write). */
1305
+ json(): string;
1306
+ }
1307
+ /** Serializable metadata about a registered projection (no `apply`) — what `query()`/`snapshot()` and
1308
+ * governance tooling read. */
1309
+ export interface FieldProjectionInfo {
1310
+ id: string;
1311
+ label: string;
1312
+ channels: string[];
1313
+ surfaces: FieldProjectionSurface[];
1314
+ reducedMotionEquivalent?: string;
1315
+ accessibilityEquivalent?: string;
1316
+ }
1317
+ /** The field's projection registry ({@link FieldHandle.projections}) — register named projections and
1318
+ * apply them. Read/output only; registering a projection never changes how matter moves. */
1319
+ export interface ProjectionRegistry {
1320
+ /** register a projection (replacing any with the same id); returns an unregister fn. */
1321
+ register(projection: FieldProjection): () => void;
1322
+ /** remove a registered projection by id. */
1323
+ unregister(id: string): void;
1324
+ /** the full projection (incl. `apply`) for an id, or undefined. */
1325
+ get(id: string): FieldProjection | undefined;
1326
+ /** serializable metadata for every registered projection. */
1327
+ list(): FieldProjectionInfo[];
1328
+ /** apply a registered projection's writer to a target (no-op if the id/`apply` is absent). */
1329
+ apply(id: string, reading: Record<string, number>, target: FieldProjectionTarget): void;
1330
+ /** bind a registered projection to a target + a live reading source — the field auto-applies it once
1331
+ * per write phase (after feedback), read-only w.r.t. the field. Returns an unbind fn. Multiple
1332
+ * bindings (even of the same id) coexist; binding an unknown/`apply`-less id is inert. **EXPERIMENTAL.** */
1333
+ bind(id: string, target: FieldProjectionTarget, source: ProjectionSource): () => void;
1334
+ /** governance lint over the registered projections (substrate 05 §governance) — flags accessibility
1335
+ * gaps (a motion-capable projection with no reduced-motion equivalent; any projection with no
1336
+ * accessibility equivalent). Pure; the standalone `lintProjections` is also exported. */
1337
+ lint(): GovernanceWarning[];
1338
+ }
1339
+ /** A governance lint finding. `rule` is a stable `field/...` id (see doc 05 §field-lint-rules). The
1340
+ * severity scale matches the visual `LintSeverity` (info / warning / error / fatal). */
1341
+ export interface GovernanceWarning {
1342
+ rule: string;
1343
+ severity: 'info' | 'warning' | 'error' | 'fatal';
1344
+ /** the offending subject — e.g. a projection id. */
1345
+ subject: string;
1346
+ message: string;
1347
+ }
1348
+ /** The lane a {@link CausalReplayStep} belongs to. */
1349
+ export type CausalCause = 'force' | 'relationship' | 'metric' | 'formation' | 'measurement';
1350
+ /** Options for {@link FieldHandle.replay}. */
1351
+ export interface ReplayOptions {
1352
+ /** restrict the replay to steps touching this body id (its metrics, or a relationship endpoint). */
1353
+ focus?: string;
1354
+ }
1355
+ /** One narrated cause in a {@link CausalReplay}. */
1356
+ export interface CausalReplayStep {
1357
+ frame: number;
1358
+ time: number;
1359
+ cause: CausalCause;
1360
+ /** the body/edge the cause originates from (a body id, or a relationship's `from`). */
1361
+ source?: string;
1362
+ /** the affected target, when the cause is a relationship. */
1363
+ target?: string;
1364
+ /** a human-readable account of the change (lane: diagnostic). */
1365
+ description: string;
1366
+ /** the structured before/after behind the description (e.g. `{ from, to }`). */
1367
+ contribution?: unknown;
1368
+ }
1369
+ /** An explanation of how the field changed between two snapshots — the ordered causal steps. Pure
1370
+ * (derived from the two snapshots); plain data, safe to serialize. */
1371
+ export interface CausalReplay {
1372
+ /** the `id`s of the two snapshots replayed. */
1373
+ from: string;
1374
+ to: string;
1375
+ /** the focus body id, if the replay was scoped to one. */
1376
+ focus?: string;
1377
+ steps: CausalReplayStep[];
1378
+ }
662
1379
  /** A registered **field channel** (`FieldHandle.addField`) — an external scalar field sampled on the
663
1380
  * engine's read path. The open *input* analog of the render surfaces. */
664
1381
  export interface FieldChannelHandle {
@@ -673,6 +1390,10 @@ export interface FieldChannelHandle {
673
1390
  export interface FieldHandle {
674
1391
  /** the running engine version (`FIELD_VERSION`) — which build this field is on. */
675
1392
  readonly version: string;
1393
+ /** the field's projection registry (substrate 05) — register named projections that reveal field
1394
+ * state on an output surface (CSS / annotation / agent-json / reduced-motion …). Read/output only:
1395
+ * a projection never changes how matter moves. See {@link ProjectionRegistry}. **EXPERIMENTAL.** */
1396
+ readonly projections: ProjectionRegistry;
676
1397
  /** (re)scan the document for `[data-body]` bodies after a layout change. */
677
1398
  scan(): void;
678
1399
  /** alias of `scan`. */
@@ -711,6 +1432,23 @@ export interface FieldHandle {
711
1432
  * quality. The platform runtime forwards the governor's tier automatically; call it directly for a
712
1433
  * custom quality policy. */
713
1434
  setQualityTier(tier: number): void;
1435
+ /** the field's current runtime {@link FieldPolicy} (a frozen copy). `{}` when none was set. */
1436
+ readonly policy: FieldPolicy;
1437
+ /** Replace the runtime {@link FieldPolicy} live — what this host/session/user/app PERMITS. This is a
1438
+ * REPLACE (not a merge): pass the full policy you want in effect (`{}` clears to the unbounded
1439
+ * default). Takes effect from the next frame. The motion budget it carries folds (via `min`) with
1440
+ * reduced-motion + perf pressure — reduced-motion always wins, so a policy can lower motion but never
1441
+ * raise it. The privacy budget gates body `data` in `snapshot()`. */
1442
+ setPolicy(policy: FieldPolicy): void;
1443
+ /**
1444
+ * Derive a READ-ONLY {@link AgentFieldView} scoped to a set of {@link AgentCapability}s — the safe
1445
+ * surface a Software Agent uses to read the field. The returned facade exposes ONLY scoped
1446
+ * `query()` / `snapshot()` (and `replay()` when `read:replay` is granted); it has NO mutation methods.
1447
+ * Readings are tightened to the granted capabilities, then any `redactions` paths are stripped, and the
1448
+ * result can never widen past what the field's {@link FieldPolicy} already permits. Purely additive —
1449
+ * `forAgent` reads the same live field; it does not fork or copy it.
1450
+ */
1451
+ forAgent(opts: AgentViewOptions): AgentFieldView;
714
1452
  /**
715
1453
  * Switch the underlay render mode (§20.6) live — the surface behind content. `'none'` is the
716
1454
  * signals-only mode (§13.7 / #297): drawing stops from the next frame while the simulation and
@@ -718,7 +1456,7 @@ export interface FieldHandle {
718
1456
  * backing store (the no-allocation guarantee belongs to fields CREATED with `render: 'none'`);
719
1457
  * switching FROM `'none'` acquires the context lazily and sizes the backing store at that moment.
720
1458
  */
721
- setRender(mode: 'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none'): void;
1459
+ setRender(mode: 'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'knockout' | 'redshift' | 'blackbody' | 'depth' | 'none'): void;
722
1460
  /**
723
1461
  * Render field READINGS on the OVERLAY surface — in front of page content (Field Surfaces). Pairs
724
1462
  * with `setRender` (the underlay); set both for an immersive look. No-op unless the field was created
@@ -786,6 +1524,22 @@ export interface FieldHandle {
786
1524
  * consumer: each edge's endpoint `data`, type, live `strength`/`memory`, and whether it's active this
787
1525
  * tick. Pure, read-only. Shipped-but-unfrozen. */
788
1526
  readEdges(): ReadonlyArray<EdgeView>;
1527
+ /** Ask the live field a structured question and get back plain, serializable data — bodies,
1528
+ * metrics, relationships, and per-force influence — for a point, a rect, or the whole field.
1529
+ * Read-only and render-agnostic (works headless). The substrate's agent-/tool-readable surface;
1530
+ * see {@link FieldQuery}. **EXPERIMENTAL** — not yet in the frozen API set. */
1531
+ query(q?: FieldQuery): FieldQueryResult;
1532
+ /** Capture *what the field is doing* right now — a portable, serializable {@link FieldSnapshot}
1533
+ * (bodies, metrics, relationships, active formations; optionally particles). Read-only; works
1534
+ * headless. Pair with {@link FieldHandle.diff} to compare two snapshots. **EXPERIMENTAL.** */
1535
+ snapshot(opts?: FieldSnapshotOptions): FieldSnapshot;
1536
+ /** Compare two snapshots and report what changed in the field — body, relationship, metric, and
1537
+ * formation changes. Pure (takes two snapshots; ignores live state). **EXPERIMENTAL.** */
1538
+ diff(a: FieldSnapshot, b: FieldSnapshot): FieldDiff;
1539
+ /** Explain how the field changed between two snapshots — an ordered, narrated sequence of causes
1540
+ * (formation activations, relationship shifts, body measurements/metrics). Pure (derived from the
1541
+ * two snapshots). Pass `{ focus }` to scope it to one body. **EXPERIMENTAL.** */
1542
+ replay(a: FieldSnapshot, b: FieldSnapshot, opts?: ReplayOptions): CausalReplay;
789
1543
  /**
790
1544
  * Register a named **field channel** — a read-back substrate the host samples via `sampleField`; the
791
1545
  * engine does not (yet) couple it into forces. The open *input* analog of the render surfaces