@hyperfixi/testing-framework 4.4.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 +197 -5
- package/package.json +7 -7
- package/src/multilingual/README.md +9 -6
- package/src/multilingual/command-shapes.ts +7 -91
- package/src/multilingual/direct-path-shapes.ts +60 -26
- package/src/multilingual/english-leaks.test.ts +118 -6
- package/src/multilingual/english-leaks.ts +208 -43
- package/src/multilingual/hand-written-input.tasks.json +162 -0
- package/src/multilingual/hand-written-input.test.ts +62 -0
- package/src/multilingual/hand-written-input.ts +201 -0
- package/src/multilingual/value-matrix.ts +1 -1
- package/src/vocab/batch3-roundtrip.test.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,196 @@ 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
|
+
|
|
135
|
+
## [4.5.0] - 2026-10-11
|
|
136
|
+
|
|
137
|
+
Pronouns, suffixes and handler heads read as their language. In 4.4.0 every translation wrote `me` and
|
|
138
|
+
`it` in the nominative beside a marker (es `a yo`, de `zu ich`, tr `ben e`, ar `إلى أنا`), wrote every
|
|
139
|
+
case particle as a word of its own (ko `#out 에`, ja `"x" を`), and wrote a Japanese, Bengali or Turkish
|
|
140
|
+
handler head with two markers (`クリック を で`, `ক্লিক তে`, `tıklama i üzerinde`). Each of these is now
|
|
141
|
+
the language's own form, and every reader still takes the old one. The forms are listed for native
|
|
142
|
+
review in `packages/semantic/NATIVE_REVIEW_NEEDED.md`; none has been reviewed yet.
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
|
|
146
|
+
- **`@lokascript/testing-framework`: the English-leak gate counts pronoun case and marker spacing.**
|
|
147
|
+
`case:it` and an unmarked object pronoun (`case:me-object`, `case:it-object`) beside `case:me`; and
|
|
148
|
+
`spacing:<marker>`, a particle ko, ja, bn or tr attaches to its noun written as a word of its own.
|
|
149
|
+
Both shrink-only, both halves (corpus and command shapes), with per-language exit targets in
|
|
150
|
+
`LEAK_TARGETS`.
|
|
151
|
+
- **`@lokascript/semantic`: `toggle … between`** in every language's own words (ja `の間で`, hi `के बीच`,
|
|
152
|
+
he `בין`), with the style form's element written in the language.
|
|
153
|
+
- **`docs-internal/multilingual/FUSED_FORMS.md`**: the design for pronouns fused with their markers and
|
|
154
|
+
suffixes attached to words, with the owner's four questions and their answers.
|
|
155
|
+
|
|
156
|
+
### Changed
|
|
157
|
+
|
|
158
|
+
- **`@lokascript/semantic` writes a pronoun in the case its marker takes.** Free forms: es `en mí`,
|
|
159
|
+
pt `a mim`, it `a me`, tl `sa akin`, pl `do mnie`, ru `ко мне` / `во мне`, uk `до мене`, de `zu mir`
|
|
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 `إليّ` / `فيه`;
|
|
163
|
+
he `אליי` / `ממני`; bn `আমাকে` / `আমাতে`; hi `मुझे`; tl `dito`; pt `nele`. Object clitics on the verb:
|
|
164
|
+
es `mostrarme`, it `metterlo`, pt `colocá-lo`, ar `ضعه`; fr writes the object pronoun before the
|
|
165
|
+
infinitive (`le mettre`, `l'afficher`, `me cacher`). Every reader takes the nominative and the English
|
|
166
|
+
forms as before.
|
|
167
|
+
- **`@lokascript/semantic` writes a case particle onto its word** in ko (`"x"을 #out에 넣다`, `나를`,
|
|
168
|
+
each particle in the form the syllable takes), ja (`#outに`, `.aを`) and bn (`#outএ`, `.xকে`), and tr
|
|
169
|
+
fuses a suffix with its own reference words (`sonucu`, `hedefe`, `belgeye`) while code keeps its
|
|
170
|
+
suffix apart (`#out e`) until a reviewer picks a code word's harmony. tr also reads a suffix on code
|
|
171
|
+
after an apostrophe (`#out'a`, `.active'i`).
|
|
172
|
+
- **`@lokascript/semantic`: a custom event in a wait takes the language's `event` word** (`esperar evento foo`,
|
|
173
|
+
ja `イベント foo 待つ`), as `repeat until event X` already does; a known DOM event keeps `esperar transitionend`.
|
|
174
|
+
`wait for <custom event>` was refused in every language. A possessive whose property is a reference word
|
|
175
|
+
(`its.detail`) now reads back as that property, not the language's word for it.
|
|
176
|
+
- **`@lokascript/semantic`: ms and pl read their lexicon's `undefined` word** (`tidak_ditakrifkan`, `niezdefiniowane`),
|
|
177
|
+
which read back as a variable name.
|
|
178
|
+
- **`@lokascript/testing-framework`: the hand-written-input gate** (`hand-written-input.test.ts`): twenty tasks a
|
|
179
|
+
native speaker writes from English, run as written on the engine against upstream; committed with no inputs.
|
|
180
|
+
- **`@lokascript/semantic` writes a handler head as one word**: ja `クリックで` (was `クリック を で`),
|
|
181
|
+
bn `ক্লিকে` (the locative in the form the word takes: `জমাতে` after a vowel; was `ক্লিক তে`),
|
|
182
|
+
tr `tıklamada` (the locative in the form harmony gives: `göndermede`, `değişiklikte`; code keeps it apart, `keyup da`; was `tıklama i üzerinde`). The readers keep taking the two-marker heads.
|
|
183
|
+
- **`@lokascript/semantic` writes a query's scope before the query** in bn, hi, tr, ja and ko (tr
|
|
184
|
+
`bendeki <p/>`, hi `मुझ में स्थित <p/>`, ja `自分 の中の <p/>`), where it wrote English's order.
|
|
185
|
+
- **`@lokascript/semantic`: es and pt write the `focus` and `blur` commands as verbs** (`enfocarme`,
|
|
186
|
+
`focar-me`); the nouns were the events' and still read.
|
|
187
|
+
- **`@lokascript/semantic`: ja's head pattern outranks its fused handler patterns**, so a ja handler's
|
|
188
|
+
body always takes the clause walk: a `js … end` block after `then` and `fetch … as T` round-trip
|
|
189
|
+
where the fused path dropped them. The 2026-08 transformer's verb-first `繰り返し イベント X を まで`
|
|
190
|
+
body no longer reads in ja (the render since 2026-08-28 does).
|
|
191
|
+
|
|
192
|
+
- **`@lokascript/htmx-adapter`: the tr vocab gains `hx-da` as an alias of `hx-on`;** `hx-üzerinde` stays the name
|
|
193
|
+
(the profile's `on` word moved to the locative; a suffix is the wrong register for an attribute name).
|
|
194
|
+
|
|
195
|
+
### Fixed
|
|
196
|
+
|
|
197
|
+
- **`publish.yml`: a repair run skips a version npm is still processing** (`E409 previously staged`)
|
|
198
|
+
instead of failing the whole run.
|
|
199
|
+
|
|
10
200
|
## [4.4.0] - 2026-10-10
|
|
11
201
|
|
|
12
202
|
Translations now read as their language. In 4.3.0, 19% of the corpus's translations and 24% of the
|
|
@@ -23,8 +213,8 @@ language has a word for. The words are the owner's vocabulary sheet
|
|
|
23
213
|
counts, per render, the English grammar words (the tokens the engine's own parser matched in the
|
|
24
214
|
source), English event names the language's lexicon has a word for, and a nominative `me` beside a
|
|
25
215
|
marker in the 13 languages where that case is wrong. Two shrink-only baselines (the corpus, in
|
|
26
|
-
`test:canonical`, and the command-shape cases, in their shards);
|
|
27
|
-
--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
|
|
28
218
|
code-switched input, beside a primary that is the language's own, counts as English.
|
|
29
219
|
- **`@lokascript/semantic`: `transition … from … to`** reads and writes in every language (a `source`
|
|
30
220
|
role), and the bare command reads in English. The schema's `destination` role is gone: upstream has
|
|
@@ -41,8 +231,8 @@ language has a word for. The words are the owner's vocabulary sheet
|
|
|
41
231
|
back in English as upstream's first person), `does not match`, `fetch … do not throw`, `on first`,
|
|
42
232
|
`go back`, `from elsewhere`, a loop's `index`, `start view transition`, a query's `in`, a conversion's
|
|
43
233
|
`as` (the type name stays English), `the X of Y` inside an expression (es `valor de #price`, ja
|
|
44
|
-
`#priceの値`), `catch` and `finally`, `element x`, and `def` (as the noun "function", es
|
|
45
|
-
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
|
|
46
236
|
these and for `matches`, `exists`, `or`, `not`, `first`, `next`, `previous`, `closest`, `empty`, `is`,
|
|
47
237
|
`document` and `window`; and the 15 languages that wrote English `forever` have their own (es
|
|
48
238
|
`repetir siempre`, de `endlos`, ja `ずっと`). Every reader still takes the English words. Where a word
|
|
@@ -1546,7 +1736,9 @@ _Synchronized version release. See git history for details._
|
|
|
1546
1736
|
- npm access token stored in GitHub Secrets
|
|
1547
1737
|
- 2FA recommended for npm organization
|
|
1548
1738
|
|
|
1549
|
-
[Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.
|
|
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
|
|
1741
|
+
[4.5.0]: https://github.com/codetalcott/hyperfixi/compare/v4.4.0...v4.5.0
|
|
1550
1742
|
[4.4.0]: https://github.com/codetalcott/hyperfixi/compare/v4.3.0...v4.4.0
|
|
1551
1743
|
[4.3.0]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...v4.3.0
|
|
1552
1744
|
[4.2.0]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...v4.2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperfixi/testing-framework",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
61
|
-
"@lokascript/compilation-service": "^4.
|
|
62
|
-
"@lokascript/i18n": "^4.
|
|
63
|
-
"@lokascript/semantic": "^4.
|
|
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.
|
|
76
|
-
"@lokascript/hyperscript-adapter": "^4.
|
|
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",
|
|
@@ -297,14 +297,17 @@ parse lost the value, and every translation is rendered from it. Fix that first.
|
|
|
297
297
|
## English left in a translation
|
|
298
298
|
|
|
299
299
|
Every gate above asks whether a translation means what the source means; none asks
|
|
300
|
-
whether it reads as the language. `english-leaks.ts` counts, in each render,
|
|
300
|
+
whether it reads as the language. `english-leaks.ts` counts, in each render, four kinds
|
|
301
301
|
of finding:
|
|
302
302
|
|
|
303
|
-
| Finding
|
|
304
|
-
|
|
|
305
|
-
| `<word>`
|
|
306
|
-
| `event:<name>`
|
|
307
|
-
| `case:me`
|
|
303
|
+
| Finding | What it is |
|
|
304
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
305
|
+
| `<word>` | an English word the engine reads as grammar in the source, written in English |
|
|
306
|
+
| `event:<name>` | an English event name where the language's lexicon has a word and the renderer may use it |
|
|
307
|
+
| `case:me` | the nominative `me` beside a role marker, in the 13 languages where that form is wrong |
|
|
308
|
+
| `case:it` | the nominative `it` beside a role marker that wants another form (`CASE_RULES`) |
|
|
309
|
+
| `case:me-object` / `case:it-object` | the nominative pronoun as a command's unmarked object, where that form is wrong |
|
|
310
|
+
| `spacing:<marker>` | a marker ko, ja, bn or tr attaches to its noun, written apart (`#out 에`; `ATTACHED_MARKERS`) |
|
|
308
311
|
|
|
309
312
|
"Grammar" is derived per source: the tokens the engine's own parser matched as words, plus
|
|
310
313
|
the references in its parse. A word the language spells the same (es `a`, de `in`) and a
|
|
@@ -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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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,35 +665,9 @@ export interface SilentFamily {
|
|
|
723
665
|
ids: readonly string[];
|
|
724
666
|
}
|
|
725
667
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
reason:
|
|
730
|
-
"a language's word inside a value reads back as another English word: pl `to` → " +
|
|
731
|
-
'`it`, qu `where` → `target`, ms/pl `undefined`',
|
|
732
|
-
ids: [
|
|
733
|
-
'a30e7d6174',
|
|
734
|
-
'69537d01ae',
|
|
735
|
-
'd74a86f3a0',
|
|
736
|
-
'da24c9d768',
|
|
737
|
-
'b948ed872b',
|
|
738
|
-
'a4c56acf1e',
|
|
739
|
-
'54a65f4602',
|
|
740
|
-
],
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
name: 'it-locative',
|
|
744
|
-
reason: 'it `in me` (a query scope) reads back as `on me`',
|
|
745
|
-
ids: ['2159462e64', '0ff2f70080', 'd7b35fefe6'],
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
name: 'possessive-split',
|
|
749
|
-
reason:
|
|
750
|
-
"a possessive reads back split or reworded: qu `$data 's inner' s val`, id `my` → " +
|
|
751
|
-
'`me punya`',
|
|
752
|
-
ids: ['76130fbf7d', '36e2294330', 'b839394853'],
|
|
753
|
-
},
|
|
754
|
-
];
|
|
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[] = [];
|
|
755
671
|
|
|
756
672
|
/** The silent family a case belongs to, if any. */
|
|
757
673
|
export function silentFamilyOf(id: string): SilentFamily | undefined {
|
|
@@ -57,6 +57,11 @@ export interface DirectPathCase {
|
|
|
57
57
|
event: string | null;
|
|
58
58
|
/** The languages the core test ran; `[]` = English only. */
|
|
59
59
|
languages: 'all23' | string[];
|
|
60
|
+
/**
|
|
61
|
+
* The source defines a behavior of this name (a hand-written task): it is
|
|
62
|
+
* defined under a fresh name and installed with no arguments.
|
|
63
|
+
*/
|
|
64
|
+
behaviorName?: string;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
const CASES_PATH = path.resolve(
|
|
@@ -234,6 +239,12 @@ export function caseFails(r: DirectPathResult): boolean {
|
|
|
234
239
|
|
|
235
240
|
export interface DirectPathHosts {
|
|
236
241
|
runCase(c: DirectPathCase): Promise<DirectPathResult>;
|
|
242
|
+
/**
|
|
243
|
+
* One translation someone WROTE (the hand-written-input gate), run as
|
|
244
|
+
* runCase runs a render: `written` as written on the engine with the
|
|
245
|
+
* adapter, against upstream on the English source.
|
|
246
|
+
*/
|
|
247
|
+
runWritten(c: DirectPathCase, lang: string, written: string): Promise<PairResult>;
|
|
237
248
|
/** Restore the console, the rejection listeners and EventTarget. */
|
|
238
249
|
close(): Promise<void>;
|
|
239
250
|
}
|
|
@@ -321,14 +332,17 @@ export async function initDirectPathHosts(): Promise<DirectPathHosts> {
|
|
|
321
332
|
document.documentElement.removeAttribute('lang');
|
|
322
333
|
try {
|
|
323
334
|
let install = code;
|
|
324
|
-
|
|
335
|
+
const coreBehavior = c.id.startsWith('behavior-handlers');
|
|
336
|
+
if (coreBehavior || c.behaviorName) {
|
|
325
337
|
// The source defines a behavior: define it (under a fresh name), then
|
|
326
|
-
// install it in English, as the core test does.
|
|
338
|
+
// install it in English, as the core test does. A hand-written task
|
|
339
|
+
// names its behavior (`Hider`) and installs it with no arguments.
|
|
327
340
|
const name = `Demo${letter(behaviorSerial)}${letter(Math.floor(behaviorSerial / 26))}`;
|
|
328
341
|
behaviorSerial++;
|
|
329
|
-
const
|
|
342
|
+
const pattern = coreBehavior ? /Demo[A-Z]+/g : new RegExp(`\\b${c.behaviorName}\\b`, 'g');
|
|
343
|
+
const definition = code.replace(pattern, name);
|
|
330
344
|
host.evaluate(lang === 'en' ? definition : preprocess(definition, lang));
|
|
331
|
-
install = `install ${name}(h: 1)`;
|
|
345
|
+
install = coreBehavior ? `install ${name}(h: 1)` : `install ${name}()`;
|
|
332
346
|
lang = 'en';
|
|
333
347
|
}
|
|
334
348
|
document.body.innerHTML = c.fixture;
|
|
@@ -386,15 +400,44 @@ export async function initDirectPathHosts(): Promise<DirectPathHosts> {
|
|
|
386
400
|
}
|
|
387
401
|
};
|
|
388
402
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
403
|
+
/** Upstream on the English source: the oracle, or why there is none. */
|
|
404
|
+
type Oracle = { rejected: string | undefined; want: string };
|
|
405
|
+
const oracle = async (c: DirectPathCase): Promise<Oracle> => {
|
|
406
|
+
// A behavior definition is not a handler: it is checked by running it.
|
|
407
|
+
const rejected =
|
|
408
|
+
c.id.startsWith('behavior-handlers') || c.behaviorName
|
|
393
409
|
? undefined
|
|
394
410
|
: rejects(upstream, c.source);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
411
|
+
const want = rejected
|
|
412
|
+
? `✗upstream rejects: ${rejected}`
|
|
413
|
+
: await runAll(upstream, c, c.source, 'en');
|
|
414
|
+
return { rejected, want };
|
|
415
|
+
};
|
|
416
|
+
/** One translation, written or rendered, on the engine against the oracle. */
|
|
417
|
+
const pairFor = async (
|
|
418
|
+
c: DirectPathCase,
|
|
419
|
+
{ rejected, want }: Oracle,
|
|
420
|
+
lang: string,
|
|
421
|
+
written: string
|
|
422
|
+
): Promise<PairResult> => {
|
|
423
|
+
let english: string;
|
|
424
|
+
try {
|
|
425
|
+
english = preprocess(written, lang);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
english = `✗${(e as Error).message}`;
|
|
428
|
+
}
|
|
429
|
+
const got = await runAll(engine, c, written, lang);
|
|
430
|
+
let ref = want;
|
|
431
|
+
if (rejected && !english.startsWith('✗') && !rejects(upstream, english)) {
|
|
432
|
+
ref = await runAll(upstream, c, english, 'en');
|
|
433
|
+
}
|
|
434
|
+
const ok = got === ref && !EMPTY.test(ref) && !ref.startsWith('✗');
|
|
435
|
+
return { lang, written, english, got, ref, ok };
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
async runCase(c) {
|
|
440
|
+
const o = await oracle(c);
|
|
398
441
|
const eng = await runAll(engine, c, c.source, 'en');
|
|
399
442
|
const languages = c.languages === 'all23' ? [...FOREIGN_LANGUAGES] : c.languages;
|
|
400
443
|
const node = parseSemantic(c.source, 'en').node;
|
|
@@ -407,24 +450,15 @@ export async function initDirectPathHosts(): Promise<DirectPathHosts> {
|
|
|
407
450
|
written = null;
|
|
408
451
|
}
|
|
409
452
|
if (written === null) {
|
|
410
|
-
pairs.push({ lang, written, english: '', got: '✗untranslatable', ref: want, ok: false });
|
|
453
|
+
pairs.push({ lang, written, english: '', got: '✗untranslatable', ref: o.want, ok: false });
|
|
411
454
|
continue;
|
|
412
455
|
}
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
english = preprocess(written, lang);
|
|
416
|
-
} catch (e) {
|
|
417
|
-
english = `✗${(e as Error).message}`;
|
|
418
|
-
}
|
|
419
|
-
const got = await runAll(engine, c, written, lang);
|
|
420
|
-
let ref = want;
|
|
421
|
-
if (rejected && !english.startsWith('✗') && !rejects(upstream, english)) {
|
|
422
|
-
ref = await runAll(upstream, c, english, 'en');
|
|
423
|
-
}
|
|
424
|
-
const ok = got === ref && !EMPTY.test(ref) && !ref.startsWith('✗');
|
|
425
|
-
pairs.push({ lang, written, english, got, ref, ok });
|
|
456
|
+
pairs.push(await pairFor(c, o, lang, written));
|
|
426
457
|
}
|
|
427
|
-
return { id: c.id, want, eng, pairs };
|
|
458
|
+
return { id: c.id, want: o.want, eng, pairs };
|
|
459
|
+
},
|
|
460
|
+
async runWritten(c, lang, written) {
|
|
461
|
+
return pairFor(c, await oracle(c), lang, written);
|
|
428
462
|
},
|
|
429
463
|
async close() {
|
|
430
464
|
await new Promise(resolve => setTimeout(resolve, 0));
|