@opetope/lint 0.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,212 @@
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
+
3
210
  ## 0.12.0
4
211
 
5
212
  ### Minor Changes