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