@hyperfixi/engine 7.5.0 → 7.6.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 CHANGED
@@ -7,6 +7,130 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.6.0] - 2026-11-10
11
+
12
+ The engine takes extensions: upstream's `tailwind` and `socket` load from a script tag or
13
+ `register(...)`, and the Vite plugin bundles them. Spanish and Portuguese handlers agree with their
14
+ event (`a la entrada`, `à mudança`), and German writes an event as the noun it is (`wenn Klick`).
15
+ A minor: new features, and renders that change in es, pt and de; everything written before still
16
+ reads.
17
+
18
+ ### Added
19
+
20
+ - **`@hyperfixi/vite-plugin`: `extensions: ['socket', 'tailwind']`.** The generated bundle
21
+ imports each `@hyperfixi/engine/ext/<name>` and registers it after the modules, and the dev
22
+ `everything` fallback does too. Extensions are opt-in, so the scan never adds one. A project
23
+ that lists an extension gets an engine bundle even when the scan found nothing else (a page
24
+ whose only hyperscript is a `socket`).
25
+ - **Which module comes with `toggle` is now read from the grammar.** The plugin used to have
26
+ the rule "`toggleElement` comes with `toggle`" written in; it now reads which modules add
27
+ a reading to a command (`Grammar.fallbacks`).
28
+ - **`verify:reference` checks the engine's extensions as its own.** It registers each
29
+ `dist/ext/<name>.esm.js`, documents their keywords like the engine's, and parses their
30
+ examples: `socket`'s hover example is now checked, where it was skipped as "upstream's".
31
+
32
+ - **`@hyperfixi/engine`: extensions that a script tag can load.** After `hyperfixi-hs.js`, a page
33
+ can load `ext/<name>.js`, which registers its module with the page's engine. Or, from ESM,
34
+ `register(...everything, tailwind)` with `@hyperfixi/engine/ext/tailwind`.
35
+ - **The first extension is upstream's `tailwind`** (the `twDisplay`, `twVisibility` and
36
+ `twOpacity` hide/show strategies), and upstream's 12 tests for it are now in the engine's
37
+ gate: 1,434 of 1,498.
38
+ - **The public object gains `register` and `extensions`, and the script-tag bundle's also gains
39
+ `kit`.** `register` adds modules. `extensions` lists the extension files the page has
40
+ loaded. `kit` holds what a module may call: the same names the library entry exports for
41
+ module authors. Only `hyperfixi-hs.js` carries the kit, so a bundle built from ESM (the Vite
42
+ plugin's) keeps only the parts of the engine it uses; it registers extensions from ESM, and an
43
+ extension file loaded beside it says so.
44
+ - **`register` skips a module it has already added.** When it does add one, it reads again
45
+ the elements whose scripts failed to parse, so an `async` or late-loaded extension still
46
+ applies.
47
+ - **Upstream's own `ext/tailwind.js` also runs on the engine unchanged**, through `use`.
48
+ - **`@hyperfixi/engine/ext/socket`: upstream's `socket` feature.** `socket Chat ws://… [with
49
+ timeout n] [on message [as JSON] …] end` defines `Chat`, a WebSocket with `raw`,
50
+ `dispatchEvent` (`send ping to Chat` sends the event as JSON) and an `rpc` proxy
51
+ (`Chat.rpc.f(…)` returns a promise for the reply).
52
+ - **Upstream's 16 socket tests pass:** the suite is 1,450 of 1,498, with 48 known failures.
53
+ - **Two differences from upstream:** the socket opens when the feature is installed, not
54
+ when it is parsed, so a parse alone opens nothing; and with `as JSON` a message that is
55
+ JSON `0`, `false` or `null` is that value, where upstream rejects it.
56
+ - **The kit grows to what a feature needs:** `dotOrColonPath`, `urlOrExpression`,
57
+ `evalStatic`, `commandList`, `makeContext`, `runList` and `assignToNamespace`. Each is
58
+ exported from `@hyperfixi/engine` and carried on `hyperfixi-hs.js`'s `_hyperscript.kit`.
59
+
60
+ ### Changed
61
+
62
+ - **`@lokascript/semantic`: a Spanish or Portuguese handler's head agrees with its event, and
63
+ German writes an event as a noun.** Renders only; everything written before still reads.
64
+ - **es, pt:** `al` and `ao` hold the masculine article, and were written before every event
65
+ (`al entrada agregar .a`, `ao mudança de #search …`). A feminine event takes `a la` and `à`:
66
+ `a la entrada agregar .a`, `à mudança de #search disparar envio em #form`. The first event of
67
+ an `or` decides; an infinitive (`al redimensionar`) and a custom name (`al my:event`) keep the
68
+ masculine. Each profile lists its feminine event words (`eventHandler.feminineEventMarker`).
69
+ es reads `a la` as the head only before one of them, so `poner x a la` is still a put into a
70
+ variable named `la`. pt's `à` joins its letters and prepositions.
71
+ - **de:** an event's word takes its capital wherever a command names one: `wenn Klick`,
72
+ `wenn Änderung von #search auslösen Absenden auf #form`, `warten Änderung`. An event with no
73
+ German word stays code (`wenn keyup`). The event table lists each one-word name both ways, so
74
+ `wenn klick` and `wenn doppelklick` read as before.
75
+
76
+ - **`@hyperfixi/engine`: a module can add a reading to a command without changing what the
77
+ command reads (`Grammar.fallbacks`).** A fallback registered for a keyword is tried, from just
78
+ after the keyword, only when the command's own rule fails to parse, and it counts only if it
79
+ reads at least as far as that rule got. `toggle <element>` (the `toggleElement` module) is now
80
+ one. Every `toggle` that upstream reads now goes through upstream's rule exactly (registering
81
+ `toggleElement` used to send every `toggle <target>` through a different expression rule).
82
+ An unfinished upstream `toggle` now reports upstream's error: `toggle #d as` used to say
83
+ `Expected 'modal'`, and now says `Expected dotOrColonPath`, as upstream does. The
84
+ `Grammar.toggleElement` slot is gone; a module that set it registers a fallback instead.
85
+ `Parser` gains `mark()` and `reset()`.
86
+
87
+ ### Fixed
88
+
89
+ - **`@lokascript/semantic`: a translated `socket` comes back as the socket upstream reads.**
90
+ A socket's `on message [as JSON] …` is its own clause, closed by the socket's one `end` (or
91
+ by the end of the script). Semantic read it as an event handler, eventsource's shape, so:
92
+ - a clause closed by `end` came back with a second `end`, in every language. The engine
93
+ rejects that, so a translated socket failed to parse on the page;
94
+ - a clause with no `end` (upstream's tests write them that way) lost its body, and a
95
+ clause's `as JSON` was read as leftover input. `translate` refused both;
96
+ - a dotted name came back split: `socket MyApp.chat …` became `socket MyApp .chat …`, a
97
+ different socket.
98
+
99
+ `as JSON` is now written with the language's own word for `as`, as a conversion is (es
100
+ `como JSON`, ja `として JSON`). Text semantic wrote before, with two `end`s, still reads as
101
+ the one socket it was.
102
+
103
+ Where a language puts its handler word after the event (ja `messageで`, tr `message da`), a
104
+ socket or eventsource whose body starts with a word that looks like an event (ja
105
+ `messageで messageを 記録`, "on message log message") now reads. The handler word used to
106
+ be taken as the start of the body.
107
+ - **Measured by the command-shape gate, which now reads extension syntax.** Its engine
108
+ registers every extension the engine builds. Its harvest reads upstream's `ext/` tests,
109
+ and the scripts a test writes as `textContent` (how upstream's socket tests write theirs).
110
+ That adds 33 cases (1,168 in all), and each passes in all 24 lanes. Before this fix, 4 of
111
+ the 16 socket cases were refused in every language and 1 was silent in every language.
112
+
113
+ ## [7.5.1] - 2026-10-23
114
+
115
+ The last two silent misreads of hand-written non-English hyperscript that the 7.3.0 live test
116
+ found: one is now refused, the other reads right.
117
+
118
+ ### Fixed
119
+
120
+ - **ko: an event word followed by its object particle is the verb's object, not a handler.** At
121
+ top level, hand-written `로드 를 대기` (wait for load) read as `on load wait 를`: a handler on
122
+ `load` waiting on a variable named `를`, and translate() did not refuse it. Top level is now
123
+ refused, as ja `ロード を 待つ` and tr `yükleme i bekle` already were; inside a handler,
124
+ `클릭 할 때 로드 를 대기` still reads `wait for load`. ko's own render of
125
+ `trigger click on #btn …` (`#btn에 클릭을 트리거 …`) had been refused for the same reason and now
126
+ translates.
127
+ - **es `eso`, `esto` and a detached `lo` read as `it`, as do pt `isto` and fr `cela`/`ceci`.**
128
+ `poner eso en #o` read `put eso into #o`, a variable nobody set, with no error. Most profiles
129
+ already write `it` as a demonstrative (pt `isso`, fr `ça`, ru `это`, he `זה`, ja `それ`). What
130
+ `it` renders to is unchanged. A variable spelled like one of these words now reads as `it` in
131
+ that language, as one named `isso` already did in pt; MCP `translate_code` reports it
132
+ (`NAME_COLLISION`) with a rename.
133
+
10
134
  ## [7.5.0] - 2026-10-22
11
135
 
12
136
  What a live test of 7.3.0 found users hitting. The Vite plugin loads in a CommonJS project and
package/README.md CHANGED
@@ -39,15 +39,22 @@ feature, or an optional expression kind. A rule that is not registered does not
39
39
  script that uses it fails with a parse error that names the token. `src/bundles/full.ts` is the
40
40
  list of every module.
41
41
 
42
+ A module can also add a reading to a command another module defines, for syntax upstream does
43
+ not have: a fallback (`(g.fallbacks.toggle ??= []).push(rule)`). It is tried from just after the
44
+ keyword, and only when the command's own rule fails to parse. Its reading counts only if it
45
+ reads at least as far as that rule got before failing; otherwise the rule's error stands. So a
46
+ fallback cannot change what the command reads. `toggle <element>` (`src/additions.ts`) is one.
47
+
42
48
  A module can live in another package. `@hyperfixi/speech` is one (upstream's `speak`): it imports
43
49
  `expr` and the `Grammar` / `Cmd` / `Expr` types from here, sets `g.commands.speak` to a rule that
44
50
  reads its operands off the `Parser` it is handed, and returns a node whose `run` may return a
45
- promise. `register(...everything, speak)` builds an engine with it. (The script-tag bundle is a
46
- fixed list; nothing adds a module to a page's `hyperfixi-hs.js` after it loads.)
51
+ promise. `register(...everything, speak)` builds an engine with it. What a module may call is
52
+ `src/kit.ts` (below); `register` skips a module it has already added.
47
53
 
48
54
  The public object is shaped like upstream's `_hyperscript` (`evaluate`, `parse`, `process`,
49
55
  `config`, `use(plugin)`, `addBeforeProcessHook`), so a plugin written for upstream's public API
50
- can be used on it; the multilingual adapter is. It adds one hook upstream lacks:
56
+ can be used on it; the multilingual adapter is, and so is upstream's own `ext/tailwind.js`,
57
+ unchanged. It adds a hook upstream lacks:
51
58
 
52
59
  ```ts
53
60
  _hyperscript.addSourceTransform((source, element) => english);
@@ -55,7 +62,55 @@ _hyperscript.addSourceTransform((source, element) => english);
55
62
 
56
63
  A script is rewritten as it is read. The element keeps the text its author wrote, and a parse
57
64
  error in the rewritten script carries that text (`error.written`). `@lokascript/hyperscript-adapter`
58
- uses the hook when the host has it.
65
+ uses the hook when the host has it. It also adds `register` and `extensions`, and in the
66
+ script-tag bundle `kit`, for extensions (next).
67
+
68
+ ### Extensions
69
+
70
+ An extension is a module that is not in `everything`. It is added with `register`: from ESM, or
71
+ on a page, after the script-tag bundle, by its own file:
72
+
73
+ ```html
74
+ <script src="hyperfixi-hs.js"></script>
75
+ <script src="ext/tailwind.js"></script>
76
+ ```
77
+
78
+ ```ts
79
+ import { register, everything } from '@hyperfixi/engine';
80
+ import { tailwind } from '@hyperfixi/engine/ext/tailwind';
81
+ register(...everything, tailwind);
82
+ ```
83
+
84
+ With `@hyperfixi/vite-plugin`: `hyperfixi({ extensions: ['tailwind'] })`. The plugin's scan
85
+ never adds an extension; a project lists the ones it uses.
86
+
87
+ - **What a module may call is `src/kit.ts`**: for parsing, `expr`, `dotOrColonPath`,
88
+ `urlOrExpression`, `evalStatic` and `commandList`; for running, `makeContext`, `runList` and
89
+ `assignToNamespace`; and `config`. ESM gets
90
+ it from the library entry; the script-tag bundle (`hyperfixi-hs.js`) carries it as
91
+ `_hyperscript.kit`, so an extension file calls the page's engine instead of carrying a copy
92
+ of its parser. Only that bundle carries it: on every bundle's `api` it would keep the kit's
93
+ functions in a bundle that uses none of them (~0.25 KB gzipped on a small one, measured), so
94
+ a bundle built from ESM (the Vite plugin's) registers extensions from ESM. An extension's value
95
+ imports come from `../kit` only, and the build fails on any other. A name is added to the
96
+ kit when an extension needs it, as a change to the public API.
97
+ - **An extension file** (`dist/ext/<name>.js`, built from `src/ext/<name>.ts`) registers its
98
+ module once per page (it records its name in `_hyperscript.extensions`, so a script htmx
99
+ boost runs again does not register twice), and says so on the console when no engine, or
100
+ one without the kit, was loaded before it. `dist/ext/<name>.esm.js` is the same module for ESM
101
+ (`@hyperfixi/engine/ext/<name>`), taking the kit from `@hyperfixi/engine`.
102
+ - **Loaded late** (`async`, a lazy import, a page's own `register` call), an extension still
103
+ applies: `register` reads again, and only again, the elements whose scripts failed to parse.
104
+ An extension that claims only what upstream rejects cannot change a script that parsed. A
105
+ dialect (an extension that changes what a parsed script means) has to be registered before
106
+ the document is read.
107
+ - **Which extensions there are**, both upstream's and each held to upstream's own tests for it:
108
+ `tailwind` (the `twDisplay`, `twVisibility` and `twOpacity` hide/show strategies) and
109
+ `socket` (`socket Chat ws://… [on message [as JSON] …] end`: a WebSocket as a named object
110
+ with `raw`, `dispatchEvent` and an `rpc` proxy). Here the socket opens when the feature is
111
+ installed; upstream's opens when it is parsed (`tests/extensions.js` holds the difference). An extension ported
112
+ from upstream is held to upstream's tests; one of this repository's own will be held to
113
+ `check:claims`, as the additions are.
59
114
 
60
115
  ### htmx and fixi
61
116
 
@@ -85,10 +140,12 @@ whenever htmx loads.
85
140
  | `src/reactivity.ts`, `src/templates.ts`, `src/live-templates.ts` | Optional: `when` / `live` / `bind`; `render` and template text; live templates |
86
141
  | `src/commands/*` | 52 command keywords, one module export each |
87
142
  | `src/additions.ts` | The two forms upstream does not have: `new X()` and `toggle <element>` |
143
+ | `src/kit.ts`, `src/ext/*` | What an extension module may call; the extensions, each built to `dist/ext/<name>.js` (script tag) and `.esm.js` |
88
144
  | `src/bundles/*` | Browser bundle entries. `hyperfixi-hs` is the script-tag bundle; `full` is the same modules, for the gate; `core`, `minimal`, `common` exist to measure size |
89
145
  | `upstream-suite/` | The acceptance gate: upstream's Playwright suite (vendored) against a bundle |
90
- | `tests/` | This package's own tests (`src/additions.ts`, and regressions upstream's tests did not show), in the form of upstream's and on the same fixtures |
146
+ | `tests/` | This package's own tests (`src/additions.ts`, extensions, and regressions upstream's tests did not show), in the form of upstream's and on the same fixtures |
91
147
  | `tools/probe.mts` | One source on this engine and on upstream, side by side, on the value matrix's fixture |
148
+ | `tools/claims.mts`, `tests/claims.json` | What each addition claims, read and run on upstream: the tier each addition may have |
92
149
 
93
150
  A node is typed data plus the closure that runs it, bound when the node is parsed: an
94
151
  expression has `ev`, a command has `run`, a feature has `install`. Control flow is a value a
@@ -101,6 +158,7 @@ npm run build --prefix packages/engine # dist/: library, bundles, decla
101
158
  npm run typecheck --prefix packages/engine
102
159
  npm run test:upstream --prefix packages/engine # the gate (needs a build; about 30 s)
103
160
  npm run test:own --prefix packages/engine # additions and regressions (needs a build; 2 s)
161
+ npm run check:claims --prefix packages/engine # what each addition claims, on upstream (3 s)
104
162
  npm run cost --prefix packages/engine # what each module costs in the full bundle
105
163
 
106
164
  # Any engine bundle can be scored on the same suite:
@@ -120,15 +178,32 @@ release 0.9.93, the one this repository's other gates use) against `dist/full.js
120
178
  that fail must be exactly the ones listed in `upstream-suite/known-failures.json`. A new failure
121
179
  fails the gate. So does a listed test that now passes: prune it with
122
180
  `npm run test:upstream:update` in the same change, so the list stays what does not pass. CI runs
123
- it in the `browser-tests` job.
124
-
125
- **The package's own tests.** `test:own` runs `tests/` against the same bundle, for the two forms
126
- upstream has no tests for. Every one must pass. On upstream's own bundle 2 of the 21 pass (the
127
- two that check an upstream form still reads as it did), which is what shows the rest test the
128
- additions. `tests/regressions.js` holds the other kind: places where this engine differed from
181
+ it in the `browser-tests` job. The engine the tests run carries the extensions this engine
182
+ builds (`src/ext/`), as upstream's own test bundle carries its extensions (`test/entry.js`):
183
+ the bundle, then each `ext/<name>.js` next to it, as one file. (For upstream's bundle those
184
+ are upstream's own files.) The package's own tests run on the engine alone.
185
+
186
+ **`test:upstream:update` does not retry**, so a timing flake in its run becomes a listed
187
+ failure. Diff the `failing` list it writes before committing it.
188
+
189
+ **The package's own tests.** `test:own` runs `tests/` against the same bundle, for what
190
+ upstream has no tests for: the two forms, and the extension tier (`tests/extensions.js`). Every one must pass. On upstream's own bundle 3 of `additions.js`'s 27 pass (the
191
+ three that check an upstream form still reads, or fails, as it did), which is what shows the
192
+ rest test the additions. `tests/regressions.js` holds the other kind: places where this engine differed from
129
193
  upstream and upstream's tests did not show it (each of those passes on upstream). CI runs both
130
194
  after the upstream suite.
131
195
 
196
+ **The claims.** `check:claims` (`tools/claims.mts`) asks what each addition takes from upstream.
197
+ `tests/claims.json` lists, per addition, the sources it gives a meaning to; each is read and
198
+ run on upstream, and the worst outcome sets the tier the addition may be listed with:
199
+ an **addition** claims only what upstream rejects, a **trap repair** also what upstream reads
200
+ but cannot run, a **dialect** something that runs on upstream. The listed tier must be the
201
+ measured one, the engine must read and run every claim, and the engine without the addition
202
+ must read each claim as upstream does (otherwise the claim is not the addition's). A claim
203
+ whose run needs an API jsdom lacks (`showModal`, `togglePopover`) is only read, with the
204
+ reason listed; `tests/additions.js` runs it in Chromium. `--report` prints every claim and
205
+ what each reader did with it.
206
+
132
207
  **The multilingual gates**, in the packages that own them:
133
208
 
134
209
  - The value matrix (`packages/testing-framework`, `value-matrix.ts`) has an `eng` lane, the
@@ -200,14 +275,16 @@ engine want `increment #count's textContent`. The examples write the second, whi
200
275
  bundle runs to the same count (the bundle matrix's Counter row).
201
276
 
202
277
  Since 2026-10-21 the gate also runs upstream's htmx tests (`integration/htmx.js` and
203
- `ext/hs-include.js`, 19), which the first vendoring left out, so the whole suite is 1,486. The
204
- engine failed 10 of them until it had upstream's htmx integration; it passes all 19.
278
+ `ext/hs-include.js`, 19), which the first vendoring left out. The engine failed 10 of them until
279
+ it had upstream's htmx integration; it passes all 19. Since 2026-10-25 it also runs upstream's
280
+ tests for each extension this engine builds (`ext/tailwind.js`, 12; see Extensions), so the
281
+ whole suite is 1,498; and the socket tests (`features/socket.js`, 16), vendored from the
282
+ start, pass since the engine builds `socket`.
205
283
 
206
- The 64 known failures: upstream's internal-API surface (`internals.tokenizer`,
207
- `evalStatically`, source info, error collection: 45), sockets and workers (17, upstream
208
- extensions that are not built), and two collection-expression tests that need upstream's
209
- component extension. Upstream's one failure is a test that expects its worker extension to be
210
- absent.
284
+ The 48 known failures: upstream's internal-API surface (`internals.tokenizer`,
285
+ `evalStatically`, source info, error collection: 45), the worker feature (1: upstream's
286
+ extension, not built), and two collection-expression tests that need upstream's component
287
+ extension. Upstream's one failure is a test that expects its worker extension to be absent.
211
288
 
212
289
  As a host for the multilingual text path (semantic renders a translation, the adapter's
213
290
  `preprocess` turns it back into English, the engine runs the English):
@@ -262,30 +339,32 @@ two values and inserts nothing. The comparison that counts is the DOM a handler
262
339
 
263
340
  ## Differences from upstream
264
341
 
265
- Three, all recorded here and nowhere silent.
342
+ In syntax, two, both recorded here and nowhere silent.
266
343
 
267
344
  **Two additions** (`src/additions.ts`, decided 2026-10-01), forms `packages/core` has and the
268
- shipped examples use. Each is its own module, and neither changes how an upstream script reads:
269
- upstream's suite passes the same tests with them registered as without.
345
+ shipped examples use. Each is its own module; upstream's suite passes the same tests with them
346
+ registered as without, and `check:claims` measures what each takes from upstream (above).
270
347
 
271
348
  - `new Date()`, `new Intl.NumberFormat('en')`, `(new Date()).getDay()`: a constructor call as
272
349
  an expression. `new` is still an ordinary name unless a constructor call follows it. Upstream
273
- has the `make a Date` command and no expression. 0.20 KB.
350
+ has the `make a Date` command and no expression. 0.20 KB. Measured a **dialect**: upstream
351
+ reads `set d to new Date()` as `set d to new` (an unset variable) and a second command,
352
+ `Date()`, which runs and leaves `d` null. With a class (`new Map()`) or a call after it
353
+ (`new Date().getDay()`, the form the shipped examples use), upstream's reading fails when
354
+ run instead; inside `put … into` or parentheses, upstream rejects it.
274
355
  - `toggle #dialog`, `toggle #dialog modal` (or `as modal`), `toggle #details`: open what is
275
356
  closed and close what is open, for a dialog, a details element (or its summary), a popover
276
357
  and a select. Upstream rejects `toggle <expression>` unless `between` follows; that form is
277
- unchanged. 0.21 KB.
358
+ unchanged. 0.21 KB. Measured an **addition**: upstream rejects every form it claims. It is
359
+ a fallback for `toggle` (above), so it reads only what upstream's rule rejects.
278
360
 
279
- **One looser reading**, found by the measurement above: `my @id as String` (an attribute read
280
- through `my` / `its` / `your`, then a conversion or any further access). Upstream rejects it,
281
- because its attribute-access rule does not continue the expression chain as every other access
282
- does; the possessive form `#a's @title as String` works on both. This engine continues the
283
- chain. It is kept because the stricter reading looks like an upstream slip and a shipped
284
- example uses the form (`its @data-stock as Number > 0`).
361
+ An attribute read through `my` / `its` / `your` takes nothing after it here, as on upstream:
362
+ both reject `my @id as String` (upstream's attribute-access rule does not continue the
363
+ expression chain); the possessive `#a's @title as String` works on both.
285
364
 
286
365
  Considered and not added: core's `swap` strategies (upstream's `swap` means something else),
287
366
  `copy`, a `? :` conditional, `.debounce(n)`, `has`, prefix `unless`, `set @a to v on <target>`,
288
- `sorted by … desc`. `push url` / `replace url` are undecided.
367
+ `sorted by … desc`, `push url` / `replace url` (pages write `call history.pushState`).
289
368
 
290
369
  ## Where the bytes are
291
370
 
@@ -2,13 +2,17 @@
2
2
  * Syntax upstream `_hyperscript` does not have. Everything else in this engine
3
3
  * follows upstream's source; these two forms come from `packages/core`, are used
4
4
  * by the examples this repository ships, and were kept by decision (2026-10-01).
5
- * Each is an optional module, and neither changes how an upstream script reads:
5
+ * Each is an optional module. What each takes from upstream is measured, claim by
6
+ * claim, by `npm run check:claims` (`tests/claims.json`):
6
7
  *
7
8
  * - `construct`: `new Date()`, `new Intl.NumberFormat('en')`. Upstream has the
8
9
  * `make a Date` command and no expression; there, `set t to new Date().getDay()`
9
- * parses (`new` is a variable, the call a second command) and fails when it runs.
10
+ * parses (`new` is a variable, the call a second command) and fails when it runs,
11
+ * and `set d to new Date()` runs and leaves `d` null. A dialect, kept in every
12
+ * bundle by decision (2026-10-25).
10
13
  * - `toggleElement`: `toggle #dialog`, `toggle #dialog modal`, `toggle #details`.
11
- * Upstream rejects `toggle <expression>` without `between`.
14
+ * Upstream rejects `toggle <expression>` without `between`. A fallback for
15
+ * `toggle` (`Grammar.fallbacks`), so it reads only what that rule rejects.
12
16
  */
13
17
  import type { Cmd, Expr } from './ast';
14
18
  import type { Grammar } from './parser';
@@ -26,5 +30,8 @@ export interface ToggleElementNode extends Cmd {
26
30
  /** `toggle #d modal` / `toggle #d as modal`: open with `showModal()`. */
27
31
  modal: boolean;
28
32
  }
29
- /** `toggle <element> [modal | as modal]`: the form `toggle` takes when no `between` follows. */
33
+ /**
34
+ * `toggle <element> [modal | as modal]`: a fallback for `toggle`, so it reads only what
35
+ * upstream's rule rejects. It declines a target `between` follows: that is upstream's form.
36
+ */
30
37
  export declare function toggleElement(g: Grammar): void;