@opetope/lint 0.12.0 → 0.12.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,269 @@
1
1
  # @opetope/lint
2
2
 
3
+ ## 0.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 2d1ecfc: Two axes of a Resource state, not one word (D462)
8
+
9
+ `ResourceState` carries two independent fields, `failure` and `pending`, and each is declared on every member of the
10
+ union — `?: never` where the member cannot hold one. Both are therefore read on the whole state in one step, without
11
+ narrowing by `kind` first:
12
+
13
+ ```ts
14
+ const refused = state.failure !== undefined;
15
+ const working = state.kind === 'loading' || state.pending !== undefined;
16
+ ```
17
+
18
+ `ResourceActivity` is gone, `ResourceChannelFailure` is now `ResourceFailure` with an optional `channel`, and
19
+ `failed` carries `failure` instead of a top-level `error`.
20
+
21
+ **Why.** One frequent question — «did something fail?» — was asked in three shapes: `failed.error`,
22
+ `loading.failure`, and `ready.activity.kind === 'failed'`. «Is something in flight?» was asked in two. Both are one
23
+ read each now.
24
+
25
+ **What changes in behaviour.** The priority within an axis is unchanged: at an equal kind the connection outranks
26
+ the refresh. The priority _between_ the axes is gone — a failed channel no longer hides a working one, so a `ready`
27
+ record publishes a dead connection and a running refresh together. That is more information, not less, and it is the
28
+ difference between «the live stream is gone, what is shown will not update» and «a manual reload is running over a
29
+ live stream».
30
+
31
+ `pending` is observable structure, so **every transition of the work axis now publishes a record**. It did not
32
+ before: one projected word hid that axis on a dataless `loading`, which carried no `activity` at all, and under any
33
+ failure, because `failed` outranked `pending`. Measured on a live Resource with both channels, a connection opening
34
+ over a dataless Resource goes from 1 record to 2; a `refresh()` over a healthy `ready` from 5 to 6; and a
35
+ `refresh()` over a `ready` that already carries a dead connection from 5 to 7, because its settling used to cost no
36
+ record at all. Up to two extra publications over the life of one materialization, and they reach the subscribers of
37
+ a `ready` Resource and not only a dataless one. Each carries the fact the axes were split for.
38
+
39
+ **Migration.**
40
+
41
+ | Was | Is now |
42
+ | --------------------------------------------- | ------------------------------------------------- |
43
+ | `state.error` on `failed` | `state.failure.error` |
44
+ | `state.failure.channel` on `loading` | unchanged, and now on every kind |
45
+ | `state.activity.kind === 'failed'` | `state.failure !== undefined` |
46
+ | `state.activity.kind === 'pending'` | `state.pending !== undefined` |
47
+ | `state.activity.operation` | `state.failure?.channel` or `state.pending` |
48
+ | `state.activity.error` | `state.failure?.error` |
49
+ | `ResourceChannelFailure` from `@opetope/core` | `ResourceFailure` |
50
+ | `ResourceActivity` from `@opetope/core` | nothing: read `state.failure` and `state.pending` |
51
+
52
+ `failure.channel` is absent only on a `failed` record whose every configured channel died, where no single channel
53
+ is the answer. `opetope/no-retired-vocabulary` reports `ResourceActivity` at both entries that ever spelled it, and
54
+ reports `state.activity` wherever it is read rather than only inside a `kind === 'loading'` narrowing; the rename to
55
+ `ResourceFailure` is printed by the compiler itself.
56
+
57
+ The inspection protocol is unchanged: `opetope.runtime-activity/4` stays `/4`, and the facts now read the published
58
+ record instead of a second copy of the channel phases.
59
+
60
+ ## 0.12.1
61
+
62
+ ### Patch Changes
63
+
64
+ - 8aa4d85: **Four positions of a call name a command's input, and the member that says so is an intersection (D455).** The
65
+ `input` of `ModelCommandContext<Input>` and of `FeatureCommandContext<Within, Input>` was
66
+ `Input extends void ? {instruction} : Input`, which is not provably `Input` while `Input` is unresolved: a generic
67
+ helper could read the member and could not hand it to anything asking for exactly its own type parameter
68
+ (`TS2345: Argument of type 'T extends void ? {…} : T' is not assignable to parameter of type 'T'`, and a constraint
69
+ on `T` did not cure it). It is `Input & (Input extends void ? {instruction} : unknown)` now. An intersection is
70
+ assignable to each of its members, so the hand-off resolves; the second branch is `unknown`, which intersects away,
71
+ so a named input reads exactly as before and a call that named none is refused on the read exactly as before. No
72
+ public name is added or removed — 59 types and 37 values before and after — and every emitted JavaScript file is
73
+ byte-identical apart from the lint rule's message, so no size budget moves. The type slices move
74
+ 11 736 → 11 756, 87 612 → 87 710 and 160 090 → 160 268 instantiations against ceilings of 12 200, 99 062 and
75
+ 181 237, which stay where they were.
76
+
77
+ - **The instruction the refusal prints names four positions in order of cost, and says why there is no fifth.**
78
+ D444 wrote that the annotation of the destructured context was the one place an input could be named. It is the
79
+ most expensive of four. `Input` is inferred from every position of the call except the body: the result type of a
80
+ model factory names the input of every command in the literal it returns; an option whose callback is annotated
81
+ (`dedupe`, `memoize`, `concurrency: { by }`) names it where a call was writing an option anyway; a constant
82
+ annotated `Command<Input, Output>` names it and keeps `Output` checked; and the annotation of the context is what
83
+ is left. `command<Deposit>(…)` is not a position — one explicit type argument pins `Output` to its default, and
84
+ the body that answers a value is refused for answering one. A feature has the last two only: `own.command`
85
+ answers a ref carrying the feature's id, which no public name annotates.
86
+ - **The authoring documents now teach the cheap positions instead of the expensive one.** The spec, the cookbook,
87
+ the authoring guide, the primitives reference, the wave migration and the lint rule and its README were all
88
+ teaching the context annotation as the one route; the compiled examples that sit inside a factory with a result
89
+ type name nothing now, and the ones bound to a constant annotate the constant. The examples that keep the context
90
+ annotation say in a comment why it is the route left there.
91
+ - **Both directions are held by type tests.** A generic helper handing `context.input` to something asking for
92
+ exactly its parameter compiles, bare and constrained; a pair of identically shaped generic factories, one writing
93
+ a typed option and one not, both compile, which is what says the intersection is the cure and not the option; and
94
+ a call whose only option names nothing is refused on the read, with each assertion naming the diagnostic it
95
+ expects — `TS2339` at a member read, `TS2345` at a hand-off, `TS2322` at the pinned `Output`.
96
+
97
+ - 8aec982: **A command's one waiting place is written as the record that names it: `concurrency: { pending: 'latest' }`
98
+ (D459).** The scalar `concurrency: 'latest'` named the input that wins and never named what it wins — «the latest»
99
+ reads as a place in a queue, as the body already running and as a cached answer, and only the first was true. The
100
+ shared form of the same fact never had that gap: `{ lane, pending: 'latest' }` says the place out loud, with the
101
+ `pending` an event and a live Resource already write for their own one waiting place. One fact was being written in
102
+ two vocabularies, and the shorter one left out the part that mattered. The word is removed rather than kept beside
103
+ the record, which makes this a breaking change to the authoring surface of a model command, of a model selection
104
+ and of a feature command.
105
+
106
+ - **Migrate by writing the place.** `concurrency: 'latest'` becomes `concurrency: { pending: 'latest' }` in
107
+ `ctx.command`, `ctx.select` and `own.command`. `concurrency: { lane, pending: 'latest' }` is unchanged: the lane
108
+ is the one parameter this record takes, and it is optional now instead of required. Nothing else about the option
109
+ moves — the same record, the same siblings, the same `dedupe` prohibition beside it.
110
+ - **No law moved with the spelling.** While call 1 runs, calls 2 and 3 take one waiting place: the body of 2 never
111
+ starts, its wait ends as `CommandError('cancelled', 'Command <id> replaced a waiting input.')`, and 3 runs when 1
112
+ finishes. A newer request never reaches the call in flight. The regression that holds this reads `signal.aborted`
113
+ inside the running body — after both newer requests were admitted and before the body returns — rather than
114
+ counting abort events, because the abort that follows a call's own end belongs to its cleanup and says nothing
115
+ about the policy. It was proved by substitution: with the kernel changed to cancel whatever is in flight, the
116
+ test fails on that line.
117
+ - **The retired word is answered with its replacement, not with a list of everything else.** The runtime throws
118
+ `Model command concurrency latest is now a record: write { pending: 'latest' }, or { lane, pending: 'latest' }.`
119
+ — and `Model select`, `Feature command` the same. Two neighbouring messages move with it: the choice sentence
120
+ loses the word — `concurrency must be queue, parallel, a lane of this surface or a record.` — and the record's own
121
+ refusal now says the lane is optional. A record that names `lane` and hands it `undefined` is still refused, by
122
+ the type under `exactOptionalPropertyTypes` and by the runtime, which reads the shape of the record by its keys.
123
+ - **The compiler names the replacement too, because the literal stays in the union.** The options record of a
124
+ command gained a third member — the retired word beside a key an author cannot write — so a refusal arrives as a
125
+ missing property carrying the instruction. Without it the union held no string at all, every word written there
126
+ widened to `string`, and one sentence answered a typo and a retired word alike. What the two now print, the same
127
+ on a model and on a feature: `{ concurrency: 'quee' }` is a `TS2820` that names the word and suggests `'queue'`,
128
+ which is better than it was before this change, and `{ concurrency: 'latest' }` is a `TS2345` whose elaboration
129
+ carries «`concurrency: "latest"` is now a record: write `{ pending: "latest" }`, or `{ lane, pending: "latest" }`».
130
+ A selection is the one surface where the compiler names the word written and not its replacement — its options
131
+ record is not a union — and there the runtime and the lint rule carry it.
132
+ - **`opetope/no-retired-vocabulary` reports it.** `concurrency: 'latest'` is a retired _value_ of a live key, the
133
+ shape `overflow: 'reject'` already had, so the rule now reads the values of a command's options record as well as
134
+ its keys and reports on the word that was written. A project that pins the rule's output by its own fixtures gains
135
+ one row.
136
+ - **What the gates read.** Instantiations are unchanged — 11 756, 87 710 and 160 268 against ceilings of 12 200,
137
+ 99 062 and 181 237 — because a scalar became a record that already existed. The tightest size row, the headless
138
+ session consumer of `@opetope/devtools`, stays at 6.95 kB of 7.1 kB. The two runtime rows carry the new branch and
139
+ its message: the application graph consumer moves 48.56 → 48.66 kB of 49 kB and the public feature consumer
140
+ 39.88 → 39.92 kB of 41 kB, measured by rebuilding the validator both ways. No budget moves.
141
+
142
+ - be0613e: **The cell is the only place that says what a write is (D457).** `ModelStateWriter.update` was
143
+ `update<Value>(state: OwnedState<Value>, update: (previous: NoInfer<Value>) => Value)`, and a type parameter in the
144
+ result of a callback is an inference site: `Value` was not fixed while the body was checked, so the object literal
145
+ the updater returned had no contextual type and its fields widened — and the widened candidate then passed the
146
+ state, because `OwnedState` is covariant. `update(sessions, previous => ({ ...previous, kind: 'loading' }))` into a
147
+ cell whose `kind` is `'idle' | 'loading'` was therefore refused with a type nobody wrote,
148
+ `(previous: NoInfer<Sessions>) => { items: readonly string[]; kind: string }`, and the remedies were the author's:
149
+ `as const`, an annotated updater result, or a cell widened to `string`. The signature is
150
+ `update: NoInfer<(previous: Value) => Value>` now, so the state is the only inference site and the updater is
151
+ checked against the cell. No public name is added or removed — 59 types and 37 values before and after — and every
152
+ emitted JavaScript file is byte-identical, so no size budget moves; the three type slices write no state at all and
153
+ stand at 11 756, 87 710 and 160 268 instantiations against ceilings of 12 200, 99 062 and 181 237. Measured on 1 200
154
+ identical writes that compile under both signatures, the change costs 2 instantiations and 2 types.
155
+
156
+ - **Breaking: three writes that used to compile are refused now, all for the same reason.** A value wider than the
157
+ cell (`update(sessions, () => wider)` where `wider.kind` is `string`); a member of a discriminated union that
158
+ leaves out a field that member declares (`update(phase, () => ({ kind: 'loading' }))` where that member also
159
+ declares `since`); and a word outside a union of literals (`update(mode, () => 'stopped')` into
160
+ `OwnedState<'off' | 'on'>`). Each was accepted because the widened inference and the covariance of `OwnedState`
161
+ met, and each reached the cell through an updater that takes no parameter — the one shape whose inference
162
+ resolved before the body was checked. Each is a `TS2769` where it is written now. Recompiled with the previous
163
+ signature, the four new type tests report 15 refusals of correct writes and 12 asserted refusals that do not
164
+ fire.
165
+ - **An updater written at the call needs no `as const`, no annotated result and no widened field.** A literal it
166
+ returns keeps its type through a spread, in a nested record and in a member of a union; a mutable array and a
167
+ tuple in a cell keep their own mutability, which is why the `const` type parameter that also fixes the literals
168
+ was rejected. The value form of `update` is untouched, and so is the rule that a function in that position is the
169
+ updater. Diagnostics improve with the fix: the compiler prints `kind: "done"` instead of `kind: string`.
170
+ - **Two limits of that promise, both measured.** An updater hoisted into a constant widens at its own declaration,
171
+ where there is no contextual type at all, so it still needs an annotated result — before and after this change.
172
+ And the updater half performs no excess-property check, because TypeScript does not apply one to a literal
173
+ returned from a contextually typed function: a stray key written inside the updater now compiles, where it used
174
+ to be caught in passing by the false refusal whenever the literal also had something to widen. The value half
175
+ still catches it and names the key.
176
+ - **`StateCommit.update` and the reactive calculations are unchanged, and the type tests say why.** A commit's
177
+ `Value` is already one cell, and so is a pipe's, so neither `commit.update` nor `fold` has a callback result to
178
+ infer from. `derive`, `computed`, `fromMaybe`, `fromExternal` and the `project` of a collection do put a type
179
+ parameter in the result of a callback, and none of them has this defect: a widened result has nowhere to go
180
+ there, because no covariant container stands in the call for a supertype to pass through — the assignment of the
181
+ result refuses it. The `apply.change` of a live Resource is the one public callback left where the same widening
182
+ is live; it belongs to a separate change.
183
+ - **One workaround is gone.** The `@opetope/lint` README declared its example state with `kind: string` and a
184
+ comment saying the widening was a defect of the writer's own signature; the field is `'idle' | 'loading'` again
185
+ and the comment is gone. Spec §2.38 is the new compile-checked example, and it asserts all three closed holes.
186
+
187
+ - 4e65393: **The published archive of every package loses `README.ru.md`, and `@opetope/runtime` loses the Russian pages of
188
+ its `docs/` as well (D456).** The Russian half of the documentation is removed: nineteen `*.ru.md` files,
189
+ **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.
190
+ Documentation is written once, in English, from here on. No public name, no `exports` entry and no subpath moves —
191
+ a `.ru.md` was never a resolvable specifier, only a file read by its path — and no line of shipped JavaScript
192
+ changes, which is why this is a patch. The composition of what is published does change, and this is the line that
193
+ says so.
194
+
195
+ - **What goes.** `CONTRIBUTING.ru.md`, `README.ru.md`, the README of the minimal React example, the nine guides of
196
+ `docs/` — agent guide, cookbook, devtools, how-it-works, primitives, releases, both migration guides and the
197
+ spec — and the `README.ru.md` of all seven packages. `docs/decisions.md` stays Russian and keeps no pair, as it
198
+ never had one. The nine copies under `packages/runtime/docs/` are generated by packaging and leave on their own.
199
+ - **The rule that required a pair is cancelled, with its gate.** `CLAUDE.md` said «README and user guides have
200
+ English/Russian pairs, updated together» with four exceptions; it now says a document is written once, in
201
+ English, and gets no second copy. `ci:docs` no longer looks for a `*.ru.md` beside a document and no longer
202
+ compares the heading skeleton of a pair, and its report reads «links and example section order passed».
203
+ - **One gate is lost outright, and nothing replaces it.** The fixture generator held every English example against
204
+ its Russian twin as the same program — syntax without comments and without JSX text — which is how D441 caught a
205
+ real drift in a spec §2 block, a string literal that differed in one copy. The defect class that stops being
206
+ caught is a silent edit to a detail of an example that still compiles: a string literal, a number, the order of
207
+ two arguments of the same type. `ci:type` accepts any literal of the right type, `ci:docs` reads section order
208
+ and links rather than values, `ci:asserted-refusals` holds pragmas, and the generator's own `--check` compares a
209
+ fixture with the very document it was lifted from, so one `fixtures:generate` makes any such edit the new
210
+ baseline. The twin was the only place in this repository where one program was written twice and the two
211
+ writings were compared. It also never knew which copy was right — in the one case it fired, the wrong copy was
212
+ the Russian one — and it protected above all the copy that nothing compiled.
213
+ - **Where the archive change is written down, so it cannot drift from its gate.** The composition of an archive is
214
+ stated in six places and all six are rewritten: the `files` of the seven manifests; the required-file list and
215
+ the archive-member allowlist of the pack archive check; the fixture of that check's own unit test; the
216
+ allowlists of the two real pack smoke tests; and what packaging demands of each package before it packs. The
217
+ requirement that `@opetope/runtime` ship `docs/spec.ru.md` is gone; `docs/spec.md` is required exactly as before.
218
+ - **The numbers the gates print.** `ci:docs` reads 28 documents instead of 46 — nineteen leave and this changeset
219
+ is itself a document — with 4 pending changesets instead of 3 and 424 decision identifiers instead of 423. Its
220
+ feature examples halve, 64 to 32: that gate lints every fenced example that declares a feature, and it was
221
+ linting both copies of each one. Packaging prepares 10 documents instead of 19, and the merge-marker scan reads
222
+ 1101 text files instead of 1117 — seventeen of the nineteen removed files live under a scanned root, and this
223
+ changeset is one file back. The document examples do not move at all: 38 spec §2 blocks and 132 blocks of 11
224
+ other documents, 2 fragments, 14 asserted refusals, exactly as before, because only the English block was ever
225
+ lifted into a fixture.
226
+
227
+ - e12b0b6: **An event takes its handler second: `event(source, run, subscribe, options?)` (D458).** The natural declaration was
228
+ refused although its payload was already annotated — `emit` typed as
229
+ `(payload: EventPayloadNotNamed | undefined) => boolean`, and `run` as taking the marker. The cause is the order of
230
+ the arguments and nothing else. TypeScript types a call's arguments in order and leaves a context-sensitive callback
231
+ out of the first inference pass; in the second it walks them left to right, and resolves the type parameters a
232
+ callback's own type refers to before giving that callback a contextual type. `subscribe` stood second and its one
233
+ unannotated parameter is a context carrying `Payload`, so `Payload` answered its default on the way past, before the
234
+ annotation on `run` was ever read. With `run` second, its unannotated parameter is a run context, which carries no
235
+ payload at all, so nothing about it resolves `Payload` and one annotation is the whole naming site. This is a
236
+ breaking change to the authoring surface of both a model and a feature.
237
+
238
+ - **Migrate by swapping the two callbacks.** `ctx.event(source, subscribe, run, options?)` becomes
239
+ `ctx.event(source, run, subscribe, options?)`, and `own.event` of a feature the same. Nothing else about the node
240
+ moves: the same four arguments, the same options record, the same `request`, the same delivery policies. A call
241
+ left in the old order does not compile silently — each callback is checked against the other's signature.
242
+ - **One annotation on `run` is now the whole naming site, whatever it does with its context.** The condition the
243
+ previous decision recorded — that a run annotating its payload and destructuring its context names nothing — was
244
+ true only while `subscribe` stood first. It is gone, and so is the shape it forced: the polling recipe of the
245
+ cookbook and the spec no longer annotate a context they only destructure. The marker's instruction narrowed with
246
+ it, from «annotate every parameter of `run`, the payload first — a `run` that leaves its context unannotated names
247
+ nothing — or annotate a `delivery.by` parameter» to «annotate the first parameter of `run`, or annotate a
248
+ `delivery.by` parameter», and the diagnostics gate holds those words.
249
+ - **The node now reads like every other one.** `effect`, `scope.while` and `scope.switch` all take the working
250
+ callback straight after the source; the event was the one node with a second callback wedged between them. The
251
+ rule the spec states for the whole vocabulary — the source first, the work next, modifiers last — is true here
252
+ without an exception now.
253
+ - **No single-signature cure exists, and that was measured rather than assumed.** On TypeScript 7.0.2, 6.0.2 and
254
+ 6.0.3: carrying the payload through a separate inferred parameter as `Parameters<Run>[0]` answers `never`;
255
+ `NoInfer` on `emit` and `NoInfer` over the whole `subscribe` type both fail, because a reference to the parameter
256
+ is still a reference; a separate `Emitted extends Payload` fails; and a conditional `emit` that widens to
257
+ `unknown` or `any` stops the complaint at `emit` and poisons the inference instead. While `subscribe` stands
258
+ before `run` and mentions the payload, the order is the only lever there is.
259
+ - **`@opetope/lint` moves with it.** The ingress of an event is the third argument now, so
260
+ `opetope/no-subscribe-outside-models` reads it there; a project that pins the rule's behaviour by its own fixtures
261
+ updates them the same way an application updates its calls.
262
+ - **What the gates read.** Instantiations are unchanged — 11 756, 87 710 and 160 268 against ceilings of 12 200,
263
+ 99 062 and 181 237 — because a reorder adds no type. The tightest size row, the headless session consumer of
264
+ `@opetope/devtools`, stays at 6.95 kB of 7.1 kB: the only emitted JavaScript that changes is one `TypeError`
265
+ message, `Model event run and subscribe must be functions.`, and the argument index the lint rule carries.
266
+
3
267
  ## 0.12.0
4
268
 
5
269
  ### Minor Changes