@lokascript/compilation-service 4.1.0 → 4.3.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,250 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.3.0] - 2026-10-07
11
+
12
+ More of what upstream reads now translates, and what does not is refused instead of lost. On the
13
+ command-shape gate, round trips that came back silently different fell from 174 in 4.2.0 to 31,
14
+ none of them in English; 24,386 of 27,240 now read as their source does. English-to-English refuses
15
+ 61 of the 1,135 scripts, down from 82, and each of the 61 now belongs to a named family with the
16
+ reason it is not carried yet.
17
+
18
+ ### Added
19
+
20
+ - **`@lokascript/semantic` reads `on every <event>` and `init immediately`.** Both lost their head
21
+ word, so the handler or the block was refused. `EventHandlerSemanticNode.eventModifiers.every` and
22
+ `FeatureSemanticNode.immediately` carry them. English writes them as upstream does; every other
23
+ language writes the English word first (`every al clic …`), as it already did for `on first`.
24
+ - **`@hyperfixi/testing-framework`: two new value-matrix axes.** A `branch` position is an `if` with
25
+ no `then` before a command that names no target (`if 2 is in arr add .a end`). The matrix's `if`
26
+ cells always wrote `then` and a `put` with its target, so an SOV reader always saw where the
27
+ condition ended, and none of them could fail the way Turkish did (see Fixed). The 610 pairs that
28
+ `branch` refuses are listed in the baseline. A twelfth operand kind is a CSS length (`100px`,
29
+ `50%`). The matrix now has 5,396 cells.
30
+
31
+ ### Changed
32
+
33
+ - **`@lokascript/semantic` refuses core's swap strategies that upstream has no spelling for.**
34
+ `translate()` gains a loss kind, `'core-only'` (`TranslationLossKind`). Upstream and the engine
35
+ read `swap morph of #t with it` as an exchange with a property named `morph`, so core's morph
36
+ strategies, `none`, and a strategy with no content are now refused in every language. Before,
37
+ English wrote them as they were read. Core's `swap into #t with x` and `swap over #t with x` are its
38
+ innerHTML and outerHTML, and they are now written as `put x into #t` and `put x into #t's
39
+ outerHTML`. Before, English wrote `swap into of #t with x`, which runs as an exchange.
40
+ - **`@lokascript/semantic` writes a word in the target language only where that language's reader
41
+ brings it back.** A property name is localized only through the reader's property table or as a
42
+ keyword. So `add .foo to my children` keeps `children` in Spanish (`mi children`), where it wrote
43
+ `mi hijos` and read back a property named `hijos`. A call's arguments are written as written
44
+ (`call sprayInto(me)` wrote `sprayInto(yo)`). So is every bracket group inside an expression:
45
+ `2 is in [true, n]` wrote `[verdadero, n]` in Spanish, a variable no reader de-localizes. And so is a
46
+ type name after `am a`. These spots now show English words in foreign renders where they used to
47
+ show words that did not come back.
48
+ - **`@lokascript/semantic`: `innerHTML` and `outerHTML` are no longer English keywords.** A swap's
49
+ strategy slot still reads them, as words.
50
+
51
+ ### Fixed
52
+
53
+ - **`@lokascript/semantic`: Turkish keeps an `if` condition whole before a command with no target.**
54
+ `if 2 is in arr add .a end` read back as `if 2 add .a to is in arr end`, with any operand, and so
55
+ did `is a Number` and `<` after a possessive. Turkish case markers may be dropped, and the marker
56
+ test counted a marker word anywhere in the value: `in` (also the operator), `a` (also the article),
57
+ `nin` (also the possessive). The marker must now be the last token of its group.
58
+ - **`@lokascript/semantic`: a number with a CSS unit is one value.** Upstream reads `100px`,
59
+ `1.5rem` and `50%` as one string. Every tokenizer split them, so `set my *width to 100px` and
60
+ `transition height to 100px` were refused in all 24 languages. A `%` that an operand touches
61
+ (`5%2`) or that is spaced still reads as the operator.
62
+ - **`@lokascript/semantic`: more English forms that were refused in every language now read.**
63
+ `set innerHTML of #d1 to …` and `increment innerHTML of #d1`; a single-quoted string that holds a
64
+ `"` (`set #t to '<div _="…">'`), which was written back in double quotes and so closed early;
65
+ `fetch … don't throw`; `put … at the start of` / `at the end of`, where the `end` closed the
66
+ handler; `remove 3 from :arr` and `remove {color} from me`; `otherwise` as `else`. In a translation
67
+ of an empty then-branch's `otherwise`, 13 languages wrote their own word for it inside the
68
+ condition and read it back there, silently.
69
+ - **`@lokascript/semantic`: a handler's `catch` and `finally` bodies are written in upstream's
70
+ spelling.** `catch e prepend …` kept core's `prepend`, which the engine rejects.
71
+
72
+ ## [4.2.0] - 2026-10-07
73
+
74
+ A translation that would lose part of a script is now refused instead of returned short.
75
+ `@lokascript/semantic`'s `translate()` throws a `LossyTranslationError` unless the caller passes
76
+ `{ lossy: 'allow' }`, and the adapter, the Vite plugin's bundle and `@hyperscript-tools/i18n` keep
77
+ the author's text and warn. Upstream forms that translations used to drop now read whole in every
78
+ language: scripts of several features, `catch` / `finally`, conditionals and loops, `bind … and`,
79
+ scoped names such as `element x`, what `on` reads after its event, and clauses a command's
80
+ pattern does not model. On the new command-shape gate, round trips that came back silently
81
+ different fell from 7,614 at its first run to 174 (none in English); 23,763 of 27,240 now read as
82
+ their source does, and 3,303 are refused.
83
+
84
+ ### Added
85
+
86
+ - **`@lokascript/semantic` keeps a clause its pattern does not model, as written.** A command's
87
+ pattern read the start of the command and dropped the rest:
88
+ `add .foo to .bar when it matches .doh`, `toggle between .a and .b`,
89
+ `take .foo from .div for #d3`, `render #tmpl into #target`, `log me, my`. The rest is now
90
+ kept on the node as `verbatimClause` and written back after the command in every language, in
91
+ SOV and VSO word order too (ja `クリック を で .bar に .rey を 追加 when it matches .doh` reads
92
+ back as `on click add .rey to .bar when it matches .doh`). So a foreign render can now hold a
93
+ clause in English. A run that holds a command verb, a structure word (`else`, `catch`, `on`, …)
94
+ or a word of the source language (es `cuando`) is not a clause, and its translation is
95
+ refused. The parser records a `verbatim-clause` warning for each kept clause, since no role
96
+ reads it: `@lokascript/compilation-service` reports it as `UNCONSUMED_INPUT` with its repair
97
+ hint, and MCP `validate_hyperscript` lists it with the unconsumed tokens. A pattern group that
98
+ reads its marker and binds no role now gives the marker back, so
99
+ `take .foo from .div for #d3` keeps its `for` and `transition *width from 0px to 100px` its
100
+ `from`.
101
+ - **`@lokascript/semantic` keeps what upstream's `on` reads between the event and the body.**
102
+ `on click elsewhere`, a count (`on click 1`, `on click 1 to 2`), `on mutation of attributes`,
103
+ `on click in #d1`, `on foo queue first` and `on x having threshold 0.1` lost everything after
104
+ the event, so the handler ran on every event, or with a count, on the wrong ones. The run is now
105
+ kept as `EventHandlerSemanticNode.headClause`, written after the whole head in every language
106
+ (ja `クリック を で elsewhere .clicked を 追加`) and reported with the same `verbatim-clause`
107
+ warning. Outside English a count is read only in the `1 to 2` form, since a body there can open
108
+ with a number: a bare count in another language (`on click 1`) is refused.
109
+ - **`@lokascript/semantic` reads `catch` and `finally` in handlers and functions.** In a `def`, the
110
+ clauses ran as more of the function's body, so its error handling ran every time, in silence. In
111
+ a handler, `catch e …` was left unread. `EventHandlerSemanticNode` and `DefSemanticNode` now
112
+ carry `catchName`, `catchBody` and `finallyBody`. A `def` writes its clauses before its `end`,
113
+ and a handler writes them after its commands. No language has its own words for them yet, so
114
+ every language writes English's `catch` and `finally`. `wait a tick` now reads as `wait 0ms`, as
115
+ the engine runs it; every render wrote `wait tick`, a wait on a variable named `tick`.
116
+ - **`@hyperfixi/testing-framework`: the command-shape gate.** It takes the 1,135 scripts in
117
+ upstream _hyperscript's own test suite (0.9.93) and in core's reference and hover examples that
118
+ the engine reads, translates each into the other 23 languages and back (and English to English),
119
+ and has the engine parse the result and the source. A pair passes when the two parses match,
120
+ after a short list of named equivalences that are each checked on both engines. Otherwise it is
121
+ refused (`translate` threw) or silent. The baseline lists the refused and silent pairs and only
122
+ shrinks: the gate fails on a pair worse than listed and on one better. Every silent pair left
123
+ belongs to a named family with a reason (`SILENT_FAMILIES`), and none is in English.
124
+ - **`@hyperfixi/engine`'s parse keeps two values it held only in a closure.** `GoNode.url` holds
125
+ the address of `go to url <address>`, and `SymbolNode.on` the element of `^name on <element>`.
126
+ Two scripts that differed only there parsed to the same tree. Nothing reads them at run time.
127
+
128
+ ### Changed
129
+
130
+ - **`@lokascript/semantic`'s `translate()` refuses a translation that would lose part of the
131
+ script.** This is a change in behavior. It used to return what it could render:
132
+ `log it?.dataset?.customValue` came back in Spanish as `registrar ello`, and Hindi wrote
133
+ `put it.name into #r` in a form that reads back as `on its.name put #r into me`. It now throws a
134
+ `LossyTranslationError` carrying `partial` (what it would have returned, not the whole script),
135
+ `loss.kind` and `loss.lost` (what it drops). It refuses when the parse leaves input unread
136
+ (`truncation`); when the output does not read back with the input's commands, roles and `put`
137
+ positions (`read-back`), as when a foreign `wait for <event>` reads back as a time wait; and when
138
+ a string, number, selector, attribute, style or `$` / `:` / `^` name of the input is missing from
139
+ the output (`invariant`). For example,
140
+ `translate('on click ask "Name?" then put it into me', 'en', 'es')` throws with `loss.lost`
141
+ `['ask "Name?"']`, since `ask` has no schema yet. Pass `{ lossy: 'allow' }` as the fourth
142
+ argument to get the partial output instead. The root and
143
+ `@lokascript/semantic/core` export `LossyTranslationError`, `findTranslationLoss` (the same
144
+ checks on a parse and its render) and the types `TranslateOptions`, `TranslationLoss` and
145
+ `TranslationLossKind`. Comments (`--`, `//`) and Bengali's polite verb forms (`টগল করুন`) are now
146
+ read whole, so a correct translation that holds them is not refused.
147
+ `@hyperfixi/core/multilingual`'s `translate` takes the same options and rethrows the refusal. It
148
+ used to return the input unchanged on any error; input that does not parse still comes back
149
+ unchanged.
150
+ - **`@lokascript/hyperscript-adapter`, `@hyperfixi/vite-plugin`'s bundle and
151
+ `@hyperscript-tools/i18n` keep the author's text when a translation would lose part of it.**
152
+ This is a change in behavior. The adapter's full, slim and lite plugins ran the partial English:
153
+ `al clic alternar .foo cuando .bar` ran as `on click toggle .foo`. They now leave the script as
154
+ written, so the host reports a parse error naming code the author wrote, and warn once per
155
+ language with what would be dropped (`cuando .bar`). `preprocess()`'s config takes `onLossy`,
156
+ which is called with the refusal. The Vite plugin's source transform does the same, and its
157
+ `translateHyperscript` rethrows the refusal, where it returned the code unchanged on any error.
158
+ `@hyperscript-tools/i18n`'s lenient mode kept the source text in silence. Now `translateHtml`
159
+ reports a refusal through `onRefused` (`'warn'`, the default, `'error'`, or a callback), the
160
+ Eleventy filters take `'warn'` or `'error'`, and the CLI names the file.
161
+ - **`@hyperfixi/mcp-server`'s translate tools report a refusal.** `translate_code` returns
162
+ `ok: false` with a `LOSSY_TRANSLATION` diagnostic and `loss.lost`, and the partial text under
163
+ `partial`, never `code`. `translate_hyperscript` and `translate_to_english` return
164
+ `refused: true` with `lost` and `partial`. `translate_hyperscript` used to report every error as
165
+ a missing package.
166
+
167
+ ### Fixed
168
+
169
+ - **`@lokascript/semantic`: a script of several features reads as one program.** Upstream reads
170
+ each top-level feature on its own. Semantic kept only the first and dropped the rest, or chained
171
+ them with `then`, which neither engine reads: `def a … end def b … end` became one function,
172
+ `def … end on click …` lost its handler, and `bind … end live …` and `set :x to 1 on click …`
173
+ came out joined by `then`. A handler, a `def`, a `behavior`, a feature block, or a top-level
174
+ `bind`, `set`, `install` or `js` now starts a new part of the program, and a top-level `init`
175
+ parses, as a feature block (`FeatureAction` gains `'init'`). English writes one feature per
176
+ line and closes each handler with `end`. It writes a `bind`, `set` or `install` with the `end`
177
+ upstream allows when the next feature has no head word to split at, and joins consecutive
178
+ `install` and `bind` features with a space, never `then`, which the engine rejects.
179
+ - **`@lokascript/semantic`: a dotted name is one name, and a behavior's `end`s are optional.**
180
+ A dotted name is one name: `def utils.foo()` defined `utils` and left `.foo ( )` unread, and
181
+ `behavior App.Widgets.Clickable` did not parse as a behavior. A behavior's last handler needs no
182
+ `end`, nor does the behavior at the end of input: `behavior B(x) on click set @out to x` read as
183
+ a `behavior` command chained to a `set`. A trailing `init` block in such a behavior is its init.
184
+ A behavior member that is neither a handler nor `init`
185
+ (`behavior MarkIt set @data-marked to 'yes' end`) came out as `behavior MarkIt then set …`,
186
+ which the engine rejects; semantic does not model such members yet, so the translation is now
187
+ refused. So is `behavior A … behavior B …` with no `end` between them: upstream reads B as part
188
+ of A, and semantic read it as a `behavior` command in A's last handler.
189
+ - **`@lokascript/semantic`: `bind … and` / `with` and `install`'s arguments read.** Upstream reads
190
+ `bind <left> and|with|to <right>` as the same binding. Only `to` had a pattern, and its left
191
+ side took only a variable, so twelve of upstream's test scripts did not parse, among them
192
+ `bind .dark and $darkMode` and `bind my value and #slider's value`. The left side now takes
193
+ selectors and property paths too. `install Toggleable(cls: 'highlighted')`, the documented
194
+ behaviors usage, lost its arguments: an install's parentheses are arguments, not a signature.
195
+ - **`@lokascript/semantic`: conditionals and loops read whole.** An English pattern read every
196
+ top-level `if X …` as a handler for an event named `X`, so `if x log 1 else log 2 end` became
197
+ `on x log 1 then log 2`. Seven languages read their own `if` the same way (es and fr `si`, pt
198
+ `se`, zh `如果`, sw `kama` / `ikiwa`, id `jika` / `kalau` / `bila`, de `falls`), so every
199
+ conditional rendered in them read back as a handler. A bare `if … end` is now a conditional; de
200
+ `wenn` and `sobald` and id `apabila` still head a handler. An `if` on the line after `else`
201
+ opens its own block, as upstream reads it. Semantic chained the two, so the commands after the
202
+ inner `end` ran whether or not the condition held. Tokens that open a line now carry
203
+ `metadata.lineStart`, and English writes such an `if` on its own line. The `end` of
204
+ `at end of` no longer closes a conditional: in
205
+ `if x put 'a' at end of me end put 'b' at end of me`, the second `put` ran inside the branch. An
206
+ empty block (`if x then end`) stays a conditional, and a condition ends at `else`. `break` and
207
+ `continue` read. A loop whose test follows its body (`repeat … until x end`,
208
+ `repeat … while x end`) is read as one (`LoopSemanticNode.bottomTested`) and written
209
+ `repeat forever … until x end`, which the engine reads as the same loop; before, `repeat set x`
210
+ read as `repeat x times`. `repeat in X` keeps its collection, and `indexed by i` reads as
211
+ `index i`.
212
+ - **`@lokascript/semantic`: a name with its scope is one value, in every language.** Upstream reads
213
+ `global x`, `element x`, `element's x` (and `the element's x`), `local x` and `dom x` as one
214
+ variable, its idiom for a behavior's state; seven of `@hyperfixi/behaviors`' sources use it.
215
+ Semantic read the scope word alone, so the command around it was dropped, in English and in
216
+ every language: `set element x to 10`, `set global x to 10`, `init set dom count to 42`. The
217
+ tokenizers now read the scope word and the name as one token, and every language writes it as
218
+ written, as it writes `$` and `:` names (es `establecer element x a 10`). A scope word alone
219
+ (`set element to 5`) is still a word.
220
+ - **`@lokascript/semantic`: English writes upstream's postfix `unless` and drops `open`'s mode.**
221
+ `toggle .foo unless I match .bar` was written `toggle .foo then unless I match .bar`, and core's
222
+ prefix `unless C X` as it is; upstream rejects both. English now writes the guard after its
223
+ command, and other languages keep their own form. Upstream's `open` is modal, and the engine
224
+ reads `open #d as non-modal` as an expression, `(#d as non) - modal`. English now writes it
225
+ `call #d.show()`, and `open #d as modal` as `open #d`. The reader still accepts core's mode.
226
+ - **`@lokascript/semantic`: a value keeps its spelling.** A call's arguments keep their spacing:
227
+ `call navigator.clipboard.writeText(#input's value)` became `…(#input'svalue)`. A style block
228
+ kept a space between every token, so `font-family` became `font - family`, a subtraction.
229
+ `show`'s strategy argument is read raw, as upstream reads it: `display:inline-block` became
230
+ `display:inline - block`. A swap's strategy slot takes only core's strategy words (`into`,
231
+ `over`, `innerHTML`, …, `morph`), so `swap arr[0] with arr[2]` exchanges two values, as
232
+ upstream reads it; it was written `swap arr of [0] …`. An attribute with its value and a name
233
+ with its index are one value when nothing spaces them: `add @data-foo=baz` and
234
+ `remove :arr[1]` kept only their first part, and `increment arr[1]` read as
235
+ `increment arr by [1]`. Every language writes them as written. The words inside an index are
236
+ the script's: bn wrote the variable `index` in `var[(index-1)..(index+1)]` as `সূচক`.
237
+ - **`@lokascript/semantic`: events, waits and amounts keep what was written.** A quoted event name
238
+ stays quoted (`LiteralValue.quoted`): `trigger "my event"` now round-trips in all 23 languages,
239
+ and `send "hello" to ChatSocket` keeps its quotes. A spaced time unit is read with its number:
240
+ `wait 2 seconds` translated as `wait 2`, which is two milliseconds. A written `by 1` stays and
241
+ an implicit one is never shown: German wrote `um 1` for every increment, and English dropped
242
+ every written `by 1`. `measure` reads its element: `measure #other` became `measure`.
243
+ - **`@hyperfixi/core`'s reference and hover docs teach what the 4.x engine does.** `open`'s
244
+ `as modal` / `as non-modal` mode does nothing on the engine, so the docs drop it and give
245
+ `call #myDialog.show()` for a non-modal dialog. The `swap` example
246
+ `swap innerHTML of #target with result` was core's `put`, but upstream and the engine exchange
247
+ the two values: it is now `swap #a's value with #b's value`, and the hover text reads "Exchanges
248
+ two elements, or two writable values." The `fetch` example is now a template literal,
249
+ ``fetch `/api/${id}` as json``, since upstream sends a naked URL's `${id}` as written.
250
+ Translation keeps core's meaning for both old forms: English writes `put result into #target`
251
+ and a template literal. The Japanese example in `docs/EXAMPLES.md` is written as semantic writes
252
+ it, `クリック で 自分 に .active を 切り替え`; the old one left `を 私` unread.
253
+
10
254
  ## [4.1.0] - 2026-10-06
11
255
 
12
256
  Translations keep what the source says. `tell` blocks, swaps, view transitions, chains of
@@ -1241,7 +1485,8 @@ _Synchronized version release. See git history for details._
1241
1485
  - npm access token stored in GitHub Secrets
1242
1486
  - 2FA recommended for npm organization
1243
1487
 
1244
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...HEAD
1488
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...HEAD
1489
+ [4.2.0]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...v4.2.0
1245
1490
  [4.1.0]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...v4.1.0
1246
1491
  [4.0.1]: https://github.com/codetalcott/hyperfixi/compare/v4.0.0...v4.0.1
1247
1492
  [4.0.0]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...v4.0.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