@opetope/lint 0.11.0 → 0.12.1

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 (48) hide show
  1. package/CHANGELOG.md +667 -4
  2. package/README.md +69 -596
  3. package/dist/ast.d.ts +3 -1
  4. package/dist/ast.js +1 -1
  5. package/dist/ast.js.map +1 -1
  6. package/dist/command-hooks.d.ts +2 -2
  7. package/dist/command-hooks.js +1 -1
  8. package/dist/command-hooks.js.map +1 -1
  9. package/dist/context-members.d.ts +22 -0
  10. package/dist/context-members.js +2 -0
  11. package/dist/context-members.js.map +1 -0
  12. package/dist/declaration-ingress.d.ts +10 -2
  13. package/dist/declaration-ingress.js +1 -1
  14. package/dist/declaration-ingress.js.map +1 -1
  15. package/dist/effect-declarations.d.ts +20 -0
  16. package/dist/effect-declarations.js +2 -0
  17. package/dist/effect-declarations.js.map +1 -0
  18. package/dist/index.d.ts +11 -3
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/retired-vocabulary.d.ts +31 -0
  22. package/dist/retired-vocabulary.js +2 -0
  23. package/dist/retired-vocabulary.js.map +1 -0
  24. package/dist/rules/capture-command-cleanup.js +1 -1
  25. package/dist/rules/capture-command-cleanup.js.map +1 -1
  26. package/dist/rules/define-feature-property-order.js +1 -1
  27. package/dist/rules/define-feature-property-order.js.map +1 -1
  28. package/dist/rules/enabled-predicate.d.ts +6 -0
  29. package/dist/rules/enabled-predicate.js +2 -0
  30. package/dist/rules/enabled-predicate.js.map +1 -0
  31. package/dist/rules/no-command-in-deps.js +1 -1
  32. package/dist/rules/no-command-in-deps.js.map +1 -1
  33. package/dist/rules/no-internal-imports.js +1 -1
  34. package/dist/rules/no-internal-imports.js.map +1 -1
  35. package/dist/rules/no-retired-vocabulary.d.ts +5 -0
  36. package/dist/rules/no-retired-vocabulary.js +2 -0
  37. package/dist/rules/no-retired-vocabulary.js.map +1 -0
  38. package/dist/rules/no-write-after-source-write.d.ts +6 -0
  39. package/dist/rules/no-write-after-source-write.js +2 -0
  40. package/dist/rules/no-write-after-source-write.js.map +1 -0
  41. package/dist/rules/prefer-effect-current.js +1 -1
  42. package/dist/rules/prefer-effect-current.js.map +1 -1
  43. package/oxlintrc.json +3 -1
  44. package/package.json +1 -2
  45. package/README.ru.md +0 -648
  46. package/dist/rules/when-predicate.d.ts +0 -6
  47. package/dist/rules/when-predicate.js +0 -2
  48. package/dist/rules/when-predicate.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,668 @@
1
1
  # @opetope/lint
2
2
 
3
+ ## 0.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8aa4d85: **Four positions of a call name a command's input, and the member that says so is an intersection (D455).** The
8
+ `input` of `ModelCommandContext<Input>` and of `FeatureCommandContext<Within, Input>` was
9
+ `Input extends void ? {instruction} : Input`, which is not provably `Input` while `Input` is unresolved: a generic
10
+ helper could read the member and could not hand it to anything asking for exactly its own type parameter
11
+ (`TS2345: Argument of type 'T extends void ? {…} : T' is not assignable to parameter of type 'T'`, and a constraint
12
+ on `T` did not cure it). It is `Input & (Input extends void ? {instruction} : unknown)` now. An intersection is
13
+ assignable to each of its members, so the hand-off resolves; the second branch is `unknown`, which intersects away,
14
+ so a named input reads exactly as before and a call that named none is refused on the read exactly as before. No
15
+ public name is added or removed — 59 types and 37 values before and after — and every emitted JavaScript file is
16
+ byte-identical apart from the lint rule's message, so no size budget moves. The type slices move
17
+ 11 736 → 11 756, 87 612 → 87 710 and 160 090 → 160 268 instantiations against ceilings of 12 200, 99 062 and
18
+ 181 237, which stay where they were.
19
+
20
+ - **The instruction the refusal prints names four positions in order of cost, and says why there is no fifth.**
21
+ D444 wrote that the annotation of the destructured context was the one place an input could be named. It is the
22
+ most expensive of four. `Input` is inferred from every position of the call except the body: the result type of a
23
+ model factory names the input of every command in the literal it returns; an option whose callback is annotated
24
+ (`dedupe`, `memoize`, `concurrency: { by }`) names it where a call was writing an option anyway; a constant
25
+ annotated `Command<Input, Output>` names it and keeps `Output` checked; and the annotation of the context is what
26
+ is left. `command<Deposit>(…)` is not a position — one explicit type argument pins `Output` to its default, and
27
+ the body that answers a value is refused for answering one. A feature has the last two only: `own.command`
28
+ answers a ref carrying the feature's id, which no public name annotates.
29
+ - **The authoring documents now teach the cheap positions instead of the expensive one.** The spec, the cookbook,
30
+ the authoring guide, the primitives reference, the wave migration and the lint rule and its README were all
31
+ teaching the context annotation as the one route; the compiled examples that sit inside a factory with a result
32
+ type name nothing now, and the ones bound to a constant annotate the constant. The examples that keep the context
33
+ annotation say in a comment why it is the route left there.
34
+ - **Both directions are held by type tests.** A generic helper handing `context.input` to something asking for
35
+ exactly its parameter compiles, bare and constrained; a pair of identically shaped generic factories, one writing
36
+ a typed option and one not, both compile, which is what says the intersection is the cure and not the option; and
37
+ a call whose only option names nothing is refused on the read, with each assertion naming the diagnostic it
38
+ expects — `TS2339` at a member read, `TS2345` at a hand-off, `TS2322` at the pinned `Output`.
39
+
40
+ - 8aec982: **A command's one waiting place is written as the record that names it: `concurrency: { pending: 'latest' }`
41
+ (D459).** The scalar `concurrency: 'latest'` named the input that wins and never named what it wins — «the latest»
42
+ reads as a place in a queue, as the body already running and as a cached answer, and only the first was true. The
43
+ shared form of the same fact never had that gap: `{ lane, pending: 'latest' }` says the place out loud, with the
44
+ `pending` an event and a live Resource already write for their own one waiting place. One fact was being written in
45
+ two vocabularies, and the shorter one left out the part that mattered. The word is removed rather than kept beside
46
+ the record, which makes this a breaking change to the authoring surface of a model command, of a model selection
47
+ and of a feature command.
48
+
49
+ - **Migrate by writing the place.** `concurrency: 'latest'` becomes `concurrency: { pending: 'latest' }` in
50
+ `ctx.command`, `ctx.select` and `own.command`. `concurrency: { lane, pending: 'latest' }` is unchanged: the lane
51
+ is the one parameter this record takes, and it is optional now instead of required. Nothing else about the option
52
+ moves — the same record, the same siblings, the same `dedupe` prohibition beside it.
53
+ - **No law moved with the spelling.** While call 1 runs, calls 2 and 3 take one waiting place: the body of 2 never
54
+ starts, its wait ends as `CommandError('cancelled', 'Command <id> replaced a waiting input.')`, and 3 runs when 1
55
+ finishes. A newer request never reaches the call in flight. The regression that holds this reads `signal.aborted`
56
+ inside the running body — after both newer requests were admitted and before the body returns — rather than
57
+ counting abort events, because the abort that follows a call's own end belongs to its cleanup and says nothing
58
+ about the policy. It was proved by substitution: with the kernel changed to cancel whatever is in flight, the
59
+ test fails on that line.
60
+ - **The retired word is answered with its replacement, not with a list of everything else.** The runtime throws
61
+ `Model command concurrency latest is now a record: write { pending: 'latest' }, or { lane, pending: 'latest' }.`
62
+ — and `Model select`, `Feature command` the same. Two neighbouring messages move with it: the choice sentence
63
+ loses the word — `concurrency must be queue, parallel, a lane of this surface or a record.` — and the record's own
64
+ refusal now says the lane is optional. A record that names `lane` and hands it `undefined` is still refused, by
65
+ the type under `exactOptionalPropertyTypes` and by the runtime, which reads the shape of the record by its keys.
66
+ - **The compiler names the replacement too, because the literal stays in the union.** The options record of a
67
+ command gained a third member — the retired word beside a key an author cannot write — so a refusal arrives as a
68
+ missing property carrying the instruction. Without it the union held no string at all, every word written there
69
+ widened to `string`, and one sentence answered a typo and a retired word alike. What the two now print, the same
70
+ on a model and on a feature: `{ concurrency: 'quee' }` is a `TS2820` that names the word and suggests `'queue'`,
71
+ which is better than it was before this change, and `{ concurrency: 'latest' }` is a `TS2345` whose elaboration
72
+ carries «`concurrency: "latest"` is now a record: write `{ pending: "latest" }`, or `{ lane, pending: "latest" }`».
73
+ A selection is the one surface where the compiler names the word written and not its replacement — its options
74
+ record is not a union — and there the runtime and the lint rule carry it.
75
+ - **`opetope/no-retired-vocabulary` reports it.** `concurrency: 'latest'` is a retired _value_ of a live key, the
76
+ shape `overflow: 'reject'` already had, so the rule now reads the values of a command's options record as well as
77
+ its keys and reports on the word that was written. A project that pins the rule's output by its own fixtures gains
78
+ one row.
79
+ - **What the gates read.** Instantiations are unchanged — 11 756, 87 710 and 160 268 against ceilings of 12 200,
80
+ 99 062 and 181 237 — because a scalar became a record that already existed. The tightest size row, the headless
81
+ session consumer of `@opetope/devtools`, stays at 6.95 kB of 7.1 kB. The two runtime rows carry the new branch and
82
+ its message: the application graph consumer moves 48.56 → 48.66 kB of 49 kB and the public feature consumer
83
+ 39.88 → 39.92 kB of 41 kB, measured by rebuilding the validator both ways. No budget moves.
84
+
85
+ - be0613e: **The cell is the only place that says what a write is (D457).** `ModelStateWriter.update` was
86
+ `update<Value>(state: OwnedState<Value>, update: (previous: NoInfer<Value>) => Value)`, and a type parameter in the
87
+ result of a callback is an inference site: `Value` was not fixed while the body was checked, so the object literal
88
+ the updater returned had no contextual type and its fields widened — and the widened candidate then passed the
89
+ state, because `OwnedState` is covariant. `update(sessions, previous => ({ ...previous, kind: 'loading' }))` into a
90
+ cell whose `kind` is `'idle' | 'loading'` was therefore refused with a type nobody wrote,
91
+ `(previous: NoInfer<Sessions>) => { items: readonly string[]; kind: string }`, and the remedies were the author's:
92
+ `as const`, an annotated updater result, or a cell widened to `string`. The signature is
93
+ `update: NoInfer<(previous: Value) => Value>` now, so the state is the only inference site and the updater is
94
+ checked against the cell. No public name is added or removed — 59 types and 37 values before and after — and every
95
+ emitted JavaScript file is byte-identical, so no size budget moves; the three type slices write no state at all and
96
+ stand at 11 756, 87 710 and 160 268 instantiations against ceilings of 12 200, 99 062 and 181 237. Measured on 1 200
97
+ identical writes that compile under both signatures, the change costs 2 instantiations and 2 types.
98
+
99
+ - **Breaking: three writes that used to compile are refused now, all for the same reason.** A value wider than the
100
+ cell (`update(sessions, () => wider)` where `wider.kind` is `string`); a member of a discriminated union that
101
+ leaves out a field that member declares (`update(phase, () => ({ kind: 'loading' }))` where that member also
102
+ declares `since`); and a word outside a union of literals (`update(mode, () => 'stopped')` into
103
+ `OwnedState<'off' | 'on'>`). Each was accepted because the widened inference and the covariance of `OwnedState`
104
+ met, and each reached the cell through an updater that takes no parameter — the one shape whose inference
105
+ resolved before the body was checked. Each is a `TS2769` where it is written now. Recompiled with the previous
106
+ signature, the four new type tests report 15 refusals of correct writes and 12 asserted refusals that do not
107
+ fire.
108
+ - **An updater written at the call needs no `as const`, no annotated result and no widened field.** A literal it
109
+ returns keeps its type through a spread, in a nested record and in a member of a union; a mutable array and a
110
+ tuple in a cell keep their own mutability, which is why the `const` type parameter that also fixes the literals
111
+ was rejected. The value form of `update` is untouched, and so is the rule that a function in that position is the
112
+ updater. Diagnostics improve with the fix: the compiler prints `kind: "done"` instead of `kind: string`.
113
+ - **Two limits of that promise, both measured.** An updater hoisted into a constant widens at its own declaration,
114
+ where there is no contextual type at all, so it still needs an annotated result — before and after this change.
115
+ And the updater half performs no excess-property check, because TypeScript does not apply one to a literal
116
+ returned from a contextually typed function: a stray key written inside the updater now compiles, where it used
117
+ to be caught in passing by the false refusal whenever the literal also had something to widen. The value half
118
+ still catches it and names the key.
119
+ - **`StateCommit.update` and the reactive calculations are unchanged, and the type tests say why.** A commit's
120
+ `Value` is already one cell, and so is a pipe's, so neither `commit.update` nor `fold` has a callback result to
121
+ infer from. `derive`, `computed`, `fromMaybe`, `fromExternal` and the `project` of a collection do put a type
122
+ parameter in the result of a callback, and none of them has this defect: a widened result has nowhere to go
123
+ there, because no covariant container stands in the call for a supertype to pass through — the assignment of the
124
+ result refuses it. The `apply.change` of a live Resource is the one public callback left where the same widening
125
+ is live; it belongs to a separate change.
126
+ - **One workaround is gone.** The `@opetope/lint` README declared its example state with `kind: string` and a
127
+ comment saying the widening was a defect of the writer's own signature; the field is `'idle' | 'loading'` again
128
+ and the comment is gone. Spec §2.38 is the new compile-checked example, and it asserts all three closed holes.
129
+
130
+ - 4e65393: **The published archive of every package loses `README.ru.md`, and `@opetope/runtime` loses the Russian pages of
131
+ its `docs/` as well (D456).** The Russian half of the documentation is removed: nineteen `*.ru.md` files,
132
+ **2 027 997 bytes**, which is 39.0 % of what `ci:docs` counts as a document and 35.1 % of every `*.md` in the tree.
133
+ Documentation is written once, in English, from here on. No public name, no `exports` entry and no subpath moves —
134
+ a `.ru.md` was never a resolvable specifier, only a file read by its path — and no line of shipped JavaScript
135
+ changes, which is why this is a patch. The composition of what is published does change, and this is the line that
136
+ says so.
137
+
138
+ - **What goes.** `CONTRIBUTING.ru.md`, `README.ru.md`, the README of the minimal React example, the nine guides of
139
+ `docs/` — agent guide, cookbook, devtools, how-it-works, primitives, releases, both migration guides and the
140
+ spec — and the `README.ru.md` of all seven packages. `docs/decisions.md` stays Russian and keeps no pair, as it
141
+ never had one. The nine copies under `packages/runtime/docs/` are generated by packaging and leave on their own.
142
+ - **The rule that required a pair is cancelled, with its gate.** `CLAUDE.md` said «README and user guides have
143
+ English/Russian pairs, updated together» with four exceptions; it now says a document is written once, in
144
+ English, and gets no second copy. `ci:docs` no longer looks for a `*.ru.md` beside a document and no longer
145
+ compares the heading skeleton of a pair, and its report reads «links and example section order passed».
146
+ - **One gate is lost outright, and nothing replaces it.** The fixture generator held every English example against
147
+ its Russian twin as the same program — syntax without comments and without JSX text — which is how D441 caught a
148
+ real drift in a spec §2 block, a string literal that differed in one copy. The defect class that stops being
149
+ caught is a silent edit to a detail of an example that still compiles: a string literal, a number, the order of
150
+ two arguments of the same type. `ci:type` accepts any literal of the right type, `ci:docs` reads section order
151
+ and links rather than values, `ci:asserted-refusals` holds pragmas, and the generator's own `--check` compares a
152
+ fixture with the very document it was lifted from, so one `fixtures:generate` makes any such edit the new
153
+ baseline. The twin was the only place in this repository where one program was written twice and the two
154
+ writings were compared. It also never knew which copy was right — in the one case it fired, the wrong copy was
155
+ the Russian one — and it protected above all the copy that nothing compiled.
156
+ - **Where the archive change is written down, so it cannot drift from its gate.** The composition of an archive is
157
+ stated in six places and all six are rewritten: the `files` of the seven manifests; the required-file list and
158
+ the archive-member allowlist of the pack archive check; the fixture of that check's own unit test; the
159
+ allowlists of the two real pack smoke tests; and what packaging demands of each package before it packs. The
160
+ requirement that `@opetope/runtime` ship `docs/spec.ru.md` is gone; `docs/spec.md` is required exactly as before.
161
+ - **The numbers the gates print.** `ci:docs` reads 28 documents instead of 46 — nineteen leave and this changeset
162
+ is itself a document — with 4 pending changesets instead of 3 and 424 decision identifiers instead of 423. Its
163
+ feature examples halve, 64 to 32: that gate lints every fenced example that declares a feature, and it was
164
+ linting both copies of each one. Packaging prepares 10 documents instead of 19, and the merge-marker scan reads
165
+ 1101 text files instead of 1117 — seventeen of the nineteen removed files live under a scanned root, and this
166
+ changeset is one file back. The document examples do not move at all: 38 spec §2 blocks and 132 blocks of 11
167
+ other documents, 2 fragments, 14 asserted refusals, exactly as before, because only the English block was ever
168
+ lifted into a fixture.
169
+
170
+ - e12b0b6: **An event takes its handler second: `event(source, run, subscribe, options?)` (D458).** The natural declaration was
171
+ refused although its payload was already annotated — `emit` typed as
172
+ `(payload: EventPayloadNotNamed | undefined) => boolean`, and `run` as taking the marker. The cause is the order of
173
+ the arguments and nothing else. TypeScript types a call's arguments in order and leaves a context-sensitive callback
174
+ out of the first inference pass; in the second it walks them left to right, and resolves the type parameters a
175
+ callback's own type refers to before giving that callback a contextual type. `subscribe` stood second and its one
176
+ unannotated parameter is a context carrying `Payload`, so `Payload` answered its default on the way past, before the
177
+ annotation on `run` was ever read. With `run` second, its unannotated parameter is a run context, which carries no
178
+ payload at all, so nothing about it resolves `Payload` and one annotation is the whole naming site. This is a
179
+ breaking change to the authoring surface of both a model and a feature.
180
+
181
+ - **Migrate by swapping the two callbacks.** `ctx.event(source, subscribe, run, options?)` becomes
182
+ `ctx.event(source, run, subscribe, options?)`, and `own.event` of a feature the same. Nothing else about the node
183
+ moves: the same four arguments, the same options record, the same `request`, the same delivery policies. A call
184
+ left in the old order does not compile silently — each callback is checked against the other's signature.
185
+ - **One annotation on `run` is now the whole naming site, whatever it does with its context.** The condition the
186
+ previous decision recorded — that a run annotating its payload and destructuring its context names nothing — was
187
+ true only while `subscribe` stood first. It is gone, and so is the shape it forced: the polling recipe of the
188
+ cookbook and the spec no longer annotate a context they only destructure. The marker's instruction narrowed with
189
+ it, from «annotate every parameter of `run`, the payload first — a `run` that leaves its context unannotated names
190
+ nothing — or annotate a `delivery.by` parameter» to «annotate the first parameter of `run`, or annotate a
191
+ `delivery.by` parameter», and the diagnostics gate holds those words.
192
+ - **The node now reads like every other one.** `effect`, `scope.while` and `scope.switch` all take the working
193
+ callback straight after the source; the event was the one node with a second callback wedged between them. The
194
+ rule the spec states for the whole vocabulary — the source first, the work next, modifiers last — is true here
195
+ without an exception now.
196
+ - **No single-signature cure exists, and that was measured rather than assumed.** On TypeScript 7.0.2, 6.0.2 and
197
+ 6.0.3: carrying the payload through a separate inferred parameter as `Parameters<Run>[0]` answers `never`;
198
+ `NoInfer` on `emit` and `NoInfer` over the whole `subscribe` type both fail, because a reference to the parameter
199
+ is still a reference; a separate `Emitted extends Payload` fails; and a conditional `emit` that widens to
200
+ `unknown` or `any` stops the complaint at `emit` and poisons the inference instead. While `subscribe` stands
201
+ before `run` and mentions the payload, the order is the only lever there is.
202
+ - **`@opetope/lint` moves with it.** The ingress of an event is the third argument now, so
203
+ `opetope/no-subscribe-outside-models` reads it there; a project that pins the rule's behaviour by its own fixtures
204
+ updates them the same way an application updates its calls.
205
+ - **What the gates read.** Instantiations are unchanged — 11 756, 87 710 and 160 268 against ceilings of 12 200,
206
+ 99 062 and 181 237 — because a reorder adds no type. The tightest size row, the headless session consumer of
207
+ `@opetope/devtools`, stays at 6.95 kB of 7.1 kB: the only emitted JavaScript that changes is one `TypeError`
208
+ message, `Model event run and subscribe must be functions.`, and the argument index the lint rule carries.
209
+
210
+ ## 0.12.0
211
+
212
+ ### Minor Changes
213
+
214
+ - 34bdbfb: **A retired word is named, not rewritten: `opetope/no-retired-vocabulary` (D404).** The rule reports every word the
215
+ D365–D401 wave retired, together with the word that replaced it: `invalidate()` → `reset()`; `backpressure` →
216
+ `delivery`, with `kind` → `pending` and `key` → `by` inside the record; `activity` of a `loading` state → `failure`;
217
+ `scope.keyed(source, open, { key })` → `scope.each(source, key, open)`; `skipInitial` → `initial`, `when` of an
218
+ effect → `filter` or `initial`, `onDispose` → `finalize`; `externalReadable` → `fromExternal`;
219
+ `Call`/`ctx.call`/`runCall` → `Command`/`ctx.command`/`runCommand`; `policy`/`queueBy`/`lane` → `concurrency`;
220
+ `ctx.calls`/`own.calls`/`useCommands` → `ctx.select`/`own.select`/`useModel(Declaration, select)`; `once` →
221
+ `memoize`. It joins `configs.recommended` and the shipped `oxlintrc.json` as an error.
222
+
223
+ ```ts
224
+ ctx.command(run, { policy: 'latest' });
225
+ // ^ `policy` is now `concurrency`: one word for the order the runs take …
226
+ ctx.effect(draft, save, { skipInitial: true });
227
+ // ^ `skipInitial` is now `initial`, and the polarity is inverted …
228
+ ```
229
+
230
+ - **There is no autofix, and that is the decision.** Several of these renames are not one to one: `policy`,
231
+ `queueBy` and `lane` collapse into one `concurrency` whose shape depends on which of the three a record wrote
232
+ together, `when` at an effect becomes `filter` **or** `initial` depending on what the predicate asks, and
233
+ `skipInitial` → `initial` inverts the polarity. A fix that guessed wrong would rewrite a consumer's source
234
+ silently, which is worse than refusing, so each report names the word, names the replacement, and says so where
235
+ the replacement is a choice.
236
+ - **The severity is `error` because every one of these words is already a compile error on this release** — there
237
+ are no aliases — so the rule cannot redden a build that was green; what it adds is the one message that names the
238
+ replacement. The cases where the compiler is silent, such as an options record assembled beside the call, are
239
+ exactly the ones a warning would lose, and `--quiet`, which both linters are run with here, prints no warnings at
240
+ all. A consumer who wants one pass softer writes the rule into its own `rules` after `extends`.
241
+ - **Ownership is proved by symbol, never by spelling.** A retired member is reported only on a declaring context of
242
+ this library — a parameter annotated `ModelContext` of `@opetope/core`, or an `own` section of a `defineFeature`
243
+ of `@opetope/runtime` — a retired name only where an `@opetope/*` module exports it, and `activity` only inside a
244
+ narrowing that proves the state is `loading`. `Call` in «Margin Call» and in translation keys, the `{ once: true }`
245
+ of `addEventListener`, a host's own `policy` option, `Navigator.reset()` and `when` at a feature, at a
246
+ contribution and at `scope.while` are all left alone.
247
+ - The rule is written for one migration pass and never fires after it. Its silence is not proof that a migration is
248
+ complete: a Resource reached through a model record, a state narrowed in a `switch`, and an options record
249
+ assembled in another module stay with the compiler, which since D401 prints the replacement for every word it can
250
+ name.
251
+
252
+ - aaaa92c: **A collection scope is `each`, and its key is written where it belongs (D378).** `scope.keyed(source, open, { key })`
253
+ is now `scope.each(source, key, open)`, in a model and in `own` alike. Nothing about the node changes — the same
254
+ controller, the same replace → drain → open order, the same refusals and the same key identity.
255
+
256
+ ```ts
257
+ // before
258
+ ctx.scope.keyed(rows, (row, { signal }) => watch(row.id, signal), { key: row => row.id });
259
+ // after
260
+ ctx.scope.each(
261
+ rows,
262
+ row => row.id,
263
+ (row, { signal }) => watch(row.id, signal),
264
+ );
265
+ ```
266
+
267
+ `keyed` named the mechanism rather than the relationship its two neighbours name: `while` says «as long as»,
268
+ `switch` says «instead of», and `keyed` only said that there are keys inside — which is also what `keyedLane`, a
269
+ keyed Resource and a keyed delivery say, where a key tells apart the parallel queues of one node instead of deciding
270
+ how many children exist. `each` says what the node does: one child per element.
271
+
272
+ - The key moved into the signature because it is required, and a required member of a modifier record is not a
273
+ modifier: of the nodes that have one calling form, `scope.keyed` was the only one whose trailing record was
274
+ mandatory. D279 is not repealed — its principle holds and D378 names this one exception to its letter. The keyed
275
+ form of a command, `call(run, { lane: ctx.keyedLane<Key>(), queueBy })`, keeps its required record and is
276
+ unaffected: there the record selects which scheduling form the node has, so what is required is required by the
277
+ chosen form rather than by the node.
278
+ - `Value` is still inferred from the source alone. Both callbacks read it through `NoInfer`, so an annotation on the
279
+ key or on the child is checked against the list rather than widening it; a compiled type test records that.
280
+ - The messages of the node carry the word the author wrote: `Feature each scope contains duplicate key d.`,
281
+ `Model each scope source must contain a readonly array.`
282
+ - `@opetope/lint` knows the new argument index: for `scope.each` the callback that opens is the third argument, so
283
+ `opetope/no-subscribe-outside-models` stays silent exactly where it did.
284
+ - There is no alias: a `scope` object has no `keyed` member at all.
285
+
286
+ - bb05f21: **The advice a library gives has to be one an author can carry out (D444).** Three places where it was not. Nothing
287
+ runs differently: no public name, no lifecycle law and no runtime branch changes, and the four size consumers are
288
+ byte-identical to the ones before this change.
289
+
290
+ **The `call` → `command` report named a replacement that does not compile.** The rule said the body «takes one
291
+ argument, its context, and reads the declared input off it as `input`» and never said that the input is named by
292
+ **annotating** that context — and there is no other place to name it, because TypeScript has no partial inference.
293
+ Measured on a consumer tree migrated to the letter of that report: the lint run is **green, 0 messages**, and `tsc`
294
+ is **red with 4 errors**, every one of them standing on code the author wrote correctly — at an `invoke`, at a host
295
+ call, at the shape a factory returns — and the last of them reading backwards, `Type 'Deposit' is not assignable to
296
+ type 'void'`. The report names both annotations now,
297
+ `({ input, update }: ModelCommandContext<Deposit>)` and `({ input, source }: FeatureCommandContext<typeof within,
298
+ Deposit>)`, and so do both language pairs of the lint README. The type says it too: reading an input off a context
299
+ the body never annotated is refused **on the read**, with that annotation printed, instead of answering `void` and
300
+ letting the refusal land somewhere else. The `void` default, the result type and both inference forms D386 promises
301
+ are untouched.
302
+
303
+ **The order the upgrade guide argued from could not be carried out.** «Taken against the tree as it stands before
304
+ any rename, the same report is what it is» — measured, it is not: on a pre-wave tree carrying a real D288 defect the
305
+ first lint run reports seven `opetope/no-retired-vocabulary` and **nothing else**. Four rules of the six read a
306
+ shape this wave introduced and say nothing until it is written: `no-write-after-source-write` reads the writer off
307
+ the effect run's second parameter, `capture-command-cleanup` off a command body's one parameter, `enabled-predicate`
308
+ reads the field `enabled`, and `no-command-in-deps` names the record `useModel(Declaration, select)` answers.
309
+ `prefer-effect-current` reports in both shapes and writes its fix only on the new one. The guide names that limit in
310
+ both pairs and gives a two-pass order: the four shape renames first — all four named by the first run or by `tsc`,
311
+ and none of them moving a write, a commit or a dependency array — then the lint again, and that is the run the rest
312
+ of the guide is about. Teaching the rules the old shape was weighed and refused: the pre-wave effect run and a legal
313
+ new run destructuring a value that carries `update` are the same syntax, so a rule taught the old one reports
314
+ correct new code.
315
+
316
+ **Six retired words were refused without a replacement.** `skipInitial`, `when` and `onDispose` at an effect,
317
+ `backpressure` at an event, `when` at a contribution and `from` at `defineCondition`. Five of them answered one
318
+ sentence — «is not a field this record names» — which is true of a word that never existed and false of a word that
319
+ did, and on `skipInitial`, the one rename of the wave a word-for-word transfer gets wrong in silence, it advised
320
+ deleting the word; the sixth, `from`, named the word and not the replacement. Each record names its retired word now, in the form `policy` and `once` already use, and `skipInitial` names
321
+ the inverted polarity as well. A retired word whose value is a callback is spelled as a callback, so the predicate
322
+ an author wrote keeps its contextual type: on the same probe the six words cost **10 errors before and 7 after**,
323
+ and the three that went were implicit-`any` noise around the instruction.
324
+
325
+ - aaaa92c: **An effect run takes its value first, and its modifiers are `initial`, `filter` and `finalize` (D379).** The run is
326
+ `(current, context)` instead of one context that carried `current` inside it, and the three tail options are renamed.
327
+ No branch of the runtime changes: the same loop, the same supersession, the same failures, the same release order.
328
+
329
+ ```ts
330
+ // before
331
+ ctx.effect(draft, async execution => save(execution.current, execution.signal), {
332
+ skipInitial: true,
333
+ when: (current, previous) => current.text !== previous?.text,
334
+ onDispose: ({ source }) => close(source),
335
+ });
336
+ // after
337
+ ctx.effect(draft, async (current, { signal }) => save(current, signal), {
338
+ filter: (current, previous) => current.text !== previous?.text,
339
+ finalize: ({ source }) => close(source),
340
+ initial: false,
341
+ });
342
+ ```
343
+
344
+ - Position 1 of a body is what the body is about, everywhere else in the vocabulary: `call` takes `(input, context)`
345
+ and the child of a scope takes `(value, context)`. The effect run hid its subject inside the context; the `run` of
346
+ an event still does, carrying `payload` as a member, and this release does not change it. That context is one of
347
+ the places an event's payload is named (D324, D347), so moving the subject there moves an inference site — its own
348
+ decision, with its own measurement — and an event's `subscribe` cannot follow at all, since its subject is `emit`,
349
+ a channel rather than one value.
350
+ - `previous` stays on the context. It is not the subject of the run but a circumstance of it, beside `signal`,
351
+ `timers` and `source`; a run that needs both writes `(current, { previous })`. D379 names the price — in a run that
352
+ reads both, the pair now spans two levels — and the measurement behind the choice.
353
+ - `when` is `filter`. The same word names a feature's and a slot's condition of existence, and as an effect option it
354
+ was never that: it selects values without gating the node's lifetime.
355
+ - `skipInitial: true` is `initial: false`, and the modifier now defaults to `true`. The old name promised to skip the
356
+ initial _value_; D296 says that value is observed either way and only the _run_ is skipped.
357
+ - `onDispose` is `finalize`: an effect has two releases — the disposer a run returns, and the one the node itself
358
+ gets once — and an `on*` prefix did not tell them apart. No word of the authoring vocabulary carries that prefix
359
+ any more.
360
+ - `opetope/prefer-effect-current` now rewrites `source.getSnapshot()` to the run's first parameter, under whatever
361
+ name the author bound it; `opetope/no-write-after-source-write` reads the writer off the context in its new
362
+ position. There are no aliases: the three retired words are refused as unknown fields of the record.
363
+
364
+ - dae5079: **The one section order now reaches the body file, and a selection from a required import says why it takes no
365
+ options (D434, D435).** Two named limits of the wave, both of them a refusal that was missing rather than a
366
+ behaviour that was wrong. Nothing runs differently: the runtime checks the same records in the same order, and
367
+ every call that compiled still compiles.
368
+
369
+ - **`opetope/define-feature-property-order` reads both callees.** It compared the callee with `defineFeature`, and
370
+ the body form's callee is `defineFeature.body`, so the three sections of every body file were unordered — a
371
+ `{ exports, own }` body passed in silence under a rule that stands at `error` in `recommended`. Both callees are
372
+ now read against the one order of spec §2.1 and D280, `imports`, `requires`, `own`, `exports`, `provides`,
373
+ `when`, `body`, of which a body writes a subsequence. **Breaking for a consumer** whose `defineFeature.body(…)`
374
+ is written in another order: it becomes an `error`, with the same autofix that reorders the header form. A key
375
+ no body takes, a spread, and `defineFeature.body(header)` are still left to the type checker.
376
+ - **The message names the sections of the callee it reports.** It enumerated all seven; a body takes three, and the
377
+ other four are not keys an author can write there. The body form now reads `own, exports, provides`. The
378
+ `messageId` and the options schema are unchanged, so a configuration that names either keeps working.
379
+ - **`own.select` beside a required import answers with an instruction instead of `never`.** `whenMissing` answers
380
+ an absence only an `optional(…)` import can have, and scheduling belongs to `own.command`, so there is no legal
381
+ options record there at all — and the parameter typed `never` printed back the key the author had written,
382
+ correct as written, refused against a word that says nothing about the record. The key set is inferred and
383
+ mapped to the sentence, the way every other unnamed word of this family is, and the sentence says there is
384
+ nothing to write here and that the call is two arguments long. An empty record and an explicit `undefined` are
385
+ named too, instead of asking for a `whenMissing` that the next line would refuse.
386
+ - **Where the report lands moved, for `own.select` only.** It now points at the word inside the options record
387
+ rather than at the whole argument. Source that pins a refusal to a line — a `@ts-expect-error` above a
388
+ multi-line call — moves the directive to the line of the word. No call shape changes.
389
+
390
+ - aaaa92c: **One word for a command, one for its order (D385–D389).** The primitive is `Command<Input, Output>`: the type was
391
+ the last place the vocabulary still said `Call`, while the hook was already `useCommand`, the record `CommandHook`,
392
+ the outcome `CommandOutcome` and the fixture `command(...)`. `CallError` is `CommandError`, `runCall` is
393
+ `runCommand`, `RunCallOptions` is `RunCommandOptions`, `CallTimeoutError` is `CommandTimeoutError` and
394
+ `CallInputArgs` is `CommandInputArgs`. There are no aliases.
395
+
396
+ - **A body takes one argument, its context (D386).** `ctx.command(({ input, update }) => …)` and
397
+ `own.command(within, ({ input, source, signal }) => …)`. `Input` and `Output` default to `void`, so the
398
+ `_input: void` placeholder is gone from every command that takes none. A typed input is declared once, by
399
+ annotating the destructured context: `({ input, update }: ModelCommandContext<Deposit>)` or
400
+ `({ input, source }: FeatureCommandContext<typeof polling, Deposit>)`. Both types are new public exports. Give a
401
+ model factory a result type — `function createCounter(context: ModelContext): ModelOf<typeof Counter>`, the form
402
+ the examples use: inside an inline arrow handed straight to `openModel(Decl, ctx => …)` or `model(Decl, ctx => …)`
403
+ the expected type is not instantiated yet, so a body that names no input reads as `Command<never, …>` instead of
404
+ taking the `void` default.
405
+ - **`concurrency` replaces `policy`, `queueBy` and `lane` (D387).** One word that also takes the queue node itself:
406
+ `'parallel' | 'queue' | 'latest' | lane | { by } | { by, lane } | { lane, pending: 'latest' }`. A lane is a
407
+ reference commands share, which is why the word accepts it; `'parallel'` beside a lane and a bare `by` without a
408
+ queue are now unwritable rather than refused. `dedupe` stays a sibling option and `'latest'` still refuses it.
409
+ A selection writes the same word with the members it can answer for — the keyed record is not one of them, and
410
+ a feature has no keyed queue either, so both are refused with the reason. The retired `policy`, `queueBy`, `lane`
411
+ and `once` are named by each options record, so they are refused in a written literal and in an assigned record
412
+ alike — on `ctx.command`, on `own.command` and on `ctx.select`, which also refuses `memoize` and `dedupe`.
413
+ - **Declaring and selecting are two words (D388).** `ctx.command(run, options?)` declares;
414
+ `ctx.select(source, 'name' | ['a', 'b'], options?)` selects what a dependency already declares — one name answers
415
+ one command, a list answers a record. `ctx.calls` and `own.calls` are gone, and so is `useCommands`: a record of command consumers
416
+ comes from `useModel(Declaration, select)`, which has an owner and a lifetime. `bind` of `@opetope/runtime` is
417
+ unchanged.
418
+ - **`once` is `memoize` (D389).** The cache keeps its behaviour and loses a name the platform already owns:
419
+ `addEventListener(type, handler, { once: true })` means "then stop", which is not what this option ever did.
420
+
421
+ `@opetope/lint` follows the vocabulary: `opetope/capture-command-cleanup` reads the context off the body's one
422
+ parameter and recognises `concurrency: 'parallel'`, and `opetope/no-command-in-deps` names the selection a record
423
+ comes from.
424
+
425
+ - 34bdbfb: **One word for one thing, and one sentence for one question (D410, D411).** A vocabulary pass over the public API:
426
+ the refusals that asked one question in five sentences now ask it in one, the position a signature calls `source`
427
+ is called `source` everywhere, and eight words that named one behaviour twice are one word apiece. No branch of the
428
+ runtime changes: the same checks run in the same order.
429
+
430
+ **Breaking, word by word.** Every row is a source change with no alias; `opetope/no-retired-vocabulary` reports each
431
+ one and names the replacement.
432
+
433
+ | Before | After |
434
+ | -------------------------------------------------------------- | --------------------------------------------------- |
435
+ | `defineCondition(id, { from, select })` | `defineCondition(id, { select, source })` |
436
+ | `slot(target, value, { when })`, same at `pipe`, `register` | `slot(target, value, { enabled })` |
437
+ | `resource.live({ delivery: { overflow: 'reject' } })` | `resource.live({ delivery: { overflow: 'drop' } })` |
438
+ | `outcome.status === 'ok' \| 'failed' \| 'cancelled'` | `outcome.kind === 'ok' \| 'failed' \| 'cancelled'` |
439
+ | `ResourceActivity` from `@opetope/devtools` | `ResourceNodeActivity` from `@opetope/devtools` |
440
+ | `ResourceDisposer` from `@opetope/core/internal` | `Disposer` from `@opetope/core/internal` |
441
+ | `NavigatorPolicy` from `@opetope/navigation` | `NavigatorCatalogue` |
442
+ | `surface.policy` | `surface.catalogue` |
443
+ | `ApplicationConditionBinding` from `@opetope/runtime/internal` | the same name, from `@opetope/runtime` |
444
+
445
+ `@opetope/lint` renames the rule that read that field: `opetope/when-predicate` is `opetope/enabled-predicate`,
446
+ and its message ids are `enabledIsAsync`, `enabledReturnsBareSource` and `enabledReturnsSource`. A configuration
447
+ that named the old rule has to name the new one; `configs.recommended` and the shipped `oxlintrc.json` fragment
448
+ already do.
449
+
450
+ Two of these are record keys, so a source with `sort-keys` reorders the literal as well as renaming the key:
451
+ `{ from, select }` becomes `{ select, source }`, and `{ Component, when }` becomes `{ Component, enabled }`.
452
+ `{ status: 'ok', value }` becomes `{ kind: 'ok', value }`, which sorts unchanged, while
453
+ `{ reason, status: 'cancelled' }` becomes `{ kind: 'cancelled', reason }`, which does not.
454
+
455
+ **Why each one.** `source` is the word every context already answers the position with, and `from*` stays the
456
+ prefix of a factory that names where a `Readable` comes from (`fromExternal`, `fromMaybe`). `enabled` is the truth
457
+ the runtime reads while a node exists — one word at a contribution and at a Resource — while `when` keeps the other
458
+ mechanism, the declared `Condition`s an application resolves before the node exists; `scope.while(source, open,
459
+ { when })` and an effect's `filter` are untouched. `drop` is what a full capacity does with the value that does not
460
+ fit, at both nodes that admit a producer; `flush-oldest` is still the default of a keyed delivery and still the
461
+ answer for a producer that may lose nothing. `kind` is the discriminant eleven settled answers already carried.
462
+ `NavigatorCatalogue` is named for its job, beside `NavigatorPersistence`, because a policy is data (D291) and this
463
+ is four callbacks a navigator asks.
464
+
465
+ **Not breaking, and worth reading.** Every options record now refuses an unknown field with the sentence spec §3
466
+ promised — `<subject> <node> <record> contains unknown field <key>.` — and says which field it owes with
467
+ `<subject> <node> <record> requires <key>.` Five sentences became one, and the three that never printed the field
468
+ at all (`requires exact data fields`, `own.attach takes open, or open and close, and nothing else`) now print it. A
469
+ node that reads a source says so in two sentences per surface instead of three, and the word of the position is the
470
+ author's. `own.select` lost two of its four overloads: a call that writes the options record has one candidate of
471
+ its arity now, so a fault in that record is reported as the record's own refusal instead of
472
+ `Argument of type '"echo"' is not assignable to parameter of type 'never'`, and the record names the five words a
473
+ selection does not take — `concurrency`, `dedupe`, `lane`, `memoize`, `policy` — each with what to write instead.
474
+
475
+ **Measured.** Runtime size consumers, rebuilt: `application graph consumer` 48 248 B against a ceiling of 49 000
476
+ (48 318 before), `public feature consumer` 39 579 B against 41 000 (39 712 before). Type stress: 11 695 / 87 068 /
477
+ 159 371 instantiations, unchanged to the digit. No ceiling moved.
478
+
479
+ ### Patch Changes
480
+
481
+ - dae5079: **A refusal names a remedy the author can execute, and a gate holds what the tree prints (D415).** Four debts the
482
+ wave D365–D414 left behind, each reproduced before it was fixed and measured after. No branch of the runtime
483
+ changes.
484
+
485
+ **The instruction of `EventPayloadNotNamed` carries its condition.** `run` names the payload of an event only while
486
+ it is not context-sensitive, and a callback becomes context-sensitive as soon as one parameter it writes has no
487
+ annotation. The old text said «annotate the first parameter of `run`», which for a `run` that also destructures its
488
+ context names nothing and earns a second refusal — the shape the cookbook's `polling` recipe was written in. The
489
+ marker now reads «name the event payload: annotate every parameter of `run`, the payload first — a `run` that
490
+ leaves its context unannotated names nothing — or annotate a `delivery.by` parameter», the recipe is written in the
491
+ form it names, and spec §2.23 compiles that form. A source that compares the marker text line by line updates the
492
+ line; the shape of author code is unchanged.
493
+
494
+ **`ctx.select` refuses the record and not the key.** Its four overloads are two, differing by how many arguments
495
+ they take, the way D410 collapsed `own.select`. A misspelled or retired field of the one-name form used to print
496
+ `Argument of type '"load"' is not assignable to parameter of type 'never'`, naming the key the author got right; it
497
+ now prints the instruction the record carries. Both call forms and every written type argument list are accepted as
498
+ before.
499
+
500
+ **spec §3 names the three sentences an owed field actually has**, instead of promising one that two nodes of six
501
+ hold: a record all of whose keys are required says `requires <key>.`, a key that has to be a callback says
502
+ `<key> must be a function.` — which names its type as well — and a key owed only because a sibling was written says
503
+ which sibling.
504
+
505
+ **`opetope/no-retired-vocabulary` names the last three migrations it was missing.** `ApplicationExecution` and
506
+ `ApplicationImportBinding` of `@opetope/runtime/internal` are reported beside `ApplicationConditionBinding`, and
507
+ `surface.policy` is reported on a surface `defineSurface` of `@opetope/navigation/react` declared — the word the
508
+ message of `NavigatorPolicy` already named and the rule never fired on. The shared message id is
509
+ `runtimeInternalEntry`, renamed from `conditionBindingEntry` because it answers three names now.
510
+
511
+ Size: `application graph consumer` 48 248 B against 49 000, `public feature consumer` 39 579 B against 41 000 —
512
+ byte for byte what D410 measured. Type stress: 11 695 / 87 068 / 159 371 against ceilings 12 034 / 99 062 /
513
+ 181 237, to the digit the recorded baseline, which is not rewritten.
514
+
515
+ - bb31983: **Five debts the wave named out loud, closed together (D437).** None of them was found by reading the code: each was
516
+ written down as a limit or a debt by the decision that left it, and left because the files it needed were being
517
+ edited on another branch. No authoring word moves, no runtime behaviour changes, and no package `src` outside the
518
+ lint rule's message is in this diff.
519
+
520
+ - **`opetope/no-write-after-source-write` stops saying «in silence».** D432 made the first write such a run loses
521
+ reach the reporter as `LostWrite`, once per run and saying the rest went with it, so the rule's message and its
522
+ section in both READMEs were describing a silence that is no longer whole. All three are corrected in one hand:
523
+ the drop is still a drop, the refused `invoke` after it is still silent, and so is every other way a write is
524
+ lost — a newer value, a cancelled caller, a generation fence, a closed model. The README also stops implying that
525
+ a successor which hides the loss from the cell hides it from the record; it does not.
526
+ - **The example gate of `ci:docs` reads both callees that declare sections.** D434 recorded it as a named limit:
527
+ blocks were selected by `\bdefineFeature\s*\(`, which does not match `defineFeature.body(`, so a document block
528
+ declaring only a body reached the packaged order rule through nothing. The selection is
529
+ `\bdefineFeature(?:\.body)?\s*\(` now, spec §5.8 carries both forms in both languages, and the gate reads 60
530
+ feature examples where it read 58. Nothing was passing through the hole — both bodies of the repository hold the
531
+ order — so the change is proved by injection instead.
532
+ - **`docs/resource-migration.md` joins the compiled examples (D419).** Nine blocks are lifted into checked
533
+ fixtures, which makes eleven documents whose examples `ci:type` reads — the second migration guide to join, under
534
+ the «was» mechanism the first one brought with it. Three of its blocks are «before» blocks and cannot compile: each
535
+ carries `// @ts-expect-error` on the line that shows the removed form, so the count of asserted refusals is five
536
+ for one guide, three for this one and zero for the other nine. Nothing in the guide was wrong: the six live
537
+ blocks compiled as written.
538
+ - **The comment over four rows of `selected-call-diagnostics.test.mjs` stopped being true.** It pinned the shape
539
+ D435 has since replaced — a parameter of type `never`, a report anchored at the argument. It now quotes what the
540
+ tree prints.
541
+ - **The type ceiling of slice `01` is raised by the owner, 12 034 → 12 200.** The slice measures 11 695
542
+ instantiations, which left 2.8 % under the ceiling where D400 named 3.9 % over the measurement as its alarm; the
543
+ raise restores 4.1 % under the ceiling and 4.3 % over the measurement. No other ceiling moves, no bundle byte
544
+ changes, and the granted headroom may not be cited as an argument: it is given, not measured.
545
+
546
+ - aaaa92c: **The write a run loses after moving its own source (D365).** A write of an effect run into a cell its own source
547
+ reads changes the value that source publishes, and the notification is synchronous: the run is aborted inside that
548
+ write, so every write of the run after it is dropped and an `invoke` after it is refused. That is the law of spec
549
+ §2.11 and of a late write (D288), and neither changes here. The drop was silent when this was written; D432, later in
550
+ the same release, sends the first of the dropped writes to the reporter as `LostWrite`, and the refused `invoke`
551
+ stays silent. What was missing is the reason the defect reaches production instead of a test, and the tool that
552
+ catches it.
553
+
554
+ - **`opetope/no-write-after-source-write`**, `error` in `configs.recommended` and in the shipped `oxlintrc.json`. It
555
+ reports the write that moves the effect's own source when another write of the same run can follow it, and it
556
+ proves three things first: the effect is declared on a context of this library; the cell the write names is part of
557
+ the source — the cell itself, or a `derive` of `@opetope/core` whose dependencies name it **and whose selector
558
+ publishes the value of that dependency**, which is the law's own carve-out for a write that leaves the published
559
+ value equal; and the later write stands in the same block, in a later statement, with no `return` or `throw` of the
560
+ run between the two. A source assembled in another function, a source read off an object, a cell that arrived as a
561
+ parameter, a `derive` over a `derive` and a commit as the moving write are all outside what it reads, and its
562
+ README lists them. There is no fix: make the write into the source the run's last write, or keep what the run
563
+ accumulates out of its source and read it with `getSnapshot()`.
564
+ - **The successor run is now written down** in spec §2.11, in spec §4 «Effect runs» and in `primitives` §4.1 step 8:
565
+ the loop opens a successor run for the new value **where `when` admits it**, and a successor that repeats the write
566
+ hides the loss, so the cell ends up correct through the second run. The loss stands whole where a guard makes that
567
+ successor return early, and where a `when` that rejects the new value leaves no successor at all (D329). A third
568
+ form is recorded with it: a `capture` taken after the moving write throws the run's own cancellation, and nothing
569
+ is reported for it either.
570
+ - **A repeated commit names its cure**: `State commit has already been used.` became
571
+ `State commit has already been used. Capture per write.` — legitimate by the same section, which says two commits
572
+ of one target in one call are what a command with two mutually exclusive places to write needs.
573
+
574
+ This change moves no budget by itself. Measured against a clean rebuild of `af278de`: the runtime
575
+ `public feature consumer` is 38 771 → 38 784 B and the `application graph consumer` 47 394 → 47 403 B. The two
576
+ runtime ceilings are ratcheted once for the whole wave this ships in, over a measurement of the merged tree
577
+ (D372).
578
+
579
+ - bb05f21: **A lost write is heard by the whole authority of a run, not by one writer (D447).** D432 made the most expensive
580
+ defect of this library audible: a write of a run into a cell its own source reads ends that run inside the write, and
581
+ the writes after it are dropped where everything still looks repaired. It heard only the writer that made the moving
582
+ write — so the shape the library itself recommends stayed mute. When one operation is shared by an effect and a
583
+ command (D288), the effect invokes the command, the command makes the write, and the command has nothing more to
584
+ write: the run lost the money and nobody said anything.
585
+
586
+ - **The authority is the run and the commands it `invoke`s.** A command inherits the cancellation of the run that
587
+ started it, so one abort ends them both, and the moving write may be made by any of them. The first write that
588
+ authority drops reaches the reporter as `LostWrite`, once, with the same three remedies as before.
589
+ - **Nothing outside that authority changed.** A write dropped by a newer value, a cancelled caller, a generation
590
+ fence or a closed model is as silent as it was — the abort each of them carries is its own, and the runtime proves
591
+ the authority by the identity of the abort, not by a guess about timing. This is still the half of D288 that stands.
592
+ - **The record's sentence moved with the law**: «the run had already ended inside an earlier write under its own
593
+ authority into a cell its source reads». A test that pinned the old sentence word for word needs the new one.
594
+ - **`opetope/no-write-after-source-write` now reports the call**, not just the write: an `invoke` of a command that
595
+ writes a cell the run's source reads, where another write of the run follows it. That form is the one the runtime
596
+ usually cannot report — the run is awaiting that very call when the write lands, so its promise is cancelled and
597
+ the body reaches no write at all — which is why the editor has to name it.
598
+
599
+ Measured by rebuilding the size consumers of one tree with and without the change: `application graph consumer`
600
+ 48 455 → 48 515 B against a 49 000 ceiling, `public feature consumer` 39 835 → 39 884 B against 41 000. The three
601
+ Core lines did not move. No ceiling moved, and no public name, signature or type changed.
602
+
603
+ - bb05f21: **The prose of `opetope/enabled-predicate` names the field the rule reads (D220, D411).** D411 renamed the field of
604
+ a contribution from `when` to `enabled`, and the rule that reads it from `opetope/when-predicate` to
605
+ `opetope/enabled-predicate`. The heading and the table of reported shapes moved with it; four sentences around them
606
+ did not, and they were the ones telling the reader which word to write. Both package READMEs now say:
607
+
608
+ - a contribution's `enabled` — not its `when` — answers the visibility fact of this instance, not the source
609
+ that carries it;
610
+ - the rule reads the `enabled` of a `slot`, `pipe` or `register` contribution, and any `enabled` whose function
611
+ destructures the evaluation context;
612
+ - a decision the predicate cannot change is hoisted out of `enabled`, or the line is silenced;
613
+ - a `when` is a different field and is left alone wherever it stands. The sentence used to set «a `when` outside a
614
+ contribution» against the `when` inside one, and since D411 a contribution has no `when` at all, so the
615
+ qualification had stopped dividing anything.
616
+
617
+ The word stays where it is still written: the `when` section of a feature declaration, the source value of
618
+ `scope.while`, the sentence that an effect's change filter is `filter` and not a `when` at all (D379), and the
619
+ historical rows of the retired-vocabulary table, which exist to name what a word used to be. Documentation only —
620
+ no rule, message, fix or configuration changes.
621
+
622
+ - bb31983: **Three places where the text an author reads did not match what the code does (D442).** No law of the runtime
623
+ moves and no public name is added; what changes is what the author is told when they are wrong.
624
+
625
+ - **The lint rule names all three ways out of a lost write, not two of them.** D432 made the first write a run
626
+ loses after moving its own source reach the reporter as `LostWrite`, naming three remedies, and recorded that
627
+ `opetope/no-write-after-source-write` already named them «in the same words». It named two. The one it left out —
628
+ `execution.capture(state)` taken _before_ the moving write — is the only one that saves that write whole, and the
629
+ author reads the rule in the editor long before any run reports. The message now carries the runtime's sentence
630
+ word for word, both package READMEs say so, and `ci:source` reads the sentence out of the runtime and looks for
631
+ it in the rule, so the two cannot drift again. Spec §2.11 also stops saying such a defect «is found in
632
+ production»: it used to be, and since D432 the record finds it.
633
+
634
+ - **A `request` written as `undefined` is refused on the key, and the refusal says so.** Every option of a Resource
635
+ declaration is read from the key the record carries (D407), so `{ ...shared, request: on ? account : undefined }`
636
+ writes the word. Under `exactOptionalPropertyTypes` the compiler refused that record — but for `request` alone it
637
+ refused it in the wrong place: `request` is where `Request` is inferred, so the written `undefined` became part of
638
+ the type parameter, `identity` turned required, and the message asked for a record the author never wrote
639
+ (`Property 'identity' is missing … key: (request: string | undefined) => never`), naming neither the key nor
640
+ `undefined`. An author who follows that message writes an `identity` and keeps the defect. The refusal now stands
641
+ on the key and carries the clause the runtime prints, plus the second remedy the migration guides teach: «a key
642
+ written as `undefined` is a word this declaration wrote: leave the key out where the word is not meant, or branch
643
+ the declaration». Both surfaces spell it, out of one string. Nothing legal moves — a request written as a value,
644
+ as a `Readable` (including one that publishes `undefined`, which closes the materialization instead of writing a
645
+ word), a spread that carries the key only where it is meant, a branch on the record, and a declaration with no
646
+ `request` are all accepted as before. Two forms do change: a `request` whose type is `any` is now refused, where
647
+ it used to be accepted and answered `Resource<Data, any>`, and a `request` whose type is `unknown` is refused with
648
+ this clause instead of with the old demand for an `identity`.
649
+
650
+ - **`PaginationUnavailableReason` is on `@opetope/core/internal`.** The union standing in `reason` of the public
651
+ `PaginationState` and `PaginationOutcome` was a file-local alias on no entry at all, while the gate that records
652
+ that choice already called it an internal type. It is now where that comment says it is. It is not published: the
653
+ compiler does print `'retired'` to a consumer whose exhaustive `switch` breaks, which is the argument D364 and
654
+ D375 used, but the union is reachable through the public record that carries it —
655
+ `Extract<PaginationState<Cursor>, { kind: 'unavailable' }>['reason']` — which is the reason D375 left
656
+ `ResourceChannel` internal. Publishing it would cost the last type of the public budget, and D364 reserved that to
657
+ the owner.
658
+
659
+ Measured, isolated, by rebuilding the same tree with and without the change: all five size rows are byte-identical
660
+ (1 047 / 7 885 / 4 856 B on Core, 48 455 and 39 835 B on the runtime consumers), because a marker is a type and
661
+ types are erased. The type budget moves +11 / +70 / +117 instantiations on the three stress slices, to
662
+ 11 706 / 87 138 / 159 488 against unchanged ceilings of 12 200 / 99 062 / 181 237. The form was chosen by
663
+ measurement and not by taste: the same instruction written as a fourth member of the declaration intersection costs
664
+ +71 / +1 429 / +2 795. `ci:public-surface` reads 59 types and 37 values before and after.
665
+
3
666
  ## 0.11.0
4
667
 
5
668
  No changes in this release.
@@ -155,11 +818,11 @@ No changes in this release.
155
818
 
156
819
  ### Patch Changes
157
820
 
158
- - b964772: Модель подтверждает мутации принадлежащего ей Resource, получает явный исход обновления, выполняет Call из
821
+ - b964772: Модель подтверждает мутации принадлежащего ей Resource, получает явный исход обновления, выполняет Command из
159
822
  effect и сериализует независимые объекты через очередь по ключу. Stream умеет накапливать кадры в границах
160
823
  открытия. Контекст данных отделён от ключа, а ёмкость кэша перенесена из retention в самостоятельную опцию cache.
161
824
  Retention теперь задаётся литералом, model resource/stream принимает готовый Readable, а `queueBy` создаёт
162
- приватную keyed queue команды или использует явно переданную общую lane. Effect переносит отмену вложенного Call
825
+ приватную keyed queue команды или использует явно переданную общую lane. Effect переносит отмену вложенного Command
163
826
  через `rethrowIfCancelled`.
164
827
 
165
828
  ## 0.9.2
@@ -228,7 +891,7 @@ opetope/no-subscribe-outside-models` in that application sits on a subscription
228
891
 
229
892
  - d7f346a: `opetope/no-command-in-deps` keeps a command hook out of a React dependency array.
230
893
 
231
- A hook read from `useCommand`, from a key of `useCommands` or from a `Call` field of a `useModel` selection is a
894
+ A hook read from `useCommand`, from a key of `useCommands` or from a `Command` field of a `useModel` selection is a
232
895
  snapshot of the render it was read in: the object is rebuilt on every `inFlight` and `outcome` change, while `run`
233
896
  keeps one identity for the life of the binding. An application that wrote
234
897
  `useEffect(() => { load.run(); return () => { unload.run(); }; }, [load, unload])` therefore started the command,
@@ -320,7 +983,7 @@ No changes in this release.
320
983
 
321
984
  ### Minor Changes
322
985
 
323
- - 410f2f0: Run authentic Calls in headless model tests with `runCall` from `@opetope/react/testing`, preserving their existing policies, cancellation, errors and owner lifetime.
986
+ - 410f2f0: Run authentic Calls in headless model tests with `runCommand` from `@opetope/react/testing`, preserving their existing policies, cancellation, errors and owner lifetime.
324
987
 
325
988
  Add the recommended `opetope/no-internal-imports` rule for static imports, re-exports, literal dynamic imports, unshadowed `require` and TypeScript import types. `configs.internalImports` uses this rule for JavaScript and TypeScript, including tests, without replacing the host's `no-restricted-imports`. The redundant `internalImportPattern` export is removed; allow trusted implementation files through the config or an explicit rule override.
326
989