@hyperfixi/testing-framework 4.3.0 → 4.4.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,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.4.0] - 2026-10-10
11
+
12
+ Translations now read as their language. In 4.3.0, 19% of the corpus's translations and 24% of the
13
+ command-shape scripts' translations kept at least one English word, and a third of the corpus kept an
14
+ English event name. Counting only the English a recorded decision does not keep (type names, `url`,
15
+ `debounced at`, …), every one of the 23 languages is now at or under 5% of corpus renders (0.7–4.6%)
16
+ and 10% of command-shape renders (3.9–6.2%), and no translation writes an English event name its
17
+ language has a word for. The words are the owner's vocabulary sheet
18
+ (`docs-internal/multilingual/VOCABULARY_SHEET.md`), decided 2026-10-08.
19
+
20
+ ### Added
21
+
22
+ - **`@hyperfixi/testing-framework`: the English left in a translation is gated.** `english-leaks.ts`
23
+ counts, per render, the English grammar words (the tokens the engine's own parser matched in the
24
+ source), English event names the language's lexicon has a word for, and a nominative `me` beside a
25
+ 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); `tools/regen-english-leaks-baseline.ts
27
+ --report` prints each language against M2's targets. An English alternative a profile keeps for
28
+ code-switched input, beside a primary that is the language's own, counts as English.
29
+ - **`@lokascript/semantic`: `transition … from … to`** reads and writes in every language (a `source`
30
+ role), and the bare command reads in English. The schema's `destination` role is gone: upstream has
31
+ none, and SOV destination markers read an owned property's owner as an element (tr and qu swapped
32
+ `#foo's *width` silently). English writes `using` for an easing.
33
+ - **`@lokascript/semantic`: `add/remove/hide/show … when <condition>`** in every language, through a
34
+ `condition` role marked with each profile's `when`. It was English in every translation, and refused
35
+ as a bare command.
36
+
37
+ ### Changed
38
+
39
+ - **`@lokascript/semantic` writes each language's own words** for: loop heads (`repeat for x in`,
40
+ `while`, `until`; es `repetir para x en .items`), `I match` / `I am` (written as the third person, and
41
+ back in English as upstream's first person), `does not match`, `fetch … do not throw`, `on first`,
42
+ `go back`, `from elsewhere`, a loop's `index`, `start view transition`, a query's `in`, a conversion's
43
+ `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 `función
45
+ greet(name)`). bn, th and vi have their own `document` and `window`; he has its own words for all of
46
+ these and for `matches`, `exists`, `or`, `not`, `first`, `next`, `previous`, `closest`, `empty`, `is`,
47
+ `document` and `window`; and the 15 languages that wrote English `forever` have their own (es
48
+ `repetir siempre`, de `endlos`, ja `ずっと`). Every reader still takes the English words. Where a word
49
+ would read back as another (es `en` is also a marker), the translation keeps English's.
50
+ - **`@lokascript/semantic` writes native event names** in he, hi, it, pl, qu, ru, th, uk and vi where
51
+ the lexicon's word reads back; the rest are denylisted with their reasons.
52
+ - **`@lokascript/semantic`: a foreign `put X at start of Y` is written with the language's `prepend`**,
53
+ which English writes back as `put … at start of`.
54
+
55
+ ### Fixed
56
+
57
+ - **`@lokascript/semantic` reads `me` after a marker in the case the marker takes**, as authors write it:
58
+ de `zu mir`, pt `a mim`, pl `do mnie`, ru `ко мне`, uk `до мене`, hi `मुझ में`. Each was read as a
59
+ variable (`zu mir` ran as `to mir`) or refused. Translations still write the nominative.
60
+
61
+ - **`@lokascript/semantic`: `put X at start of Y` translates.** It was refused in all 23 languages: no
62
+ language but English had a pattern for it, and the render wrote `at end of` in its place.
63
+ - **`@lokascript/semantic`: a one-line handler's `put … at end of`** translates in es, id, it, pt, tl and
64
+ uk. The fused handler pattern took the end noun for the destination wherever the language's `at` word
65
+ is also a destination marker.
66
+ - **`@lokascript/semantic`: `^x is …` keeps its copula** in ar, hi and th, which read the `is` word after a
67
+ `^` variable as the pronoun `it`.
68
+ - **`@lokascript/semantic`: an SOV loop body may open with a variable named `index`** (ja `index を …`),
69
+ and `repeat` is never read as an event name.
70
+
10
71
  ## [4.3.0] - 2026-10-07
11
72
 
12
73
  More of what upstream reads now translates, and what does not is refused instead of lost. On the
@@ -1485,7 +1546,9 @@ _Synchronized version release. See git history for details._
1485
1546
  - npm access token stored in GitHub Secrets
1486
1547
  - 2FA recommended for npm organization
1487
1548
 
1488
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...HEAD
1549
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.4.0...HEAD
1550
+ [4.4.0]: https://github.com/codetalcott/hyperfixi/compare/v4.3.0...v4.4.0
1551
+ [4.3.0]: https://github.com/codetalcott/hyperfixi/compare/v4.2.0...v4.3.0
1489
1552
  [4.2.0]: https://github.com/codetalcott/hyperfixi/compare/v4.1.0...v4.2.0
1490
1553
  [4.1.0]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...v4.1.0
1491
1554
  [4.0.1]: https://github.com/codetalcott/hyperfixi/compare/v4.0.0...v4.0.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Cross-platform behavior testing suite for LokaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -40,7 +40,7 @@
40
40
  "analyze-failures": "tsx src/multilingual/tools/analyze-failures.ts",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "test:check": "VITEST_TIMEOUT=240 VITEST_QUIET=1 bash ../../scripts/vitest-run.sh --reporter=dot",
43
- "test:canonical": "FOREIGN_CANONICAL_VALIDITY=1 VITEST_TIMEOUT=300 bash ../../scripts/vitest-run.sh --reporter=dot src/multilingual/canonical-validity.test.ts src/multilingual/foreign-canonical-validity.test.ts src/multilingual/render-fidelity.test.ts src/multilingual/bare-render-fidelity.test.ts src/multilingual/en-reference-preservation.test.ts",
43
+ "test:canonical": "FOREIGN_CANONICAL_VALIDITY=1 VITEST_TIMEOUT=300 bash ../../scripts/vitest-run.sh --reporter=dot src/multilingual/canonical-validity.test.ts src/multilingual/foreign-canonical-validity.test.ts src/multilingual/render-fidelity.test.ts src/multilingual/bare-render-fidelity.test.ts src/multilingual/en-reference-preservation.test.ts src/multilingual/english-leaks.corpus.test.ts",
44
44
  "test:shipped-sources": "VITEST_TIMEOUT=300 bash ../../scripts/vitest-run.sh --reporter=dot src/multilingual/shipped-sources-engine.test.ts src/multilingual/shipped-sources-localized.test.ts src/multilingual/shipped-examples-execution.test.ts"
45
45
  },
46
46
  "keywords": [
@@ -57,10 +57,10 @@
57
57
  "author": "LokaScript Contributors",
58
58
  "license": "MIT",
59
59
  "dependencies": {
60
- "@hyperfixi/patterns-reference": "^4.3.0",
61
- "@lokascript/compilation-service": "^4.3.0",
62
- "@lokascript/i18n": "^4.3.0",
63
- "@lokascript/semantic": "^4.3.0",
60
+ "@hyperfixi/patterns-reference": "^4.4.0",
61
+ "@lokascript/compilation-service": "^4.4.0",
62
+ "@lokascript/i18n": "^4.4.0",
63
+ "@lokascript/semantic": "^4.4.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.3.0",
76
- "@lokascript/hyperscript-adapter": "^4.3.0",
75
+ "@hyperfixi/engine": "^4.4.0",
76
+ "@lokascript/hyperscript-adapter": "^4.4.0",
77
77
  "@types/diff": "^5.0.0",
78
78
  "@types/node": "^26.6.3",
79
79
  "@vitest/coverage-v8": "^5.0.0",
@@ -294,6 +294,38 @@ npx tsx tools/regen-value-matrix-baseline.ts --dry-run --results /tmp/matrix.jso
294
294
  A lane that fails in `en-rt` fails in nearly every translation: semantic's English
295
295
  parse lost the value, and every translation is rendered from it. Fix that first.
296
296
 
297
+ ## English left in a translation
298
+
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, three kinds
301
+ of finding:
302
+
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
+
309
+ "Grammar" is derived per source: the tokens the engine's own parser matched as words, plus
310
+ the references in its parse. A word the language spells the same (es `a`, de `in`) and a
311
+ name spelled like a keyword are never counted; strings, `js` blocks and brace interiors
312
+ are not read. The report splits each word by context (`clause`: in a clause semantic keeps
313
+ as written; `bracket`, `call`, `property`, `plain`) and by whether the language has its own
314
+ word for it.
315
+
316
+ Two halves, each with a shrink-only baseline: `baselines/english-leaks.corpus.json` (every
317
+ non-markup corpus row, `render(parse(en), L)`; in `test:canonical`, after `populate`) and
318
+ `baselines/english-leaks.shapes.json` (the renders the command-shape shards already make).
319
+
320
+ ```bash
321
+ # The burn-down: per language, per word (with contexts), per event
322
+ npx tsx tools/regen-english-leaks-baseline.ts --report
323
+
324
+ # After a fix: prune the findings that are gone (refuses to add new ones)
325
+ npx tsx tools/regen-english-leaks-baseline.ts # both halves (corpus needs a fresh populate)
326
+ npx tsx tools/regen-english-leaks-baseline.ts --shapes # one half
327
+ ```
328
+
297
329
  ## Direct-path shapes on the text path
298
330
 
299
331
  Core's `src/multilingual/*-direct-path.test.ts` files pin shapes core's direct path ran
@@ -13,6 +13,10 @@
13
13
  *
14
14
  * The cases file, the baseline's own consistency and the oracle are checked
15
15
  * in command-shapes.cases.test.ts and command-shapes.oracle.test.ts.
16
+ *
17
+ * Each shard also judges the English left in its renders (english-leaks.ts),
18
+ * against baselines/english-leaks.shapes.json: the same renders, a second
19
+ * question, so the gate costs no second translation pass.
16
20
  */
17
21
 
18
22
  import { readFileSync } from 'node:fs';
@@ -27,6 +31,16 @@ import {
27
31
  type CommandShapesBaseline,
28
32
  type PairChange,
29
33
  } from './command-shapes';
34
+ import {
35
+ diffLeakBaseline,
36
+ gainedLeaks,
37
+ prunableLeaks,
38
+ initLeakScanner,
39
+ loadLeakBaseline,
40
+ shapeLeaks,
41
+ type LeakChange,
42
+ type LeakResults,
43
+ } from './english-leaks';
30
44
 
31
45
  export const COMMAND_SHAPE_SHARDS = 3;
32
46
 
@@ -47,11 +61,16 @@ export function describeCommandShapesShard(shard: number): void {
47
61
  `${c.id} [${c.lane}] ${c.listed} → ${c.got}: ${sources.get(c.id)?.slice(0, 80)}` +
48
62
  (c.detail ? ` ⇒ ${JSON.stringify(collapse(c.detail).slice(0, 80))}` : '');
49
63
 
64
+ const leakLine = (c: LeakChange, words: string[]): string =>
65
+ `${c.id} [${c.language}] ${words.join(' ')}: ${sources.get(c.id)?.slice(0, 80)}`;
66
+
50
67
  describe(`command shapes [${shard + 1}/${COMMAND_SHAPE_SHARDS}]`, () => {
51
68
  let results: CaseResult[] = [];
69
+ let leaks: LeakResults = new Map();
52
70
 
53
71
  beforeAll(async () => {
54
72
  results = await runCommandShapes(own);
73
+ leaks = shapeLeaks(await initLeakScanner(), results, own);
55
74
  const pairs = results.reduce((n, r) => n + Object.keys(r.lanes).length, 0);
56
75
  const failing = results.reduce(
57
76
  (n, r) => n + Object.values(r.lanes).filter(l => l.outcome !== 'pass').length,
@@ -72,5 +91,18 @@ export function describeCommandShapesShard(shard: number): void {
72
91
  const { better } = diffBaseline(results, loadCommandShapesBaseline());
73
92
  expect(capped(better.map(line))).toEqual([]);
74
93
  });
94
+
95
+ it('no render holds English the leak baseline does not list (english-leaks.ts)', () => {
96
+ expect(leaks.size).toBeGreaterThan(300);
97
+ const gained = gainedLeaks(diffLeakBaseline(leaks, loadLeakBaseline('shapes')));
98
+ expect(capped(gained.map(c => leakLine(c, c.added)))).toEqual([]);
99
+ });
100
+
101
+ it('no listed English is gone, nor a listed refusal rendering now (prune with tools/regen-english-leaks-baseline.ts --shapes)', () => {
102
+ const prune = prunableLeaks(diffLeakBaseline(leaks, loadLeakBaseline('shapes')));
103
+ expect(capped(prune.map(c => leakLine(c, c.newRender ? ['now renders'] : c.gone)))).toEqual(
104
+ []
105
+ );
106
+ });
75
107
  });
76
108
  }
@@ -111,6 +111,8 @@ export interface LaneResult {
111
111
  /** The lane's English (a silent lane), or the error's first line (a refused one). */
112
112
  detail?: string;
113
113
  kind?: SilentKind;
114
+ /** The language's render (`translate(src, 'en', L)`), when there was one: what english-leaks.ts reads. */
115
+ rendered?: string;
114
116
  }
115
117
 
116
118
  export interface CaseResult {
@@ -497,11 +499,6 @@ export async function initCommandShapes(): Promise<CommandShapeRunner> {
497
499
  const readers = await loadEngineReaders();
498
500
  const { translate } = (await import('@lokascript/semantic')) as { translate: Translate };
499
501
 
500
- const lane = (source: string, language: string): string =>
501
- language === 'en'
502
- ? translate(source, 'en', 'en')
503
- : translate(translate(source, 'en', language), language, 'en');
504
-
505
502
  return {
506
503
  runCase(shape, lanes = LANES) {
507
504
  const engine = readers[readerOf(shape)];
@@ -516,14 +513,26 @@ export async function initCommandShapes(): Promise<CommandShapeRunner> {
516
513
  lanes: {},
517
514
  };
518
515
  for (const language of lanes) {
516
+ let rendered: string | undefined;
519
517
  let english: string;
520
518
  try {
521
- english = lane(shape.source, language);
519
+ if (language === 'en') english = translate(shape.source, 'en', 'en');
520
+ else {
521
+ rendered = translate(shape.source, 'en', language);
522
+ english = translate(rendered, language, 'en');
523
+ }
522
524
  } catch (e) {
523
- result.lanes[language] = { outcome: 'refused', detail: firstLine(e) };
525
+ result.lanes[language] = {
526
+ outcome: 'refused',
527
+ detail: firstLine(e),
528
+ ...(rendered !== undefined ? { rendered } : {}),
529
+ };
524
530
  continue;
525
531
  }
526
- result.lanes[language] = judge(engine, want, wantPlain, english);
532
+ result.lanes[language] = {
533
+ ...judge(engine, want, wantPlain, english),
534
+ ...(rendered !== undefined ? { rendered } : {}),
535
+ };
527
536
  }
528
537
  return result;
529
538
  },
@@ -718,9 +727,8 @@ export const SILENT_FAMILIES: readonly SilentFamily[] = [
718
727
  {
719
728
  name: 'value-words',
720
729
  reason:
721
- "a language's word inside a value reads back as another English word: `is not` → " +
722
- '`it not` (ar/hi/ms/pl/th), pl `to` → `it`, qu `where` → `target`, ms/pl `undefined`, ' +
723
- 'ms `it` → `its`',
730
+ "a language's word inside a value reads back as another English word: pl `to` → " +
731
+ '`it`, qu `where` → `target`, ms/pl `undefined`',
724
732
  ids: [
725
733
  'a30e7d6174',
726
734
  '69537d01ae',
@@ -728,7 +736,6 @@ export const SILENT_FAMILIES: readonly SilentFamily[] = [
728
736
  'da24c9d768',
729
737
  'b948ed872b',
730
738
  'a4c56acf1e',
731
- '87250fbc5e',
732
739
  '54a65f4602',
733
740
  ],
734
741
  },
@@ -741,8 +748,8 @@ export const SILENT_FAMILIES: readonly SilentFamily[] = [
741
748
  name: 'possessive-split',
742
749
  reason:
743
750
  "a possessive reads back split or reworded: qu `$data 's inner' s val`, id `my` → " +
744
- "`me punya`, qu `#box's *opacity` → `#box … on *opacity`",
745
- ids: ['76130fbf7d', '36e2294330', 'b839394853', '7bc08adb98'],
751
+ '`me punya`',
752
+ ids: ['76130fbf7d', '36e2294330', 'b839394853'],
746
753
  },
747
754
  ];
748
755
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The English-leak gate, corpus half (see english-leaks.ts): every non-markup
3
+ * corpus row rendered into each language as `populate` writes it, judged
4
+ * against baselines/english-leaks.corpus.json. Fails on a finding a pair did
5
+ * not have, and on a listed one that is gone (prune with
6
+ * tools/regen-english-leaks-baseline.ts --corpus), so the list only shrinks.
7
+ *
8
+ * Needs FOREIGN_CANONICAL_VALIDITY=1 and a freshly populated patterns.db: the
9
+ * SET of rows depends on `populate`, the same contract the other corpus gates
10
+ * carry (`npm run test:canonical`).
11
+ *
12
+ * @vitest-environment node
13
+ */
14
+ import { describe, it, expect, beforeAll } from 'vitest';
15
+ import { getAllPatterns } from '@hyperfixi/patterns-reference';
16
+ import {
17
+ corpusLeaks,
18
+ diffLeakBaseline,
19
+ gainedLeaks,
20
+ prunableLeaks,
21
+ initLeakScanner,
22
+ loadLeakBaseline,
23
+ type LeakResults,
24
+ } from './english-leaks';
25
+
26
+ const DB_FRESHLY_POPULATED = process.env.FOREIGN_CANONICAL_VALIDITY === '1';
27
+
28
+ /** At most `max` lines, then a count: a broad regression must not print thousands. */
29
+ function capped(lines: string[], max = 40): string[] {
30
+ return lines.length <= max ? lines : [...lines.slice(0, max), `… and ${lines.length - max} more`];
31
+ }
32
+
33
+ describe.skipIf(!DB_FRESHLY_POPULATED)('english-leaks: corpus renders', () => {
34
+ let results: LeakResults = new Map();
35
+ let skipped: string[] = [];
36
+ const sources = new Map<string, string>();
37
+
38
+ beforeAll(async () => {
39
+ const rows = await getAllPatterns({ limit: 5000 });
40
+ for (const row of rows) sources.set(row.id, row.rawCode.replace(/\s+/g, ' ').trim());
41
+ ({ results, skipped } = await corpusLeaks(await initLeakScanner(), rows));
42
+ }, 300_000);
43
+
44
+ it('scores the corpus (sanity: guards a false green)', () => {
45
+ expect(results.size).toBeGreaterThan(140);
46
+ // Rows the engine rejects have no grammar to read (worker, socket, eventsource, …).
47
+ expect(skipped.length).toBeLessThan(10);
48
+ });
49
+
50
+ it('no render holds English the baseline does not list', () => {
51
+ const gained = gainedLeaks(diffLeakBaseline(results, loadLeakBaseline('corpus')));
52
+ expect(
53
+ capped(
54
+ gained.map(
55
+ c => `${c.id} [${c.language}] ${c.added.join(' ')}: ${sources.get(c.id)?.slice(0, 80)}`
56
+ )
57
+ )
58
+ ).toEqual([]);
59
+ });
60
+
61
+ it('no listed English is gone (prune it with tools/regen-english-leaks-baseline.ts --corpus)', () => {
62
+ const prune = prunableLeaks(diffLeakBaseline(results, loadLeakBaseline('corpus')));
63
+ expect(
64
+ capped(
65
+ prune.map(c => `${c.id} [${c.language}] ${c.newRender ? 'now renders' : c.gone.join(' ')}`)
66
+ )
67
+ ).toEqual([]);
68
+ });
69
+
70
+ it('keeps no entry for a row the corpus no longer holds', () => {
71
+ const stale = Object.keys(loadLeakBaseline('corpus').entries).filter(id => !results.has(id));
72
+ expect(stale).toEqual([]);
73
+ });
74
+ });