@hyperfixi/testing-framework 4.5.0 → 4.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,131 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.6.0] - 2026-10-15
11
+
12
+ ### Added
13
+
14
+ - **`@lokascript/semantic`: `ask` and `answer`** in every language's own word (es `preguntar` /
15
+ `responder`, ja `尋ねる` / `答える`, de `frage` / `antworte`): upstream's two dialog commands
16
+ (`ask "Name?"` prompts; `answer "Save?" with "Yes" or "No"` confirms, the choices after the language's
17
+ `with`). Both were refused in every language. Listed for native review in
18
+ `packages/semantic/NATIVE_REVIEW_NEEDED.md`.
19
+ - **`@lokascript/semantic`: `scroll … by` with a direction** in every language's own words
20
+ (`scroll #box down by 200px`: es `desplazamiento #box abajo por 200px`, ja `#boxを 下 200px スクロール`,
21
+ ru `прокрутить #box вниз 200px`). Upstream's relative scroll was refused in every language: no
22
+ direction had a word, and scroll's schema modeled `scroll to <el>` alone. Listed for native review in
23
+ `packages/semantic/NATIVE_REVIEW_NEEDED.md`.
24
+
25
+ ### Changed
26
+
27
+ - **`@lokascript/framework`: a domain DSL's parse refuses input it did not read.**
28
+ `createMultilingualDSL`'s `parse` (and so `compile`, `validate`, `translate`) tries every pattern and
29
+ takes the best match that reads the WHOLE input: highest priority, then confidence. When the only
30
+ matches leave input unread, it throws `UnreadInputError` (exported; `code: 'unread-input'`, with the
31
+ longest match's `reading` and the `unread` text). It used to return the first match however much it
32
+ read, so a trailing word was dropped without a word: the domain gate (lokascript-domains) found
33
+ 1,744 of 1,744 renders accepted with an unknown word after them, and `ask what is 2+2` asked the
34
+ model "what". Against the gate (domains 4.0.0, measured unpublished with `gate:upstream`): 1,527
35
+ cases better, 62 worse, every one `pass → refused` (a value such as `click`'s that the generator
36
+ ignored, now refused as unread rather than dropped); no case became silent, and no accepted parse
37
+ changed its reading. A multi-word llm question is quoted: `ask "what is 2+2"`.
38
+ - **`@lokascript/framework`: a domain DSL's `translate` refuses a rendering that does not read
39
+ back.** It reads its output in the target language and requires the source command: the same
40
+ action and role values, or the same compiled code. Otherwise it throws `TranslationReadBackError`
41
+ (exported; the text under `partial`, what it reads as under `reading`), unless `{ lossy: 'allow' }`,
42
+ as semantic's `translate`. `DomainRegistry`'s tools (the MCP `parse_` / `compile_` / `validate_` /
43
+ `translate_<domain>` tools) report refusals as `ok: false` with an `UNREAD_INPUT` or
44
+ `TRANSLATION_DOES_NOT_READ_BACK` diagnostic instead of a generic tool error, and `translate_<domain>`
45
+ reads its own rendering back the same way. Translating every domain gate command from English: 46
46
+ renders that used to be returned are now refused (45 do not parse in the target language).
47
+ - **`@lokascript/semantic`: the parser is split into modules.** `semantic-parser.ts` went from 9,680
48
+ lines to 751, across 22 modules under `parser/`, and each question about an end word (does it close a
49
+ block, does it stop a clause) has one reader. Each step was checked against 27,194 parses; the steps
50
+ that change a reading are listed under Fixed. `SemanticParseError` keeps its import path.
51
+
52
+ ### Fixed
53
+
54
+ - **`@lokascript/semantic`: a handler's count** (`on click 1 …`, the first click only; `on click 2 …`)
55
+ is read in every language. The renders already wrote it (es `al clic 1 ocultar`), but every reader
56
+ outside English dropped it, so the translation was refused in all 23 languages. Bengali, Italian and
57
+ Thai also read every clause between a handler's event and its body (`on click elsewhere`,
58
+ `on mutation of attributes`, `on foo queue first`), which their head pattern skipped. In the command-shape
59
+ gate, 375 lanes that were refused now pass.
60
+ - **`@lokascript/semantic`: a query of a pseudo-class alone** (`<:checked/>`, `<:not(.done)/>`) reads
61
+ as a query in every language, English too. Every tokenizer read `<` and then a local variable
62
+ `:checked`, so a translation was refused. In the command-shape gate, 161 lanes that were refused now
63
+ pass.
64
+ - **`@lokascript/semantic`: value spellings from upstream's own tests** read in every language,
65
+ English too: an escaped class name (`.group-\[:nth-of-type\(3\)_\&\]:block`), a query of several
66
+ selectors (`<#d1, #d2/>`), a unit after parentheses (`(startWidth)px`), `[@data-foo]` and an
67
+ attribute after a pronoun (`set it @disabled to true`, written `its @disabled`), a dotted property
68
+ before `of` (`set parentNode.innerHTML of #d1 to "foo"`), and `set {bar: 2, baz: 3} on obj`. Each was
69
+ refused. `get` no longer has a destination slot, which no reader used and which took Italian's `in`:
70
+ `get #p1 in me` read back as `get #p1 on me` in Italian, and `get result as JSONString` was refused
71
+ in Quechua. In the command-shape gate, 202 lanes are better, 3 of them silent before.
72
+ - **`@lokascript/semantic`: upstream's collection expressions** read in English and translate:
73
+ `set :items to <span/> in #box where it matches .a`,
74
+ `put matches mapped to its textContent into #out`, `repeat for x in :items where x.val > 10 …`. They were refused in every language, English
75
+ too, or kept as an English clause after the command. A translation writes the language's own `where`,
76
+ as it already did in a condition (es `donde`, de `wo`), and keeps `mapped to` in English until native
77
+ review. Also read now: an element-scoped variable (`:items`) inside a longer value, the article in
78
+ `in the closest <table/>` and `checked of the :boxes`, and zh/tl's `no` before a sigil variable. Quechua and
79
+ Vietnamese read their own `where` back (it read as `target`, or split at its `_`), which also clears a
80
+ silent pair in each. In the command-shape gate, 154 lanes are better, 2 of them silent before.
81
+ - **`@lokascript/semantic`: a query's scope inside a phrase** reads in English and translates:
82
+ `put the length of <:checked/> in #box into me` (the value ended at the query and `in` was taken as
83
+ put's marker), `bind $n to the length of <:checked/> in the next <tbody/> …` (a positional scope),
84
+ `put <span/> in #box where it matches .a into me` (English came back silently as `… into #box …`), and
85
+ `put the length of <p/> into #out` and `put 'x' into the next <div/>'s textContent`, which the lossy
86
+ check refused because it read the `'s` after `<p/>` as a quote. In the command-shape gate, 63 lanes are
87
+ better, 1 of them silent before.
88
+ - **`@lokascript/semantic`: no translation reads back as other English without saying so.** The
89
+ command-shape gate's last five silent pairs read back as their source: pl's `closest .bar to .foo`
90
+ (pl's own `to` is "it"), `closest @foo` in pl and sw, a Quechua chain of `'s` (`$data's inner's val`,
91
+ read as a string), and Indonesian's `saya punya @data-x`. English `set closest @foo to "doh"` and
92
+ `put closest @foo into me`, refused in all 24 languages, read now too.
93
+ - **`@lokascript/semantic`: commands that semantic already models, refused in some languages,** read
94
+ and translate. In the command-shape gate, 168 lanes that were refused now pass; with the rows above,
95
+ the refused lanes of shapes semantic models went from 911 to 86, and the gate reads 26,531 pass,
96
+ 709 refused and 0 silent. The commands:
97
+ - `fetch $u as JSON`, with a `$` or `:` variable as the URL (23 languages);
98
+ - `toggle my *display`, `*opacity` and `*visibility` (ar, de, fr, he, ja, tr);
99
+ - a `js(…) … end` block after `then` behind a handler head (bn, es, it, ko, pl, ru, th, tl, uk);
100
+ - a snake_case name (`call global_function()`), which 11 tokenizers split at its `_`;
101
+ - a custom event after a handler head's `or` (`on click or foo`: bn, it, ja, ko, th, tr, zh). English
102
+ no longer drops the second event of `on foo or bar`;
103
+ - `put … at end of` in Vietnamese, and in bn, hi, ja, qu and tr after a bare name
104
+ (`put response at end of #results`);
105
+ - a `set` target with no marker after it, in it, pl, ru and uk (`set .divs.innerHTML to "foo"`,
106
+ `#d1.parentNode`, `bar["count"]`), and `on keydown[key=="Enter"]` in ar, de, fr and he;
107
+ - `fetch … as Object` and `as Number` behind a bn, hi or ko handler head;
108
+ - a top-level `for item in items …`, which bn, hi, ko and sw read as a handler on `item`;
109
+ - a loop's bottom test (`repeat … until x is 3 end`) in de, fr and ko;
110
+ - a Korean second handler on a parameterized event (`on exception(error)`), and Polish
111
+ `then throw` after `do` (Polish for `into`);
112
+ - bn, th and vi's own word for `checked`.
113
+ - **`@lokascript/semantic`: `at end of` inside a behavior or a def** reads in every language:
114
+ `behavior B on click put "x" at end of me end` was refused, English to English too, because the
115
+ `end` of `at end of` closed the block. A variable spelled like an end word in such a body reads too
116
+ (es `fin`).
117
+ - **`@lokascript/semantic`: two copied type lists that had lost `property-path`.** Chinese reads
118
+ `put the textContent of last <.picked/> in #list`, which it refused, and ja, ko and zh write the count
119
+ of `repeat the textContent of #a times` in their own words (`#aのtextContent`), not English.
120
+ - **`@lokascript/semantic`: Quechua's `repeat until event … from …`** writes Quechua
121
+ (`kama ruway pointerup ta kutipay #box manta …`), not a leftover of the retired i18n transformer with
122
+ English `until event` and `repeat` in it.
123
+ - **Test runs no longer fill the temp directory**: `scripts/vitest-run.sh` gives each run its own
124
+ `TMPDIR` and removes it. Vitest 5.0.2 leaves a copy of every transformed module in `$TMPDIR` per run.
125
+
126
+ ### Removed
127
+
128
+ - **`@hyperfixi/mcp-server`: the jsx domain's four tools** (`parse_jsx`, `compile_jsx`, `validate_jsx`,
129
+ `translate_jsx`), and `jsx` as a `domain` for `lse_validate_and_feedback`, `generate_training_data`
130
+ and the `lse_generate` prompt. The server now depends on `@lokascript/domains` ^4.0.0, which retired
131
+ jsx with six other preview domains (`docs-internal/DOMAINS_RETHINK.md`, D1). 8 domains remain (sql,
132
+ bdd, behaviorspec, llm, flow, voice, todo, learn): 32 domain tools, 102 tools defined, 97 listed by
133
+ default.
134
+
10
135
  ## [4.5.0] - 2026-10-11
11
136
 
12
137
  Pronouns, suffixes and handler heads read as their language. In 4.4.0 every translation wrote `me` and
@@ -32,9 +157,9 @@ review in `packages/semantic/NATIVE_REVIEW_NEEDED.md`; none has been reviewed ye
32
157
 
33
158
  - **`@lokascript/semantic` writes a pronoun in the case its marker takes.** Free forms: es `en mí`,
34
159
  pt `a mim`, it `a me`, tl `sa akin`, pl `do mnie`, ru `ко мне` / `во мне`, uk `до мене`, de `zu mir`
35
- / `in mich` (direction) / `auf mir` (location), hi `मुझ में`; `it` after a marker (ru `к этому`, pl `do
36
- tego`, de `dazu` / `darin` / `darauf`, fr `à lui`, hi `इस में`); `me` as a verb's object (de `zeige
37
- mich`, ru `покажи меня`). Fused forms: tr `bana` / `beni` / `benden`, `ona` / `onu`; ar `إليّ` / `فيه`;
160
+ / `in mich` (direction) / `auf mir` (location), hi `मुझ में`; `it` after a marker (ru `к этому`, pl
161
+ `do tego`, de `dazu` / `darin` / `darauf`, fr `à lui`, hi `इस में`); `me` as a verb's object (de
162
+ `zeige mich`, ru `покажи меня`). Fused forms: tr `bana` / `beni` / `benden`, `ona` / `onu`; ar `إليّ` / `فيه`;
38
163
  he `אליי` / `ממני`; bn `আমাকে` / `আমাতে`; hi `मुझे`; tl `dito`; pt `nele`. Object clitics on the verb:
39
164
  es `mostrarme`, it `metterlo`, pt `colocá-lo`, ar `ضعه`; fr writes the object pronoun before the
40
165
  infinitive (`le mettre`, `l'afficher`, `me cacher`). Every reader takes the nominative and the English
@@ -88,8 +213,8 @@ language has a word for. The words are the owner's vocabulary sheet
88
213
  counts, per render, the English grammar words (the tokens the engine's own parser matched in the
89
214
  source), English event names the language's lexicon has a word for, and a nominative `me` beside a
90
215
  marker in the 13 languages where that case is wrong. Two shrink-only baselines (the corpus, in
91
- `test:canonical`, and the command-shape cases, in their shards); `tools/regen-english-leaks-baseline.ts
92
- --report` prints each language against M2's targets. An English alternative a profile keeps for
216
+ `test:canonical`, and the command-shape cases, in their shards);
217
+ `tools/regen-english-leaks-baseline.ts --report` prints each language against M2's targets. An English alternative a profile keeps for
93
218
  code-switched input, beside a primary that is the language's own, counts as English.
94
219
  - **`@lokascript/semantic`: `transition … from … to`** reads and writes in every language (a `source`
95
220
  role), and the bare command reads in English. The schema's `destination` role is gone: upstream has
@@ -106,8 +231,8 @@ language has a word for. The words are the owner's vocabulary sheet
106
231
  back in English as upstream's first person), `does not match`, `fetch … do not throw`, `on first`,
107
232
  `go back`, `from elsewhere`, a loop's `index`, `start view transition`, a query's `in`, a conversion's
108
233
  `as` (the type name stays English), `the X of Y` inside an expression (es `valor de #price`, ja
109
- `#priceの値`), `catch` and `finally`, `element x`, and `def` (as the noun "function", es `función
110
- greet(name)`). bn, th and vi have their own `document` and `window`; he has its own words for all of
234
+ `#priceの値`), `catch` and `finally`, `element x`, and `def` (as the noun "function", es
235
+ `función greet(name)`). bn, th and vi have their own `document` and `window`; he has its own words for all of
111
236
  these and for `matches`, `exists`, `or`, `not`, `first`, `next`, `previous`, `closest`, `empty`, `is`,
112
237
  `document` and `window`; and the 15 languages that wrote English `forever` have their own (es
113
238
  `repetir siempre`, de `endlos`, ja `ずっと`). Every reader still takes the English words. Where a word
@@ -1611,7 +1736,8 @@ _Synchronized version release. See git history for details._
1611
1736
  - npm access token stored in GitHub Secrets
1612
1737
  - 2FA recommended for npm organization
1613
1738
 
1614
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.4.0...HEAD
1739
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.6.0...HEAD
1740
+ [4.6.0]: https://github.com/codetalcott/hyperfixi/compare/v4.5.0...v4.6.0
1615
1741
  [4.5.0]: https://github.com/codetalcott/hyperfixi/compare/v4.4.0...v4.5.0
1616
1742
  [4.4.0]: https://github.com/codetalcott/hyperfixi/compare/v4.3.0...v4.4.0
1617
1743
  [4.3.0]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...v4.3.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "4.5.0",
3
+ "version": "4.6.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.5.0",
61
- "@lokascript/compilation-service": "^4.5.0",
62
- "@lokascript/i18n": "^4.5.0",
63
- "@lokascript/semantic": "^4.5.0",
60
+ "@hyperfixi/patterns-reference": "^4.6.0",
61
+ "@lokascript/compilation-service": "^4.6.0",
62
+ "@lokascript/i18n": "^4.6.0",
63
+ "@lokascript/semantic": "^4.6.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.5.0",
76
- "@lokascript/hyperscript-adapter": "^4.5.0",
75
+ "@hyperfixi/engine": "^4.6.0",
76
+ "@lokascript/hyperscript-adapter": "^4.6.0",
77
77
  "@types/diff": "^5.0.0",
78
78
  "@types/node": "^26.6.3",
79
79
  "@vitest/coverage-v8": "^5.0.0",
@@ -573,13 +573,6 @@ function someNode(plain: unknown, test: (node: PlainNode) => boolean): boolean {
573
573
  }
574
574
 
575
575
  export const LOUD: readonly LoudFamily[] = [
576
- {
577
- name: 'ask-answer',
578
- reason:
579
- 'semantic has no schema and no dictionary word for `ask` / `answer`; adding them is an ' +
580
- 'owner vocabulary decision (the M2 sheet)',
581
- matches: (_source, types) => types.has('askCommand') || types.has('answerCommand'),
582
- },
583
576
  {
584
577
  name: 'templated-selector',
585
578
  reason:
@@ -600,14 +593,6 @@ export const LOUD: readonly LoudFamily[] = [
600
593
  matches: source => /'s(?=\s+(then|end)\b|\s*$)/.test(source),
601
594
  }, // M1 step 3 (2026-10-07): the English refusals left after the value and
602
595
  // command-form fixes, each family with why it is not carried.
603
- {
604
- name: 'scroll-by',
605
- reason:
606
- "scroll's relative form (`scroll #box down by 200px`) names a direction, which no " +
607
- "language's dictionary has: adding them is an owner vocabulary decision (the M2 sheet)",
608
- matches: (_source, _types, plain) =>
609
- someNode(plain, node => node.type === 'scrollCommand' && 'by' in node),
610
- },
611
596
  {
612
597
  name: 'beep',
613
598
  reason:
@@ -623,44 +608,13 @@ export const LOUD: readonly LoudFamily[] = [
623
608
  'a halt-only reading (OPEN_ITEMS P56)',
624
609
  matches: source => /\bhalt\s+(default\b|the event's\s+(bubbling|default)\b)/.test(source),
625
610
  },
626
- {
627
- name: 'collection-expressions',
628
- reason:
629
- "upstream 0.9.93's collection expressions (`… where it matches .a`, `mapped to`) are an " +
630
- "expression form semantic's reader does not have: a parser-track item (OPEN_ITEMS P57)",
631
- // Not `if no <q/> in … where …`, which English reads (its foreign lanes are
632
- // the value-words family).
633
- matches: (source, types) => types.has('collectionExpression') && !/\bif\s+no\s+</.test(source),
634
- },
635
- {
636
- name: 'pseudo-class-query',
637
- reason:
638
- 'a query of a pseudo-class (`<:checked/>`): the tokenizers read `:checked` as a local ' +
639
- 'variable; a tokenizer item, rare in pages',
640
- matches: source => /<:[\w-]+\/>/.test(source),
641
- },
642
611
  {
643
612
  name: 'closest-forms',
644
- reason:
645
- "`closest @attr` (an ancestor's attribute) and `closest parent <q/>`: the forms of " +
646
- '`closest` besides a query, rare in pages',
613
+ // `closest @attr` (an ancestor's attribute) left this family when the positional
614
+ // run took an attribute query (2026-10-13).
615
+ reason: '`closest parent <q/>`: the form of `closest` that starts at the parent, rare in pages',
647
616
  matches: (_source, _types, plain) =>
648
- someNode(
649
- plain,
650
- node =>
651
- node.type === 'closestExpr' &&
652
- // Not `closest @foo to #d1`, which English reads (value-words).
653
- (node.parentSearch === true ||
654
- (String(node.css ?? '').startsWith('[') &&
655
- (node.to as { type?: unknown } | undefined)?.type === 'implicitMeTarget'))
656
- ),
657
- },
658
- {
659
- name: 'possessive-after-query',
660
- reason:
661
- "a possessive after a positional query (`the next <div/>'s textContent`), rare in pages; " +
662
- 'the `of` form (`the textContent of the next <div/>`) is carried',
663
- matches: (source, types) => types.has('relativePositionalExpression') && /\/>'s\b/.test(source),
617
+ someNode(plain, node => node.type === 'closestExpr' && node.parentSearch === true),
664
618
  },
665
619
  {
666
620
  name: 'colon-event-arguments',
@@ -683,18 +637,6 @@ export const LOUD: readonly LoudFamily[] = [
683
637
  source
684
638
  ),
685
639
  },
686
- {
687
- name: 'rare-values',
688
- reason:
689
- 'value spellings rare in pages: an escaped class name (`.group-\\[…\\]`), a comma ' +
690
- 'list in a query (`<#d1, #d2/>`), an attribute after a bare or bracketed reference ' +
691
- '(`it @a`, `my [@a]`), a dotted path before `of`, a unit after parentheses ' +
692
- '(`(w)px`), `set {…} on <obj>`',
693
- matches: source =>
694
- /\\\[|<[^<>]*,[^<>]*\/>|\b(my|its|your)\s+\[@|\bset\s+(it|me|you)\s+@|\b[a-z]\w*\.[a-z]\w*\s+of\b|\)(px|em|rem|%)|\bset\s+\{[^}]*\}\s+on\b/i.test(
695
- source
696
- ),
697
- },
698
640
  {
699
641
  name: 'rare-commands',
700
642
  reason:
@@ -723,27 +665,9 @@ export interface SilentFamily {
723
665
  ids: readonly string[];
724
666
  }
725
667
 
726
- export const SILENT_FAMILIES: readonly SilentFamily[] = [
727
- {
728
- name: 'value-words',
729
- reason:
730
- "a language's word inside a value reads back as another English word: pl `to` → " +
731
- '`it` (its `it` word, after `closest`), qu `where` → `target`, vi `where` split at `_`',
732
- ids: ['d74a86f3a0', 'da24c9d768', 'b948ed872b'],
733
- },
734
- {
735
- name: 'it-locative',
736
- reason: 'it `in me` (a query scope) reads back as `on me`',
737
- ids: ['2159462e64', '0ff2f70080', 'd7b35fefe6'],
738
- },
739
- {
740
- name: 'possessive-split',
741
- reason:
742
- "a possessive reads back split or reworded: qu `$data 's inner' s val`, id `my` → " +
743
- '`me punya`',
744
- ids: ['76130fbf7d', '36e2294330', 'b839394853'],
745
- },
746
- ];
668
+ // Empty since 2026-10-13: the last two families (pl `to` after `closest`, qu and id
669
+ // possessives) were reader fixes. A new silent pair needs a family here, with its reason.
670
+ export const SILENT_FAMILIES: readonly SilentFamily[] = [];
747
671
 
748
672
  /** The silent family a case belongs to, if any. */
749
673
  export function silentFamilyOf(id: string): SilentFamily | undefined {
@@ -596,7 +596,7 @@ const CODE_NAMES: readonly string[] = ['userData'];
596
596
  * Not a name the window already holds (`focus`, `length`), nor `then`: the
597
597
  * fixture sets each name there, and a `then` on the window makes upstream
598
598
  * take it for a promise. Not yet `if` or `end`, which open and close a block
599
- * in every language's reader (OPEN_ITEMS P53: 93 and 505 failing pairs).
599
+ * in every language's reader (OPEN_ITEMS P53: 93 and 501 failing pairs, 2026-10-13).
600
600
  */
601
601
  export const KEYWORD_NAMES: readonly string[] = [
602
602
  'click',