@lokascript/compilation-service 4.0.1 → 4.2.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,334 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.2.0] - 2026-10-07
11
+
12
+ A translation that would lose part of a script is now refused instead of returned short.
13
+ `@lokascript/semantic`'s `translate()` throws a `LossyTranslationError` unless the caller passes
14
+ `{ lossy: 'allow' }`, and the adapter, the Vite plugin's bundle and `@hyperscript-tools/i18n` keep
15
+ the author's text and warn. Upstream forms that translations used to drop now read whole in every
16
+ language: scripts of several features, `catch` / `finally`, conditionals and loops, `bind … and`,
17
+ scoped names such as `element x`, what `on` reads after its event, and clauses a command's
18
+ pattern does not model. On the new command-shape gate, round trips that came back silently
19
+ different fell from 7,614 at its first run to 174 (none in English); 23,763 of 27,240 now read as
20
+ their source does, and 3,303 are refused.
21
+
22
+ ### Added
23
+
24
+ - **`@lokascript/semantic` keeps a clause its pattern does not model, as written.** A command's
25
+ pattern read the start of the command and dropped the rest:
26
+ `add .foo to .bar when it matches .doh`, `toggle between .a and .b`,
27
+ `take .foo from .div for #d3`, `render #tmpl into #target`, `log me, my`. The rest is now
28
+ kept on the node as `verbatimClause` and written back after the command in every language, in
29
+ SOV and VSO word order too (ja `クリック を で .bar に .rey を 追加 when it matches .doh` reads
30
+ back as `on click add .rey to .bar when it matches .doh`). So a foreign render can now hold a
31
+ clause in English. A run that holds a command verb, a structure word (`else`, `catch`, `on`, …)
32
+ or a word of the source language (es `cuando`) is not a clause, and its translation is
33
+ refused. The parser records a `verbatim-clause` warning for each kept clause, since no role
34
+ reads it: `@lokascript/compilation-service` reports it as `UNCONSUMED_INPUT` with its repair
35
+ hint, and MCP `validate_hyperscript` lists it with the unconsumed tokens. A pattern group that
36
+ reads its marker and binds no role now gives the marker back, so
37
+ `take .foo from .div for #d3` keeps its `for` and `transition *width from 0px to 100px` its
38
+ `from`.
39
+ - **`@lokascript/semantic` keeps what upstream's `on` reads between the event and the body.**
40
+ `on click elsewhere`, a count (`on click 1`, `on click 1 to 2`), `on mutation of attributes`,
41
+ `on click in #d1`, `on foo queue first` and `on x having threshold 0.1` lost everything after
42
+ the event, so the handler ran on every event, or with a count, on the wrong ones. The run is now
43
+ kept as `EventHandlerSemanticNode.headClause`, written after the whole head in every language
44
+ (ja `クリック を で elsewhere .clicked を 追加`) and reported with the same `verbatim-clause`
45
+ warning. Outside English a count is read only in the `1 to 2` form, since a body there can open
46
+ with a number: a bare count in another language (`on click 1`) is refused.
47
+ - **`@lokascript/semantic` reads `catch` and `finally` in handlers and functions.** In a `def`, the
48
+ clauses ran as more of the function's body, so its error handling ran every time, in silence. In
49
+ a handler, `catch e …` was left unread. `EventHandlerSemanticNode` and `DefSemanticNode` now
50
+ carry `catchName`, `catchBody` and `finallyBody`. A `def` writes its clauses before its `end`,
51
+ and a handler writes them after its commands. No language has its own words for them yet, so
52
+ every language writes English's `catch` and `finally`. `wait a tick` now reads as `wait 0ms`, as
53
+ the engine runs it; every render wrote `wait tick`, a wait on a variable named `tick`.
54
+ - **`@hyperfixi/testing-framework`: the command-shape gate.** It takes the 1,135 scripts in
55
+ upstream _hyperscript's own test suite (0.9.93) and in core's reference and hover examples that
56
+ the engine reads, translates each into the other 23 languages and back (and English to English),
57
+ and has the engine parse the result and the source. A pair passes when the two parses match,
58
+ after a short list of named equivalences that are each checked on both engines. Otherwise it is
59
+ refused (`translate` threw) or silent. The baseline lists the refused and silent pairs and only
60
+ shrinks: the gate fails on a pair worse than listed and on one better. Every silent pair left
61
+ belongs to a named family with a reason (`SILENT_FAMILIES`), and none is in English.
62
+ - **`@hyperfixi/engine`'s parse keeps two values it held only in a closure.** `GoNode.url` holds
63
+ the address of `go to url <address>`, and `SymbolNode.on` the element of `^name on <element>`.
64
+ Two scripts that differed only there parsed to the same tree. Nothing reads them at run time.
65
+
66
+ ### Changed
67
+
68
+ - **`@lokascript/semantic`'s `translate()` refuses a translation that would lose part of the
69
+ script.** This is a change in behavior. It used to return what it could render:
70
+ `log it?.dataset?.customValue` came back in Spanish as `registrar ello`, and Hindi wrote
71
+ `put it.name into #r` in a form that reads back as `on its.name put #r into me`. It now throws a
72
+ `LossyTranslationError` carrying `partial` (what it would have returned, not the whole script),
73
+ `loss.kind` and `loss.lost` (what it drops). It refuses when the parse leaves input unread
74
+ (`truncation`); when the output does not read back with the input's commands, roles and `put`
75
+ positions (`read-back`), as when a foreign `wait for <event>` reads back as a time wait; and when
76
+ a string, number, selector, attribute, style or `$` / `:` / `^` name of the input is missing from
77
+ the output (`invariant`). For example,
78
+ `translate('on click ask "Name?" then put it into me', 'en', 'es')` throws with `loss.lost`
79
+ `['ask "Name?"']`, since `ask` has no schema yet. Pass `{ lossy: 'allow' }` as the fourth
80
+ argument to get the partial output instead. The root and
81
+ `@lokascript/semantic/core` export `LossyTranslationError`, `findTranslationLoss` (the same
82
+ checks on a parse and its render) and the types `TranslateOptions`, `TranslationLoss` and
83
+ `TranslationLossKind`. Comments (`--`, `//`) and Bengali's polite verb forms (`টগল করুন`) are now
84
+ read whole, so a correct translation that holds them is not refused.
85
+ `@hyperfixi/core/multilingual`'s `translate` takes the same options and rethrows the refusal. It
86
+ used to return the input unchanged on any error; input that does not parse still comes back
87
+ unchanged.
88
+ - **`@lokascript/hyperscript-adapter`, `@hyperfixi/vite-plugin`'s bundle and
89
+ `@hyperscript-tools/i18n` keep the author's text when a translation would lose part of it.**
90
+ This is a change in behavior. The adapter's full, slim and lite plugins ran the partial English:
91
+ `al clic alternar .foo cuando .bar` ran as `on click toggle .foo`. They now leave the script as
92
+ written, so the host reports a parse error naming code the author wrote, and warn once per
93
+ language with what would be dropped (`cuando .bar`). `preprocess()`'s config takes `onLossy`,
94
+ which is called with the refusal. The Vite plugin's source transform does the same, and its
95
+ `translateHyperscript` rethrows the refusal, where it returned the code unchanged on any error.
96
+ `@hyperscript-tools/i18n`'s lenient mode kept the source text in silence. Now `translateHtml`
97
+ reports a refusal through `onRefused` (`'warn'`, the default, `'error'`, or a callback), the
98
+ Eleventy filters take `'warn'` or `'error'`, and the CLI names the file.
99
+ - **`@hyperfixi/mcp-server`'s translate tools report a refusal.** `translate_code` returns
100
+ `ok: false` with a `LOSSY_TRANSLATION` diagnostic and `loss.lost`, and the partial text under
101
+ `partial`, never `code`. `translate_hyperscript` and `translate_to_english` return
102
+ `refused: true` with `lost` and `partial`. `translate_hyperscript` used to report every error as
103
+ a missing package.
104
+
105
+ ### Fixed
106
+
107
+ - **`@lokascript/semantic`: a script of several features reads as one program.** Upstream reads
108
+ each top-level feature on its own. Semantic kept only the first and dropped the rest, or chained
109
+ them with `then`, which neither engine reads: `def a … end def b … end` became one function,
110
+ `def … end on click …` lost its handler, and `bind … end live …` and `set :x to 1 on click …`
111
+ came out joined by `then`. A handler, a `def`, a `behavior`, a feature block, or a top-level
112
+ `bind`, `set`, `install` or `js` now starts a new part of the program, and a top-level `init`
113
+ parses, as a feature block (`FeatureAction` gains `'init'`). English writes one feature per
114
+ line and closes each handler with `end`. It writes a `bind`, `set` or `install` with the `end`
115
+ upstream allows when the next feature has no head word to split at, and joins consecutive
116
+ `install` and `bind` features with a space, never `then`, which the engine rejects.
117
+ - **`@lokascript/semantic`: a dotted name is one name, and a behavior's `end`s are optional.**
118
+ A dotted name is one name: `def utils.foo()` defined `utils` and left `.foo ( )` unread, and
119
+ `behavior App.Widgets.Clickable` did not parse as a behavior. A behavior's last handler needs no
120
+ `end`, nor does the behavior at the end of input: `behavior B(x) on click set @out to x` read as
121
+ a `behavior` command chained to a `set`. A trailing `init` block in such a behavior is its init.
122
+ A behavior member that is neither a handler nor `init`
123
+ (`behavior MarkIt set @data-marked to 'yes' end`) came out as `behavior MarkIt then set …`,
124
+ which the engine rejects; semantic does not model such members yet, so the translation is now
125
+ refused. So is `behavior A … behavior B …` with no `end` between them: upstream reads B as part
126
+ of A, and semantic read it as a `behavior` command in A's last handler.
127
+ - **`@lokascript/semantic`: `bind … and` / `with` and `install`'s arguments read.** Upstream reads
128
+ `bind <left> and|with|to <right>` as the same binding. Only `to` had a pattern, and its left
129
+ side took only a variable, so twelve of upstream's test scripts did not parse, among them
130
+ `bind .dark and $darkMode` and `bind my value and #slider's value`. The left side now takes
131
+ selectors and property paths too. `install Toggleable(cls: 'highlighted')`, the documented
132
+ behaviors usage, lost its arguments: an install's parentheses are arguments, not a signature.
133
+ - **`@lokascript/semantic`: conditionals and loops read whole.** An English pattern read every
134
+ top-level `if X …` as a handler for an event named `X`, so `if x log 1 else log 2 end` became
135
+ `on x log 1 then log 2`. Seven languages read their own `if` the same way (es and fr `si`, pt
136
+ `se`, zh `如果`, sw `kama` / `ikiwa`, id `jika` / `kalau` / `bila`, de `falls`), so every
137
+ conditional rendered in them read back as a handler. A bare `if … end` is now a conditional; de
138
+ `wenn` and `sobald` and id `apabila` still head a handler. An `if` on the line after `else`
139
+ opens its own block, as upstream reads it. Semantic chained the two, so the commands after the
140
+ inner `end` ran whether or not the condition held. Tokens that open a line now carry
141
+ `metadata.lineStart`, and English writes such an `if` on its own line. The `end` of
142
+ `at end of` no longer closes a conditional: in
143
+ `if x put 'a' at end of me end put 'b' at end of me`, the second `put` ran inside the branch. An
144
+ empty block (`if x then end`) stays a conditional, and a condition ends at `else`. `break` and
145
+ `continue` read. A loop whose test follows its body (`repeat … until x end`,
146
+ `repeat … while x end`) is read as one (`LoopSemanticNode.bottomTested`) and written
147
+ `repeat forever … until x end`, which the engine reads as the same loop; before, `repeat set x`
148
+ read as `repeat x times`. `repeat in X` keeps its collection, and `indexed by i` reads as
149
+ `index i`.
150
+ - **`@lokascript/semantic`: a name with its scope is one value, in every language.** Upstream reads
151
+ `global x`, `element x`, `element's x` (and `the element's x`), `local x` and `dom x` as one
152
+ variable, its idiom for a behavior's state; seven of `@hyperfixi/behaviors`' sources use it.
153
+ Semantic read the scope word alone, so the command around it was dropped, in English and in
154
+ every language: `set element x to 10`, `set global x to 10`, `init set dom count to 42`. The
155
+ tokenizers now read the scope word and the name as one token, and every language writes it as
156
+ written, as it writes `$` and `:` names (es `establecer element x a 10`). A scope word alone
157
+ (`set element to 5`) is still a word.
158
+ - **`@lokascript/semantic`: English writes upstream's postfix `unless` and drops `open`'s mode.**
159
+ `toggle .foo unless I match .bar` was written `toggle .foo then unless I match .bar`, and core's
160
+ prefix `unless C X` as it is; upstream rejects both. English now writes the guard after its
161
+ command, and other languages keep their own form. Upstream's `open` is modal, and the engine
162
+ reads `open #d as non-modal` as an expression, `(#d as non) - modal`. English now writes it
163
+ `call #d.show()`, and `open #d as modal` as `open #d`. The reader still accepts core's mode.
164
+ - **`@lokascript/semantic`: a value keeps its spelling.** A call's arguments keep their spacing:
165
+ `call navigator.clipboard.writeText(#input's value)` became `…(#input'svalue)`. A style block
166
+ kept a space between every token, so `font-family` became `font - family`, a subtraction.
167
+ `show`'s strategy argument is read raw, as upstream reads it: `display:inline-block` became
168
+ `display:inline - block`. A swap's strategy slot takes only core's strategy words (`into`,
169
+ `over`, `innerHTML`, …, `morph`), so `swap arr[0] with arr[2]` exchanges two values, as
170
+ upstream reads it; it was written `swap arr of [0] …`. An attribute with its value and a name
171
+ with its index are one value when nothing spaces them: `add @data-foo=baz` and
172
+ `remove :arr[1]` kept only their first part, and `increment arr[1]` read as
173
+ `increment arr by [1]`. Every language writes them as written. The words inside an index are
174
+ the script's: bn wrote the variable `index` in `var[(index-1)..(index+1)]` as `সূচক`.
175
+ - **`@lokascript/semantic`: events, waits and amounts keep what was written.** A quoted event name
176
+ stays quoted (`LiteralValue.quoted`): `trigger "my event"` now round-trips in all 23 languages,
177
+ and `send "hello" to ChatSocket` keeps its quotes. A spaced time unit is read with its number:
178
+ `wait 2 seconds` translated as `wait 2`, which is two milliseconds. A written `by 1` stays and
179
+ an implicit one is never shown: German wrote `um 1` for every increment, and English dropped
180
+ every written `by 1`. `measure` reads its element: `measure #other` became `measure`.
181
+ - **`@hyperfixi/core`'s reference and hover docs teach what the 4.x engine does.** `open`'s
182
+ `as modal` / `as non-modal` mode does nothing on the engine, so the docs drop it and give
183
+ `call #myDialog.show()` for a non-modal dialog. The `swap` example
184
+ `swap innerHTML of #target with result` was core's `put`, but upstream and the engine exchange
185
+ the two values: it is now `swap #a's value with #b's value`, and the hover text reads "Exchanges
186
+ two elements, or two writable values." The `fetch` example is now a template literal,
187
+ ``fetch `/api/${id}` as json``, since upstream sends a naked URL's `${id}` as written.
188
+ Translation keeps core's meaning for both old forms: English writes `put result into #target`
189
+ and a template literal. The Japanese example in `docs/EXAMPLES.md` is written as semantic writes
190
+ it, `クリック で 自分 に .active を 切り替え`; the old one left `を 私` unread.
191
+
192
+ ## [4.1.0] - 2026-10-06
193
+
194
+ Translations keep what the source says. `tell` blocks, swaps, view transitions, chains of
195
+ handlers without their `end`s, events with names of several words, and variables named like
196
+ events or keywords now survive `translate`, MCP `translate_code` and the adapter in every
197
+ language. The adapter's per-language bundles render English with semantic's renderer, and
198
+ `@lokascript/semantic/core` gains `registerHandcrafted`.
199
+
200
+ ### Added
201
+
202
+ - **`@lokascript/semantic` reads upstream's `start view transition … end`, in all 24 languages.**
203
+ `on click start view transition swap #a with #b end` parsed as a `transition` command whose
204
+ patient was `swap`, so `translate`, MCP `translate_code` and the corpus lost the block and its
205
+ body (`on click transition swap`), and validation reported the rest as unconsumed input. The
206
+ block is now a `viewTransition` command with its `body` (`ViewTransitionSemanticNode`;
207
+ `isViewTransitionBlock` from the package root), nested the way a loop's body is: a command after
208
+ the block's `end` stays after it, and the `end` no longer closes the handler, behavior or `def`
209
+ around it. `start [a] view transition [using "<type>"]` is written in English in every language,
210
+ as core's `using view transition` tail already is (a browser API's name, which no profile
211
+ translates). The corpus row `swap-view-transition` is now written this way and parses on both
212
+ engines.
213
+
214
+ ### Changed
215
+
216
+ - **Single-language bundles hold only their own language's hand-crafted patterns.** Each
217
+ command's pattern dispatcher in `@lokascript/semantic` named all 24 languages' hand-crafted
218
+ patterns, so every bundle that could build patterns held all of them (~200 KB minified). Each
219
+ language module now registers its own (`src/patterns/handcrafted/<lang>.ts`, through
220
+ `@lokascript/semantic/core`'s new `registerHandcrafted`), and bundlers drop the rest:
221
+ semantic's single-language bundles are 14–15 KB gzipped smaller (`browser-de` 116 KB instead
222
+ of 130), and a Vite or Rollup build that imports `@lokascript/semantic/core` and one
223
+ `languages/<lang>` gets the same saving. The patterns each language builds are unchanged
224
+ (hashed for all 24 before and after, from source and from the split `dist/`). The adapter's
225
+ bundles, which also changed renderer in this release, are measured in its entry under Fixed.
226
+
227
+ ### Fixed
228
+
229
+ - **`@lokascript/semantic`: a variable named like an event or a keyword keeps its name.** Upstream
230
+ and the engine run `set input to "a"` on a variable named `input`, but semantic's English parse
231
+ dropped the whole command (an event name alone took the value as the text "input", which a
232
+ `set` cannot write), so `translate`, MCP `translate_code` and the adapter lost it in every
233
+ language; `put "a" into input` was lost in ar, de, fr, id and zh, whose tokenizers read `input`
234
+ as the event, and `toggle .a on input` in six. An event name alone where a value stands is now
235
+ the variable it spells, as a structure word or a verb already was, except where an event stands
236
+ (`send`, `trigger`, `repeat until event`, `wait`). And the renderer no longer writes a variable
237
+ in the language's own word where that word reads back as another variable: `set when to 1`
238
+ wrote es `establecer cuando a 1`, read back as a variable named `cuando`, in all 23 languages,
239
+ and bn, ms, th and tl wrote `increment i by index` with their word for `index`. A variable keeps
240
+ its localized word only where that word alone is the same value (es `ello` for `it`), and the
241
+ verified render spells one whose neighbours change it (vi `đặt giá trị`, `set`, for `put value`).
242
+ The value matrix now runs ten such names (`input`, `click`, `when`, `index`, …) in every
243
+ position: all pass on both engines. No corpus row changes.
244
+ - **`@lokascript/semantic`: the event a command names reads back in every language, and a
245
+ `wait` keeps its source for any event.** The handler head read a native event name through the
246
+ event table, but `send`, `trigger` and `repeat until event` read it only when the language's
247
+ tokenizer knew the word: es `enviar dobleclic a #x` read back `send dobleclic to #x`, so the
248
+ event fired as `dobleclic` (55 of the 170 localized names per command: es, pt, fr, de, ko and zh
249
+ coinages, and id and sw `unload`). They now read it as its English name, as the head does. A `wait` kept the source of an
250
+ event only from a fixed list, in English first: `wait for myEvent from #b`,
251
+ `wait for unload from #b` and `wait for focusin from window` lost the source in every language, and
252
+ `wait for click or myEvent from #b` everything after `click`. Upstream reads every leg of
253
+ `wait for` that is not a number as an event, so English now does, and a name followed by a
254
+ source is an event in every language (a time wait has none); tr and tl no longer read `focusin`
255
+ as `focus`. bn writes the event `send` sends with `কে`, as `trigger`'s, since
256
+ `#x তে স্ক্রোল তে পাঠান` read as `scroll to #x`; the old `তে` still reads. And fr's `change`
257
+ event, `changement`, no longer reads as the reactive `when … changes` word, which turned
258
+ `repeat until event change` into a `when` feature. Four corpus rows change, the bn renders of
259
+ `send` (`কে` for `তে`).
260
+ - **`@lokascript/semantic`: a hand-written event name of several words is read whole.** The event
261
+ table reads 20 names of several words that the renderer never writes (ar `تمرير الماوس`, de
262
+ `taste runter`, id `lepas tombol`, pt `pressionar tecla`, qu `mana q'away`, sw `bonyeza chini`,
263
+ tr `fare_bas`, zh `鼠标进入`, and twelve more), but their tokenizers split them, and the reader took
264
+ the first word for the event: de `wenn taste runter protokollieren 1` read `on taste log 1`, id
265
+ `ketika lepas tombol …` `on lepas`, sw `unapo bonyeza chini …` `on click`, qu
266
+ `maykama mana q'away …` `on false`, and ar `على تمرير الماوس من #b …` `on scroll`. A `wait for`,
267
+ `send`, `trigger` or `repeat until event` lost the event the same way. The tokenizers now read
268
+ each name whole; tr reads an `_`-joined word as one token when it is a keyword, as sw already did
269
+ (its own `tuş_bas` had never been read), and splits any other at `_` as before. No corpus row
270
+ changes.
271
+ - **`@lokascript/semantic`: an event name of several words keeps what is written after it.** ar
272
+ writes keydown, keyup and resize as `ضغط المفتاح`, `رفع المفتاح` and `تغيير حجم`, id writes
273
+ keydown as `tekan tombol`, and vi writes resize as `đổi kích thước`, but each tokenizer read only
274
+ the first word as the event. So `on keyup from #b log 1` read back `on keyup log 1` in ar,
275
+ `on resize from window` read back `on change` in ar and vi, and `send keyup to #x`,
276
+ `trigger keydown on #x`, `repeat until event keyup from #b` and `wait for resize` lost their
277
+ `to`, `on` or `from`, or waited for `change`. `translate`, MCP `translate_code` and the adapter
278
+ carried the loss. The three tokenizers now read each name whole, as es, fr, pt and qu already
279
+ did. The corpus's ar and vi `window-resize` rows, which the parser's compound repair already
280
+ read right, now parse at full confidence (1.0, was 0.82); no other corpus row changes.
281
+ - **`@lokascript/semantic`: a behavior's handlers need no `end` of their own.** Upstream ends a
282
+ handler's commands at the next feature, so `behavior F on click add .a on keyup log 1 end end`
283
+ is two handlers. The behavior parser split its body only at `end`, read both handlers as one,
284
+ and English wrote `behavior F then add .a then log 1`, which the engine rejects: `translate`,
285
+ MCP `translate_code` and the adapter broke such a behavior in every language. Each piece now goes
286
+ through the splitter a top-level chain of handlers already used, and parses to the same node as
287
+ the form with every `end` written. No corpus row changes (every one writes its `end`s).
288
+ - **`@lokascript/semantic`: a chain of handlers written natively without their `end`s reads as
289
+ written, in every language.** In 9 languages such a chain merged into one handler, at top level
290
+ and inside a behavior, and the result was valid English that ran wrong: de, fr, id, zh, ko, qu
291
+ and tr dropped the second event and ran its commands on the first (`on click add .a then log
292
+ 1`), and hi and bn read it as a destination (`add .a to keyup`). The split knew a handler head
293
+ only as `on`'s forms and ja/ko's `を で` / `을 에`; it now reads each language's own: the `when`
294
+ word the renderer writes (de `wenn`, fr `quand`, id `ketika`, qu `maykama`), zh's `一 … 就`, and
295
+ the words after the event (ko `할 때`, tr `i üzerinde`). The same rule ends an `init` with no
296
+ `end` in SOV languages too, and he and vi no longer read a handler after `toggle .a on #x` as the
297
+ toggle's. hi `पर` and bn `তে` are also the destination marker, which almost every command there
298
+ may write first, so a split at one is never trusted: it reads as written, at a confidence below
299
+ the adapter's threshold, so the adapter leaves the script as written and the engine reports it.
300
+ No corpus row changes.
301
+ - **`@lokascript/semantic`: English writes core's view-transition tail as upstream's block.**
302
+ `swap #a with #b using view transition` (and `morph`'s tail) renders to English as
303
+ `start view transition swap #a with #b end`, which upstream _hyperscript and `@hyperfixi/engine`
304
+ both parse; the tail itself is rejected by both. Every language reads the tail, and
305
+ `@lokascript/hyperscript-adapter` hands the host the English render, so a page written in
306
+ another language with a view-transition swap failed to parse and ran nothing. Foreign renders
307
+ keep the tail.
308
+ - **`@lokascript/semantic`: a `swap` keeps both operands in every language.** Only a swap of two
309
+ selectors round-tripped. `swap #target with me`, `swap :x with :y` and a swap of properties
310
+ (`swap my textContent with #a`, `swap #a's textContent with #b's textContent`) lost an operand
311
+ or its property in up to 21 languages, so a translated page swapped the wrong thing or nothing.
312
+ Four causes: swap's roles took no reference or property path; a handler's repair re-parse was
313
+ vetoed when the swap's first operand moved to its real role; an optional slot that declined a
314
+ value by type still consumed it (any command's pattern could lose the next role that way); and
315
+ in pl/uk the with-word is also the of-word, so `swap el with #t` read as `#t's el`.
316
+ - **`@lokascript/semantic`: a `tell` block keeps its extent.** Semantic kept `tell` flat, its body
317
+ being every statement after it, so in English and every translation a command after the tell's
318
+ `end` was lost (`tell #modal show end then log 2` lost the `log`) or pulled inside it
319
+ (`… end log 2`), a nested tell took its parent's remaining commands, and in a behavior or `def`
320
+ the tell's `end` closed the enclosing block, dropping the next handler or command. The parser
321
+ now nests a tell's body as it nests a loop's (`BlockCommandSemanticNode`, `isBlockCommand`), and
322
+ reads an `end` as the tell's exactly when the engine does: before a new `on <event>`, `def`,
323
+ `init` or `behavior`, a tell needs none.
324
+ - **`@lokascript/hyperscript-adapter`'s per-language and regional bundles render English with
325
+ semantic's renderer.** They rendered with their own English writer, which saved English's
326
+ language data and had drifted far from semantic's: over the corpus's 3,772 translations, 1,242
327
+ rendered to valid English that did something else, and 345 to English the engine rejects (the
328
+ script then kept its author's text and did not run). Every `if` lost its branches,
329
+ `put … before`/`after` became `put … into`, `from window`, `from elsewhere`, `debounced` and
330
+ `or <event>` vanished, `<button/> in me` widened to the whole page, `repeat for x in …` lost
331
+ its binding, and every `swap #a with #b` was written `swap of #a with #b`. They now give exactly
332
+ what the full `hyperscript-i18n.global.js` gives. English's own module builds through
333
+ `@lokascript/semantic/core` (its pattern generator and repeat heads,
334
+ `getRepeatPatternsForLanguage` now exported there), so a bundle holds one copy. Against 4.0.1,
335
+ the single-language bundles are the same size (`-de` 116 KB gzipped) and `-en` is 109 KB
336
+ instead of 148; the regional bundles are 8–16 KB larger (`western` 141 KB instead of 125).
337
+
10
338
  ## [4.0.1] - 2026-10-05
11
339
 
12
340
  Fixes for 4.0.0: the per-language adapter bundles read every language again, and the MCP server's
@@ -1095,7 +1423,9 @@ _Synchronized version release. See git history for details._
1095
1423
  - npm access token stored in GitHub Secrets
1096
1424
  - 2FA recommended for npm organization
1097
1425
 
1098
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...HEAD
1426
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...HEAD
1427
+ [4.2.0]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...v4.2.0
1428
+ [4.1.0]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...v4.1.0
1099
1429
  [4.0.1]: https://github.com/codetalcott/hyperfixi/compare/v4.0.0...v4.0.1
1100
1430
  [4.0.0]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...v4.0.0
1101
1431
  [3.3.0]: https://github.com/codetalcott/hyperfixi/compare/v3.2.0...v3.3.0
@@ -2427,13 +2427,14 @@ function liftNodeDiagnostics(node, diagnostics) {
2427
2427
  if (!Array.isArray(nodeDiags)) return;
2428
2428
  for (const d of nodeDiags) {
2429
2429
  if (d.severity !== "warning" && d.severity !== "error") continue;
2430
+ const unread = d.code === "unconsumed-input" || d.code === "verbatim-clause";
2430
2431
  const diag = {
2431
2432
  severity: d.severity,
2432
2433
  // Parser codes are kebab-case; this surface uses UPPER_SNAKE (PARSE_ERROR &c).
2433
- code: (d.code ?? "PARSE_DIAGNOSTIC").replace(/-/g, "_").toUpperCase(),
2434
+ code: unread ? "UNCONSUMED_INPUT" : (d.code ?? "PARSE_DIAGNOSTIC").replace(/-/g, "_").toUpperCase(),
2434
2435
  message: d.message ?? "parser diagnostic"
2435
2436
  };
2436
- if (d.code === "unconsumed-input") {
2437
+ if (unread) {
2437
2438
  diag.suggestion = "Dropped tokens were parsed but bound to no role \u2014 usually a missing role marker (e.g. 'to'/'on'/'from' before the target), so the role fell back to a default like `me`. Compare the returned roles against your intent.";
2438
2439
  }
2439
2440
  diagnostics.push(diag);
@@ -2656,6 +2657,9 @@ function nodeToParseResult(node) {
2656
2657
  }
2657
2658
 
2658
2659
  // src/service.ts
2660
+ function isLossyTranslation(error) {
2661
+ return error instanceof Error && error.name === "LossyTranslationError" && typeof error.partial === "string";
2662
+ }
2659
2663
  var CompilationService = class _CompilationService {
2660
2664
  constructor(options = {}) {
2661
2665
  this.translateFn = null;
@@ -2816,6 +2820,21 @@ var CompilationService = class _CompilationService {
2816
2820
  diagnostics: this.translationCollisions(request)
2817
2821
  };
2818
2822
  } catch (error) {
2823
+ if (isLossyTranslation(error)) {
2824
+ return {
2825
+ ok: false,
2826
+ partial: error.partial,
2827
+ loss: { kind: error.loss.kind, lost: [...error.loss.lost] },
2828
+ diagnostics: [
2829
+ {
2830
+ severity: "error",
2831
+ code: "LOSSY_TRANSLATION",
2832
+ message: error.message,
2833
+ suggestion: "The translation would drop the part listed in `loss.lost`. Rewrite that part of the source, or translate the rest without it."
2834
+ }
2835
+ ]
2836
+ };
2837
+ }
2819
2838
  return {
2820
2839
  ok: false,
2821
2840
  diagnostics: [
@@ -3286,4 +3305,4 @@ export {
3286
3305
  SvelteRenderer,
3287
3306
  CompilationService
3288
3307
  };
3289
- //# sourceMappingURL=chunk-GWYYJAZY.js.map
3308
+ //# sourceMappingURL=chunk-QUA3XGWW.js.map