@loomwork/ui-hydration 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +398 -2
  4. package/dist/cache.d.ts +71 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/cache.js +157 -0
  7. package/dist/cache.js.map +1 -0
  8. package/dist/chatSource.d.ts +105 -0
  9. package/dist/chatSource.d.ts.map +1 -0
  10. package/dist/chatSource.js +259 -0
  11. package/dist/chatSource.js.map +1 -0
  12. package/dist/client.d.ts +17 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +36 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/contentRefs.d.ts +78 -0
  17. package/dist/contentRefs.d.ts.map +1 -0
  18. package/dist/contentRefs.js +129 -0
  19. package/dist/contentRefs.js.map +1 -0
  20. package/dist/controller.d.ts +228 -0
  21. package/dist/controller.d.ts.map +1 -0
  22. package/dist/controller.js +454 -0
  23. package/dist/controller.js.map +1 -0
  24. package/dist/dataProvider.d.ts +53 -0
  25. package/dist/dataProvider.d.ts.map +1 -0
  26. package/dist/dataProvider.js +77 -0
  27. package/dist/dataProvider.js.map +1 -0
  28. package/dist/esm/cache.js +151 -0
  29. package/dist/esm/cache.js.map +1 -0
  30. package/dist/esm/chatSource.js +254 -0
  31. package/dist/esm/chatSource.js.map +1 -0
  32. package/dist/esm/client.js +15 -0
  33. package/dist/esm/client.js.map +1 -0
  34. package/dist/esm/contentRefs.js +119 -0
  35. package/dist/esm/contentRefs.js.map +1 -0
  36. package/dist/esm/controller.js +449 -0
  37. package/dist/esm/controller.js.map +1 -0
  38. package/dist/esm/dataProvider.js +72 -0
  39. package/dist/esm/dataProvider.js.map +1 -0
  40. package/dist/esm/index.js +15 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/dist/esm/invoker.js +2 -0
  43. package/dist/esm/invoker.js.map +1 -0
  44. package/dist/esm/invokers.js +310 -0
  45. package/dist/esm/invokers.js.map +1 -0
  46. package/dist/esm/loomEffectProvider.js +96 -0
  47. package/dist/esm/loomEffectProvider.js.map +1 -0
  48. package/dist/esm/params.js +207 -0
  49. package/dist/esm/params.js.map +1 -0
  50. package/dist/esm/path.js +24 -0
  51. package/dist/esm/path.js.map +1 -0
  52. package/dist/esm/plan.js +190 -0
  53. package/dist/esm/plan.js.map +1 -0
  54. package/dist/esm/projection.js +230 -0
  55. package/dist/esm/projection.js.map +1 -0
  56. package/dist/esm/provider.js +416 -0
  57. package/dist/esm/provider.js.map +1 -0
  58. package/dist/esm/repair.js +53 -0
  59. package/dist/esm/repair.js.map +1 -0
  60. package/dist/esm/reshape.js +156 -0
  61. package/dist/esm/reshape.js.map +1 -0
  62. package/dist/esm/restricted.js +54 -0
  63. package/dist/esm/restricted.js.map +1 -0
  64. package/dist/esm/scheduler.js +247 -0
  65. package/dist/esm/scheduler.js.map +1 -0
  66. package/dist/esm/select.js +18 -0
  67. package/dist/esm/select.js.map +1 -0
  68. package/dist/esm/shape.js +34 -0
  69. package/dist/esm/shape.js.map +1 -0
  70. package/dist/esm/split/client.js +309 -0
  71. package/dist/esm/split/client.js.map +1 -0
  72. package/dist/esm/split/index.js +3 -0
  73. package/dist/esm/split/index.js.map +1 -0
  74. package/dist/esm/split/server.js +488 -0
  75. package/dist/esm/split/server.js.map +1 -0
  76. package/dist/esm/split/shared.js +13 -0
  77. package/dist/esm/split/shared.js.map +1 -0
  78. package/dist/index.d.ts +29 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +63 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/invoker.d.ts +10 -0
  83. package/dist/invoker.d.ts.map +1 -0
  84. package/dist/invoker.js +3 -0
  85. package/dist/invoker.js.map +1 -0
  86. package/dist/invokers.d.ts +36 -0
  87. package/dist/invokers.d.ts.map +1 -0
  88. package/dist/invokers.js +315 -0
  89. package/dist/invokers.js.map +1 -0
  90. package/dist/loomEffectProvider.d.ts +67 -0
  91. package/dist/loomEffectProvider.d.ts.map +1 -0
  92. package/dist/loomEffectProvider.js +101 -0
  93. package/dist/loomEffectProvider.js.map +1 -0
  94. package/dist/params.d.ts +103 -0
  95. package/dist/params.d.ts.map +1 -0
  96. package/dist/params.js +221 -0
  97. package/dist/params.js.map +1 -0
  98. package/dist/path.d.ts +12 -0
  99. package/dist/path.d.ts.map +1 -0
  100. package/dist/path.js +28 -0
  101. package/dist/path.js.map +1 -0
  102. package/dist/plan.d.ts +56 -0
  103. package/dist/plan.d.ts.map +1 -0
  104. package/dist/plan.js +199 -0
  105. package/dist/plan.js.map +1 -0
  106. package/dist/projection.d.ts +122 -0
  107. package/dist/projection.d.ts.map +1 -0
  108. package/dist/projection.js +238 -0
  109. package/dist/projection.js.map +1 -0
  110. package/dist/provider.d.ts +152 -0
  111. package/dist/provider.d.ts.map +1 -0
  112. package/dist/provider.js +421 -0
  113. package/dist/provider.js.map +1 -0
  114. package/dist/repair.d.ts +18 -0
  115. package/dist/repair.d.ts.map +1 -0
  116. package/dist/repair.js +57 -0
  117. package/dist/repair.js.map +1 -0
  118. package/dist/reshape.d.ts +32 -0
  119. package/dist/reshape.d.ts.map +1 -0
  120. package/dist/reshape.js +164 -0
  121. package/dist/reshape.js.map +1 -0
  122. package/dist/restricted.d.ts +21 -0
  123. package/dist/restricted.d.ts.map +1 -0
  124. package/dist/restricted.js +61 -0
  125. package/dist/restricted.js.map +1 -0
  126. package/dist/scheduler.d.ts +82 -0
  127. package/dist/scheduler.d.ts.map +1 -0
  128. package/dist/scheduler.js +254 -0
  129. package/dist/scheduler.js.map +1 -0
  130. package/dist/select.d.ts +34 -0
  131. package/dist/select.d.ts.map +1 -0
  132. package/dist/select.js +21 -0
  133. package/dist/select.js.map +1 -0
  134. package/dist/shape.d.ts +3 -0
  135. package/dist/shape.d.ts.map +1 -0
  136. package/dist/shape.js +37 -0
  137. package/dist/shape.js.map +1 -0
  138. package/dist/split/client.d.ts +132 -0
  139. package/dist/split/client.d.ts.map +1 -0
  140. package/dist/split/client.js +313 -0
  141. package/dist/split/client.js.map +1 -0
  142. package/dist/split/index.d.ts +5 -0
  143. package/dist/split/index.d.ts.map +1 -0
  144. package/dist/split/index.js +14 -0
  145. package/dist/split/index.js.map +1 -0
  146. package/dist/split/server.d.ts +160 -0
  147. package/dist/split/server.d.ts.map +1 -0
  148. package/dist/split/server.js +496 -0
  149. package/dist/split/server.js.map +1 -0
  150. package/dist/split/shared.d.ts +6 -0
  151. package/dist/split/shared.d.ts.map +1 -0
  152. package/dist/split/shared.js +19 -0
  153. package/dist/split/shared.js.map +1 -0
  154. package/package.json +77 -3
  155. package/src/cache.ts +211 -0
  156. package/src/chatSource.ts +374 -0
  157. package/src/client.ts +46 -0
  158. package/src/contentRefs.ts +195 -0
  159. package/src/controller.ts +708 -0
  160. package/src/dataProvider.ts +141 -0
  161. package/src/index.ts +86 -0
  162. package/src/invoker.ts +14 -0
  163. package/src/invokers.ts +348 -0
  164. package/src/loomEffectProvider.ts +136 -0
  165. package/src/params.ts +314 -0
  166. package/src/path.ts +26 -0
  167. package/src/plan.ts +197 -0
  168. package/src/projection.ts +297 -0
  169. package/src/provider.ts +657 -0
  170. package/src/repair.ts +70 -0
  171. package/src/reshape.ts +193 -0
  172. package/src/restricted.ts +69 -0
  173. package/src/scheduler.ts +284 -0
  174. package/src/select.ts +59 -0
  175. package/src/shape.ts +39 -0
  176. package/src/split/client.ts +449 -0
  177. package/src/split/index.ts +18 -0
  178. package/src/split/server.ts +746 -0
  179. package/src/split/shared.ts +18 -0
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
package/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ loom-ui
2
+ Copyright 2026 Sushil Chhetri
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this project except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
package/README.md CHANGED
@@ -1,3 +1,399 @@
1
- # Temporary Holding Version
1
+ # @loomwork/ui-hydration
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Resolves an Experience's provider handles through loom's transient effects and publishes them as
4
+ one loom projection, so a renderer can bind to `values.<handle>` / `status.<handle>` and redraw as
5
+ they arrive. It is the provider plane of the pipeline: `@loomwork/ui-core` compiles an Experience to a
6
+ `Blueprint`; this package answers what the Blueprint's bindings need, in-process or split across a
7
+ trust boundary; `@loomwork/ui-renderer-lit` and `@loomwork/ui-renderer-terminal` read the resulting
8
+ projection state. Hydration is **read-only and log-free** — it never emits a durable event; drift
9
+ repair is the app's own loom execution.
10
+
11
+ ## Install / import
12
+
13
+ ```jsonc
14
+ // package.json of a consumer checked out beside this repo (the packages are not published)
15
+ "@loomwork/ui-hydration": "link:../loom-ui/packages/hydration"
16
+ ```
17
+
18
+ ```ts
19
+ // Node / server: executes providers — imports loom's sandbox (QuickJS WASM) and MCP plugin.
20
+ import { HydrationController, handleInventory, serveHandles } from "@loomwork/ui-hydration";
21
+
22
+ // Browser: split hydration only. Never import the root entry from a browser bundle.
23
+ import { SplitHydrationController, HydrationStore } from "@loomwork/ui-hydration/client";
24
+ ```
25
+
26
+ ## Key concepts
27
+
28
+ - **Two entries, one trust boundary.** The root entry (`@loomwork/ui-hydration`) is server-side: it
29
+ executes providers, so its module graph reaches loom's sandbox and MCP plugin. The client entry
30
+ (`@loomwork/ui-hydration/client`, `src/client.ts`) carries only what split hydration needs in a
31
+ browser: the split controller, the loader contract, the store, and param checks — nothing that
32
+ imports the sandbox, MCP, `@loomwork/effect-runtime` or a Node builtin. `clientEntry.test.ts` walks
33
+ the *built* client bundle to enforce this, and keeps it under a 56 KiB size budget (the
34
+ minified ESM bundle). The client entry also carries the pure helpers other client-side packages need
35
+ (`terminalSteps`, `stepReferences`, `topoWaves`, `jsonPathSelect`, `outputShapeHashOf`, a
36
+ client-safe mirror of the sandbox's shape hash, parity-tested). `@loomwork/ui-planner`,
37
+ `renderer-terminal`, `renderer-lit` and `core` import ONLY `@loomwork/ui-hydration/client`;
38
+ `clientConsumers.test.ts` fails if any of them reaches the root entry, QuickJS or the MCP plugin,
39
+ by source import or built module graph.
40
+ - **The hydration projection.** Every handle publishes to one loom projection
41
+ (`HYDRATION_PROJECTION_ID = "loom-ui.hydration"`, version `HYDRATION_PROJECTION_VERSION`) that a
42
+ `HydrationStore` wraps; a compiled Blueprint's bindings read `values.<handle>` / `status.<handle>`
43
+ from it. A handle's `HandleState` is `"pending" | "ready" | "stale" | "error" | "empty"`.
44
+ - **In-process vs. split.** `HydrationController` executes providers directly through an injected
45
+ `TransientInvoker` (any loom `EffectRuntime` satisfies it structurally).
46
+ `SplitHydrationController` (client) talks to a `HandleLoader` — the app's transport — which calls
47
+ `serveHandle`/`serveHandles` (server) over that boundary; the server answers with the exact same
48
+ `HandleLoadResult` shape the in-process controller publishes, so the two paths cannot disagree.
49
+ - **`scopeInvoker` and `principal`.** When the invoker is a planner `scopeInvoker` (a `ctx.decide`
50
+ scope), do not configure `principal`: the scope already carries the execution's principal and the
51
+ call is refused otherwise.
52
+ - **Bounded cache (U17).** `new HydrationCache(clock, { maxEntries })` evicts the least recently
53
+ used result past `maxEntries` (default `DEFAULT_CACHE_MAX_ENTRIES` = 10,000; a hit or a set
54
+ refreshes an entry; a non-positive-integer cap throws `RangeError`). An evicted result is a miss,
55
+ exactly like an expired one: the next load calls the provider again. Before this the cache only
56
+ dropped an entry on a re-read after its TTL, so a long-lived ambient host grew without bound.
57
+ - **Scheduling.** Both controllers schedule by priority: P0 loads immediately, P1 batches within a
58
+ short window, P2 waits for `markVisible`. An identical step (same tool, same arguments) runs once
59
+ per pass no matter how many handles share it.
60
+ - **Params, time tokens, drift.** A step declares which argument names a load may override
61
+ (`MCPToolNode.overridable`); `$now`/`$now±<n><s|m|h|d|w>` tokens resolve once per pass against an
62
+ injected clock; drift (an input schema pin that moved, or an output that no longer reshapes or
63
+ fits its contract) is reported through `onDrift`, never repaired here.
64
+
65
+ ## API
66
+
67
+ ### The projection and its store
68
+
69
+ - `HYDRATION_PROJECTION_ID`, `HYDRATION_PROJECTION_VERSION`, `HYDRATION_SET_EVENT` — the fixed
70
+ projection id/version and the internal set event it folds.
71
+ - `hydrationProjection()`, `HydrationState`, `HandleSlice`, `HandleState`, `HandleStatus`,
72
+ `HydrationScope` — the loom `Projection` itself and its state shape. Isolation is by (experience,
73
+ principal, audience): `HydrationScope.audience` (`"broadcast"`) is its own state, and
74
+ `HydrationAudienceMismatchError` is thrown when a controller's `audience` differs from its
75
+ store's.
76
+ - `HydrationStore` — wraps a `projectionRuntime` and exposes `.state()` and `.runtimeSubscribe(fn)`
77
+ for a renderer or `watch` to read/observe.
78
+ - `isReservedHandle(handle)` — true for a handle name the projection itself reserves.
79
+
80
+ ### Running providers (server / Node only)
81
+
82
+ - `HydrationController` — in-process controller. Constructor takes `ControllerOptions` (`invoker`,
83
+ `store`, `clock?`, `setTimer?`, `batchWindowMs?`, `concurrency?`, `maxConcurrentCalls?`,
84
+ `principal?`, `cache?`, `viewer?`, `audience?`, `readPolicy?`, `declaredToolClasses?`, `undeclared?`,
85
+ `onDrift?`, `outputKinds?`, `defaultParams?`, `inputDrift?`,
86
+ `argumentHook?`, `contentRefArgs` (required), `acceptContentRefParam?`, `reshapers?`,
87
+ `providers?`, `captureSample?`). Methods: `load(exp)`, `recompose(next)`, `markVisible(handle)`,
88
+ `setParams(params, scope?)`, `invalidate(handle?)`, `inFlight()`, `dispose()`, plus `setViewer(viewer)`,
89
+ `append(handle, chunk)` and `publishValue(handle, value, { state?, kind? })` — the ones that can
90
+ fail return `{ findings: Finding[] }` or a `{ ok: false, finding }` refusal.
91
+ - **Content-reference arguments (U9b).** `contentRefArgs` is REQUIRED on `ControllerOptions` and
92
+ `ServeOptions`: the table `contentRefArgsFrom(capabilities)` builds (persist it with the
93
+ capability set; `parseContentRefTable` re-reads it), or the explicit opt-out `"none"` (an
94
+ experience with media nodes then reports `media-content-refs-unguarded`). Before ANY call a step's
95
+ marked argument must be absent / null, a `{{steps.…}}` reference to an output its tool declares to
96
+ be a content reference, or a param the host accepted; otherwise the step is refused
97
+ (`media-arg-literal` / `media-arg-unproven-ref`, or `media-arg-param-refused` for a param the gate
98
+ refused: three distinct codes on `onFinding`) and the call is NOT made. Behaviour change: when
99
+ every failed step was such a refusal, the outcome's finding (and what a split client receives) is
100
+ the public code `content-ref-refused` with one fixed message, no longer the opaque
101
+ `provider-failed`; it says nothing of which argument, step or whether a reference exists. A
102
+ refusal mixed with another failure stays `provider-failed`. A param the gate refuses used to be
103
+ reported as `media-arg-literal` and is now `media-arg-param-refused`. The same check runs BEFORE
104
+ every cache lookup (in-process and split), asking the gate again: a cached value is never served
105
+ to a principal whose gate refuses, nor after a revocation, and a result a param filled is cached
106
+ per principal only, never globally. A param that fills a marked argument (in-process `setParams`,
107
+ or a split client's params, which are client input) is refused unless `acceptContentRefParam({
108
+ handle, stepId, toolName, kind?, param, value, principalId?, principal? }) => boolean` returns
109
+ `true`. The gate MUST check that the value was ISSUED TO `principalId` (e.g. keep the `MediaRef`s
110
+ `acceptMediaAttach` accepted for each principal and return
111
+ `issued.get(principalId)?.has(value.address)`) and is still current (a revoked share answers
112
+ `false`). `null` at a marked path is stripped before the call. The `argumentHook` runs after the
113
+ check and is the host's own word. A tool whose schema mentions the marker but was unreadable
114
+ (`"unknown"`) has every replaced param gated and every literal string refused.
115
+ - `executeProvider` — runs one `ProviderSpec`'s execution graph and reshaping; what both controllers
116
+ and `serveHandle`/`serveHandles` call underneath.
117
+ - `ExecuteOptions`, `ProviderOutcome`, `DriftRecord`, `InputDriftMode` — the execution options and
118
+ outcome/drift shapes `executeProvider` and `onDrift` use.
119
+ - `DriftReport` — what `ControllerOptions.onDrift` actually receives: `{ experienceId, versionHash,
120
+ handle, spec, drift: readonly DriftRecord[], findings, proceeded }` — one or more drift records
121
+ for the handle, plus the provider spec that drifted and whether the load retried and published
122
+ (`inputDrift: "report-and-proceed"`).
123
+ - `HandleLoadResult` — what every load resolves to and what both controllers publish: `{ state:
124
+ "ready", value, finding? } | { state: "stale" | "error", finding } | { state: "empty", finding }`.
125
+ - `HydrationCache`, `CacheVariant` (`params`, `kinds`, `contracts` — the bound kinds'
126
+ `ValueContract.version`s are folded into the key; unversioned contracts leave keys unchanged, so
127
+ call `invalidate()` after changing one) — the per-principal, per-params cache
128
+ `HydrationController` uses by default (inject your own, or a fresh one per controller).
129
+ - `limitInvoker`, `memoInvoker`, `signalInvoker` — `TransientInvoker` wrappers: cap concurrency,
130
+ de-duplicate identical calls within one pass, and bind an `AbortSignal`. `memoInvoker` keys on the
131
+ principal AND every other call option except `signal` (e.g. a stamped execution identity `as`, or
132
+ the `label` that names the transient execution when `as` is absent — an authorizer may grant by
133
+ either), so calls made as different executions never share a result.
134
+ - `defineReshapers(entries)`, `jsonPathSelect(value, path)`, `outputShapeHashOf(value)` — build a
135
+ `ReshaperRegistry` for `reshaping.engine: "Named"`, run a JSONPath reshape, and hash a value's
136
+ structural shape for pinning `ProviderSpec.outputShapeHash`.
137
+ - `stepReferences`, `terminalSteps`, `topoWaves` — inspect an execution graph's `{{steps.…}}`
138
+ references, find its terminal step(s), and compute its topological waves.
139
+ - `DEFAULT_TIME_RANGE`, `checkParams` — the default `{ from: "$now-24h", to: "$now" }` params, and a
140
+ params validity check.
141
+
142
+ ### The data plane: data-provider kinds (server / Node only)
143
+
144
+ A provider step used to be an MCP tool call and nothing else. A step now names a **kind**
145
+ (`step.kind`; absent = `mcp`, the original) and hydration runs it through the `DataProvider`
146
+ registered for that kind. A loom app whose data lives in a loom effect (a `tool$`, a read-model
147
+ query, an A2A call) binds it directly, without wrapping it as an MCP server.
148
+
149
+ - `DataProvider` — `{ kind, invoke(call, invoker, opts) }`. `call` is `{ stepId, target, args,
150
+ expectedPin }`: `target` is the step's `toolName` (its id WITHIN the kind), `args` the prepared
151
+ arguments (params, time tokens, templates, hook already applied), `expectedPin` the step's
152
+ `toolSchemaHash`. `invoke` runs the target THROUGH the transient `invoker` it is handed with
153
+ `opts` (signal, principal, label) — so the invoker's authoriser, budget and per-principal identity
154
+ stay in force, the read path stays log-free, and a kind never sees the runtime.
155
+ - **The pin is the contract.** `invoke` MUST compare `expectedPin` with the target's current pin and
156
+ throw `TypedFailure("schemaDrift", { observedHash })` BEFORE running the target. That is what
157
+ makes drift a `DriftRecord` (`kind: "input"`, `providerKind` set for a non-MCP kind), what
158
+ `remapOperation` repairs, and what `inputDrift: "report-and-proceed"` retries on. A kind that
159
+ cannot say what its target's contract is must not be registered: there is no unpinned kind.
160
+ `checkDataProviderConformance(provider, { target, pin, invoker })` returns the violations (empty =
161
+ conformant), including a violation if a stale pin reached a direct call to `target`; it is a TEST
162
+ HELPER — `defineDataProviders` cannot enforce the rule, so run it in a kind's own tests.
163
+ - `defineDataProviders(providers)` → `DataProviderRegistry` (`has`/`get`/`kinds`) — composition-time
164
+ only, no discovery, no later registration; a malformed or duplicate kind id throws. Pass it as
165
+ `providers` to `HydrationController`, `serveHandle(s)` or `executeProvider`. Default
166
+ (`DEFAULT_DATA_PROVIDERS`): MCP only, byte-identical to before kinds.
167
+ - `mcpProvider` — the first registered kind: `mcp.invoke` with `expectedPin` as `expectedSchemaHash`
168
+ (the plugin verifies it per call).
169
+ - `loomEffectProvider({ runtime, effects })` — the second kind, `loom-effect` (`LOOM_EFFECT_KIND`):
170
+ a step whose `toolName` is a registered loom effect id, invoked via `invokeTransient`. Hydration
171
+ is a read path (per viewer load, unmemoised, retried), so at CONSTRUCTION every listed effect is
172
+ checked against `runtime` (`has` / `modeOf` / `kindOf`): it must be registered, explicitly `mode:
173
+ "ambient"` (ADR 0073 — an UNDECLARED mode is refused, since loom itself only refuses an explicit
174
+ `journaled` one), and not a `human` / `llm` kind. `effects` is an ALLOWLIST: a stored or
175
+ planner-authored step can name any string. Each entry is `{ inputSchema }` (the pin is computed
176
+ with `effectPin(schema)` = `jsonSchemaHash` of its JSON-Schema form, so editing the effect's zod
177
+ schema drifts every stored step with no edit to the map) or `{ pin }` (HOST-ASSERTED: nothing
178
+ checks it against the effect). Use `effectPin` also when building a planner
179
+ `CapabilityRef.toolSchemaHash`, so both come from one source. An unlisted target fails
180
+ `tool-targetUnregistered` and nothing runs; `mcp.invoke` and the sandbox effect cannot be listed.
181
+ A target that itself throws `schemaDrift` surfaces as `tool-targetFailed` (`TARGET_FAILED`), never
182
+ as pin drift — only the provider's own pin check emits `schemaDrift`, or a repair would flip the
183
+ pin forever. An effect call is not memoised within a pass (`memoInvoker` coalesces `mcp.invoke`
184
+ only); memoising `ambient` effects is a possible follow-up. A persisted experience must cross
185
+ version boundaries only through the log: an older in-memory `executeProvider` / `seal` that
186
+ predates kinds would run a kinded step as an MCP call.
187
+ - `ArgumentHookContext.kind` — the step's provider kind (absent for MCP): a hook keyed on `toolName`
188
+ must also read it, since MCP tool `x` and loom effect `x` are different targets.
189
+ - An unregistered kind fails the WHOLE provider as `provider-kind-unregistered` before any step runs
190
+ (never a throw, no partial calls); served publicly as a fixed text naming neither kind nor target.
191
+
192
+ A step's class identity is `toolKey(step)` (core): the bare `toolName` for MCP — every class
193
+ recorded before kinds still applies — and `<kind>:<toolName>` otherwise, so `toolClasses` stickiness
194
+ and `effectiveClasses` cover every kind and a loom effect never shares a class with a same-named MCP
195
+ tool.
196
+
197
+ ### Split hydration — server half
198
+
199
+ - `handleInventory(exp, opts: InventoryOptions): { inventory, findings }` — the client-safe
200
+ inventory: per bound handle, its priority, the overridable param names, and an opaque, keyed
201
+ revision.
202
+ - `InventoryOptions` — `{ revisionOf: RevisionFn, viewer?, audience?, declaredToolClasses?, undeclared? }`.
203
+ - `createServeSurface(policy?: ReadPolicy): ServeSurface` — `handleInventory` / `serveHandle` /
204
+ `serveHandles` with the read policy bound once (prefer it to the free functions); it also
205
+ exposes the merged `policy`.
206
+ - `RevisionFn` — `(handle: string, fingerprint: string) => string`. MUST be a keyed hash (e.g. an
207
+ HMAC over a server secret) — never the provider's own content fingerprint, which a client could
208
+ confirm a guessed provider against offline; `handleInventory` throws if it returns a non-string,
209
+ an empty string, or a string containing the fingerprint.
210
+ - `serveHandle(exp, request, opts: ServeOptions)`, `serveHandles(exp, requests, opts: ServeOptions)`
211
+ — answer one or many `HandleLoadRequest`s; every request is treated as untrusted and re-checked
212
+ server-side. `ServeOptions` extends `ExecuteOptions` (minus `params`/`kinds`) with `maxBatch?`
213
+ (default `DEFAULT_MAX_BATCH`, 256) and `maxConcurrentCalls?` (default
214
+ `DEFAULT_MAX_CONCURRENT_CALLS`, 32).
215
+ - `publicFinding(handle, finding)` — the public form of a finding a client may see (fixed code,
216
+ handle name only — never a tool, argument, script or exception text).
217
+
218
+ ### Split hydration — client half (also on `@loomwork/ui-hydration/client`)
219
+
220
+ - `SplitHydrationController` — same scheduling as `HydrationController`, over a `HandleInventory`,
221
+ loading through an injected `HandleLoader`. Methods: `load(inventory)`, `recompose(next)`,
222
+ `markVisible(handle)`, `setParams(params, scope?)`, `invalidate(handle?)`, `inFlight()`,
223
+ `dispose()`, plus `publishValue(handle, value, { state?, kind })` (`kind` is required here).
224
+ - `HandleLoader` — the app's transport: `load(request, signal)` and `loadMany(requests, signal)`
225
+ (both required — loads the scheduler starts together go out as one `loadMany` round trip).
226
+ - `HandleInventory`, `HandleRef`, `HandleLoadRequest`, `SplitControllerOptions` — the wire shapes.
227
+ - `handleOfPath(path): string | undefined`, `HYDRATION_VALUES_PREFIX` — client entry only. Maps a
228
+ widget's binding path (`values.<handle>`) back to its hydration handle: the exact inverse of
229
+ `@loomwork/ui-core`'s `hydrationPath`. `undefined` unless the path is `values.` plus a non-empty
230
+ handle (the bare prefix, `status.<handle>` and near-misses like `valuesh` all give `undefined`);
231
+ everything after the first `values.` is the handle, spelled exactly as `hydrationPath` spelled
232
+ it (a handle containing `.` round-trips here but will not resolve at render time, because
233
+ binding paths are resolved by splitting on `.` — keep handles dot-free). The browser takes these
234
+ from here so the client entry never pulls core's runtime graph into a bundle. The prefix is
235
+ defined in both packages (core cannot import hydration); a type-level `satisfies` and a test pin
236
+ them equal.
237
+
238
+ ### Repair (app-side, durable)
239
+
240
+ - `SCHEMA_REPAIRED`, `SchemaRepairedPayload` — the userspace event name and payload an app emits
241
+ after applying a repair, from inside a loom execution.
242
+ - `remapOperation(spec, drift)` — turns an INPUT drift record into the `Operation` that repairs it
243
+ (refuses output/reshape drift with `repair-needs-planner` — that needs `@loomwork/ui-planner`'s
244
+ `reviseForDrift`).
245
+
246
+ ## Examples
247
+
248
+ In-process hydration (illustrative — not a compiling snippet):
249
+
250
+ ```ts
251
+ import { HydrationController, HydrationStore, SCHEMA_REPAIRED, remapOperation } from "@loomwork/ui-hydration";
252
+
253
+ const store = new HydrationStore(projectionRuntime, {
254
+ experienceId: experience.experienceId,
255
+ principalId: viewer.id,
256
+ });
257
+ const controller = new HydrationController({
258
+ invoker: effectRuntime, // any EffectRuntime satisfies TransientInvoker
259
+ store,
260
+ principal: { id: viewer.id, chain: viewer.principalChain },
261
+ onDrift: (report) => durableRepair(report), // the app's own loom execution
262
+ });
263
+ controller.load(experience); // P0 now, P1 in ~50ms, P2 on markVisible
264
+ ```
265
+
266
+ Split hydration, client side (illustrative):
267
+
268
+ ```ts
269
+ import { HydrationStore, SplitHydrationController } from "@loomwork/ui-hydration/client";
270
+
271
+ const split = new SplitHydrationController({
272
+ store,
273
+ loader: {
274
+ load: (request, signal) => postJson("/hydrate", request, signal),
275
+ loadMany: (requests, signal) => postJson("/hydrate/batch", requests, signal),
276
+ },
277
+ });
278
+ split.load(inventory); // fetched from the server's handleInventory(exp, { revisionOf })
279
+ ```
280
+
281
+ ## Notable behaviours and guarantees
282
+
283
+ - **A handle that feeds a media widget is always contract-checked (U9).** When the controller is
284
+ given `outputKinds` and a handle's bound nodes resolve to `Image`/`Audio`/`Video`/`File`, the load
285
+ runs as `outputCheck: "contract"` whatever the provider declared: a shape pin passes any value of
286
+ the pinned shape (inline bytes or a locator included), and what is published reaches the store, a
287
+ seen log and a split-hydration payload. A value that is not a `MediaRef` is drift
288
+ (`output-contract-drift`), never published. A restricted media handle is never invoked, as for any
289
+ restricted handle. Kinds are resolved through the catalogue when `outputKinds` is given AND by
290
+ node type alone: a node whose `type` is a framework media kind marks its handle as media even with
291
+ NO `outputKinds`, so forgetting it fails closed (a `media-output-kinds-missing` warning says to
292
+ pass it). The split server (`serveHandle`/`serveHandles`) applies the same rule, including its
293
+ cache variant. Resolving a ref to a URL is NOT hydration's job: it is the host's `MediaResolver`,
294
+ called by the renderer (see `@loomwork/ui-core` "Media").
295
+
296
+ - **A handle that feeds a chat transcript is always contract-checked, and never a stream (U8).**
297
+ Like media: a node of type `Chat` (or a kind that resolves to it) marks its handle, the provider
298
+ is checked against the `ChatValue` contract whatever `outputCheck` it declared (controller AND
299
+ split server), and a text chunk (`append`) cannot overwrite a transcript. A live reply therefore
300
+ rides the transcript VALUE (a `ChatFeed` republishes it), with `pending` status while it streams,
301
+ rather than `appendChunk` on a string handle; the bounds of `appendChunk` still apply inside the
302
+ feed.
303
+ - **`connectConversation` (U8, `@loomwork/ui-hydration/client`)** is the browser's read half over loom's
304
+ conversation routes (`GET {base}/history`, `GET {base}/events`, ADR 0122 D6). The transport is
305
+ injected (`fetch`; `openStream(url)` so the host builds the `EventSource` with its credentials),
306
+ it imports no loom package, and it folds history pages and SSE frames into a `ChatFeed`,
307
+ publishing the result through `publish` (`SplitHydrationController.publishValue` can check only
308
+ that the handle is one the server LISTED; it cannot know the listed handle is a `Chat`, so the
309
+ `kind` it checks against is the page author's own claim). (`publishChat(controller, handle)`
310
+ writes the handle THROUGH the controller's gate: `publishValue` refuses a handle the experience
311
+ does not bind or the server did not list, a restricted one, one that is not a `Chat`, and a value
312
+ that fails the `ChatValue` contract; `pending` while a reply streams, `ready` once durable).
313
+ `start()` loads the newest page then opens the stream after its cursor; `loadOlder()` follows the
314
+ feed's cursor, one load at a time; **reasoning** (ADR 0133) rides the same connection: the
315
+ `reasoning` / `reasoning-partial` events go to the feed (which ignores them unless
316
+ `ChatFeed({ reasoning })`), and with `reasoning: true` here and a durable feed,
317
+ `loadReasoning(cursor)` fetches `GET {base}/reasoning?cursor=` for ONE reply, once, only when the host
318
+ calls it (route a Chat's `LOAD_REASONING` action to it, like `LOAD_OLDER`); a 404/403 is a settled
319
+ "none for you", a transport failure is a fixed finding and retryable, never the error text; a `reconnect` event reopens from the feed's resume cursor after
320
+ at least 250 ms (never 0), consecutive failures back off (the first wait is `reconnectMs`, 2 s,
321
+ then doubling to 30 s; a stream that stayed up 5 s or more starts the streak afresh), and a failed
322
+ first page is retried the same way instead of giving up. Failures are FIXED findings
323
+ (`history-unavailable`, `frame-invalid`, `frame-too-large`, `stream-error`, and
324
+ `reasoning-unavailable` for the reasoning fetch): no status code,
325
+ response body or exception text is carried, and an `error` event's body is not read. Sending is
326
+ NOT here: it is the host's authenticated write-back (`createWriteBack`, `@loomwork/ui-core` "Chat and
327
+ inputs").
328
+ - **A host-declared tool class floors it too.** `declaredToolClasses` (on `HydrationController`,
329
+ `handleInventory` and `serveHandle(s)`; `@loomwork/ui-core`'s `DeclaredToolClasses`) makes a handle
330
+ whose provider calls a declared tool at least that class, even when no stored node carries one.
331
+ **Bind it once with `createServeSurface(policy)`**: it returns `handleInventory`, `serveHandle`
332
+ and `serveHandles` with a core `ReadPolicy` (`{ declaredToolClasses, undeclared }`) applied to
333
+ every call, so the inventory and the loader cannot disagree because one call forgot the table. A
334
+ per-call `declaredToolClasses` / `undeclared` only ADDS (`{}` or `undefined` cannot lower it). The
335
+ free `handleInventory` / `serveHandle(s)` take the table per call and are the low-level API.
336
+ `undeclared: "deny"` refuses every handle whose provider calls a tool the table does not list (no
337
+ table = an empty one: all tool-bound handles), for every viewer, until the tool is declared; a
338
+ per-call table cannot list a new tool once deny is bound. `HydrationController` takes the SAME
339
+ `readPolicy` (`new HydrationController({ readPolicy })`; the separate `declaredToolClasses` /
340
+ `undeclared` options join it raise-only), so a host passes ONE policy to the controller and to
341
+ `createServeSurface`.
342
+ - **A classified handle is never fetched for a viewer who may not see it (fail-closed).** Pass
343
+ `viewer: { clearance }` (`@loomwork/ui-core`'s `Viewer`) to `HydrationController`, `handleInventory`
344
+ and `serveHandle`/`serveHandles`. A handle bound by any node whose `sensitivity` is above the
345
+ clearance is not planned, not cached and its provider is never invoked, and the controller writes
346
+ NOTHING to the store for it: no value and no status, so a store projected to a client does not
347
+ even name the handle (a stale entry is removed). The viewer is snapshotted at construction;
348
+ `setViewer` re-plans (drops now-restricted values, aborts their in-flight loads, loads newly
349
+ allowed ones). Token streams obey the same gate: `ctl.append(handle, chunk)` (and `store.append`
350
+ once a controller fronts the store) refuses a handle that is restricted for the viewer
351
+ (`restricted`), not bound (`stream-unbound`), or — with `outputKinds` — not a `StreamingText`
352
+ widget (`stream-kind-mismatch`); nothing is written. `handleInventory` omits it (the client learns
353
+ neither name nor revision); the server answers a client that names it anyway exactly as for a
354
+ handle the experience does not bind (`unknown-handle`, no existence oracle) and logs the full
355
+ `restricted` finding to `onFinding`. No viewer = no clearance, with one `viewer-not-supplied` warn
356
+ when anything is classified; unclassified handles are unaffected. Use the same viewer you pass to
357
+ `compile`, and one store per viewer.
358
+
359
+ - **`empty` is an answer, not an error.** A step whose `{{steps.<id>.output.<path>}}` indexes past
360
+ the end of a real (but short) upstream array publishes `empty` — a warn (`upstream-empty`), no
361
+ value, never cached, never drift, never repaired. Every other miss (a bad key, `null` mid-path, a
362
+ step with no output) stays `unresolved-step-reference` → `error`.
363
+ - **Relative time is read once per pass.** `$now`/`$now±<n><unit>` resolve against one clock read,
364
+ so two steps in the same load asking for "the last day" agree; anything else starting `$now` is
365
+ refused (`invalid-time-token`) before it reaches a tool.
366
+ - **Calls are capped and de-duplicated.** `maxConcurrentCalls` bounds the whole controller (or one
367
+ server batch); beneath it, one step memo per pass runs an identical step once no matter how many
368
+ handles share it, ref-counted so one caller's abort never cancels another caller's still-pending
369
+ wait on the same call.
370
+ - **A provider never reaches a browser.** Split hydration ships only the inventory
371
+ (`handleInventory`) and answers requests (`serveHandle`); a client only ever sees a finding's
372
+ PUBLIC form (fixed code + handle name) — the full finding (tool, step, script, exception text)
373
+ reaches only the server's own `onFinding` hook.
374
+ - **Drift is reported, never repaired here.** `onDrift` fires with a `DriftReport`
375
+ (`{ experienceId, versionHash, handle, spec, drift: readonly DriftRecord[], findings, proceeded
376
+ }`) — one or more typed `DriftRecord`s for the handle, not a single record. Applying a fix
377
+ (`remapOperation` for input drift, `@loomwork/ui-planner`'s `reviseForDrift` for output drift) and
378
+ emitting `SCHEMA_REPAIRED` is the app's job, inside its own durable execution. Nothing in this
379
+ package dedupes or bounds repairs — an app must dedupe on `(versionHash, handle)` and cap
380
+ attempts itself.
381
+ - **A `scriptError` payload is untrusted, unlogged text.** A reshaping script's own exception can
382
+ echo the raw output; loom-ui classifies drift by the failure's typed channel alone and never
383
+ copies that text into a finding message, handle status or drift record.
384
+ - **Recompose keeps shared handles.** `controller.load(nextExperience)` (or
385
+ `SplitHydrationController.recompose`) aborts and drops handles the new artifact no longer binds
386
+ and keeps the ones still bound — no full re-hydrate on every revision.
387
+
388
+ ## See also
389
+
390
+ - [../core/README.md](../core/README.md) — the Experience Model and `compile` this package
391
+ hydrates.
392
+ - [../planner/README.md](../planner/README.md) — `reviseForDrift` repairs the output drift this
393
+ package reports.
394
+ - [../renderer-lit/README.md](../renderer-lit/README.md) and
395
+ [../renderer-terminal/README.md](../renderer-terminal/README.md) — read the projection this
396
+ package publishes.
397
+ - [../../docs/reference/hydration.md](../../docs/reference/hydration.md) — the full hydration
398
+ options reference and split-hydration wiring.
399
+ - [../../README.md](../../README.md) — root overview.