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