@hyperfixi/testing-framework 3.2.0 → 4.0.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +203 -1
  2. package/package.json +9 -9
  3. package/src/agent-bench/README.md +48 -1
  4. package/src/agent-bench/agent-bench.test.ts +7 -0
  5. package/src/agent-bench/harness.ts +39 -50
  6. package/src/multilingual/README.md +30 -11
  7. package/src/multilingual/bare-render-fidelity.ts +1 -1
  8. package/src/multilingual/cli.ts +5 -3
  9. package/src/multilingual/direct-path-shapes-gate.ts +56 -0
  10. package/src/multilingual/direct-path-shapes.1.test.ts +12 -0
  11. package/src/multilingual/direct-path-shapes.2.test.ts +12 -0
  12. package/src/multilingual/direct-path-shapes.3.test.ts +12 -0
  13. package/src/multilingual/direct-path-shapes.cases.json +2621 -0
  14. package/src/multilingual/direct-path-shapes.ts +459 -0
  15. package/src/multilingual/engine-parser-parity.test.ts +67 -0
  16. package/src/multilingual/orchestrator.ts +2 -1
  17. package/src/multilingual/render-fidelity.ts +1 -1
  18. package/src/multilingual/shipped-examples-execution.test.ts +44 -33
  19. package/src/multilingual/shipped-examples-execution.ts +55 -75
  20. package/src/multilingual/shipped-sources-engine.test.ts +106 -0
  21. package/src/multilingual/shipped-sources-localized.test.ts +87 -0
  22. package/src/multilingual/shipped-sources-validity.ts +127 -74
  23. package/src/multilingual/validators/execution-validator.test.ts +81 -23
  24. package/src/multilingual/validators/execution-validator.ts +140 -94
  25. package/src/multilingual/validators/parse-validator.ts +7 -20
  26. package/src/multilingual/value-matrix.accepted.test.ts +8 -11
  27. package/src/multilingual/value-matrix.isolation.test.ts +7 -6
  28. package/src/multilingual/value-matrix.ts +72 -86
  29. package/src/multilingual/shipped-sources-validity.test.ts +0 -95
package/CHANGELOG.md CHANGED
@@ -7,6 +7,206 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.0.0] - 2026-10-04
11
+
12
+ `@hyperfixi/core`'s own hyperscript engine is gone: its root re-exports `@hyperfixi/engine`, and
13
+ its `hyperfixi.js` is the engine's 34 KB script-tag bundle where core's ~352 KB bundle used to be.
14
+ The engine follows upstream `_hyperscript`'s grammar and is gated by upstream's own test suite, so
15
+ scripts in core-only syntax need upstream's spelling (two additions stay: `new X()` and
16
+ `toggle <element>`). Core's other bundles, its htmx layer, the core-era plugin packages and the
17
+ AOT compiler retire: htmx 4 or fixi supply hypermedia attributes beside the engine, and
18
+ `@lokascript/hyperscript-adapter` runs hyperscript written in other languages. Every package moves
19
+ to 4.0.0 together. Everything under **Removed** is breaking, as is each **Changed** item marked ⚠;
20
+ [MIGRATION.md](MIGRATION.md) names each replacement.
21
+
22
+ ### Removed
23
+
24
+ - **Core's engine and its API** (#1368). These root exports had no engine counterpart:
25
+ `hyperscript` (`compile`, `compileSync`, `compileAsync`, `eval`, `validate`), `lokascript`,
26
+ `Runtime`, `RuntimeBase`, `createContext` and the element-scope helpers, `installPlugin`,
27
+ `getParserExtensionRegistry`, `setGlobal`, `DebugController`, `validatePartialContent` and its
28
+ helpers, `fromCoreAST` / `toCoreAST`. `parse` is now the engine's. The root is ESM-only, like the
29
+ engine: `dist/index.cjs` and the UMD `dist/index.min.js` (`LokaScriptCore`) are gone, though the
30
+ kept subpaths still ship CommonJS.
31
+ - **21 of core's 29 entry points**: `/commands`, `/expressions`, `/parser/*`, `/behaviors`,
32
+ `/bundle-generator`, `/registry/*`, `/lse`, and `/browser/` `hybrid-complete`, `hybrid-hx`,
33
+ `hybrid-hx-v4`, `multilingual`, `modular` (#1343–#1368). Instead of `/commands`, import the
34
+ grammar modules from the root: `import { toggle } from '@hyperfixi/core'`.
35
+ - **Core's browser bundles** (#1348, #1350, #1352, #1368): `hyperfixi-hx.js`,
36
+ `hyperfixi-hx-v4.js`, `hyperfixi-hybrid-complete.js`, `hyperfixi-multilingual.js`,
37
+ `hyperfixi-classic-i18n.js` and the code-split `hyperfixi.mjs`, plus every alias
38
+ (`hyperfixi-browser.js`, `hyperfixi-hybrid-hx.js`, each `lokascript-*.js`). Only
39
+ `hyperfixi.js` remains. CDN URLs pinned to 3.x keep working.
40
+ - **Core's embedded htmx layer** (#1342, #1348, #1350): `hx-*` and fixi `fx-*` on core's runtime,
41
+ `hx-live`, `sse-connect` / `sse-swap`, `ws-connect` / `ws-send`. Load htmx 4 or fixi beside the
42
+ engine. `hx-live` becomes the engine's `live` block, and SSE and WebSockets are htmx 4's
43
+ `hx-sse` / `hx-ws`.
44
+ - **Core-only members of `window.hyperfixi`**: `compile`, `compileSync`, `execute`, `setLocale`,
45
+ `evalLSENode`, `debugControl`, `semanticDebug`, the `hyperfixi:semantic-parse` event,
46
+ `hyperfixi:debug` logging, `_hyperscript.behaviors.resolve`, and the deprecated
47
+ `window.lokascript` (#1352, #1355).
48
+ - **Core-only hyperscript syntax** (#1346, #1363, #1365). The engine rejects `has` / `have`,
49
+ `prepend`, `copy`, `push url` / `replace url`, `process partials`, the `pseudo` keyword, prefix
50
+ `unless`, bare `beep`, `a ? b : c`, core's `swap <strategy> of X with Y`, `set @a to v on X`,
51
+ `.debounce(n)` / `.throttle(n)`, `my?.x` and `tell X to …`. A few forms still parse but no longer
52
+ do what they did, such as `toggle .a on this` and `go forward`. MIGRATION.md gives each form's
53
+ upstream spelling. The multilingual reader still accepts them all.
54
+ - **`@hyperfixi/core/multilingual`'s class API** (#1345, #1368): `MultilingualHyperscript` (with
55
+ `parseToAST`, `getAllTranslations`, …), `getMultilingual`, `multilingual`, `LanguageInfo`,
56
+ `schemaRoleInferrer`. The entry is three async functions: `parse`, `render`, `translate`.
57
+ - **The AOT compiler** (#1364). Its output ran only on a runtime that was never published.
58
+ `@lokascript/compilation-service` loses `compile()`, `POST /compile`, `CompileResponse`, the
59
+ compile cache (`SemanticCache`, `getCacheStats`, `clearCache`, `/cache`), `generateTests`'
60
+ `executionMode` and `generate()`'s `'js'` target. `generate()` now requires `target`
61
+ (`react` | `vue` | `svelte` | `intent-element`). `@hyperfixi/mcp-server` loses
62
+ `compile_hyperscript`, `execute_lse`, `lse_to_hyperscript`'s `compile` option and
63
+ `generate_tests`' `executionMode`, leaving 106 tools. The loop is now `validate_and_compile`
64
+ (name kept), then repair, then the hyperscript as is in `_="…"`.
65
+ - **`@hyperfixi/vite-plugin`'s compile mode** (#1360). `mode: 'compile'` warns and builds the
66
+ engine bundle. The exports `CompiledHandler`, `CompileOptions` and
67
+ `set/clear/hasSemanticParser` are removed.
68
+ - **Smaller removals.** `@hyperfixi/core/reference` loses `availability`, `BundleAvailability`
69
+ and `getCommandsByAvailability` (#1363). `@hyperfixi/speech` loses `speechPlugin`,
70
+ `speakCommand`, `askCommand` and `answerCommand`; the engine has `ask` / `answer` (#1358).
71
+ `@hyperfixi/intent-element` loses its `evalLSENode` fallback (#1356).
72
+ `@hyperfixi/patterns-reference` loses four rows that held only retired htmx attributes, and
73
+ `hx-live-with-mutator` is renamed `live-with-handler` (#1348). Of the never-published VS Code
74
+ extensions, the LokaScript one loses its debugger and the standalone `_hyperscript` one is
75
+ retired (#1361).
76
+
77
+ ### Deprecated
78
+
79
+ - **`@hyperfixi/reactivity`, `@hyperfixi/realtime`, `@hyperfixi/components`**: deprecated on npm,
80
+ last version 3.3.0 (#1358). Use the engine's built-in `live` / `when` / `bind`, htmx 4's
81
+ `hx-sse` / `hx-ws`, and upstream's component extension.
82
+ - **`@hyperfixi/types-browser`'s `LokaScriptCoreAPI`**: a deprecated alias of `HyperfixiAPI`
83
+ (#1356).
84
+
85
+ ### Changed
86
+
87
+ - ⚠ **`@hyperfixi/core` is the engine plus tooling** (#1368). The root is
88
+ `export * from '@hyperfixi/engine'` and `VERSION`: `register`, `everything` and each grammar
89
+ module, `api`, `parse`, `evaluate`, `processNode`, `boot`, and the AST types. The engine stays
90
+ external, so both packages share one grammar. Kept: `/multilingual`, `/ast-utils`,
91
+ `/reference`, `/metadata`, `/lsp-metadata`, `/browser`.
92
+ - ⚠ **`hyperfixi.js` (`@hyperfixi/core/browser`) is the engine's `hyperfixi-hs.js`**, byte for
93
+ byte: 34 KB gzipped instead of ~352 KB (#1355). `window.hyperfixi` is `window._hyperscript`,
94
+ upstream-shaped (`evaluate`, `parse`, `process`, `use`, `config`, `addSourceTransform`). Parse
95
+ errors are a `hyperscript:parse-error` event on the element plus a `console.error` (#1354).
96
+ - ⚠ **Runtime semantics follow upstream** wherever core's differed. `swap #a with #b` exchanges
97
+ the elements; `show` / `hide … with *opacity` honour the strategy; `tell` binds `you`, not `me`;
98
+ `set` on a selector sets every match; and `no ""` is true. MIGRATION.md has the full list.
99
+ - ⚠ **`@hyperfixi/vite-plugin` emits a bundle on `@hyperfixi/engine`** (#1343). There is one
100
+ tier: the grammar modules the scan finds. It weighs 17.9 KB gzipped for three commands (3.x lite:
101
+ 3.9 KB) and 34.4 KB for everything; 3.x fell back to 352 KB on `fetch`. htmx is not bundled.
102
+ Non-English scripts are translated as the engine reads them. `devFallback: 'everything'` is new,
103
+ and `'full'` / `'hybrid-complete'` mean the same. The plugin no longer depends on core.
104
+ - ⚠ **`@hyperfixi/behaviors` runs on the engine** (peer `@hyperfixi/engine`; upstream works too).
105
+ Each behavior is defined with `host.evaluate(source)` and written in upstream's idioms.
106
+ Toggleable's `target` parameter is now `targetEl`. `LokaScriptInstance` / `LokaScriptWindow`
107
+ are now `HyperscriptHost` / `HyperscriptWindow` (#1338).
108
+ - ⚠ **`@hyperfixi/speech` is an engine module** (#1358). Install it with
109
+ `register(...everything, speak)`. It is ESM-only and peers on the engine. `speak` takes
110
+ upstream's syntax (`speak <text> [with voice|rate|pitch|volume <x>]…`) and waits for the
111
+ utterance to end.
112
+ - ⚠ **The localized htmx vocabulary moved** from `@hyperfixi/core/vocab/htmx/{lang}.js` to
113
+ `@lokascript/htmx-adapter/vocab/{lang}.js`; the data is unchanged (#1349).
114
+ - **`@hyperfixi/intent-element`** renders an intent to English with the page's semantic bundle
115
+ and runs it through the host's `evaluate` (`NO_RENDERER` without one). It peers on
116
+ `@hyperfixi/engine` and `@lokascript/semantic` (#1339, #1356).
117
+ - **`@lokascript/semantic` writes upstream's spelling in English** for core-only forms (#1346).
118
+ For example, `has` becomes `matches`, `prepend` becomes `put … at start of`, and swap strategies
119
+ become `put … into` / `before` / `after`. This reaches `translate(…, 'en')`,
120
+ `render(node, 'en')`, both adapters and MCP `translate_to_english`. In `fromSemanticAST`, a
121
+ handler's `then` chain is now its body (#1359).
122
+ - **The language tools read the engine's parse** (#1356, #1359, #1365).
123
+ `@lokascript/language-server` and MCP `validate_hyperscript` report the engine's errors
124
+ (`source: 'engine'`). Hover and symbols come from `@lokascript/semantic`, and some long handlers
125
+ show fewer commands than in 3.x. Hyperscript mode flags exactly `new X()` and `toggle <element>`.
126
+ `@lokascript/compilation-service` rejects natural-language input the engine cannot read
127
+ (`ENGINE_PARSE_ERROR`), and its generated Playwright tests load `hyperfixi-hs.js`. The language
128
+ server, the MCP server and the compilation service now peer on `@hyperfixi/engine`.
129
+ - **MCP**: `get_bundle_config` recommends `hyperfixi-hs.js`, plus an `adapter` field for
130
+ non-English. `analyze_complexity` / `analyze_metrics` no longer count every comparison as a
131
+ decision. The `debug_*` tools point to `log` and `breakpoint`. `UNSUPPORTED_QUERY_LITERAL` is
132
+ gone (#1350, #1352, #1359, #1361, #1364).
133
+ - **`@hyperfixi/core/reference`, `/lsp-metadata`, `/metadata` describe the engine** (#1355,
134
+ #1363). They cover its 53 commands, adding `for`, `ask`, `answer` and `beep!`. Examples are in
135
+ upstream spelling and each is checked to parse; descriptions of core-only semantics (`pick`,
136
+ `beep`, `swap`) are fixed. `FEATURE_KEYWORDS` gains `install`, `when`, `live` and `bind`, and
137
+ `bundleInfo` holds one row.
138
+ - **Dependencies.** `@lokascript/i18n`, `@hyperfixi/testing-framework`, `@hyperfixi/vite-plugin`,
139
+ `@hyperfixi/behaviors`, `@hyperfixi/speech` and `@hyperfixi/intent-element` no longer depend
140
+ on core. Core drops `@lokascript/intent`, morphlex, tslib and its optional peers. Versions are
141
+ lockstep, so upgrade `@hyperfixi/*` and `@lokascript/*` together. `@hyperfixi/mcp-server`,
142
+ `@lokascript/framework` (tests) and `@hyperfixi/server-bridge` (tests) range on
143
+ `@lokascript/domains ^3.0.1`, the first domains release whose peers accept framework,
144
+ semantic and intent 4.x (`^3.1.0 || ^4.0.0`).
145
+
146
+ ### Added
147
+
148
+ - **`@hyperfixi/engine` exports `expr`**, so a grammar module can live in another package
149
+ (#1358).
150
+ - **`@hyperfixi/core/ast-utils`'s `withEnginePositions`** gives interchange nodes the source
151
+ spans of the engine's parse (#1359).
152
+
153
+ ### Fixed
154
+
155
+ - **`@hyperfixi/engine`** accepts upstream's `beep!` command (`beep! a, b`); 3.3.0 rejected it
156
+ (#1362).
157
+ - **`@hyperfixi/types-browser`** types `window.hyperfixi` / `window._hyperscript` as the engine's
158
+ `HyperfixiAPI`. 3.3.0 re-exported a `./globals` it never emitted, so `window.hyperfixi` was
159
+ untyped (#1356).
160
+ - **Semantic browser bundles** (#1351). Single-language and regional bundles (es, ja,
161
+ east-asian, …) registered no English, so the hyperscript adapter left scripts untranslated. They
162
+ now register it (+~2.2 KB gzipped) and export `translate`. The lite adapter finds any
163
+ `LokaScriptSemantic*` global.
164
+ - **`@lokascript/semantic`** (#1338, #1343, #1347). `/core` plus a language module no longer
165
+ throws "No patterns registered". `on click from (x or me)` parses in th and zh. A `js … end`
166
+ block no longer ends its handler in ja / ko / hi / tr / qu / bn. An options object's `}` is no
167
+ longer read as an SOV event name.
168
+ - **Language server and MCP** (#1359, #1363, #1365). `beep!` has hover docs. `set X to`
169
+ completions offer values. Hyperscript-mode errors land on the form, not line 0. Hover no longer
170
+ shows a second `on click`.
171
+
172
+ ## [3.3.0] - 2026-10-02
173
+
174
+ The first publication of `@hyperfixi/engine`, the hyperscript engine meant to replace the one
175
+ in `@hyperfixi/core`, and the example gallery moved onto it.
176
+
177
+ ### Added
178
+
179
+ - **`@hyperfixi/engine` is published.** A hyperscript engine written against upstream
180
+ `_hyperscript`'s source, with upstream's own test suite as the acceptance oracle (1,401 of
181
+ 1,467 tests; the 66 known failures are upstream's internal API, sockets and workers). It
182
+ ships `dist/hyperfixi-hs.js`, a script-tag bundle of hyperscript and nothing else, 34.1 KB
183
+ gzipped, which 36 of the repository's example pages now load instead of `hyperfixi.js`. It
184
+ keeps two forms upstream lacks, `new X(...)` and `toggle <element>`; the other hyperfixi-only
185
+ forms are not in it. It is typed (`tsc --strict`, no `any`), built from modules, and exposes
186
+ upstream's public API shape plus one hook, `addSourceTransform`, which
187
+ `@lokascript/hyperscript-adapter` uses to run the 24 languages on it.
188
+
189
+ ### Changed
190
+
191
+ - **Examples and docs are written in upstream `_hyperscript`'s spelling** wherever upstream has
192
+ one: `put Y into X` for the `swap` strategies, `debounced at 300ms`, `matches`, `set X's @a`,
193
+ `increment #count's textContent` (core counted in a bare `#count`; upstream does not),
194
+ `on mutation of childList from #x`, `my offsetLeft` in place of `measure x`. The multilingual
195
+ reader still accepts the old forms; the renderer writes upstream's. Corpus rows follow.
196
+ - **`@lokascript/semantic` renders `repeat for x in xs index i`** (was `with index`) and
197
+ `tell X show` (was `tell X to show`), upstream's spellings.
198
+ - **The examples' bundle loader** takes a per-page default (`data-default="hs"`), and `?bundle=hs`
199
+ switches any page to the engine bundle.
200
+
201
+ ### Fixed
202
+
203
+ - **`@hyperfixi/core`**: `toggle *display of X` parsed and then threw.
204
+ - **Hybrid bundles (`hyperfixi-hx.js`)**: `increment` / `decrement` of a possessive wrote only
205
+ style properties; `increment #count's textContent` evaluated to the text and threw on
206
+ `querySelectorAll('0')`. They now write the property.
207
+ - **`@lokascript/semantic`**: a template-literal URL in a Korean handler was read as a custom
208
+ event name.
209
+
10
210
  ## [3.2.0] - 2026-09-30
11
211
 
12
212
  A correctness release for multilingual hyperscript. The semantic parser behind
@@ -861,7 +1061,9 @@ _Synchronized version release. See git history for details._
861
1061
  - npm access token stored in GitHub Secrets
862
1062
  - 2FA recommended for npm organization
863
1063
 
864
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.1.0...HEAD
1064
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...HEAD
1065
+ [3.3.0]: https://github.com/codetalcott/hyperfixi/compare/v3.2.0...v3.3.0
1066
+ [3.2.0]: https://github.com/codetalcott/hyperfixi/compare/v3.1.0...v3.2.0
865
1067
  [3.1.0]: https://github.com/codetalcott/hyperfixi/compare/v3.0.0...v3.1.0
866
1068
  [2.10.0]: https://github.com/codetalcott/hyperfixi/compare/v2.9.0...v2.10.0
867
1069
  [2.9.0]: https://github.com/codetalcott/hyperfixi/compare/v2.8.0...v2.9.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "Cross-platform behavior testing suite for LokaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -29,7 +29,7 @@
29
29
  "scripts": {
30
30
  "build": "tsup",
31
31
  "dev": "tsup --watch",
32
- "pretest": "../../scripts/ensure-fresh.sh ../intent ../framework ../semantic ../hyperscript-adapter ../patterns-reference ../core ../aot-compiler ../compilation-service",
32
+ "pretest": "../../scripts/ensure-fresh.sh ../intent ../framework ../semantic ../hyperscript-adapter ../engine ../patterns-reference ../compilation-service",
33
33
  "test": "vitest run",
34
34
  "test:watch": "vitest",
35
35
  "test:coverage": "vitest run --coverage",
@@ -41,7 +41,7 @@
41
41
  "typecheck": "tsc --noEmit",
42
42
  "test:check": "VITEST_TIMEOUT=240 VITEST_QUIET=1 bash ../../scripts/vitest-run.sh --reporter=dot",
43
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",
44
- "test:shipped-sources": "VITEST_TIMEOUT=300 bash ../../scripts/vitest-run.sh --reporter=dot src/multilingual/shipped-sources-validity.test.ts"
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": [
47
47
  "hyperscript",
@@ -57,11 +57,10 @@
57
57
  "author": "LokaScript Contributors",
58
58
  "license": "MIT",
59
59
  "dependencies": {
60
- "@hyperfixi/core": "^3.2.0",
61
- "@hyperfixi/patterns-reference": "^3.2.0",
62
- "@lokascript/compilation-service": "^3.2.0",
63
- "@lokascript/i18n": "^3.2.0",
64
- "@lokascript/semantic": "^3.2.0",
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",
65
64
  "diff": "^8.0.3",
66
65
  "esbuild": "^0.25.12",
67
66
  "happy-dom": "^20.14.5",
@@ -73,7 +72,8 @@
73
72
  "vite": "^8.3.1"
74
73
  },
75
74
  "devDependencies": {
76
- "@lokascript/hyperscript-adapter": "^3.2.0",
75
+ "@hyperfixi/engine": "^4.0.0",
76
+ "@lokascript/hyperscript-adapter": "^4.0.0",
77
77
  "@types/diff": "^5.0.0",
78
78
  "@types/node": "^26.6.3",
79
79
  "@vitest/coverage-v8": "^5.0.0",
@@ -138,7 +138,9 @@ class selector (`#box .has class .danger`) so the condition is always falsy;
138
138
  compilation-service validation pipeline (`validation/inert-shapes.ts`) matches
139
139
  those fingerprints and warns (`INERT_QUANTIFIER_TARGET`,
140
140
  `HALF_PARSED_CONDITION`, `UNSUPPORTED_QUERY_LITERAL`, `INERT_PROPERTY_WRITE`)
141
- — warnings only, still no parser change.
141
+ — warnings only, still no parser change. (`UNSUPPORTED_QUERY_LITERAL` retired with
142
+ the AOT compiler in 4.0: the engine reads query literals, and this benchmark,
143
+ which executes on it since C4, scores `<body/>` correct.)
142
144
 
143
145
  | | pre-3b | after slice 1 | after slice 2 |
144
146
  | ------------------------------------- | ----------- | ------------- | ---------------------- |
@@ -153,6 +155,51 @@ parser-gap silent band is now zero.**
153
155
  Bands are computed by `harness.bandOf` — one function shared by the probe, the
154
156
  committed baseline, and the ratchet test, so they cannot drift apart.
155
157
 
158
+ ### Phase C4b: executed on the engine (2026-10-04)
159
+
160
+ Execution moved from `@hyperfixi/core`'s runtime (fed semantic's parse through
161
+ `buildAST`) to `@hyperfixi/engine`, the engine the product ships: the candidate
162
+ is the button's `_` attribute, as on a page. Validation is unchanged
163
+ (`CompilationService.validate`, semantic).
164
+
165
+ | | core runtime | engine |
166
+ | ------------------------------------- | ------------ | -------------- |
167
+ | parse | 36/37 | 36/37 |
168
+ | behave correctly | 20/37 | **17/37** |
169
+ | wrong but **warned** (loop can react) | 14/37 | 12/37 |
170
+ | wrong and **silent** | 2/37 (5%) | **8/37 (22%)** |
171
+
172
+ Four rows got better: core misbehaved on `add @aria-expanded="true" to`,
173
+ `set the innerHTML of`, `set the style.backgroundColor of`, and `on "refresh"`
174
+ (a quoted event name), all of which the engine runs as upstream does. Six got
175
+ worse, and all six are the same kind: phrasings only core's lenient parser
176
+ read — `and` between commands (two rows: `… and put …`, `remove … and add …`),
177
+ `add .x on #item`, `put "x" in #el`, `toggle .active on this`, `toggle class
178
+ .active` (and `, put …` warns). The engine rejects each, as upstream does, while
179
+ the validator still says `ok`: they are silent because validation reads
180
+ semantic's grammar, not the engine's. Phase C4d moves the diagnostics onto the
181
+ engine's parse (owner decision 5), which should make all six visible.
182
+
183
+ ### Phase C4d: validated on the engine (2026-10-04)
184
+
185
+ `CompilationService.validate` now also puts natural-language input to `@hyperfixi/engine`'s
186
+ parser (the English as written, or a translation's English render), and a rejection is an
187
+ error (`ENGINE_PARSE_ERROR`). Behavior is unchanged; what the loop can SEE changes:
188
+
189
+ | | C4b (semantic validator) | engine validator |
190
+ | ------------------------------------- | ------------------------ | ---------------- |
191
+ | parse (validator says ok) | 36/37 | **20/37** |
192
+ | behave correctly | 17/37 | 17/37 |
193
+ | wrong but **warned** | 12/37 | 1/37 |
194
+ | wrong and **rejected** (loop repairs) | 0/37 | **16/37** |
195
+ | wrong and **silent** | 8/37 | **3/37** |
196
+
197
+ Sixteen rows moved to `rejected`: eleven that only warned (the engine rejects what the
198
+ inert-shape and unconsumed-input warnings described) and five of the six silent ones C4b
199
+ measured. The three silent rows left are code the engine reads: `toggle .active on this`
200
+ (`this` is a variable there, null at run time), and the valid-code-different-intent pair
201
+ (`add .hidden to #menu`, `on mouseover`).
202
+
156
203
  ## Adding a task
157
204
 
158
205
  Append to [`tasks.ts`](./tasks.ts): a prompt with no hyperscript in it, a
@@ -21,6 +21,13 @@
21
21
  * Deterministic and generator-free: no LLM runs here, so this IS a legitimate
22
22
  * CI gate (the A/B run in README.md, which needs a generator, deliberately is
23
23
  * not). Full sweep measures ~6s.
24
+ *
25
+ * @vitest-environment node
26
+ * Required, not a preference: candidates execute on @hyperfixi/engine in jsdom,
27
+ * and under the suite default (happy-dom) the DOM constructors already exist on
28
+ * globalThis, so the engine binds happy-dom's and every jsdom element fails its
29
+ * instanceof checks (measured: every reference produced no effect). Same reason
30
+ * as shipped-examples-execution.test.ts.
24
31
  */
25
32
 
26
33
  import { describe, it, expect, beforeAll } from 'vitest';
@@ -18,15 +18,19 @@
18
18
  * catch, so `parseRate` and `behaviorRate` are reported side by side and their
19
19
  * GAP is a headline number in its own right.
20
20
  *
21
- * Determinism: fresh JSDOM + fresh Runtime per execution, no network, no timers
22
- * beyond a fixed settle window. Effect-signature primitives are imported from
21
+ * Execution is `@hyperfixi/engine`, the product's engine: the candidate is the
22
+ * button's `_` attribute, as on a page. (Until Phase C4 it was `@hyperfixi/core`'s
23
+ * runtime, fed semantic's parse through `buildAST`.)
24
+ *
25
+ * Determinism: a fresh JSDOM per execution, no network, no timers beyond a
26
+ * fixed settle window. Effect-signature primitives are imported from
23
27
  * ../multilingual/effect-signature so this harness and the R2 ratchet can never
24
28
  * disagree about what a DOM effect is.
25
29
  */
26
30
 
27
31
  import { JSDOM } from 'jsdom';
28
- import { parseSemantic, buildAST } from '@lokascript/semantic';
29
32
  import { snapshot, diffSnapshots } from '../multilingual/effect-signature.js';
33
+ import { installGlobals } from '../multilingual/shipped-examples-execution.js';
30
34
  import { SHARED_FIXTURE, type BenchTask } from './tasks.js';
31
35
 
32
36
  /** Settle window for the dispatched handler. No task waits/fetches. */
@@ -156,43 +160,29 @@ function buildDocument(task: BenchTask): JSDOM {
156
160
  return new JSDOM(`<!DOCTYPE html><html><body>${SHARED_FIXTURE}${task.fixture}</body></html>`);
157
161
  }
158
162
 
159
- function installGlobals(dom: JSDOM): void {
160
- const g = globalThis as Record<string, unknown>;
161
- g.window = dom.window;
162
- g.document = dom.window.document;
163
- g.Event = dom.window.Event;
164
- g.CustomEvent = dom.window.CustomEvent;
165
- g.HTMLElement = dom.window.HTMLElement;
166
- g.Element = dom.window.Element;
167
- g.Node = dom.window.Node;
168
- g.MutationObserver = dom.window.MutationObserver;
169
- g.getComputedStyle = dom.window.getComputedStyle;
163
+ /** The engine's public surface this harness uses. */
164
+ interface ScriptHost {
165
+ parse(source: string): { errors: Array<{ message: string }> };
166
+ processNode(node: unknown): void;
170
167
  }
171
168
 
172
- let core: {
173
- Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
174
- createContext: (el: HTMLElement) => unknown;
175
- } | null = null;
176
-
177
- let listenerErrors: string[] = [];
178
- let trapInstalled = false;
169
+ let engine: ScriptHost | null = null;
179
170
 
180
- /** Bootstraps jsdom globals BEFORE loading core (its dist evaluates `Element`). */
171
+ /** Bootstraps jsdom globals, then loads the engine with every module registered. */
181
172
  export async function initialize(): Promise<void> {
182
- if (core) return;
173
+ if (engine) return;
183
174
  installGlobals(new JSDOM('<!DOCTYPE html><html><body></body></html>'));
184
- const mod = (await import('@hyperfixi/core')) as unknown as {
185
- Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
186
- createContext: (el: HTMLElement) => unknown;
187
- };
188
- core = { Runtime: mod.Runtime, createContext: mod.createContext };
189
- if (!trapInstalled) {
190
- // Handler bodies are async, so a throw inside one surfaces as an unhandled
191
- // rejection rather than propagating to our await.
192
- process.on('unhandledRejection', (reason: unknown) => {
193
- listenerErrors.push(reason instanceof Error ? reason.message : String(reason));
194
- });
195
- trapInstalled = true;
175
+ const mod = await import('@hyperfixi/engine');
176
+ mod.register(...mod.everything);
177
+ engine = mod.api;
178
+ }
179
+
180
+ /** The engine's first parse error for a source, first line; undefined when it parses. */
181
+ function parseError(code: string): string | undefined {
182
+ try {
183
+ return engine!.parse(code).errors[0]?.message.split('\n')[0];
184
+ } catch (e: unknown) {
185
+ return (e instanceof Error ? e.message : String(e)).split('\n')[0];
196
186
  }
197
187
  }
198
188
 
@@ -203,22 +193,21 @@ async function executeInner(task: BenchTask, code: string): Promise<ExecutionOut
203
193
  const document = dom.window.document;
204
194
  task.setup?.(document);
205
195
  const btn = document.getElementById('btn')!;
206
- listenerErrors = [];
207
196
 
208
- // The runtime logs every failing command; across a full run that is noise.
197
+ // The engine reports a failing handler on the console (`hyperscript errors were
198
+ // found…`): record those, and drop the rest of the noise.
199
+ const runtimeErrors: string[] = [];
209
200
  const saved = { log: console.log, warn: console.warn, error: console.error };
210
- console.log = console.warn = console.error = () => {};
201
+ console.log = console.warn = () => {};
202
+ console.error = (...args: unknown[]) => {
203
+ const error = args.find(a => a instanceof Error);
204
+ runtimeErrors.push(error instanceof Error ? error.message : String(args[0]));
205
+ };
211
206
  try {
212
- const parsed = parseSemantic(code, 'en') as { node?: unknown; confidence?: number };
213
- if (!parsed.node || (parsed.confidence ?? 0) < 0.5) {
214
- return { effects: [], error: `parse failed (confidence ${parsed.confidence ?? 0})` };
215
- }
216
- const built = buildAST(parsed.node as never) as { ast?: unknown };
217
- if (!built.ast) return { effects: [], error: 'buildAST returned no AST' };
218
-
219
- const runtime = new core!.Runtime();
220
- const ctx = core!.createContext(btn as unknown as HTMLElement);
221
- await runtime.execute(built.ast, ctx);
207
+ const rejected = parseError(code);
208
+ if (rejected !== undefined) return { effects: [], error: `parse failed (engine: ${rejected})` };
209
+ btn.setAttribute('_', code);
210
+ engine!.processNode(btn);
222
211
 
223
212
  const before = snapshot(document);
224
213
  const trig = task.trigger ?? { event: 'click' };
@@ -232,8 +221,8 @@ async function executeInner(task: BenchTask, code: string): Promise<ExecutionOut
232
221
  await new Promise(r => setTimeout(r, SETTLE_MS));
233
222
 
234
223
  const effects = diffSnapshots(before, snapshot(document));
235
- return listenerErrors.length > 0
236
- ? { effects, error: `runtime: ${listenerErrors.join('; ')}` }
224
+ return runtimeErrors.length > 0
225
+ ? { effects, error: `runtime: ${runtimeErrors.join('; ')}` }
237
226
  : { effects };
238
227
  } catch (e: unknown) {
239
228
  return { effects: [], error: e instanceof Error ? e.message : String(e) };
@@ -256,17 +256,22 @@ Add to `.github/workflows/test.yml`:
256
256
  (literal, variable, selector, possessive, `of`, dotted, call, array, parens), the
257
257
  operators, and five positions (`put` and `set` values, `if` and `repeat while`
258
258
  conditions, `increment … by`). Each cell's English runs on the real
259
- `hyperscript.org` engine, which is the oracle, and then in 48 lanes:
260
-
261
- | Lane | What runs |
262
- | -------- | ---------------------------------------------------------------------------- |
263
- | `en` | hyperfixi's English path (core's parser and runtime) |
264
- | `en-rt` | semantic's English parse, rendered back to English, on upstream |
265
- | `<L>` | each of 23 languages on hyperfixi's direct path |
266
- | `<L>/up` | the same translation, through `@lokascript/hyperscript-adapter`, on upstream |
267
-
268
- `baselines/value-matrix.json` lists every failing (cell, lane) pair; `*direct` and
269
- `*up` stand for all 23 lanes of each. The gate runs in five shards,
259
+ `hyperscript.org` engine, which is the oracle, and then in 49 lanes:
260
+
261
+ | Lane | What runs |
262
+ | --------- | ---------------------------------------------------------------------------- |
263
+ | `en` | hyperfixi's English path (core's parser and runtime) |
264
+ | `en-rt` | semantic's English parse, rendered back to English, on upstream |
265
+ | `eng` | the English source on `@hyperfixi/engine` |
266
+ | `<L>/up` | each of 23 languages, through `@lokascript/hyperscript-adapter`, on upstream |
267
+ | `<L>/eng` | the same adapter English on `@hyperfixi/engine` |
268
+
269
+ The `<L>` lane (each language on core's direct path) retired in Phase C2 of the
270
+ engine cutover, when text became the multilingual interchange; no cell failed
271
+ `<L>/eng` while passing `<L>`.
272
+
273
+ `baselines/value-matrix.json` lists every failing (cell, lane) pair; `*up` and
274
+ `*eng` stand for all 23 lanes of each. The gate runs in fifteen shards,
270
275
  `value-matrix.<position>.test.ts`, and fails on a failing pair the baseline does not
271
276
  list and on a listed pair that passes, so the list only shrinks.
272
277
 
@@ -289,6 +294,20 @@ npx tsx tools/regen-value-matrix-baseline.ts --dry-run --results /tmp/matrix.jso
289
294
  A lane that fails in `en-rt` fails in nearly every translation: semantic's English
290
295
  parse lost the value, and every translation is rendered from it. Fix that first.
291
296
 
297
+ ## Direct-path shapes on the text path
298
+
299
+ Core's `src/multilingual/*-direct-path.test.ts` files pin shapes core's direct path ran
300
+ (an English source rendered into 23 languages, compiled with `{ language }` on core,
301
+ installed in a fixture, triggered). That path retires with core's parser, so
302
+ `direct-path-shapes.ts` runs the same 287 cases — extracted once into
303
+ `direct-path-shapes.cases.json` — the way a page now runs a translation: the foreign
304
+ text as written on `@hyperfixi/engine` with the adapter's plugin and `lang` on `<html>`.
305
+ Upstream running the English is the oracle; where upstream rejects a core-only English
306
+ source, upstream running the adapter's English is. Signatures are the body diff (text
307
+ nodes normalized) plus a log of `fetch` / `history` / `window.open` / `scrollIntoView`
308
+ calls. `KNOWN` lists the 4 failing cases with their reasons and only shrinks; the gate
309
+ runs in three shards, `direct-path-shapes.<n>.test.ts`.
310
+
292
311
  ## Troubleshooting
293
312
 
294
313
  ### Bundle not found
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * It matters because the bare form is a first-class public surface: MCP
16
16
  * `translate_code`, `hyperfixi.translate`/`getAllTranslations`, core's
17
- * `MultilingualHyperscript` and the VS Code "Show in my language" badge are all
17
+ * `multilingual` entry and the VS Code "Show in my language" badge are all
18
18
  * routinely handed a single command with no handler around it.
19
19
  *
20
20
  * WHAT IT ASSERTS
@@ -25,7 +25,9 @@ const DIST_GUARD_PACKAGES = [
25
25
  'semantic',
26
26
  'i18n',
27
27
  'patterns-reference',
28
- 'core',
28
+ 'hyperscript-adapter',
29
+ // The R2 execution validator's host since Phase C2 (core's left in C4).
30
+ 'engine',
29
31
  ];
30
32
 
31
33
  /** True if any .ts under dir is newer than builtAt (early-exit walk). */
@@ -54,8 +56,8 @@ function findStaleDists(): string[] {
54
56
  for (const name of DIST_GUARD_PACKAGES) {
55
57
  const pkg = path.join(packagesRoot, name);
56
58
  const srcDir = path.join(pkg, 'src');
57
- // Whichever entry the build emits: `.js` for most packages, `.mjs` for core
58
- // (its CJS twin is `.cjs` — see scripts/ensure-fresh.sh).
59
+ // Whichever entry the build emits: `.js` for most packages, `.mjs` / `.cjs`
60
+ // for a dual build (see scripts/ensure-fresh.sh).
59
61
  const marker = ['index.js', 'index.mjs', 'index.cjs']
60
62
  .map(f => path.join(pkg, 'dist', f))
61
63
  .filter(f => fs.existsSync(f))
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The direct-path shapes gate (see direct-path-shapes.ts), in shards —
3
+ * `direct-path-shapes.<n>.test.ts` — so vitest runs them in parallel.
4
+ *
5
+ * Assertions, per shard:
6
+ * 1. every case not in KNOWN holds on the text path: upstream runs its
7
+ * English, the engine runs that English the same way, and every
8
+ * language's translation runs on the engine as upstream runs the English;
9
+ * 2. every KNOWN case still fails — prune one that passes, so the list only
10
+ * shrinks;
11
+ * 3. (shard 0) every KNOWN id names a case.
12
+ */
13
+
14
+ import { describe, it, expect, beforeAll } from 'vitest';
15
+ import {
16
+ KNOWN,
17
+ caseFails,
18
+ describeFailure,
19
+ loadDirectPathCases,
20
+ runDirectPathShard,
21
+ type DirectPathResult,
22
+ } from './direct-path-shapes';
23
+
24
+ export const DIRECT_PATH_SHARDS = 3;
25
+
26
+ export function describeDirectPathShard(shard: number): void {
27
+ describe(`direct-path shapes on the text path [${shard + 1}/${DIRECT_PATH_SHARDS}]`, () => {
28
+ let results: DirectPathResult[] = [];
29
+
30
+ beforeAll(async () => {
31
+ results = await runDirectPathShard(shard, DIRECT_PATH_SHARDS);
32
+ const pairs = results.reduce((n, r) => n + r.pairs.length, 0);
33
+ const failing = results.filter(caseFails).length;
34
+ console.log(
35
+ `direct-path shapes [${shard + 1}/${DIRECT_PATH_SHARDS}]: ${results.length} cases, ${pairs} translations, ${failing} failing case(s)`
36
+ );
37
+ }, 600_000);
38
+
39
+ it('every case not in KNOWN holds on the text path', () => {
40
+ expect(results.length).toBeGreaterThan(50);
41
+ const failing = results.filter(r => caseFails(r) && !(r.id in KNOWN));
42
+ expect(failing.map(describeFailure)).toEqual([]);
43
+ });
44
+
45
+ it('every KNOWN case still fails (prune one that passes)', () => {
46
+ const fixed = results.filter(r => r.id in KNOWN && !caseFails(r)).map(r => r.id);
47
+ expect(fixed).toEqual([]);
48
+ });
49
+
50
+ it('every KNOWN id names a case', () => {
51
+ if (shard !== 0) return;
52
+ const ids = new Set(loadDirectPathCases().map(c => c.id));
53
+ expect(Object.keys(KNOWN).filter(id => !ids.has(id))).toEqual([]);
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Direct-path shapes gate, shard 1 of 3. See direct-path-shapes.ts for what it
3
+ * runs and direct-path-shapes-gate.ts for the assertions.
4
+ *
5
+ * @vitest-environment node
6
+ * Required, not a preference: under happy-dom the engines bind happy-dom's
7
+ * DOM constructors, not the jsdom window's (see
8
+ * shipped-examples-execution.test.ts).
9
+ */
10
+ import { describeDirectPathShard } from './direct-path-shapes-gate';
11
+
12
+ describeDirectPathShard(0);