@hyperfixi/testing-framework 4.0.0 → 4.1.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,186 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.0] - 2026-10-06
11
+
12
+ Translations keep what the source says. `tell` blocks, swaps, view transitions, chains of
13
+ handlers without their `end`s, events with names of several words, and variables named like
14
+ events or keywords now survive `translate`, MCP `translate_code` and the adapter in every
15
+ language. The adapter's per-language bundles render English with semantic's renderer, and
16
+ `@lokascript/semantic/core` gains `registerHandcrafted`.
17
+
18
+ ### Added
19
+
20
+ - **`@lokascript/semantic` reads upstream's `start view transition … end`, in all 24 languages.**
21
+ `on click start view transition swap #a with #b end` parsed as a `transition` command whose
22
+ patient was `swap`, so `translate`, MCP `translate_code` and the corpus lost the block and its
23
+ body (`on click transition swap`), and validation reported the rest as unconsumed input. The
24
+ block is now a `viewTransition` command with its `body` (`ViewTransitionSemanticNode`;
25
+ `isViewTransitionBlock` from the package root), nested the way a loop's body is: a command after
26
+ the block's `end` stays after it, and the `end` no longer closes the handler, behavior or `def`
27
+ around it. `start [a] view transition [using "<type>"]` is written in English in every language,
28
+ as core's `using view transition` tail already is (a browser API's name, which no profile
29
+ translates). The corpus row `swap-view-transition` is now written this way and parses on both
30
+ engines.
31
+
32
+ ### Changed
33
+
34
+ - **Single-language bundles hold only their own language's hand-crafted patterns.** Each
35
+ command's pattern dispatcher in `@lokascript/semantic` named all 24 languages' hand-crafted
36
+ patterns, so every bundle that could build patterns held all of them (~200 KB minified). Each
37
+ language module now registers its own (`src/patterns/handcrafted/<lang>.ts`, through
38
+ `@lokascript/semantic/core`'s new `registerHandcrafted`), and bundlers drop the rest:
39
+ semantic's single-language bundles are 14–15 KB gzipped smaller (`browser-de` 116 KB instead
40
+ of 130), and a Vite or Rollup build that imports `@lokascript/semantic/core` and one
41
+ `languages/<lang>` gets the same saving. The patterns each language builds are unchanged
42
+ (hashed for all 24 before and after, from source and from the split `dist/`). The adapter's
43
+ bundles, which also changed renderer in this release, are measured in its entry under Fixed.
44
+
45
+ ### Fixed
46
+
47
+ - **`@lokascript/semantic`: a variable named like an event or a keyword keeps its name.** Upstream
48
+ and the engine run `set input to "a"` on a variable named `input`, but semantic's English parse
49
+ dropped the whole command (an event name alone took the value as the text "input", which a
50
+ `set` cannot write), so `translate`, MCP `translate_code` and the adapter lost it in every
51
+ language; `put "a" into input` was lost in ar, de, fr, id and zh, whose tokenizers read `input`
52
+ as the event, and `toggle .a on input` in six. An event name alone where a value stands is now
53
+ the variable it spells, as a structure word or a verb already was, except where an event stands
54
+ (`send`, `trigger`, `repeat until event`, `wait`). And the renderer no longer writes a variable
55
+ in the language's own word where that word reads back as another variable: `set when to 1`
56
+ wrote es `establecer cuando a 1`, read back as a variable named `cuando`, in all 23 languages,
57
+ and bn, ms, th and tl wrote `increment i by index` with their word for `index`. A variable keeps
58
+ its localized word only where that word alone is the same value (es `ello` for `it`), and the
59
+ verified render spells one whose neighbours change it (vi `đặt giá trị`, `set`, for `put value`).
60
+ The value matrix now runs ten such names (`input`, `click`, `when`, `index`, …) in every
61
+ position: all pass on both engines. No corpus row changes.
62
+ - **`@lokascript/semantic`: the event a command names reads back in every language, and a
63
+ `wait` keeps its source for any event.** The handler head read a native event name through the
64
+ event table, but `send`, `trigger` and `repeat until event` read it only when the language's
65
+ tokenizer knew the word: es `enviar dobleclic a #x` read back `send dobleclic to #x`, so the
66
+ event fired as `dobleclic` (55 of the 170 localized names per command: es, pt, fr, de, ko and zh
67
+ 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
68
+ event only from a fixed list, in English first: `wait for myEvent from #b`,
69
+ `wait for unload from #b` and `wait for focusin from window` lost the source in every language, and
70
+ `wait for click or myEvent from #b` everything after `click`. Upstream reads every leg of
71
+ `wait for` that is not a number as an event, so English now does, and a name followed by a
72
+ source is an event in every language (a time wait has none); tr and tl no longer read `focusin`
73
+ as `focus`. bn writes the event `send` sends with `কে`, as `trigger`'s, since
74
+ `#x তে স্ক্রোল তে পাঠান` read as `scroll to #x`; the old `তে` still reads. And fr's `change`
75
+ event, `changement`, no longer reads as the reactive `when … changes` word, which turned
76
+ `repeat until event change` into a `when` feature. Four corpus rows change, the bn renders of
77
+ `send` (`কে` for `তে`).
78
+ - **`@lokascript/semantic`: a hand-written event name of several words is read whole.** The event
79
+ table reads 20 names of several words that the renderer never writes (ar `تمرير الماوس`, de
80
+ `taste runter`, id `lepas tombol`, pt `pressionar tecla`, qu `mana q'away`, sw `bonyeza chini`,
81
+ tr `fare_bas`, zh `鼠标进入`, and twelve more), but their tokenizers split them, and the reader took
82
+ the first word for the event: de `wenn taste runter protokollieren 1` read `on taste log 1`, id
83
+ `ketika lepas tombol …` `on lepas`, sw `unapo bonyeza chini …` `on click`, qu
84
+ `maykama mana q'away …` `on false`, and ar `على تمرير الماوس من #b …` `on scroll`. A `wait for`,
85
+ `send`, `trigger` or `repeat until event` lost the event the same way. The tokenizers now read
86
+ each name whole; tr reads an `_`-joined word as one token when it is a keyword, as sw already did
87
+ (its own `tuş_bas` had never been read), and splits any other at `_` as before. No corpus row
88
+ changes.
89
+ - **`@lokascript/semantic`: an event name of several words keeps what is written after it.** ar
90
+ writes keydown, keyup and resize as `ضغط المفتاح`, `رفع المفتاح` and `تغيير حجم`, id writes
91
+ keydown as `tekan tombol`, and vi writes resize as `đổi kích thước`, but each tokenizer read only
92
+ the first word as the event. So `on keyup from #b log 1` read back `on keyup log 1` in ar,
93
+ `on resize from window` read back `on change` in ar and vi, and `send keyup to #x`,
94
+ `trigger keydown on #x`, `repeat until event keyup from #b` and `wait for resize` lost their
95
+ `to`, `on` or `from`, or waited for `change`. `translate`, MCP `translate_code` and the adapter
96
+ carried the loss. The three tokenizers now read each name whole, as es, fr, pt and qu already
97
+ did. The corpus's ar and vi `window-resize` rows, which the parser's compound repair already
98
+ read right, now parse at full confidence (1.0, was 0.82); no other corpus row changes.
99
+ - **`@lokascript/semantic`: a behavior's handlers need no `end` of their own.** Upstream ends a
100
+ handler's commands at the next feature, so `behavior F on click add .a on keyup log 1 end end`
101
+ is two handlers. The behavior parser split its body only at `end`, read both handlers as one,
102
+ and English wrote `behavior F then add .a then log 1`, which the engine rejects: `translate`,
103
+ MCP `translate_code` and the adapter broke such a behavior in every language. Each piece now goes
104
+ through the splitter a top-level chain of handlers already used, and parses to the same node as
105
+ the form with every `end` written. No corpus row changes (every one writes its `end`s).
106
+ - **`@lokascript/semantic`: a chain of handlers written natively without their `end`s reads as
107
+ written, in every language.** In 9 languages such a chain merged into one handler, at top level
108
+ and inside a behavior, and the result was valid English that ran wrong: de, fr, id, zh, ko, qu
109
+ and tr dropped the second event and ran its commands on the first (`on click add .a then log
110
+ 1`), and hi and bn read it as a destination (`add .a to keyup`). The split knew a handler head
111
+ only as `on`'s forms and ja/ko's `を で` / `을 에`; it now reads each language's own: the `when`
112
+ word the renderer writes (de `wenn`, fr `quand`, id `ketika`, qu `maykama`), zh's `一 … 就`, and
113
+ the words after the event (ko `할 때`, tr `i üzerinde`). The same rule ends an `init` with no
114
+ `end` in SOV languages too, and he and vi no longer read a handler after `toggle .a on #x` as the
115
+ toggle's. hi `पर` and bn `তে` are also the destination marker, which almost every command there
116
+ may write first, so a split at one is never trusted: it reads as written, at a confidence below
117
+ the adapter's threshold, so the adapter leaves the script as written and the engine reports it.
118
+ No corpus row changes.
119
+ - **`@lokascript/semantic`: English writes core's view-transition tail as upstream's block.**
120
+ `swap #a with #b using view transition` (and `morph`'s tail) renders to English as
121
+ `start view transition swap #a with #b end`, which upstream _hyperscript and `@hyperfixi/engine`
122
+ both parse; the tail itself is rejected by both. Every language reads the tail, and
123
+ `@lokascript/hyperscript-adapter` hands the host the English render, so a page written in
124
+ another language with a view-transition swap failed to parse and ran nothing. Foreign renders
125
+ keep the tail.
126
+ - **`@lokascript/semantic`: a `swap` keeps both operands in every language.** Only a swap of two
127
+ selectors round-tripped. `swap #target with me`, `swap :x with :y` and a swap of properties
128
+ (`swap my textContent with #a`, `swap #a's textContent with #b's textContent`) lost an operand
129
+ or its property in up to 21 languages, so a translated page swapped the wrong thing or nothing.
130
+ Four causes: swap's roles took no reference or property path; a handler's repair re-parse was
131
+ vetoed when the swap's first operand moved to its real role; an optional slot that declined a
132
+ value by type still consumed it (any command's pattern could lose the next role that way); and
133
+ in pl/uk the with-word is also the of-word, so `swap el with #t` read as `#t's el`.
134
+ - **`@lokascript/semantic`: a `tell` block keeps its extent.** Semantic kept `tell` flat, its body
135
+ being every statement after it, so in English and every translation a command after the tell's
136
+ `end` was lost (`tell #modal show end then log 2` lost the `log`) or pulled inside it
137
+ (`… end log 2`), a nested tell took its parent's remaining commands, and in a behavior or `def`
138
+ the tell's `end` closed the enclosing block, dropping the next handler or command. The parser
139
+ now nests a tell's body as it nests a loop's (`BlockCommandSemanticNode`, `isBlockCommand`), and
140
+ reads an `end` as the tell's exactly when the engine does: before a new `on <event>`, `def`,
141
+ `init` or `behavior`, a tell needs none.
142
+ - **`@lokascript/hyperscript-adapter`'s per-language and regional bundles render English with
143
+ semantic's renderer.** They rendered with their own English writer, which saved English's
144
+ language data and had drifted far from semantic's: over the corpus's 3,772 translations, 1,242
145
+ rendered to valid English that did something else, and 345 to English the engine rejects (the
146
+ script then kept its author's text and did not run). Every `if` lost its branches,
147
+ `put … before`/`after` became `put … into`, `from window`, `from elsewhere`, `debounced` and
148
+ `or <event>` vanished, `<button/> in me` widened to the whole page, `repeat for x in …` lost
149
+ its binding, and every `swap #a with #b` was written `swap of #a with #b`. They now give exactly
150
+ what the full `hyperscript-i18n.global.js` gives. English's own module builds through
151
+ `@lokascript/semantic/core` (its pattern generator and repeat heads,
152
+ `getRepeatPatternsForLanguage` now exported there), so a bundle holds one copy. Against 4.0.1,
153
+ the single-language bundles are the same size (`-de` 116 KB gzipped) and `-en` is 109 KB
154
+ instead of 148; the regional bundles are 8–16 KB larger (`western` 141 KB instead of 125).
155
+
156
+ ## [4.0.1] - 2026-10-05
157
+
158
+ Fixes for 4.0.0: the per-language adapter bundles read every language again, and the MCP server's
159
+ documentation and core's metadata describe the 4.0 engine.
160
+
161
+ ### Fixed
162
+
163
+ - **`@lokascript/hyperscript-adapter`: the self-contained per-language and regional bundles read
164
+ German, French, Quechua and Chinese again.** `hyperscript-i18n-<lang>.global.js` and the regional
165
+ bundles installed a generate-only pattern generator over the one `@lokascript/semantic/core`
166
+ provides, discarding its hand-crafted patterns: in de, fr, qu and zh even
167
+ `on click toggle .active` stayed untranslated (the host then rejected it), and other languages
168
+ lost shapes such as `toggle … on #target`; 1106 of the corpus's 3772 translations read
169
+ differently from the full package. Published 3.x and 4.0.0 bundles alike; the all-languages
170
+ `hyperscript-i18n.global.js` was unaffected. The bundles now keep `/core`'s generator (sizes
171
+ unchanged: the ~23 KB 4.0.0 added was this generator, unused until now). Their English renderer
172
+ also drops only an implicit `me` now, so `put "x" into me` no longer renders as the
173
+ engine-invalid `put "x"` in every language. New gate: every built bundle runs translated
174
+ handlers on the engine (`test/adapter-iife.test.ts`).
175
+ - **`@hyperfixi/mcp-server`'s documentation resources teach upstream's spelling.**
176
+ `hyperscript://docs/commands`, `/expressions`, `/events` and `hyperscript://examples/common` still
177
+ taught core 3.x: `on submit.prevent`, `on input.debounce(300ms)`, `on keydown.enter`,
178
+ `swap #t innerHTML`, `while …`, a bare `[attr]` selector, a `parent` keyword, `you` as the event
179
+ target. The engine rejects those, or reads them as something else (`on click.prevent` listens for
180
+ an event named `click.prevent`). A test now parses every example on the engine. The repo's
181
+ `hyperfixi-developer` skill, generated from them, follows (`generate:skills` had been writing to
182
+ the wrong directory).
183
+ - **`@hyperfixi/core/metadata` describes 4.0.** `packageInfo.description` still said "Modern
184
+ hyperscript engine with fixi/htmx integration" and `compatibility` "~85% official _hyperscript",
185
+ both 3.x claims. `packageInfo.upstreamSuite` (`{ version, passed, total }`) now publishes the
186
+ engine's result on upstream _hyperscript's own test suite (1401 of 1467 in 0.9.93), and
187
+ `compatibility` is derived from it; `verify:reference` fails when it disagrees with
188
+ `packages/engine/upstream-suite/known-failures.json`. `ecosystem` drops "212 LLM examples".
189
+
10
190
  ## [4.0.0] - 2026-10-04
11
191
 
12
192
  `@hyperfixi/core`'s own hyperscript engine is gone: its root re-exports `@hyperfixi/engine`, and
@@ -1061,7 +1241,10 @@ _Synchronized version release. See git history for details._
1061
1241
  - npm access token stored in GitHub Secrets
1062
1242
  - 2FA recommended for npm organization
1063
1243
 
1064
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...HEAD
1244
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...HEAD
1245
+ [4.1.0]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...v4.1.0
1246
+ [4.0.1]: https://github.com/codetalcott/hyperfixi/compare/v4.0.0...v4.0.1
1247
+ [4.0.0]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...v4.0.0
1065
1248
  [3.3.0]: https://github.com/codetalcott/hyperfixi/compare/v3.2.0...v3.3.0
1066
1249
  [3.2.0]: https://github.com/codetalcott/hyperfixi/compare/v3.1.0...v3.2.0
1067
1250
  [3.1.0]: https://github.com/codetalcott/hyperfixi/compare/v3.0.0...v3.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Cross-platform behavior testing suite for LokaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -57,10 +57,10 @@
57
57
  "author": "LokaScript Contributors",
58
58
  "license": "MIT",
59
59
  "dependencies": {
60
- "@hyperfixi/patterns-reference": "^4.0.0",
61
- "@lokascript/compilation-service": "^4.0.0",
62
- "@lokascript/i18n": "^4.0.0",
63
- "@lokascript/semantic": "^4.0.0",
60
+ "@hyperfixi/patterns-reference": "^4.1.0",
61
+ "@lokascript/compilation-service": "^4.1.0",
62
+ "@lokascript/i18n": "^4.1.0",
63
+ "@lokascript/semantic": "^4.1.0",
64
64
  "diff": "^8.0.3",
65
65
  "esbuild": "^0.25.12",
66
66
  "happy-dom": "^20.14.5",
@@ -72,8 +72,8 @@
72
72
  "vite": "^8.3.1"
73
73
  },
74
74
  "devDependencies": {
75
- "@hyperfixi/engine": "^4.0.0",
76
- "@lokascript/hyperscript-adapter": "^4.0.0",
75
+ "@hyperfixi/engine": "^4.1.0",
76
+ "@lokascript/hyperscript-adapter": "^4.1.0",
77
77
  "@types/diff": "^5.0.0",
78
78
  "@types/node": "^26.6.3",
79
79
  "@vitest/coverage-v8": "^5.0.0",
@@ -4,7 +4,7 @@
4
4
  * leaves out; the matrix shards run the names themselves.
5
5
  */
6
6
  import { describe, it, expect } from 'vitest';
7
- import { collidingNames, generateCells } from './value-matrix';
7
+ import { KEYWORD_NAMES, collidingNames, generateCells } from './value-matrix';
8
8
 
9
9
  const names = new Map(collidingNames().map(entry => [entry.name, entry]));
10
10
 
@@ -29,6 +29,16 @@ describe('value matrix: colliding names', () => {
29
29
  expect(names.has('an')).toBe(true);
30
30
  });
31
31
 
32
+ // Upstream runs each as a variable; English's own parse dropped `set input
33
+ // to "a"`, and every language wrote `when` as its keyword (P49).
34
+ it('takes the English words upstream runs as variables (KEYWORD_NAMES)', () => {
35
+ for (const name of KEYWORD_NAMES) expect(names.has(name), name).toBe(true);
36
+ expect(KEYWORD_NAMES).toContain('input'); // an event name
37
+ expect(KEYWORD_NAMES).toContain('when'); // a structure word
38
+ expect(KEYWORD_NAMES).toContain('index'); // a value-lexicon word (P28)
39
+ expect(names.has('if')).toBe(false); // P53
40
+ });
41
+
32
42
  it("leaves out upstream's `no` and the templates' variables", () => {
33
43
  expect(names.has('no')).toBe(false);
34
44
  expect(names.has('i')).toBe(false);
@@ -550,6 +550,30 @@ let collidingNamesMemo: readonly CollidingName[] | undefined;
550
550
  */
551
551
  const CODE_NAMES: readonly string[] = ['userData'];
552
552
 
553
+ /**
554
+ * Names spelled like English vocabulary that upstream and the engine run as
555
+ * variables where a value stands: event names, which every tokenizer reads as
556
+ * an event (`set input to "a"` dropped from English's own parse), structure
557
+ * words (`when`, which every language wrote as its keyword), and words of the
558
+ * value lexicon (`index`, which bn and four others wrote natively) — P49, P28.
559
+ * Not a name the window already holds (`focus`, `length`), nor `then`: the
560
+ * fixture sets each name there, and a `then` on the window makes upstream
561
+ * take it for a promise. Not yet `if` or `end`, which open and close a block
562
+ * in every language's reader (OPEN_ITEMS P53: 93 and 505 failing pairs).
563
+ */
564
+ export const KEYWORD_NAMES: readonly string[] = [
565
+ 'click',
566
+ 'input',
567
+ 'change',
568
+ 'keyup',
569
+ 'submit',
570
+ 'load',
571
+ 'drop',
572
+ 'when',
573
+ 'index',
574
+ 'value',
575
+ ];
576
+
553
577
  /**
554
578
  * Variables spelled like a structure word of a language the matrix translates
555
579
  * into: every one- or two-letter name semantic's `nameCollision` finds
@@ -580,14 +604,15 @@ export function collidingNames(): readonly CollidingName[] {
580
604
  const tokens = tokenize(word, 'en').tokens;
581
605
  return tokens.length === 1 && tokens[0]?.kind !== 'identifier';
582
606
  };
583
- collidingNamesMemo = candidates
584
- .filter(word => !reserved.has(word) && !englishKeyword(word))
607
+ collidingNamesMemo = [...new Set([...candidates, ...KEYWORD_NAMES])]
608
+ .filter(word => !reserved.has(word) && (KEYWORD_NAMES.includes(word) || !englishKeyword(word)))
585
609
  .flatMap(name => {
586
610
  const readings = FOREIGN_LANGUAGES.map(language => ({
587
611
  language,
588
612
  collision: nameCollision(name, language),
589
613
  }));
590
- if (!readings.some(reading => reading.collision === 'structure')) return [];
614
+ const structure = readings.some(reading => reading.collision === 'structure');
615
+ if (!structure && !KEYWORD_NAMES.includes(name)) return [];
591
616
  const pronounIn = readings.filter(r => r.collision === 'pronoun').map(r => r.language);
592
617
  return [{ name, pronounIn }];
593
618
  });