@opetope/runtime 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 (314) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +345 -0
  4. package/README.ru.md +344 -0
  5. package/dist/application-compiler-edges.d.ts +3 -0
  6. package/dist/application-compiler-edges.js +2 -0
  7. package/dist/application-compiler-edges.js.map +1 -0
  8. package/dist/application-compiler-graph.d.ts +8 -0
  9. package/dist/application-compiler-graph.js +2 -0
  10. package/dist/application-compiler-graph.js.map +1 -0
  11. package/dist/application-compiler.d.ts +116 -0
  12. package/dist/application-compiler.js +2 -0
  13. package/dist/application-compiler.js.map +1 -0
  14. package/dist/application-conditions.d.ts +18 -0
  15. package/dist/application-conditions.js +2 -0
  16. package/dist/application-conditions.js.map +1 -0
  17. package/dist/application-definition.d.ts +30 -0
  18. package/dist/application-definition.js +2 -0
  19. package/dist/application-definition.js.map +1 -0
  20. package/dist/application-error.d.ts +10 -0
  21. package/dist/application-error.js +2 -0
  22. package/dist/application-error.js.map +1 -0
  23. package/dist/application-execution.d.ts +43 -0
  24. package/dist/application-execution.js +2 -0
  25. package/dist/application-execution.js.map +1 -0
  26. package/dist/application-feature-bindings.d.ts +11 -0
  27. package/dist/application-feature-bindings.js +2 -0
  28. package/dist/application-feature-bindings.js.map +1 -0
  29. package/dist/application-feature-instance.d.ts +5 -0
  30. package/dist/application-feature-instance.js +2 -0
  31. package/dist/application-feature-instance.js.map +1 -0
  32. package/dist/application-group-order.d.ts +29 -0
  33. package/dist/application-group-order.js +2 -0
  34. package/dist/application-group-order.js.map +1 -0
  35. package/dist/application-instance-retirement.d.ts +36 -0
  36. package/dist/application-instance-retirement.js +2 -0
  37. package/dist/application-instance-retirement.js.map +1 -0
  38. package/dist/application-open-options.d.ts +29 -0
  39. package/dist/application-open-options.js +2 -0
  40. package/dist/application-open-options.js.map +1 -0
  41. package/dist/application-port-compiler.d.ts +28 -0
  42. package/dist/application-port-compiler.js +2 -0
  43. package/dist/application-port-compiler.js.map +1 -0
  44. package/dist/attachment-call-declaration.d.ts +43 -0
  45. package/dist/attachment-call-declaration.js +2 -0
  46. package/dist/attachment-call-declaration.js.map +1 -0
  47. package/dist/attachment-declaration.d.ts +68 -0
  48. package/dist/attachment-declaration.js +2 -0
  49. package/dist/attachment-declaration.js.map +1 -0
  50. package/dist/attachment-execution.d.ts +9 -0
  51. package/dist/attachment-execution.js +2 -0
  52. package/dist/attachment-execution.js.map +1 -0
  53. package/dist/attachment-retirement-scheduler.d.ts +14 -0
  54. package/dist/attachment-retirement-scheduler.js +2 -0
  55. package/dist/attachment-retirement-scheduler.js.map +1 -0
  56. package/dist/call-option-snapshot.d.ts +20 -0
  57. package/dist/call-option-snapshot.js +2 -0
  58. package/dist/call-option-snapshot.js.map +1 -0
  59. package/dist/compile-call-target-bindings.d.ts +14 -0
  60. package/dist/compile-call-target-bindings.js +2 -0
  61. package/dist/compile-call-target-bindings.js.map +1 -0
  62. package/dist/compile-module-template.d.ts +38 -0
  63. package/dist/compile-module-template.js +2 -0
  64. package/dist/compile-module-template.js.map +1 -0
  65. package/dist/condition-group-execution.d.ts +44 -0
  66. package/dist/condition-group-execution.js +2 -0
  67. package/dist/condition-group-execution.js.map +1 -0
  68. package/dist/condition-override.d.ts +28 -0
  69. package/dist/condition-override.js +2 -0
  70. package/dist/condition-override.js.map +1 -0
  71. package/dist/condition-source.d.ts +10 -0
  72. package/dist/condition-source.js +2 -0
  73. package/dist/condition-source.js.map +1 -0
  74. package/dist/condition-types.d.ts +14 -0
  75. package/dist/condition.d.ts +28 -0
  76. package/dist/condition.js +2 -0
  77. package/dist/condition.js.map +1 -0
  78. package/dist/control-registry.d.ts +56 -0
  79. package/dist/control-registry.js +2 -0
  80. package/dist/control-registry.js.map +1 -0
  81. package/dist/dynamic-scope-child.d.ts +23 -0
  82. package/dist/dynamic-scope-child.js +2 -0
  83. package/dist/dynamic-scope-child.js.map +1 -0
  84. package/dist/dynamic-scope-controller.d.ts +16 -0
  85. package/dist/dynamic-scope-controller.js +2 -0
  86. package/dist/dynamic-scope-controller.js.map +1 -0
  87. package/dist/feature-attachment-authoring-types.d.ts +51 -0
  88. package/dist/feature-attachment-lowering.d.ts +18 -0
  89. package/dist/feature-attachment-lowering.js +2 -0
  90. package/dist/feature-attachment-lowering.js.map +1 -0
  91. package/dist/feature-attachment.d.ts +45 -0
  92. package/dist/feature-attachment.js +2 -0
  93. package/dist/feature-attachment.js.map +1 -0
  94. package/dist/feature-authoring-types.d.ts +196 -0
  95. package/dist/feature-authoring.d.ts +15 -0
  96. package/dist/feature-authoring.js +2 -0
  97. package/dist/feature-authoring.js.map +1 -0
  98. package/dist/feature-body.d.ts +53 -0
  99. package/dist/feature-body.js +2 -0
  100. package/dist/feature-body.js.map +1 -0
  101. package/dist/feature-call-authority.d.ts +8 -0
  102. package/dist/feature-call-authority.js +2 -0
  103. package/dist/feature-call-authority.js.map +1 -0
  104. package/dist/feature-call-types.d.ts +39 -0
  105. package/dist/feature-call.d.ts +18 -0
  106. package/dist/feature-call.js +2 -0
  107. package/dist/feature-call.js.map +1 -0
  108. package/dist/feature-calls.d.ts +6 -0
  109. package/dist/feature-calls.js +2 -0
  110. package/dist/feature-calls.js.map +1 -0
  111. package/dist/feature-contract.d.ts +70 -0
  112. package/dist/feature-contract.js +2 -0
  113. package/dist/feature-contract.js.map +1 -0
  114. package/dist/feature-contribution-model.d.ts +38 -0
  115. package/dist/feature-contribution-model.js +2 -0
  116. package/dist/feature-contribution-model.js.map +1 -0
  117. package/dist/feature-contribution.d.ts +126 -0
  118. package/dist/feature-contribution.js +2 -0
  119. package/dist/feature-contribution.js.map +1 -0
  120. package/dist/feature-definition-api.d.ts +53 -0
  121. package/dist/feature-definition-support.d.ts +21 -0
  122. package/dist/feature-definition-support.js +2 -0
  123. package/dist/feature-definition-support.js.map +1 -0
  124. package/dist/feature-effect.d.ts +26 -0
  125. package/dist/feature-effect.js +2 -0
  126. package/dist/feature-effect.js.map +1 -0
  127. package/dist/feature-event.d.ts +32 -0
  128. package/dist/feature-event.js +2 -0
  129. package/dist/feature-event.js.map +1 -0
  130. package/dist/feature-generation.d.ts +34 -0
  131. package/dist/feature-generation.js +2 -0
  132. package/dist/feature-generation.js.map +1 -0
  133. package/dist/feature-lazy-generation.d.ts +6 -0
  134. package/dist/feature-lazy-generation.js +2 -0
  135. package/dist/feature-lazy-generation.js.map +1 -0
  136. package/dist/feature-lazy.d.ts +44 -0
  137. package/dist/feature-lazy.js +2 -0
  138. package/dist/feature-lazy.js.map +1 -0
  139. package/dist/feature-materialization-binding.d.ts +31 -0
  140. package/dist/feature-materialization-binding.js +2 -0
  141. package/dist/feature-materialization-binding.js.map +1 -0
  142. package/dist/feature-materialization-types.d.ts +45 -0
  143. package/dist/feature-model-dependencies.d.ts +23 -0
  144. package/dist/feature-model-dependencies.js +2 -0
  145. package/dist/feature-model-dependencies.js.map +1 -0
  146. package/dist/feature-model.d.ts +60 -0
  147. package/dist/feature-model.js +2 -0
  148. package/dist/feature-model.js.map +1 -0
  149. package/dist/feature-optional.d.ts +15 -0
  150. package/dist/feature-optional.js +2 -0
  151. package/dist/feature-optional.js.map +1 -0
  152. package/dist/feature-own-lowering.d.ts +22 -0
  153. package/dist/feature-own-lowering.js +2 -0
  154. package/dist/feature-own-lowering.js.map +1 -0
  155. package/dist/feature-port-binding.d.ts +6 -0
  156. package/dist/feature-port-binding.js +2 -0
  157. package/dist/feature-port-binding.js.map +1 -0
  158. package/dist/feature-port.d.ts +66 -0
  159. package/dist/feature-port.js +2 -0
  160. package/dist/feature-port.js.map +1 -0
  161. package/dist/feature-record.d.ts +6 -0
  162. package/dist/feature-record.js +2 -0
  163. package/dist/feature-record.js.map +1 -0
  164. package/dist/feature-resource.d.ts +29 -0
  165. package/dist/feature-resource.js +2 -0
  166. package/dist/feature-resource.js.map +1 -0
  167. package/dist/feature-scope-types.d.ts +35 -0
  168. package/dist/feature-scope.d.ts +29 -0
  169. package/dist/feature-scope.js +2 -0
  170. package/dist/feature-scope.js.map +1 -0
  171. package/dist/feature-stream.d.ts +37 -0
  172. package/dist/feature-stream.js +2 -0
  173. package/dist/feature-stream.js.map +1 -0
  174. package/dist/feature-timers.d.ts +14 -0
  175. package/dist/feature-timers.js +2 -0
  176. package/dist/feature-timers.js.map +1 -0
  177. package/dist/index.d.ts +19 -0
  178. package/dist/index.js +2 -0
  179. package/dist/index.js.map +1 -0
  180. package/dist/inspection-activity-protocol.d.ts +63 -0
  181. package/dist/inspection-activity.d.ts +54 -0
  182. package/dist/inspection-activity.js +2 -0
  183. package/dist/inspection-activity.js.map +1 -0
  184. package/dist/inspection-diff.d.ts +10 -0
  185. package/dist/inspection-diff.js +2 -0
  186. package/dist/inspection-diff.js.map +1 -0
  187. package/dist/inspection-module-activity.d.ts +4 -0
  188. package/dist/inspection-module-activity.js +2 -0
  189. package/dist/inspection-module-activity.js.map +1 -0
  190. package/dist/inspection-observer.d.ts +38 -0
  191. package/dist/inspection-observer.js +2 -0
  192. package/dist/inspection-observer.js.map +1 -0
  193. package/dist/inspection-plan.d.ts +33 -0
  194. package/dist/inspection-plan.js +2 -0
  195. package/dist/inspection-plan.js.map +1 -0
  196. package/dist/inspection-protocol.d.ts +333 -0
  197. package/dist/inspection-protocol.js +2 -0
  198. package/dist/inspection-protocol.js.map +1 -0
  199. package/dist/inspection-registry.d.ts +41 -0
  200. package/dist/inspection-registry.js +2 -0
  201. package/dist/inspection-registry.js.map +1 -0
  202. package/dist/inspection-session.d.ts +92 -0
  203. package/dist/inspection-session.js +2 -0
  204. package/dist/inspection-session.js.map +1 -0
  205. package/dist/inspection-snapshot.d.ts +4 -0
  206. package/dist/inspection-snapshot.js +2 -0
  207. package/dist/inspection-snapshot.js.map +1 -0
  208. package/dist/inspection-state.d.ts +91 -0
  209. package/dist/inspection-state.js +2 -0
  210. package/dist/inspection-state.js.map +1 -0
  211. package/dist/instance-demand.d.ts +31 -0
  212. package/dist/instance-demand.js +2 -0
  213. package/dist/instance-demand.js.map +1 -0
  214. package/dist/internal.d.ts +41 -0
  215. package/dist/internal.js +2 -0
  216. package/dist/internal.js.map +1 -0
  217. package/dist/keyed-scope-controller.d.ts +11 -0
  218. package/dist/keyed-scope-controller.js +2 -0
  219. package/dist/keyed-scope-controller.js.map +1 -0
  220. package/dist/model-kernel.d.ts +18 -0
  221. package/dist/model-kernel.js +2 -0
  222. package/dist/model-kernel.js.map +1 -0
  223. package/dist/module-call-context.d.ts +7 -0
  224. package/dist/module-call-context.js +2 -0
  225. package/dist/module-call-context.js.map +1 -0
  226. package/dist/module-call-runtime.d.ts +21 -0
  227. package/dist/module-call-runtime.js +2 -0
  228. package/dist/module-call-runtime.js.map +1 -0
  229. package/dist/module-generation.d.ts +57 -0
  230. package/dist/module-generation.js +2 -0
  231. package/dist/module-generation.js.map +1 -0
  232. package/dist/module-instance-types.d.ts +156 -0
  233. package/dist/module-instance.d.ts +19 -0
  234. package/dist/module-instance.js +2 -0
  235. package/dist/module-instance.js.map +1 -0
  236. package/dist/module-runtime-identity.d.ts +4 -0
  237. package/dist/module-runtime-identity.js +2 -0
  238. package/dist/module-runtime-identity.js.map +1 -0
  239. package/dist/module-scope-open.d.ts +4 -0
  240. package/dist/module-scope-open.js +2 -0
  241. package/dist/module-scope-open.js.map +1 -0
  242. package/dist/module-scope-retirement.d.ts +3 -0
  243. package/dist/module-scope-retirement.js +2 -0
  244. package/dist/module-scope-retirement.js.map +1 -0
  245. package/dist/module-template-ir.d.ts +59 -0
  246. package/dist/owner-generation-retirement.d.ts +6 -0
  247. package/dist/owner-generation-retirement.js +2 -0
  248. package/dist/owner-generation-retirement.js.map +1 -0
  249. package/dist/owner-generation-state.d.ts +123 -0
  250. package/dist/owner-generation-state.js +2 -0
  251. package/dist/owner-generation-state.js.map +1 -0
  252. package/dist/owner-generation.d.ts +16 -0
  253. package/dist/owner-generation.js +2 -0
  254. package/dist/owner-generation.js.map +1 -0
  255. package/dist/public-module-definition.d.ts +9 -0
  256. package/dist/public-module-definition.js +2 -0
  257. package/dist/public-module-definition.js.map +1 -0
  258. package/dist/public-module-instance.d.ts +6 -0
  259. package/dist/public-module-instance.js +2 -0
  260. package/dist/public-module-instance.js.map +1 -0
  261. package/dist/public-module-retirement-diagnostics.d.ts +5 -0
  262. package/dist/public-module-retirement-diagnostics.js +2 -0
  263. package/dist/public-module-retirement-diagnostics.js.map +1 -0
  264. package/dist/public-module-retirement.d.ts +3 -0
  265. package/dist/public-module-retirement.js +2 -0
  266. package/dist/public-module-retirement.js.map +1 -0
  267. package/dist/public-module-scope.d.ts +5 -0
  268. package/dist/public-module-scope.js +2 -0
  269. package/dist/public-module-scope.js.map +1 -0
  270. package/dist/public-module-state.d.ts +28 -0
  271. package/dist/public-module-state.js +2 -0
  272. package/dist/public-module-state.js.map +1 -0
  273. package/dist/public-module-types.d.ts +295 -0
  274. package/dist/public-module.d.ts +4 -0
  275. package/dist/resource-cache.d.ts +12 -0
  276. package/dist/resource-cache.js +2 -0
  277. package/dist/resource-cache.js.map +1 -0
  278. package/dist/resource-controller.d.ts +29 -0
  279. package/dist/resource-controller.js +2 -0
  280. package/dist/resource-controller.js.map +1 -0
  281. package/dist/resource-policy.d.ts +16 -0
  282. package/dist/resource-policy.js +2 -0
  283. package/dist/resource-policy.js.map +1 -0
  284. package/dist/resource-snapshot.d.ts +12 -0
  285. package/dist/resource-snapshot.js +2 -0
  286. package/dist/resource-snapshot.js.map +1 -0
  287. package/dist/resource-types.d.ts +3 -0
  288. package/dist/runtime-error-reporting.d.ts +4 -0
  289. package/dist/runtime-error-reporting.js +2 -0
  290. package/dist/runtime-error-reporting.js.map +1 -0
  291. package/dist/stream-backpressure.d.ts +18 -0
  292. package/dist/stream-backpressure.js +2 -0
  293. package/dist/stream-backpressure.js.map +1 -0
  294. package/dist/stream-cleanup.d.ts +17 -0
  295. package/dist/stream-cleanup.js +2 -0
  296. package/dist/stream-cleanup.js.map +1 -0
  297. package/dist/stream-controller-types.d.ts +51 -0
  298. package/dist/stream-controller.d.ts +5 -0
  299. package/dist/stream-controller.js +2 -0
  300. package/dist/stream-controller.js.map +1 -0
  301. package/docs/agent-guide.md +214 -0
  302. package/docs/agent-guide.ru.md +208 -0
  303. package/docs/cookbook.md +734 -0
  304. package/docs/cookbook.ru.md +729 -0
  305. package/docs/decisions.md +1437 -0
  306. package/docs/devtools.md +423 -0
  307. package/docs/devtools.ru.md +419 -0
  308. package/docs/how-it-works.md +521 -0
  309. package/docs/how-it-works.ru.md +495 -0
  310. package/docs/releases.md +78 -0
  311. package/docs/releases.ru.md +78 -0
  312. package/docs/spec.md +874 -0
  313. package/docs/spec.ru.md +884 -0
  314. package/package.json +72 -0
@@ -0,0 +1,521 @@
1
+ # How Opetope works: creation, computation, cleanup
2
+
3
+ Status: **DRAFT**, 2026-09-04; describes branch `tech-opetope`. This document explains in plain language what happens
4
+ inside `@opetope/core`, `@opetope/runtime` and `@opetope/react` while an application runs: what is created, when it is
5
+ recomputed, and how it is cleaned up. The normative source is [spec.md](spec.md); here you get only the mechanics and the reasons for them, and the `(Dnnn)` pointers lead to [decisions.md](decisions.md). File references are given for anyone who wants to check a statement against the code.
6
+
7
+ ## 0. Map of concepts
8
+
9
+ | Word | What it is | Who creates it | When it dies |
10
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------- |
11
+ | application | a list of features, an error sink, host-bound contracts and conditions | `defineApplication` + `openApplication` | `close()` |
12
+ | condition (`Condition`) | a named `Readable<boolean>` a group of features lives by | `defineCondition`; the value comes from the host or a source feature | with the application |
13
+ | feature | a definition: `id`, `when`, `imports`, `requires`, `own`, `exports`, `provides` | `defineFeature`, once per module load | never, it is a description |
14
+ | feature instance | the live copy of a definition: models, calls, resources, contributions | the runtime, when the feature is needed and its conditions are true | retire, when a condition turns false or demand is released |
15
+ | model | a record of `Readable` and `Call` an instance owns | the factory from `own.model()`, when the instance opens | the fence and drain of the instance |
16
+ | call (`Call`) | an operation with a queue, a lane, cancellation and single-flight | `ctx.call` in a model, or `call` in `own` | cancelled at retire |
17
+ | resource, stream, event | external data materialized with a key, a retention and cancellation | `ctx.resource`, `ctx.stream`, `ctx.event` | closed in drain |
18
+ | contribution | what a feature contributes to someone else's target: a slot, a pipe, a registry, a port | `provides`, when the instance opens | withdrawn in fence |
19
+ | target (`SlotTarget`, `Pipe`, `Registry`, `Port`) | the place a contribution lands; it lives outside features, in a contracts file | `defineSlot`, `definePipe`, `defineRegistry`, `definePort` | never |
20
+
21
+ Three cross-cutting rules explain half of the behaviour:
22
+
23
+ 1. **Authenticity is checked through a `WeakMap`, not by shape.** A definition, an instance, a target, a lane and a
24
+ binding live in closed `WeakMap`/`WeakSet` registries; anything that never entered one is refused with
25
+ `TypeError('… is not authentic.')`. A framework value therefore cannot be forged with an object literal.
26
+ 2. **Everything the author supplies is snapshotted at declaration**, with an exact key-set check, and frozen. A
27
+ mutation of the source object after that is invisible.
28
+ 3. **Order is always canonical.** Features by id, contributions by id, imports by key, attachments by slot; every
29
+ teardown walks the reverse of the assembly order.
30
+
31
+ ## 1. The application
32
+
33
+ ### 1.1 Compilation: `defineApplication({ id, features, reporter })`
34
+
35
+ Compilation is synchronous and happens once, before anything is opened. This is the "compile time" of the graph laws:
36
+ a violation fails in the first manifest test.
37
+
38
+ | Step | What happens | Where |
39
+ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
40
+ | 1 | exact key set, `id` is a string and unique among the live definitions of the process, `reporter` is a function | `runtime/src/application-definition.ts` |
41
+ | 2 | the world closes: from the enabled features along `imports` to their providers, through the export contract | same file |
42
+ | 3 | features are sorted by id; a duplicate object and a duplicate id are refused | `runtime/src/application-compiler.ts` |
43
+ | 4 | import edges: a host contract becomes an application input, a feature definition resolves to its provider | same file |
44
+ | 5 | port edges `requires ↔ provides`: exactly one provider per port | `application-port-compiler.ts` |
45
+ | 6 | a Kahn topological sort with a lexicographic tie-break; a cycle of hard imports is an error | `application-compiler-graph.ts` |
46
+ | 7 | the lifetime check: a hard edge requires `provider.when ⊆ consumer.when`; a weak `optional` edge is free of the inclusion law (D105) | `application-compiler.ts` |
47
+ | 8 | groups by identical condition sets, the activation order inside a group and the order between groups from hard edges (D123); the graph plan for the observation port | same file, `application-group-order.ts` |
48
+
49
+ The laws checked here: one provider per contract and per port, one contract identity per id, no provider is an error,
50
+ no cycles, and the provider of a hard edge lives at least as long as its consumer.
51
+
52
+ ### 1.2 Opening: `openApplication(app, { cleanupFailure, conditions, imports })`
53
+
54
+ | Step | What happens |
55
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | 1 | `imports` accepts only the results of `bind(contract, value)`; every host contract is bound exactly once, and a contract that only weak imports name may be left unbound |
57
+ | 2 | `conditions` supplies a `Readable<boolean>` for every condition without `from`; the record type is derived from the application, so TypeScript catches a missing and an extra key, and the runtime repeats both checks |
58
+ | 3 | `cleanupFailure` is optional and defaults to `report`; `quarantine` makes every instance of this application keep a failed cleanup as a retryable frontier instead of a report (D182) |
59
+ | 4 | the controller starts in its constructor: opening begins synchronously, before the first `await` |
60
+ | 5 | features with an empty `when` open **sequentially**, in compilation order: `open()`, then `await ready`, then the next one |
61
+ | 6 | conditions with `from` are computed from the exports of the source features that opened |
62
+ | 7 | every condition group gets a reconciler: the desired state is `all(conditions) === true`; before it opens, a group waits for the providers of its hard edges in other groups to become ready, and before it closes it waits for their consumers to be gone (D123) |
63
+ | 8 | an `ApplicationExecution` with `ready` and `close()` is returned |
64
+
65
+ A group reconciler is one serialized worker with epochs: a condition change raises the epoch, fences the unfinished
66
+ opening and schedules a reconcile on a microtask; members open sequentially with an epoch check before each one, and
67
+ closing always runs in reverse order and tolerates failures. A condition source must publish a boolean synchronously
68
+ when it is installed, otherwise the error is immediate.
69
+
70
+ `close()` runs once: it releases the condition subscriptions in reverse order, fences the unfinished opening,
71
+ closes the groups in reverse order, awaits `ready`, retires the permanent features in reverse order, and collects
72
+ failures into one error. The observation session closes in `finally`, after the drain, so it sees the final group,
73
+ instance and contribution transitions (D167). The shared close promise exists before any source disposer runs.
74
+ If a condition subscription closes the application before returning its disposer, that disposer is released
75
+ immediately on return, no further group is installed, readiness rejects and close drains all started work (D210).
76
+
77
+ ### 1.3 Lifetime: `when`
78
+
79
+ A feature declares the conditions it lives under: `when: [authorized, miningEnabled]`. An empty `when` is a permanent
80
+ feature. Features with an identical condition set open and close together, and imports order them inside the group.
81
+ The set-inclusion law says something simple: you may not hard-depend on something that can die before you do. A
82
+ consumer that wants to read the data of a shorter-lived feature takes the weak `optional` edge and gets a `Lookup`
83
+ projection with a `missing` state (§2.4); a weak edge neither affects the activation of its provider nor pulls it into
84
+ the application world (D106).
85
+
86
+ ## 2. The feature instance
87
+
88
+ ### 2.1 At definition, once
89
+
90
+ The eager `defineFeature` form runs the `own` factory once and turns it into an intermediate representation made
91
+ of three kinds of kernel node: scope, attachment, call. A split header instead declares topology; its
92
+ `defineFeature.body` builds that representation when the body code loads (D186, D207). Everything authored reduces
93
+ to these nodes:
94
+
95
+ | In `own` | What it lowers to |
96
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
97
+ | `attach(source, { open, close })` | a scope with one attachment |
98
+ | `call({ run, within, lane, once, singleFlight, policy })` | a call node bound to the attachment from `within` |
99
+ | `calls(imports.x, [...])` | an implicit attachment on the source with no lifecycle, plus one call per key |
100
+ | `lane({ within })` | a lane record, materialized per instance |
101
+ | `effect`, `event`, `resource`, `stream`, `scope.*` | an attachment with a controller inside |
102
+ | `requires.x` | one call on the hidden requirements attachment, which forwards to the port provider; with `optional(port)` it settles a `CallError` with code `unavailable` while there is no provider (D105) |
103
+ | `model(...)` | **not a kernel node** but a data descriptor; the model factory does not run here; owned state lives only inside the model (`ctx.state`, D139) |
104
+
105
+ `exports` is a factory of the form `({ own }) => …`. It runs when the instance opens, against a live `own`, and every
106
+ field of the record it returns must be a live value form — a `Call`, a `Readable` or a `Resource` — checked key by key
107
+ (D88). `provides` runs once, synchronously, and is split into ports and contributions by the builder that produced each
108
+ entry. Every attachment of a feature is critical: the instance is not ready until all of them are.
109
+
110
+ ### 2.2 Opening an instance
111
+
112
+ | Step | What happens | Where |
113
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ |
114
+ | 1 | options are snapshotted; `imports` and `requirements` hold exactly the declared fields; an attachment takes its value only from an import or a requirement; a port value is checked against its declaration | `runtime/src/feature-generation.ts` |
115
+ | 2 | an instance record is created in state `assembly` | `public-module-instance.ts` |
116
+ | 3 | **`prepare` runs synchronously, before the first scope opens**: the model rollback participant is registered before any factory runs; then the factories create their owned nodes; then the export facade is built (it materializes `own` values lazily); then the contribution participant is registered | `module-generation.ts`, `feature-model.ts` |
117
+ | 4 | scopes open sequentially; before each one the runtime checks whether cancellation was requested | `module-generation.ts` |
118
+ | 5 | inside a scope the attachments form one wave with no order among them (D163): critical ones start before the owner commits, readiness is counted with counters and never recurses into promises; when no critical one is left the scope publishes the owner authority, calls become live, and deferred attachments start | `module-scope-open.ts` |
119
+ | 6 | the internal ready barrier yields `{ exports }`; contributions are then published, and only after successful publication does public `instance.ready` resolve | `feature-generation.ts` |
120
+
121
+ The laws: `prepare` must return synchronously; retire participants are registered before the first scope opens; the failure of a critical attachment moves the scope straight into retire, while the failure of a deferred one leaves the others and the scope's readiness untouched; a failed attachment lands in the quarantine frontier in canonical slot order.
122
+
123
+ Seen from the inside, an attachment is a small machine with a mailbox: at most one active and one waiting intent; a new
124
+ intent either joins a compatible one, replaces one that has not started, or queues up and cancels the active one.
125
+ Opening takes a permit from the readiness controller, collects a cleanup list (the first entry is always `close`), and
126
+ any error rolls the attempt back in the reverse order of that list.
127
+
128
+ ### 2.3 Retire: fence, drain, settle
129
+
130
+ ```text
131
+ retire() → fence (synchronous, one pass) → drain (asynchronous, in parallel) → settle (policy)
132
+ ```
133
+
134
+ 1. **Fence.** The state becomes `retiring`. Participants run in registration order: contributions are withdrawn from
135
+ their targets, the data scope is fenced — models are made inactive, the `AbortController` fires, `state` cells are
136
+ closed. Then every scope is fenced. Fencing is what turns any later work into a cancellation: the call authority is
137
+ invalid, and a new call through that attachment settles with a `CallError` as a cancellation rather than as a product
138
+ failure. Nothing is thrown; only the cleanup frontiers are collected.
139
+ 2. **Drain.** All frontiers run in parallel; the attachments of one scope drain as one wave, with no order among them (D163). Calls: pending ones are cancelled, executing ones get an
140
+ abort, the `once` cache and the single-flight map are cleared, lane leases are released. Timers: the registry is
141
+ closed, and after that `delay` and `interval` throw. Model subscriptions and disposers run, and failures are
142
+ collected.
143
+ 3. **Settle.** With no failures: state `retired`, every call target revoked, references dropped, the outcome is
144
+ `closed`. With failures under the `report` policy (the default): the instance is workspace anyway, and a
145
+ `FeatureError` with code `cleanup-failed` goes to the `reporter`. Under the `quarantine` policy: the outcome is
146
+ `quarantined`, with a `FeatureError` carrying code `quarantined` and an optional `retryCleanup()` capability that repeats exactly the
147
+ remaining cleanup frontier under a re-entry guard.
148
+
149
+ Retire is idempotent and cannot be started from its own callback.
150
+
151
+ The public instance names this operation `close()`; internal coordinators keep `retire`. Logical cancellation
152
+ settles a waiter without proving that an in-progress `open` or call physically stopped. The drain joins the work
153
+ already admitted and its late cleanup before releasing the source. A stream also joins its active `consume`
154
+ before closing the connection and releasing the imported value (D170).
155
+
156
+ ### 2.4 Edges between features
157
+
158
+ | Edge | What the consumer sees | Who guarantees safety |
159
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `imports: { x: feature }` | the live `Call`, `Readable` and `Resource` values of the provider instance | order: the provider opens first, the consumer closes first |
161
+ | `optional(feature)` | `Readable<Lookup<Exports>>`: `found` while the provider instance is open, `missing` while it is closed or absent from the application | one subscription on the consumer side; the edge neither affects the provider's activation nor pulls it into the world (D105, D106) |
162
+ | `optional(port)` | a `Call` that settles a `CallError` with code `unavailable` while there is no live port provider | the same weak law (D105) |
163
+ | a host contract | the value bound with `bind` at `openApplication`; `optional(contract)` gives `Readable<Lookup<T>>` with `missing` when the host did not bind it, while `onDemand(contract)` keeps the plain value and only marks the edge as deferred | the host |
164
+ | a port, `port` | a `Call`; the application picked the provider | the compiler: exactly one provider |
165
+
166
+ Models never cross a feature boundary: the UI law "a model is read only inside its own feature" rests precisely on
167
+ that.
168
+
169
+ ## 3. The model and its kernel
170
+
171
+ ### 3.1 What is created when
172
+
173
+ | Step | What happens |
174
+ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
+ | 1 | at definition, `own.model(Decl, deps?, factory)` only records a descriptor; the factory does not run |
176
+ | 2 | when the instance opens, inside `prepare`, the descriptors are walked in declaration order and each `model` receives a fresh kernel with the id `<feature>.model.<n>` |
177
+ | 3 | the kernel is a readiness controller (which is also the fence of the model), a timer registry on a shared `AbortSignal`, and the lists of executors and disposers |
178
+ | 4 | the factory runs **synchronously**; every `ctx.call`, `ctx.calls(source, keys, { lane? })` (the same selection rules as `calls` in `own`: a source method `(input, signal) => Output` becomes a `Call`, D143), `ctx.effect`, `ctx.resource`, `ctx.stream`, `ctx.event`, `ctx.scope`, `ctx.lane` and `ctx.state` creates its node immediately with the id `<model>.<kind>.<n>`; `ctx.cleanup(disposer)` registers a model cleanup (D107) |
179
+ | 5 | the returned record is frozen and stored under the descriptor ref |
180
+ | 6 | the model fence: readiness is withdrawn and the timers are closed, then the abort fires and the cells close; the drain: executors are retired, then every model disposer runs in the reverse order of registration and failures are reported; registering anything after the fence is a `TypeError` |
181
+
182
+ The dependency descriptor is a map of authentic current-feature import and call refs. Materialization resolves
183
+ it once to a readonly map before calling the factory; optional imports stay lookup readables. The zero-dependency
184
+ form is `model(Decl, factory)`. Model-to-model refs are not a dependency graph. A model-backed port selects its
185
+ call once after model creation and wraps even a passthrough call with the provider instance's fence (D169).
186
+
187
+ The kernel owns rollback before entering user code, so a factory that creates nodes and then throws still drains
188
+ them. The same rule holds for per-mount UI models. A fence synchronously closes every owned state and rejects
189
+ all subsequent node constructors; no late call, timer or subscription can escape that lifetime (D170).
190
+
191
+ ### 3.2 `state` and `update`
192
+
193
+ `ctx.state(initial)` returns an `OwnedState<Value>`: a `Readable` with an ownership brand. `ctx.update(state, value)`
194
+ accepts only that — a derived `Readable` does not compile, and a sibling model's `OwnedState` is rejected at runtime
195
+ by a private owner lookup. A write after
196
+ the fence does not throw at the writer; it is reported as a `FeatureError` with code `retired`. Writes that are equal
197
+ under `Object.is` stay silent.
198
+
199
+ ### 3.3 The call: `ctx.call({ run, lane?, once?, singleFlight?, policy? })`
200
+
201
+ `ctx.call(options)` creates the command; the `run` callback's `context.invoke(target, input)` executes an existing
202
+ one. The execution member is `invoke` in every context that provides it, including trusted attachment steps;
203
+ the factory `call` and the selected-method sugar `calls` keep their names (D243).
204
+
205
+ Selected host methods can take no parameters, one input, or an input followed by `AbortSignal`, including optional
206
+ input/signal (D244). The runtime calls each method with `(input, signal)` and its original receiver. It does not
207
+ inspect function arity; a lone parameter is always the input. Physical work that ignores cancellation still drains,
208
+ even if its caller has already settled as cancelled.
209
+
210
+ The default `queue` policy serializes calls through a lane; `latest` serializes them the same way but keeps one
211
+ waiting place; `parallel` starts them immediately, and `parallel` with a `lane` is refused. Per call:
212
+
213
+ 1. if the parent signal or its own signal is already aborted, a cancelled call is returned;
214
+ 2. a closed executor or a fenced instance yields a cancellation;
215
+ 3. a hit in the `once` cache returns the already settled result;
216
+ 4. a hit on a `singleFlight` key joins the live execution;
217
+ 5. otherwise a new execution: `parallel` starts at once, `queue` and `latest` take a lane lease in FIFO order, and if
218
+ the lane is already on top of the caller's stack it is reused without waiting;
219
+ 6. `latest` first frees its waiting place: the invocation of the same call that still sits last on the lane is
220
+ cancelled before this one asks for the lease, so one waiting input survives and lane order is untouched (D185).
221
+
222
+ A nested `context.invoke(target, input)` inherits the lifetime of the authority and the lane stack, so cancelling the
223
+ parent cancels the child, and a lane cannot form a cycle: requesting a lane that sits below the top of the stack is a
224
+ cycle error, and so is joining your own execution.
225
+
226
+ ### 3.4 `effect` and `timers`
227
+
228
+ `ctx.effect({ from, run, when?, onDispose? })` subscribes to the source and runs immediately for the initial
229
+ value. Inside it is one serialized loop: a new value cancels the current run, waits for its disposer, checks
230
+ `when`, creates a new `AbortController` and timer registry, and starts `run`. Intermediate values are lost: only
231
+ the latest one lives. `run` returns `void` or a disposer.
232
+
233
+ `ctx.timers.delay(ms, run)` and `ctx.timers.interval(ms, run)` register their cancellation in the model registry; once
234
+ the registry is closed, both throw.
235
+
236
+ ### 3.5 The UI models of a contribution
237
+
238
+ A slot contribution may declare `models: [model(Decl, (ctx, props) => …)]`: plans that the contribution frame
239
+ instantiates on every mount. `props` is a `Readable` of the slot props of that particular mount, and the kernel of
240
+ such a model lives from mount to unmount. The law: instances are created in commit and never in render (D188). The
241
+ first pass of a contribution renders nothing, a `useLayoutEffect` keyed by the identity of the contribution builds the
242
+ mount from the props that committed and publishes it, and React flushes that extra synchronous render before paint;
243
+ the contribution identity owns it until actual replacement or unmount. A render that is thrown away — StrictMode's double invoke, an abandoned
244
+ concurrent pass, a subtree suspended by a lazy sibling — therefore creates nothing, so there is no sweep, no adoption
245
+ and no rebuild, and a child layout effect always sees a live model. Incoming slot props reach the live mount through
246
+ its own layout effect, so an interrupted render still cannot write to a model. Granting is hybrid (D158): the `own` models of the feature reach every component of the mount's subtree implicitly, while a per-mount UI model is declared with `requiresModels([X])` by the component or hook of the same module that reads it with `useModel(X)`. The declaration belongs to each reader, not only to the contribution site (D117).
247
+
248
+ StrictMode effect replay and Suspense hide/reveal reuse the same committed model bundle and preserve state.
249
+ Actual replacement or unmount releases it once; when a hidden subtree unmounts, a microtask releases it because
250
+ React has already disconnected its layout effects. Insertion effects mark retirement without notifying Readables
251
+ (D209).
252
+
253
+ A mounted model reads props from the mount-owned state. Direct props and the optional props adapter read that
254
+ same snapshot; incoming slot props are published in layout before paint. No state update runs in insertion
255
+ effects. A demand retry keeps its source identity, so an unfinished retry from a replaced source cannot block
256
+ or overwrite the new source's retry (D170).
257
+
258
+ ## 4. Reactivity
259
+
260
+ ### 4.1 The contract and the sources
261
+
262
+ ```ts
263
+ interface Readable<T> {
264
+ getSnapshot(): T;
265
+ subscribe(listener: () => void): () => void;
266
+ }
267
+ ```
268
+
269
+ A listener is handed nothing; it re-reads. A state cell (`createState`, internal only since D142; the author receives
270
+ it as `ctx.state` of a model) holds the value and a set of listeners; `set` and `update` stay silent on an `Object.is`
271
+ match; after `close()` both reading and writing raise a `ReadableError` with code `closed`. Closing propagates:
272
+ `close()` is one notification to the dependents, after which active `derive`, `computed`, `collection` and
273
+ `selectByKey` answer `closed` instead of the last value and then stay silent; an unsubscribed derived node gets the
274
+ same failure on its next read (D146).
275
+
276
+ ### 4.2 `derive` and `computed`
277
+
278
+ Both are nodes of one graph with a "push invalidation, pull recomputation" model: a node has a freshness of `clean`,
279
+ `check` or `dirty` and a monotonic revision. An unsubscribed node recomputes on demand in `getSnapshot()`, with no
280
+ upstream subscription needed. A node becomes active when it has listeners or dependents, and deactivates with a
281
+ one-microtask delay so that resubscribing within the same tick does not drop the upstream lease.
282
+
283
+ `computed({ read: get => … })` rebuilds its dependency map on every recomputation from the `get` calls and remembers
284
+ their revisions; in the `check` state the body does not run if no revision moved. Re-entering a computation is a
285
+ "cycle" error. A transition from `error` to `value` counts as a change even when the value is equal.
286
+
287
+ ### 4.3 One scheduler
288
+
289
+ Glitches are impossible by construction: a single scheduler stabilizes every pending node in topological order before
290
+ it publishes, and only then calls the listeners. Invalidation and traversal are iterative, so a chain of a thousand
291
+ nodes does not overflow the stack. Reading an active clean node returns the settled value with no graph walk, O(1)
292
+ instead of O(graph), and that is pinned by the `coreData.settledRead` budget (D130). Activation is transactional: if a
293
+ node fails to activate, the already activated nodes roll back in reverse order, and disposer failures during the
294
+ rollback are aggregated with the original cause, so no part of the graph stays active without a subscription owner
295
+ (D134).
296
+
297
+ When listeners learn about a change:
298
+
299
+ | Source | When |
300
+ | ------------------------------------------------ | -------------------------------------------------------------------------------- |
301
+ | the direct listeners of `State.set` | synchronously, inside the write transaction |
302
+ | `derive` and `computed` downstream of that write | synchronously at the end of `set()`: the transaction closes and drains the queue |
303
+ | an external source with no open transaction | it opens its own transaction, then the same |
304
+ | an invalidation with no transaction | the next microtask |
305
+ | nested writes | one publication at the outer transaction boundary |
306
+
307
+ ### 4.4 Collections
308
+
309
+ `collection({ from, key, project })` keeps a projection cache keyed by the item object, stable lookups by key, and
310
+ listeners per key: `selectByKey(source, key)` wakes only when its own key changes. Duplicate keys are an error. Unused
311
+ selectors are swept through a microtask.
312
+
313
+ ### 4.5 Hooks
314
+
315
+ | Hook | Mechanics |
316
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317
+ | `useReadable`, `useSelector` | `useSyncExternalStore` with a cache of the selected value; the same getter for client and server |
318
+ | `useResource` | `useSyncExternalStore` over the snapshot plus `retain()` in an effect: the mount is the lease |
319
+ | `useModel` | the nearest contribution mount frame; outside a frame a `ContributionError` `missing`, an inactive mount `inactive`, a foreign model `missing` |
320
+ | `useCommand` | per-consumer state, with no listener on the invoker: `run` returns a promise of the outcome, `inFlight` for the duration of the call, `result` the last `ok` outcome, which survives the next run, `lastError` the error of the last `failed`, cleared only by a success and never reset by starting a retry (D116); a cancellation changes neither of them |
321
+
322
+ `useCommand` keeps status per hook and invoker, and schedules nothing: every `run` reaches the call, whose declared
323
+ policy decides whether it queues, replaces a waiting input or runs in parallel (D203). Each request owns its
324
+ callbacks and abort subscription, an already-aborted signal is refused without reaching the call, and a run of an
325
+ unmounted consumer is refused the same way. `inFlight` is true while any run this consumer started is unsettled.
326
+
327
+ ## 5. Resources, streams, events
328
+
329
+ Both the feature and the model select their target with `(source) => Readable<T | null | undefined>`; the
330
+ returned object is checked before work starts. A scalar snapshot does not satisfy the contract. `retention` is
331
+ optional in either context and `scoped({ capacity })` fixes LRU eviction (D169).
332
+
333
+ ### 5.1 `resource(from, target, { key, load, retention, retry })`
334
+
335
+ | Step | What happens |
336
+ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | 1 | the retention `scoped({ capacity })` activates the controller immediately, so the resource is warm with no observer; the `observer` retention — the literal `{ kind: 'observer' }` and the default — activates on the first lease (D160) |
338
+ | 2 | activation subscribes to `target` and reads it; a `null` target closes the selection |
339
+ | 3 | a key change: the same key updates the target without a reload; a new key fences the execution, advances the key epoch and takes the value from the LRU cache if it is there, otherwise it publishes `opening` (no data) or `refreshing` (stale data) and loads |
340
+ | 4 | `load(target, { signal, source })`; the result is accepted only if the execution, the key epoch and the activity did not change |
341
+ | 5 | on failure: a retry per `retry: { attempts, delayMs }`, then `status: 'error'` with `retryable: true` and a report to the `reporter` |
342
+ | 6 | the last lease released under `observer`: deactivation through a microtask, guarded by a revision |
343
+ | 7 | closing in drain: fence, unsubscribe, clear the cache, `idle`, await the unfinished operations |
344
+
345
+ `refresh()` reloads the same key past the cache and keeps the data; `retry()` works only from `error`; `invalidate()`
346
+ clears the cache and reloads without keeping the data.
347
+
348
+ ### 5.2 `stream(from, target, { connect, consume, key, backpressure })`
349
+
350
+ The same key-and-epoch machine plus a one-slot admission layer: the only policy, `latest()`, keeps the newest pending value (D160). Every value is cached and published as `ready` first, then handed to the
351
+ author's `consume`. On a key change the new connection waits for the disposer of the old one: connections never
352
+ overlap. `backpressure` is required at runtime.
353
+
354
+ ### 5.3 `event(from, subscribe, { run, backpressure })`
355
+
356
+ A one-slot queue in front of a sequential `run`, and the option says who owns the slot. Without `backpressure` it
357
+ belongs to the payload that reached it first: while `run` is busy and one payload waits, a newer `emit` is dropped and
358
+ reported as a `queue-capacity` failure record — drop-newest, the shape `exhaustMap` and `takeLeading` have. With
359
+ `backpressure: latest()` the newest payload replaces the waiting one and nothing is reported — conflation, the shape
360
+ `conflate` has; unlike `switchMap` it never interrupts the `run` that is already going (D247). A lossless queue is
361
+ neither policy and stays an open question (D183). `run` executes sequentially, must resolve to `undefined`, and
362
+ errors after a cancellation are swallowed. Cancelling the owner closes the queue and drops whatever waits in it, and
363
+ closing waits for the disposer and for the queue to drain.
364
+
365
+ ## 6. Contributions and slots
366
+
367
+ ### 6.1 Publication
368
+
369
+ | Step | What happens |
370
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
371
+ | 1 | at definition `provides` yields opaque declarations `{ target, value, priority, when }`; the value is always normalized to an instance-scoped factory |
372
+ | 2 | every contribution gets the id `<feature>.<key>`, one key per declaration |
373
+ | 3 | the compiler enumerates the contributions by id |
374
+ | 4 | in `prepare` a retire participant is registered whose fence withdraws the publication; the authority is captured: the models of the instance |
375
+ | 5 | **after `ready`** the factories run with the instance context, the value is marked with its owner, and `when` is resolved: a predicate becomes one computed readable of that instance |
376
+ | 6 | every affected target is updated **in one transaction**, and listeners are called afterwards |
377
+
378
+ Entry order: `priority` ascending, ties broken by id. `when` (a `Readable<boolean>`, or a predicate of the instance
379
+ that the runtime lowers to one): an entry whose value is `false`
380
+ is not part of `entries`, so `Slot`, `fold`, `select` and emptiness checks never see it; a flip publishes in one
381
+ transaction; id uniqueness is checked across everything published, so a flip can never fail validation. Publication is
382
+ atomic: subscribing a `when` and reading it happen in a preflight before any target is mutated, releasing the watcher
383
+ of a departing entry happens after every target is committed, and a publication reads only the `when` of its own
384
+ entries, so a withdrawal reads none at all (D225). A failure rolls back the observers that were added and leaves the
385
+ targets untouched, a `when` that signals mid-projection makes the affected targets project again, and withdrawing a
386
+ publication that never reached `published` removes whatever did land (D124).
387
+
388
+ ### 6.2 Rendering a contribution
389
+
390
+ `Slot` reads the `entries` of the target through `useReadable`, returns `null` for an empty list, and otherwise renders
391
+ the `Component` of every entry with the slot props, wrapped in the authority frame of the contributing feature: inside
392
+ a contribution, `useModel` reaches the models of its own feature and nothing foreign. A contribution with no models
393
+ does not pay for the frame. A contribution is `{ Component, props?, models? }`: `props` maps the slot props into the
394
+ component's own props, `models` supplies the per-mount UI models, and a component built with `requiresModels([...])`
395
+ declares which of them it needs, which is checked by the type at the contribution boundary. A mount is memoized:
396
+ publishing or withdrawing one contribution does not re-render the other mounts of the same target (D130).
397
+
398
+ ### 6.3 `fold` on a pipe
399
+
400
+ `fold(value, meta, read?)` walks the handlers in order; with a reader passed inside a `computed`, the dependencies
401
+ include both the entry list and everything the handlers read through their `{ read }`, so the result recomputes when
402
+ their sources change rather than only when contributions are re-registered. Without a reader the reads are untracked.
403
+
404
+ A handler is declared as a descriptor, `pipe(target, { fold })`, and the runtime binds one evaluation context to it
405
+ when the contribution publishes: `exports`, `imports` and the materialized `own` are resolved once and the reader is
406
+ the only thing that changes from call to call, so folding over many handlers allocates no per-handler context
407
+ (D223). A `fold` is the only thing that runs a handler: declaring, preloading and publishing do not.
408
+
409
+ ### 6.4 The registry
410
+
411
+ `defineRegistry` is a target built on top of `collection`: `byId`, `list`, and `select(key)` with
412
+ `{ kind: 'found' | 'missing' }`. A duplicate key is an error.
413
+
414
+ ## 7. Errors
415
+
416
+ One class per subject, with the state in the `code` field.
417
+
418
+ | Class | Codes | Who throws it |
419
+ | ---------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
420
+ | `FeatureError` | `retired`, `not-ready`, `quarantined`, `cleanup-failed` | the instance fence, a failed opening, the settle of a retire |
421
+ | `CallError` | `cancelled`, `closed`, `publication-rejected`, `unavailable` | the call kernel; cancellation is only `cancelled` and `closed` (D138) |
422
+ | `ReadableError` | `closed` | reading and writing a closed cell |
423
+ | `DeclarationError` | `invalid-id` | `declarationId` |
424
+ | `ApplicationError` | `closed` | the application closed before it was ready |
425
+ | `ContributionError` | `binding-invalid`, `duplicate`, `inactive`, `missing` | a contribution mount: a hook outside a frame, an inactive frame, a duplicate model, an invalid model or command binding |
426
+ | `CancellationError` | no code, only the cancellation brand | an author cancellation, which `isCancellation` recognizes |
427
+ | `FeatureBoundaryError` | `missing` | `useFeatureRetry` outside the error subtree of a boundary |
428
+
429
+ Cancellation is a property of the error, not a class: the predicate `isCancellation` (internal, the author does not
430
+ need it — cancellation is read by `useCommand` and `resource`) looks at the brand carried by `CancellationError`, by
431
+ `CallError` with codes `cancelled` and `closed`, by `FeatureError` with code `retired`, and by `ContributionError` with
432
+ code `inactive`. `unavailable` and `publication-rejected` are product answers: `useCommand` shows them as a `failed`
433
+ outcome with the error in `lastError`, and `resource` shows them as an error (D138). Neither `useCommand` nor
434
+ `resource` shows a cancellation as a user-facing error.
435
+
436
+ The path an error takes to the application: the `reporter` from `defineApplication` is passed to every `openFeature`,
437
+ and from there into model kernels, timers and controllers; the single funnel `reportRuntimeFailure` falls back to the
438
+ global `reportError` or to a deferred `throw` when the `reporter` is absent or itself fails. A condition group reports
439
+ its failures while it is running and holds only the ones raised after `close()` started, which `close()` then throws.
440
+
441
+ ## 8. Boundaries and the host
442
+
443
+ The table names the everyday entries; the exhaustive export lists live in spec.md §3.
444
+
445
+ | Entry | For whom | What is there |
446
+ | ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
447
+ | `@opetope/core` | the feature author | `defineModel`, `definePort`, `definePipe`, `defineRegistry`, `declarationId`, `derive`, `computed`, `externalReadable`, `collection`, `selectByKey`, the errors, and the types `Readable`, `Lookup`, `Call`, `Port`, `ModelContext`, `OwnedState`, `Resource` |
448
+ | `@opetope/runtime` | the feature author and the host | `defineFeature`, `defineHostContract`, `onDemand`, `optional`, `openFeature`, `defineCondition`, `defineApplication`, `openApplication`, `bind`, `scoped`, `latest` |
449
+ | `@opetope/react` | the component author | `useModel`, `useReadable`, `useSelector`, `useCommand`, `useResource`, `defineSlot`, `defineSwitchSlot`, `Slot`, `requiresModels` |
450
+ | `@opetope/react/integration` | the host | `FeatureBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` |
451
+ | `@opetope/react/testing` | tests | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` |
452
+ | `@opetope/*/internal` | framework and host integration | the kernel, the compiler, the controllers, `createState`, `isCancellation`, the observation port `createInspectionSession` and the control port `createControlSession`; unavailable to authors (D142) |
453
+
454
+ The `exports` field of `package.json` exposes exact entry points without wildcards. Feature authors use the safe
455
+ entries; host integration and testing use their dedicated entries. Application import rules preserve this
456
+ separation between roles.
457
+
458
+ A demand-driven host shows feature UI like this: `FeatureBoundary` takes a lease on the instance through `useFeature`,
459
+ opens it if needed, shows `fallback` while it opens and `error` on failure, and inside renders `<Slot target props />`,
460
+ which is where the feature contributed its UI. Unmounting the boundary releases the lease; the last released lease
461
+ leads to a retire.
462
+
463
+ ### 8.1 Observation
464
+
465
+ The observation port (D122) lives on `@opetope/runtime/internal`:
466
+ `createInspectionSession(execution, { ringCapacity })`, where `execution` is the result of `openApplication`. A
467
+ `WeakMap` holds the link; there is no global hook and no public value, and an application has at most one session.
468
+ Until a session is opened the application records nothing; `close()` detaches the observer without touching the
469
+ application. The consumer gets exactly four methods — `getSnapshot`, `readSince`, `subscribe` and `close` — and cannot
470
+ steer the graph it reads. The runtime implements the `@opetope/devtools` protocol rather than importing it, and the
471
+ `ci:inspection` gate holds the correspondence.
472
+
473
+ Both ports stay on `/internal` because the package that calls them is `@opetope/devtools`, not the application. A
474
+ host attaches the panel with `installDevtools({ execution, … })` and touches this entry for nothing else (D248).
475
+
476
+ ### 8.2 Control
477
+
478
+ The write port is a second, separate factory on the same entry: `createControlSession(execution)` (D176). Holding an
479
+ inspection session does not mean holding the right to write, so the two have their own registries and their own
480
+ authority. The only controllable subject is a condition and, through it, its group: `suspend(conditionId)` sets a
481
+ debug override that forces the condition to read `false`, `resume(conditionId)` removes it, and the group follows its
482
+ own source again. A feature without `when` has no condition and is therefore not addressable at all — devtools never
483
+ becomes a source of lifetime the plan does not have.
484
+
485
+ The override wraps the condition's own `Readable`, so a suspended group deactivates through the ordinary
486
+ transactional path: the same scheduler, the same atomic frame, and the dependency law of
487
+ [devtools.md §5.3](devtools.md) —
488
+ suspending a provider takes its hard dependents with it through their own semantics, while suspending a consumer
489
+ leaves its provider alone. The frame the transition publishes names the control command as its cause, so «what I did»
490
+ and «what happened» read as one line of the timeline, and the group snapshot reports the override next to the product
491
+ truth: `desired.source` is what the condition says, `desired.effective` is what the runtime acts on, and
492
+ `desired.override` is `force-inactive` while the override stands.
493
+
494
+ `retryCleanup(instanceId)` repeats the cleanup of an exact quarantined frontier through the existing retry contract;
495
+ an instance that is not quarantined answers `not-controllable`. A refusal is data, never an exception:
496
+ `{ kind: 'rejected', reason: 'not-controllable' | 'stale' | 'unknown-target' }`, and an applied command answers with
497
+ the sequence of the frame its effect is visible in. `close()` releases every override the session holds, so closing a
498
+ panel never leaves the application in a debugged state, and an override lives only in the current execution — it does
499
+ not survive a reload or HMR.
500
+
501
+ ## 9. How to verify this
502
+
503
+ - A feature instance in a test: `openFeature(feature, { imports, reporter })`, then `ready`, then `close()`; fakes of
504
+ host contracts are ordinary objects.
505
+ - A contribution component: `renderSlot(target, { props, models })` mounts the contribution with a fixture of per-mount
506
+ models and gives back `updateProps`; `command(run)` mints an authentic `Call` for a fixture.
507
+ - The application graph: `defineApplication` in a manifest test catches a missing provider, a cycle and a violation of
508
+ the condition-set inclusion law before anything starts.
509
+ - Budgets and gates: `ci:public-surface` counts the public words and forbids kernel and retired words on the safe
510
+ entries; `ci:perf-memory` measures retained memory, listeners and the linearity of operation cost, including
511
+ `coreData.settledRead` and the allocations of a write with no listeners (`unobservedWrite`, the median of three runs
512
+ of 100k operations); `ci:type-stress` measures the cost of the types, and language-service latency is printed for
513
+ information only; instantiations, memory and diagnostics are enforced (D224); `ci:browser-floor` holds the syntax and API level
514
+ of Chrome 82; `ci:inspection` checks the contract of the observation port; `ci:size-limit` holds five budgets the
515
+ author sees (core `author primitives consumer` 1.1 kB and `data layer consumer` 4.9 kB, runtime
516
+ `public feature consumer` 32 kB and `application graph consumer` 48 kB, and `@opetope/react` 18 kB).
517
+ - Authoring review: every component and hook of the module that reads a per-mount contribution model with
518
+ `useModel(X)` declares `requiresModels([X])`; owner models need no declaration (D117, D158). Imports use the
519
+ package entry points permitted for their role.
520
+ - Application build verification: the consuming host checks its bundle budgets and verifies that intended dynamic
521
+ boundaries survive bundling; package size fixtures do not measure the complete application.