@opetope/lint 0.10.1 → 0.12.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 (48) hide show
  1. package/CHANGELOG.md +464 -4
  2. package/README.md +314 -63
  3. package/README.ru.md +253 -63
  4. package/dist/ast.d.ts +3 -1
  5. package/dist/ast.js +1 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/command-hooks.d.ts +2 -2
  8. package/dist/command-hooks.js +1 -1
  9. package/dist/command-hooks.js.map +1 -1
  10. package/dist/context-members.d.ts +22 -0
  11. package/dist/context-members.js +2 -0
  12. package/dist/context-members.js.map +1 -0
  13. package/dist/declaration-ingress.d.ts +10 -2
  14. package/dist/declaration-ingress.js +1 -1
  15. package/dist/declaration-ingress.js.map +1 -1
  16. package/dist/effect-declarations.d.ts +20 -0
  17. package/dist/effect-declarations.js +2 -0
  18. package/dist/effect-declarations.js.map +1 -0
  19. package/dist/index.d.ts +11 -3
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/retired-vocabulary.d.ts +31 -0
  23. package/dist/retired-vocabulary.js +2 -0
  24. package/dist/retired-vocabulary.js.map +1 -0
  25. package/dist/rules/capture-command-cleanup.js +1 -1
  26. package/dist/rules/capture-command-cleanup.js.map +1 -1
  27. package/dist/rules/define-feature-property-order.js +1 -1
  28. package/dist/rules/define-feature-property-order.js.map +1 -1
  29. package/dist/rules/enabled-predicate.d.ts +6 -0
  30. package/dist/rules/enabled-predicate.js +2 -0
  31. package/dist/rules/enabled-predicate.js.map +1 -0
  32. package/dist/rules/no-command-in-deps.js +1 -1
  33. package/dist/rules/no-command-in-deps.js.map +1 -1
  34. package/dist/rules/no-internal-imports.js +1 -1
  35. package/dist/rules/no-internal-imports.js.map +1 -1
  36. package/dist/rules/no-retired-vocabulary.d.ts +5 -0
  37. package/dist/rules/no-retired-vocabulary.js +2 -0
  38. package/dist/rules/no-retired-vocabulary.js.map +1 -0
  39. package/dist/rules/no-write-after-source-write.d.ts +6 -0
  40. package/dist/rules/no-write-after-source-write.js +2 -0
  41. package/dist/rules/no-write-after-source-write.js.map +1 -0
  42. package/dist/rules/prefer-effect-current.js +1 -1
  43. package/dist/rules/prefer-effect-current.js.map +1 -1
  44. package/oxlintrc.json +3 -1
  45. package/package.json +1 -1
  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,465 @@
1
1
  # @opetope/lint
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 34bdbfb: **A retired word is named, not rewritten: `opetope/no-retired-vocabulary` (D404).** The rule reports every word the
8
+ D365–D401 wave retired, together with the word that replaced it: `invalidate()` → `reset()`; `backpressure` →
9
+ `delivery`, with `kind` → `pending` and `key` → `by` inside the record; `activity` of a `loading` state → `failure`;
10
+ `scope.keyed(source, open, { key })` → `scope.each(source, key, open)`; `skipInitial` → `initial`, `when` of an
11
+ effect → `filter` or `initial`, `onDispose` → `finalize`; `externalReadable` → `fromExternal`;
12
+ `Call`/`ctx.call`/`runCall` → `Command`/`ctx.command`/`runCommand`; `policy`/`queueBy`/`lane` → `concurrency`;
13
+ `ctx.calls`/`own.calls`/`useCommands` → `ctx.select`/`own.select`/`useModel(Declaration, select)`; `once` →
14
+ `memoize`. It joins `configs.recommended` and the shipped `oxlintrc.json` as an error.
15
+
16
+ ```ts
17
+ ctx.command(run, { policy: 'latest' });
18
+ // ^ `policy` is now `concurrency`: one word for the order the runs take …
19
+ ctx.effect(draft, save, { skipInitial: true });
20
+ // ^ `skipInitial` is now `initial`, and the polarity is inverted …
21
+ ```
22
+
23
+ - **There is no autofix, and that is the decision.** Several of these renames are not one to one: `policy`,
24
+ `queueBy` and `lane` collapse into one `concurrency` whose shape depends on which of the three a record wrote
25
+ together, `when` at an effect becomes `filter` **or** `initial` depending on what the predicate asks, and
26
+ `skipInitial` → `initial` inverts the polarity. A fix that guessed wrong would rewrite a consumer's source
27
+ silently, which is worse than refusing, so each report names the word, names the replacement, and says so where
28
+ the replacement is a choice.
29
+ - **The severity is `error` because every one of these words is already a compile error on this release** — there
30
+ are no aliases — so the rule cannot redden a build that was green; what it adds is the one message that names the
31
+ replacement. The cases where the compiler is silent, such as an options record assembled beside the call, are
32
+ exactly the ones a warning would lose, and `--quiet`, which both linters are run with here, prints no warnings at
33
+ all. A consumer who wants one pass softer writes the rule into its own `rules` after `extends`.
34
+ - **Ownership is proved by symbol, never by spelling.** A retired member is reported only on a declaring context of
35
+ this library — a parameter annotated `ModelContext` of `@opetope/core`, or an `own` section of a `defineFeature`
36
+ of `@opetope/runtime` — a retired name only where an `@opetope/*` module exports it, and `activity` only inside a
37
+ narrowing that proves the state is `loading`. `Call` in «Margin Call» and in translation keys, the `{ once: true }`
38
+ of `addEventListener`, a host's own `policy` option, `Navigator.reset()` and `when` at a feature, at a
39
+ contribution and at `scope.while` are all left alone.
40
+ - The rule is written for one migration pass and never fires after it. Its silence is not proof that a migration is
41
+ complete: a Resource reached through a model record, a state narrowed in a `switch`, and an options record
42
+ assembled in another module stay with the compiler, which since D401 prints the replacement for every word it can
43
+ name.
44
+
45
+ - aaaa92c: **A collection scope is `each`, and its key is written where it belongs (D378).** `scope.keyed(source, open, { key })`
46
+ is now `scope.each(source, key, open)`, in a model and in `own` alike. Nothing about the node changes — the same
47
+ controller, the same replace → drain → open order, the same refusals and the same key identity.
48
+
49
+ ```ts
50
+ // before
51
+ ctx.scope.keyed(rows, (row, { signal }) => watch(row.id, signal), { key: row => row.id });
52
+ // after
53
+ ctx.scope.each(
54
+ rows,
55
+ row => row.id,
56
+ (row, { signal }) => watch(row.id, signal),
57
+ );
58
+ ```
59
+
60
+ `keyed` named the mechanism rather than the relationship its two neighbours name: `while` says «as long as»,
61
+ `switch` says «instead of», and `keyed` only said that there are keys inside — which is also what `keyedLane`, a
62
+ keyed Resource and a keyed delivery say, where a key tells apart the parallel queues of one node instead of deciding
63
+ how many children exist. `each` says what the node does: one child per element.
64
+
65
+ - The key moved into the signature because it is required, and a required member of a modifier record is not a
66
+ modifier: of the nodes that have one calling form, `scope.keyed` was the only one whose trailing record was
67
+ mandatory. D279 is not repealed — its principle holds and D378 names this one exception to its letter. The keyed
68
+ form of a command, `call(run, { lane: ctx.keyedLane<Key>(), queueBy })`, keeps its required record and is
69
+ unaffected: there the record selects which scheduling form the node has, so what is required is required by the
70
+ chosen form rather than by the node.
71
+ - `Value` is still inferred from the source alone. Both callbacks read it through `NoInfer`, so an annotation on the
72
+ key or on the child is checked against the list rather than widening it; a compiled type test records that.
73
+ - The messages of the node carry the word the author wrote: `Feature each scope contains duplicate key d.`,
74
+ `Model each scope source must contain a readonly array.`
75
+ - `@opetope/lint` knows the new argument index: for `scope.each` the callback that opens is the third argument, so
76
+ `opetope/no-subscribe-outside-models` stays silent exactly where it did.
77
+ - There is no alias: a `scope` object has no `keyed` member at all.
78
+
79
+ - bb05f21: **The advice a library gives has to be one an author can carry out (D444).** Three places where it was not. Nothing
80
+ runs differently: no public name, no lifecycle law and no runtime branch changes, and the four size consumers are
81
+ byte-identical to the ones before this change.
82
+
83
+ **The `call` → `command` report named a replacement that does not compile.** The rule said the body «takes one
84
+ argument, its context, and reads the declared input off it as `input`» and never said that the input is named by
85
+ **annotating** that context — and there is no other place to name it, because TypeScript has no partial inference.
86
+ Measured on a consumer tree migrated to the letter of that report: the lint run is **green, 0 messages**, and `tsc`
87
+ is **red with 4 errors**, every one of them standing on code the author wrote correctly — at an `invoke`, at a host
88
+ call, at the shape a factory returns — and the last of them reading backwards, `Type 'Deposit' is not assignable to
89
+ type 'void'`. The report names both annotations now,
90
+ `({ input, update }: ModelCommandContext<Deposit>)` and `({ input, source }: FeatureCommandContext<typeof within,
91
+ Deposit>)`, and so do both language pairs of the lint README. The type says it too: reading an input off a context
92
+ the body never annotated is refused **on the read**, with that annotation printed, instead of answering `void` and
93
+ letting the refusal land somewhere else. The `void` default, the result type and both inference forms D386 promises
94
+ are untouched.
95
+
96
+ **The order the upgrade guide argued from could not be carried out.** «Taken against the tree as it stands before
97
+ any rename, the same report is what it is» — measured, it is not: on a pre-wave tree carrying a real D288 defect the
98
+ first lint run reports seven `opetope/no-retired-vocabulary` and **nothing else**. Four rules of the six read a
99
+ shape this wave introduced and say nothing until it is written: `no-write-after-source-write` reads the writer off
100
+ the effect run's second parameter, `capture-command-cleanup` off a command body's one parameter, `enabled-predicate`
101
+ reads the field `enabled`, and `no-command-in-deps` names the record `useModel(Declaration, select)` answers.
102
+ `prefer-effect-current` reports in both shapes and writes its fix only on the new one. The guide names that limit in
103
+ both pairs and gives a two-pass order: the four shape renames first — all four named by the first run or by `tsc`,
104
+ and none of them moving a write, a commit or a dependency array — then the lint again, and that is the run the rest
105
+ of the guide is about. Teaching the rules the old shape was weighed and refused: the pre-wave effect run and a legal
106
+ new run destructuring a value that carries `update` are the same syntax, so a rule taught the old one reports
107
+ correct new code.
108
+
109
+ **Six retired words were refused without a replacement.** `skipInitial`, `when` and `onDispose` at an effect,
110
+ `backpressure` at an event, `when` at a contribution and `from` at `defineCondition`. Five of them answered one
111
+ sentence — «is not a field this record names» — which is true of a word that never existed and false of a word that
112
+ did, and on `skipInitial`, the one rename of the wave a word-for-word transfer gets wrong in silence, it advised
113
+ 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
114
+ the inverted polarity as well. A retired word whose value is a callback is spelled as a callback, so the predicate
115
+ an author wrote keeps its contextual type: on the same probe the six words cost **10 errors before and 7 after**,
116
+ and the three that went were implicit-`any` noise around the instruction.
117
+
118
+ - aaaa92c: **An effect run takes its value first, and its modifiers are `initial`, `filter` and `finalize` (D379).** The run is
119
+ `(current, context)` instead of one context that carried `current` inside it, and the three tail options are renamed.
120
+ No branch of the runtime changes: the same loop, the same supersession, the same failures, the same release order.
121
+
122
+ ```ts
123
+ // before
124
+ ctx.effect(draft, async execution => save(execution.current, execution.signal), {
125
+ skipInitial: true,
126
+ when: (current, previous) => current.text !== previous?.text,
127
+ onDispose: ({ source }) => close(source),
128
+ });
129
+ // after
130
+ ctx.effect(draft, async (current, { signal }) => save(current, signal), {
131
+ filter: (current, previous) => current.text !== previous?.text,
132
+ finalize: ({ source }) => close(source),
133
+ initial: false,
134
+ });
135
+ ```
136
+
137
+ - Position 1 of a body is what the body is about, everywhere else in the vocabulary: `call` takes `(input, context)`
138
+ and the child of a scope takes `(value, context)`. The effect run hid its subject inside the context; the `run` of
139
+ an event still does, carrying `payload` as a member, and this release does not change it. That context is one of
140
+ the places an event's payload is named (D324, D347), so moving the subject there moves an inference site — its own
141
+ decision, with its own measurement — and an event's `subscribe` cannot follow at all, since its subject is `emit`,
142
+ a channel rather than one value.
143
+ - `previous` stays on the context. It is not the subject of the run but a circumstance of it, beside `signal`,
144
+ `timers` and `source`; a run that needs both writes `(current, { previous })`. D379 names the price — in a run that
145
+ reads both, the pair now spans two levels — and the measurement behind the choice.
146
+ - `when` is `filter`. The same word names a feature's and a slot's condition of existence, and as an effect option it
147
+ was never that: it selects values without gating the node's lifetime.
148
+ - `skipInitial: true` is `initial: false`, and the modifier now defaults to `true`. The old name promised to skip the
149
+ initial _value_; D296 says that value is observed either way and only the _run_ is skipped.
150
+ - `onDispose` is `finalize`: an effect has two releases — the disposer a run returns, and the one the node itself
151
+ gets once — and an `on*` prefix did not tell them apart. No word of the authoring vocabulary carries that prefix
152
+ any more.
153
+ - `opetope/prefer-effect-current` now rewrites `source.getSnapshot()` to the run's first parameter, under whatever
154
+ name the author bound it; `opetope/no-write-after-source-write` reads the writer off the context in its new
155
+ position. There are no aliases: the three retired words are refused as unknown fields of the record.
156
+
157
+ - dae5079: **The one section order now reaches the body file, and a selection from a required import says why it takes no
158
+ options (D434, D435).** Two named limits of the wave, both of them a refusal that was missing rather than a
159
+ behaviour that was wrong. Nothing runs differently: the runtime checks the same records in the same order, and
160
+ every call that compiled still compiles.
161
+
162
+ - **`opetope/define-feature-property-order` reads both callees.** It compared the callee with `defineFeature`, and
163
+ the body form's callee is `defineFeature.body`, so the three sections of every body file were unordered — a
164
+ `{ exports, own }` body passed in silence under a rule that stands at `error` in `recommended`. Both callees are
165
+ now read against the one order of spec §2.1 and D280, `imports`, `requires`, `own`, `exports`, `provides`,
166
+ `when`, `body`, of which a body writes a subsequence. **Breaking for a consumer** whose `defineFeature.body(…)`
167
+ is written in another order: it becomes an `error`, with the same autofix that reorders the header form. A key
168
+ no body takes, a spread, and `defineFeature.body(header)` are still left to the type checker.
169
+ - **The message names the sections of the callee it reports.** It enumerated all seven; a body takes three, and the
170
+ other four are not keys an author can write there. The body form now reads `own, exports, provides`. The
171
+ `messageId` and the options schema are unchanged, so a configuration that names either keeps working.
172
+ - **`own.select` beside a required import answers with an instruction instead of `never`.** `whenMissing` answers
173
+ an absence only an `optional(…)` import can have, and scheduling belongs to `own.command`, so there is no legal
174
+ options record there at all — and the parameter typed `never` printed back the key the author had written,
175
+ correct as written, refused against a word that says nothing about the record. The key set is inferred and
176
+ mapped to the sentence, the way every other unnamed word of this family is, and the sentence says there is
177
+ nothing to write here and that the call is two arguments long. An empty record and an explicit `undefined` are
178
+ named too, instead of asking for a `whenMissing` that the next line would refuse.
179
+ - **Where the report lands moved, for `own.select` only.** It now points at the word inside the options record
180
+ rather than at the whole argument. Source that pins a refusal to a line — a `@ts-expect-error` above a
181
+ multi-line call — moves the directive to the line of the word. No call shape changes.
182
+
183
+ - aaaa92c: **One word for a command, one for its order (D385–D389).** The primitive is `Command<Input, Output>`: the type was
184
+ the last place the vocabulary still said `Call`, while the hook was already `useCommand`, the record `CommandHook`,
185
+ the outcome `CommandOutcome` and the fixture `command(...)`. `CallError` is `CommandError`, `runCall` is
186
+ `runCommand`, `RunCallOptions` is `RunCommandOptions`, `CallTimeoutError` is `CommandTimeoutError` and
187
+ `CallInputArgs` is `CommandInputArgs`. There are no aliases.
188
+
189
+ - **A body takes one argument, its context (D386).** `ctx.command(({ input, update }) => …)` and
190
+ `own.command(within, ({ input, source, signal }) => …)`. `Input` and `Output` default to `void`, so the
191
+ `_input: void` placeholder is gone from every command that takes none. A typed input is declared once, by
192
+ annotating the destructured context: `({ input, update }: ModelCommandContext<Deposit>)` or
193
+ `({ input, source }: FeatureCommandContext<typeof polling, Deposit>)`. Both types are new public exports. Give a
194
+ model factory a result type — `function createCounter(context: ModelContext): ModelOf<typeof Counter>`, the form
195
+ the examples use: inside an inline arrow handed straight to `openModel(Decl, ctx => …)` or `model(Decl, ctx => …)`
196
+ the expected type is not instantiated yet, so a body that names no input reads as `Command<never, …>` instead of
197
+ taking the `void` default.
198
+ - **`concurrency` replaces `policy`, `queueBy` and `lane` (D387).** One word that also takes the queue node itself:
199
+ `'parallel' | 'queue' | 'latest' | lane | { by } | { by, lane } | { lane, pending: 'latest' }`. A lane is a
200
+ reference commands share, which is why the word accepts it; `'parallel'` beside a lane and a bare `by` without a
201
+ queue are now unwritable rather than refused. `dedupe` stays a sibling option and `'latest'` still refuses it.
202
+ A selection writes the same word with the members it can answer for — the keyed record is not one of them, and
203
+ a feature has no keyed queue either, so both are refused with the reason. The retired `policy`, `queueBy`, `lane`
204
+ and `once` are named by each options record, so they are refused in a written literal and in an assigned record
205
+ alike — on `ctx.command`, on `own.command` and on `ctx.select`, which also refuses `memoize` and `dedupe`.
206
+ - **Declaring and selecting are two words (D388).** `ctx.command(run, options?)` declares;
207
+ `ctx.select(source, 'name' | ['a', 'b'], options?)` selects what a dependency already declares — one name answers
208
+ one command, a list answers a record. `ctx.calls` and `own.calls` are gone, and so is `useCommands`: a record of command consumers
209
+ comes from `useModel(Declaration, select)`, which has an owner and a lifetime. `bind` of `@opetope/runtime` is
210
+ unchanged.
211
+ - **`once` is `memoize` (D389).** The cache keeps its behaviour and loses a name the platform already owns:
212
+ `addEventListener(type, handler, { once: true })` means "then stop", which is not what this option ever did.
213
+
214
+ `@opetope/lint` follows the vocabulary: `opetope/capture-command-cleanup` reads the context off the body's one
215
+ parameter and recognises `concurrency: 'parallel'`, and `opetope/no-command-in-deps` names the selection a record
216
+ comes from.
217
+
218
+ - 34bdbfb: **One word for one thing, and one sentence for one question (D410, D411).** A vocabulary pass over the public API:
219
+ the refusals that asked one question in five sentences now ask it in one, the position a signature calls `source`
220
+ is called `source` everywhere, and eight words that named one behaviour twice are one word apiece. No branch of the
221
+ runtime changes: the same checks run in the same order.
222
+
223
+ **Breaking, word by word.** Every row is a source change with no alias; `opetope/no-retired-vocabulary` reports each
224
+ one and names the replacement.
225
+
226
+ | Before | After |
227
+ | -------------------------------------------------------------- | --------------------------------------------------- |
228
+ | `defineCondition(id, { from, select })` | `defineCondition(id, { select, source })` |
229
+ | `slot(target, value, { when })`, same at `pipe`, `register` | `slot(target, value, { enabled })` |
230
+ | `resource.live({ delivery: { overflow: 'reject' } })` | `resource.live({ delivery: { overflow: 'drop' } })` |
231
+ | `outcome.status === 'ok' \| 'failed' \| 'cancelled'` | `outcome.kind === 'ok' \| 'failed' \| 'cancelled'` |
232
+ | `ResourceActivity` from `@opetope/devtools` | `ResourceNodeActivity` from `@opetope/devtools` |
233
+ | `ResourceDisposer` from `@opetope/core/internal` | `Disposer` from `@opetope/core/internal` |
234
+ | `NavigatorPolicy` from `@opetope/navigation` | `NavigatorCatalogue` |
235
+ | `surface.policy` | `surface.catalogue` |
236
+ | `ApplicationConditionBinding` from `@opetope/runtime/internal` | the same name, from `@opetope/runtime` |
237
+
238
+ `@opetope/lint` renames the rule that read that field: `opetope/when-predicate` is `opetope/enabled-predicate`,
239
+ and its message ids are `enabledIsAsync`, `enabledReturnsBareSource` and `enabledReturnsSource`. A configuration
240
+ that named the old rule has to name the new one; `configs.recommended` and the shipped `oxlintrc.json` fragment
241
+ already do.
242
+
243
+ Two of these are record keys, so a source with `sort-keys` reorders the literal as well as renaming the key:
244
+ `{ from, select }` becomes `{ select, source }`, and `{ Component, when }` becomes `{ Component, enabled }`.
245
+ `{ status: 'ok', value }` becomes `{ kind: 'ok', value }`, which sorts unchanged, while
246
+ `{ reason, status: 'cancelled' }` becomes `{ kind: 'cancelled', reason }`, which does not.
247
+
248
+ **Why each one.** `source` is the word every context already answers the position with, and `from*` stays the
249
+ prefix of a factory that names where a `Readable` comes from (`fromExternal`, `fromMaybe`). `enabled` is the truth
250
+ the runtime reads while a node exists — one word at a contribution and at a Resource — while `when` keeps the other
251
+ mechanism, the declared `Condition`s an application resolves before the node exists; `scope.while(source, open,
252
+ { when })` and an effect's `filter` are untouched. `drop` is what a full capacity does with the value that does not
253
+ fit, at both nodes that admit a producer; `flush-oldest` is still the default of a keyed delivery and still the
254
+ answer for a producer that may lose nothing. `kind` is the discriminant eleven settled answers already carried.
255
+ `NavigatorCatalogue` is named for its job, beside `NavigatorPersistence`, because a policy is data (D291) and this
256
+ is four callbacks a navigator asks.
257
+
258
+ **Not breaking, and worth reading.** Every options record now refuses an unknown field with the sentence spec §3
259
+ promised — `<subject> <node> <record> contains unknown field <key>.` — and says which field it owes with
260
+ `<subject> <node> <record> requires <key>.` Five sentences became one, and the three that never printed the field
261
+ at all (`requires exact data fields`, `own.attach takes open, or open and close, and nothing else`) now print it. A
262
+ node that reads a source says so in two sentences per surface instead of three, and the word of the position is the
263
+ author's. `own.select` lost two of its four overloads: a call that writes the options record has one candidate of
264
+ its arity now, so a fault in that record is reported as the record's own refusal instead of
265
+ `Argument of type '"echo"' is not assignable to parameter of type 'never'`, and the record names the five words a
266
+ selection does not take — `concurrency`, `dedupe`, `lane`, `memoize`, `policy` — each with what to write instead.
267
+
268
+ **Measured.** Runtime size consumers, rebuilt: `application graph consumer` 48 248 B against a ceiling of 49 000
269
+ (48 318 before), `public feature consumer` 39 579 B against 41 000 (39 712 before). Type stress: 11 695 / 87 068 /
270
+ 159 371 instantiations, unchanged to the digit. No ceiling moved.
271
+
272
+ ### Patch Changes
273
+
274
+ - dae5079: **A refusal names a remedy the author can execute, and a gate holds what the tree prints (D415).** Four debts the
275
+ wave D365–D414 left behind, each reproduced before it was fixed and measured after. No branch of the runtime
276
+ changes.
277
+
278
+ **The instruction of `EventPayloadNotNamed` carries its condition.** `run` names the payload of an event only while
279
+ it is not context-sensitive, and a callback becomes context-sensitive as soon as one parameter it writes has no
280
+ annotation. The old text said «annotate the first parameter of `run`», which for a `run` that also destructures its
281
+ context names nothing and earns a second refusal — the shape the cookbook's `polling` recipe was written in. The
282
+ marker now reads «name the event payload: annotate every parameter of `run`, the payload first — a `run` that
283
+ leaves its context unannotated names nothing — or annotate a `delivery.by` parameter», the recipe is written in the
284
+ form it names, and spec §2.23 compiles that form. A source that compares the marker text line by line updates the
285
+ line; the shape of author code is unchanged.
286
+
287
+ **`ctx.select` refuses the record and not the key.** Its four overloads are two, differing by how many arguments
288
+ they take, the way D410 collapsed `own.select`. A misspelled or retired field of the one-name form used to print
289
+ `Argument of type '"load"' is not assignable to parameter of type 'never'`, naming the key the author got right; it
290
+ now prints the instruction the record carries. Both call forms and every written type argument list are accepted as
291
+ before.
292
+
293
+ **spec §3 names the three sentences an owed field actually has**, instead of promising one that two nodes of six
294
+ hold: a record all of whose keys are required says `requires <key>.`, a key that has to be a callback says
295
+ `<key> must be a function.` — which names its type as well — and a key owed only because a sibling was written says
296
+ which sibling.
297
+
298
+ **`opetope/no-retired-vocabulary` names the last three migrations it was missing.** `ApplicationExecution` and
299
+ `ApplicationImportBinding` of `@opetope/runtime/internal` are reported beside `ApplicationConditionBinding`, and
300
+ `surface.policy` is reported on a surface `defineSurface` of `@opetope/navigation/react` declared — the word the
301
+ message of `NavigatorPolicy` already named and the rule never fired on. The shared message id is
302
+ `runtimeInternalEntry`, renamed from `conditionBindingEntry` because it answers three names now.
303
+
304
+ Size: `application graph consumer` 48 248 B against 49 000, `public feature consumer` 39 579 B against 41 000 —
305
+ byte for byte what D410 measured. Type stress: 11 695 / 87 068 / 159 371 against ceilings 12 034 / 99 062 /
306
+ 181 237, to the digit the recorded baseline, which is not rewritten.
307
+
308
+ - bb31983: **Five debts the wave named out loud, closed together (D437).** None of them was found by reading the code: each was
309
+ written down as a limit or a debt by the decision that left it, and left because the files it needed were being
310
+ edited on another branch. No authoring word moves, no runtime behaviour changes, and no package `src` outside the
311
+ lint rule's message is in this diff.
312
+
313
+ - **`opetope/no-write-after-source-write` stops saying «in silence».** D432 made the first write such a run loses
314
+ reach the reporter as `LostWrite`, once per run and saying the rest went with it, so the rule's message and its
315
+ section in both READMEs were describing a silence that is no longer whole. All three are corrected in one hand:
316
+ the drop is still a drop, the refused `invoke` after it is still silent, and so is every other way a write is
317
+ lost — a newer value, a cancelled caller, a generation fence, a closed model. The README also stops implying that
318
+ a successor which hides the loss from the cell hides it from the record; it does not.
319
+ - **The example gate of `ci:docs` reads both callees that declare sections.** D434 recorded it as a named limit:
320
+ blocks were selected by `\bdefineFeature\s*\(`, which does not match `defineFeature.body(`, so a document block
321
+ declaring only a body reached the packaged order rule through nothing. The selection is
322
+ `\bdefineFeature(?:\.body)?\s*\(` now, spec §5.8 carries both forms in both languages, and the gate reads 60
323
+ feature examples where it read 58. Nothing was passing through the hole — both bodies of the repository hold the
324
+ order — so the change is proved by injection instead.
325
+ - **`docs/resource-migration.md` joins the compiled examples (D419).** Nine blocks are lifted into checked
326
+ fixtures, which makes eleven documents whose examples `ci:type` reads — the second migration guide to join, under
327
+ the «was» mechanism the first one brought with it. Three of its blocks are «before» blocks and cannot compile: each
328
+ carries `// @ts-expect-error` on the line that shows the removed form, so the count of asserted refusals is five
329
+ for one guide, three for this one and zero for the other nine. Nothing in the guide was wrong: the six live
330
+ blocks compiled as written.
331
+ - **The comment over four rows of `selected-call-diagnostics.test.mjs` stopped being true.** It pinned the shape
332
+ D435 has since replaced — a parameter of type `never`, a report anchored at the argument. It now quotes what the
333
+ tree prints.
334
+ - **The type ceiling of slice `01` is raised by the owner, 12 034 → 12 200.** The slice measures 11 695
335
+ instantiations, which left 2.8 % under the ceiling where D400 named 3.9 % over the measurement as its alarm; the
336
+ raise restores 4.1 % under the ceiling and 4.3 % over the measurement. No other ceiling moves, no bundle byte
337
+ changes, and the granted headroom may not be cited as an argument: it is given, not measured.
338
+
339
+ - aaaa92c: **The write a run loses after moving its own source (D365).** A write of an effect run into a cell its own source
340
+ reads changes the value that source publishes, and the notification is synchronous: the run is aborted inside that
341
+ write, so every write of the run after it is dropped and an `invoke` after it is refused. That is the law of spec
342
+ §2.11 and of a late write (D288), and neither changes here. The drop was silent when this was written; D432, later in
343
+ the same release, sends the first of the dropped writes to the reporter as `LostWrite`, and the refused `invoke`
344
+ stays silent. What was missing is the reason the defect reaches production instead of a test, and the tool that
345
+ catches it.
346
+
347
+ - **`opetope/no-write-after-source-write`**, `error` in `configs.recommended` and in the shipped `oxlintrc.json`. It
348
+ reports the write that moves the effect's own source when another write of the same run can follow it, and it
349
+ proves three things first: the effect is declared on a context of this library; the cell the write names is part of
350
+ the source — the cell itself, or a `derive` of `@opetope/core` whose dependencies name it **and whose selector
351
+ publishes the value of that dependency**, which is the law's own carve-out for a write that leaves the published
352
+ value equal; and the later write stands in the same block, in a later statement, with no `return` or `throw` of the
353
+ run between the two. A source assembled in another function, a source read off an object, a cell that arrived as a
354
+ parameter, a `derive` over a `derive` and a commit as the moving write are all outside what it reads, and its
355
+ README lists them. There is no fix: make the write into the source the run's last write, or keep what the run
356
+ accumulates out of its source and read it with `getSnapshot()`.
357
+ - **The successor run is now written down** in spec §2.11, in spec §4 «Effect runs» and in `primitives` §4.1 step 8:
358
+ the loop opens a successor run for the new value **where `when` admits it**, and a successor that repeats the write
359
+ hides the loss, so the cell ends up correct through the second run. The loss stands whole where a guard makes that
360
+ successor return early, and where a `when` that rejects the new value leaves no successor at all (D329). A third
361
+ form is recorded with it: a `capture` taken after the moving write throws the run's own cancellation, and nothing
362
+ is reported for it either.
363
+ - **A repeated commit names its cure**: `State commit has already been used.` became
364
+ `State commit has already been used. Capture per write.` — legitimate by the same section, which says two commits
365
+ of one target in one call are what a command with two mutually exclusive places to write needs.
366
+
367
+ This change moves no budget by itself. Measured against a clean rebuild of `af278de`: the runtime
368
+ `public feature consumer` is 38 771 → 38 784 B and the `application graph consumer` 47 394 → 47 403 B. The two
369
+ runtime ceilings are ratcheted once for the whole wave this ships in, over a measurement of the merged tree
370
+ (D372).
371
+
372
+ - bb05f21: **A lost write is heard by the whole authority of a run, not by one writer (D447).** D432 made the most expensive
373
+ defect of this library audible: a write of a run into a cell its own source reads ends that run inside the write, and
374
+ the writes after it are dropped where everything still looks repaired. It heard only the writer that made the moving
375
+ write — so the shape the library itself recommends stayed mute. When one operation is shared by an effect and a
376
+ command (D288), the effect invokes the command, the command makes the write, and the command has nothing more to
377
+ write: the run lost the money and nobody said anything.
378
+
379
+ - **The authority is the run and the commands it `invoke`s.** A command inherits the cancellation of the run that
380
+ started it, so one abort ends them both, and the moving write may be made by any of them. The first write that
381
+ authority drops reaches the reporter as `LostWrite`, once, with the same three remedies as before.
382
+ - **Nothing outside that authority changed.** A write dropped by a newer value, a cancelled caller, a generation
383
+ fence or a closed model is as silent as it was — the abort each of them carries is its own, and the runtime proves
384
+ the authority by the identity of the abort, not by a guess about timing. This is still the half of D288 that stands.
385
+ - **The record's sentence moved with the law**: «the run had already ended inside an earlier write under its own
386
+ authority into a cell its source reads». A test that pinned the old sentence word for word needs the new one.
387
+ - **`opetope/no-write-after-source-write` now reports the call**, not just the write: an `invoke` of a command that
388
+ writes a cell the run's source reads, where another write of the run follows it. That form is the one the runtime
389
+ usually cannot report — the run is awaiting that very call when the write lands, so its promise is cancelled and
390
+ the body reaches no write at all — which is why the editor has to name it.
391
+
392
+ Measured by rebuilding the size consumers of one tree with and without the change: `application graph consumer`
393
+ 48 455 → 48 515 B against a 49 000 ceiling, `public feature consumer` 39 835 → 39 884 B against 41 000. The three
394
+ Core lines did not move. No ceiling moved, and no public name, signature or type changed.
395
+
396
+ - bb05f21: **The prose of `opetope/enabled-predicate` names the field the rule reads (D220, D411).** D411 renamed the field of
397
+ a contribution from `when` to `enabled`, and the rule that reads it from `opetope/when-predicate` to
398
+ `opetope/enabled-predicate`. The heading and the table of reported shapes moved with it; four sentences around them
399
+ did not, and they were the ones telling the reader which word to write. Both package READMEs now say:
400
+
401
+ - a contribution's `enabled` — not its `when` — answers the visibility fact of this instance, not the source
402
+ that carries it;
403
+ - the rule reads the `enabled` of a `slot`, `pipe` or `register` contribution, and any `enabled` whose function
404
+ destructures the evaluation context;
405
+ - a decision the predicate cannot change is hoisted out of `enabled`, or the line is silenced;
406
+ - a `when` is a different field and is left alone wherever it stands. The sentence used to set «a `when` outside a
407
+ contribution» against the `when` inside one, and since D411 a contribution has no `when` at all, so the
408
+ qualification had stopped dividing anything.
409
+
410
+ The word stays where it is still written: the `when` section of a feature declaration, the source value of
411
+ `scope.while`, the sentence that an effect's change filter is `filter` and not a `when` at all (D379), and the
412
+ historical rows of the retired-vocabulary table, which exist to name what a word used to be. Documentation only —
413
+ no rule, message, fix or configuration changes.
414
+
415
+ - bb31983: **Three places where the text an author reads did not match what the code does (D442).** No law of the runtime
416
+ moves and no public name is added; what changes is what the author is told when they are wrong.
417
+
418
+ - **The lint rule names all three ways out of a lost write, not two of them.** D432 made the first write a run
419
+ loses after moving its own source reach the reporter as `LostWrite`, naming three remedies, and recorded that
420
+ `opetope/no-write-after-source-write` already named them «in the same words». It named two. The one it left out —
421
+ `execution.capture(state)` taken _before_ the moving write — is the only one that saves that write whole, and the
422
+ author reads the rule in the editor long before any run reports. The message now carries the runtime's sentence
423
+ word for word, both package READMEs say so, and `ci:source` reads the sentence out of the runtime and looks for
424
+ it in the rule, so the two cannot drift again. Spec §2.11 also stops saying such a defect «is found in
425
+ production»: it used to be, and since D432 the record finds it.
426
+
427
+ - **A `request` written as `undefined` is refused on the key, and the refusal says so.** Every option of a Resource
428
+ declaration is read from the key the record carries (D407), so `{ ...shared, request: on ? account : undefined }`
429
+ writes the word. Under `exactOptionalPropertyTypes` the compiler refused that record — but for `request` alone it
430
+ refused it in the wrong place: `request` is where `Request` is inferred, so the written `undefined` became part of
431
+ the type parameter, `identity` turned required, and the message asked for a record the author never wrote
432
+ (`Property 'identity' is missing … key: (request: string | undefined) => never`), naming neither the key nor
433
+ `undefined`. An author who follows that message writes an `identity` and keeps the defect. The refusal now stands
434
+ on the key and carries the clause the runtime prints, plus the second remedy the migration guides teach: «a key
435
+ written as `undefined` is a word this declaration wrote: leave the key out where the word is not meant, or branch
436
+ the declaration». Both surfaces spell it, out of one string. Nothing legal moves — a request written as a value,
437
+ as a `Readable` (including one that publishes `undefined`, which closes the materialization instead of writing a
438
+ word), a spread that carries the key only where it is meant, a branch on the record, and a declaration with no
439
+ `request` are all accepted as before. Two forms do change: a `request` whose type is `any` is now refused, where
440
+ it used to be accepted and answered `Resource<Data, any>`, and a `request` whose type is `unknown` is refused with
441
+ this clause instead of with the old demand for an `identity`.
442
+
443
+ - **`PaginationUnavailableReason` is on `@opetope/core/internal`.** The union standing in `reason` of the public
444
+ `PaginationState` and `PaginationOutcome` was a file-local alias on no entry at all, while the gate that records
445
+ that choice already called it an internal type. It is now where that comment says it is. It is not published: the
446
+ compiler does print `'retired'` to a consumer whose exhaustive `switch` breaks, which is the argument D364 and
447
+ D375 used, but the union is reachable through the public record that carries it —
448
+ `Extract<PaginationState<Cursor>, { kind: 'unavailable' }>['reason']` — which is the reason D375 left
449
+ `ResourceChannel` internal. Publishing it would cost the last type of the public budget, and D364 reserved that to
450
+ the owner.
451
+
452
+ Measured, isolated, by rebuilding the same tree with and without the change: all five size rows are byte-identical
453
+ (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
454
+ types are erased. The type budget moves +11 / +70 / +117 instantiations on the three stress slices, to
455
+ 11 706 / 87 138 / 159 488 against unchanged ceilings of 12 200 / 99 062 / 181 237. The form was chosen by
456
+ measurement and not by taste: the same instruction written as a fourth member of the declaration intersection costs
457
+ +71 / +1 429 / +2 795. `ci:public-surface` reads 59 types and 37 values before and after.
458
+
459
+ ## 0.11.0
460
+
461
+ No changes in this release.
462
+
3
463
  ## 0.10.1
4
464
 
5
465
  No changes in this release.
@@ -151,11 +611,11 @@ No changes in this release.
151
611
 
152
612
  ### Patch Changes
153
613
 
154
- - b964772: Модель подтверждает мутации принадлежащего ей Resource, получает явный исход обновления, выполняет Call из
614
+ - b964772: Модель подтверждает мутации принадлежащего ей Resource, получает явный исход обновления, выполняет Command из
155
615
  effect и сериализует независимые объекты через очередь по ключу. Stream умеет накапливать кадры в границах
156
616
  открытия. Контекст данных отделён от ключа, а ёмкость кэша перенесена из retention в самостоятельную опцию cache.
157
617
  Retention теперь задаётся литералом, model resource/stream принимает готовый Readable, а `queueBy` создаёт
158
- приватную keyed queue команды или использует явно переданную общую lane. Effect переносит отмену вложенного Call
618
+ приватную keyed queue команды или использует явно переданную общую lane. Effect переносит отмену вложенного Command
159
619
  через `rethrowIfCancelled`.
160
620
 
161
621
  ## 0.9.2
@@ -224,7 +684,7 @@ opetope/no-subscribe-outside-models` in that application sits on a subscription
224
684
 
225
685
  - d7f346a: `opetope/no-command-in-deps` keeps a command hook out of a React dependency array.
226
686
 
227
- A hook read from `useCommand`, from a key of `useCommands` or from a `Call` field of a `useModel` selection is a
687
+ A hook read from `useCommand`, from a key of `useCommands` or from a `Command` field of a `useModel` selection is a
228
688
  snapshot of the render it was read in: the object is rebuilt on every `inFlight` and `outcome` change, while `run`
229
689
  keeps one identity for the life of the binding. An application that wrote
230
690
  `useEffect(() => { load.run(); return () => { unload.run(); }; }, [load, unload])` therefore started the command,
@@ -316,7 +776,7 @@ No changes in this release.
316
776
 
317
777
  ### Minor Changes
318
778
 
319
- - 410f2f0: Run authentic Calls in headless model tests with `runCall` from `@opetope/react/testing`, preserving their existing policies, cancellation, errors and owner lifetime.
779
+ - 410f2f0: Run authentic Calls in headless model tests with `runCommand` from `@opetope/react/testing`, preserving their existing policies, cancellation, errors and owner lifetime.
320
780
 
321
781
  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.
322
782