@hyperfixi/testing-framework 3.3.0 → 4.0.1

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 (28) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/package.json +9 -10
  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/orchestrator.ts +2 -1
  16. package/src/multilingual/render-fidelity.ts +1 -1
  17. package/src/multilingual/shipped-examples-execution.test.ts +32 -96
  18. package/src/multilingual/shipped-examples-execution.ts +31 -121
  19. package/src/multilingual/shipped-sources-engine.test.ts +19 -19
  20. package/src/multilingual/shipped-sources-localized.test.ts +87 -0
  21. package/src/multilingual/shipped-sources-validity.ts +87 -99
  22. package/src/multilingual/validators/execution-validator.test.ts +81 -23
  23. package/src/multilingual/validators/execution-validator.ts +135 -91
  24. package/src/multilingual/validators/parse-validator.ts +7 -20
  25. package/src/multilingual/value-matrix.accepted.test.ts +8 -14
  26. package/src/multilingual/value-matrix.isolation.test.ts +7 -6
  27. package/src/multilingual/value-matrix.ts +32 -84
  28. package/src/multilingual/shipped-sources-validity.test.ts +0 -95
package/CHANGELOG.md CHANGED
@@ -7,6 +7,202 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.0.1] - 2026-10-05
11
+
12
+ Fixes for 4.0.0: the per-language adapter bundles read every language again, and the MCP server's
13
+ documentation and core's metadata describe the 4.0 engine.
14
+
15
+ ### Fixed
16
+
17
+ - **`@lokascript/hyperscript-adapter`: the self-contained per-language and regional bundles read
18
+ German, French, Quechua and Chinese again.** `hyperscript-i18n-<lang>.global.js` and the regional
19
+ bundles installed a generate-only pattern generator over the one `@lokascript/semantic/core`
20
+ provides, discarding its hand-crafted patterns: in de, fr, qu and zh even
21
+ `on click toggle .active` stayed untranslated (the host then rejected it), and other languages
22
+ lost shapes such as `toggle … on #target`; 1106 of the corpus's 3772 translations read
23
+ differently from the full package. Published 3.x and 4.0.0 bundles alike; the all-languages
24
+ `hyperscript-i18n.global.js` was unaffected. The bundles now keep `/core`'s generator (sizes
25
+ unchanged: the ~23 KB 4.0.0 added was this generator, unused until now). Their English renderer
26
+ also drops only an implicit `me` now, so `put "x" into me` no longer renders as the
27
+ engine-invalid `put "x"` in every language. New gate: every built bundle runs translated
28
+ handlers on the engine (`test/adapter-iife.test.ts`).
29
+ - **`@hyperfixi/mcp-server`'s documentation resources teach upstream's spelling.**
30
+ `hyperscript://docs/commands`, `/expressions`, `/events` and `hyperscript://examples/common` still
31
+ taught core 3.x: `on submit.prevent`, `on input.debounce(300ms)`, `on keydown.enter`,
32
+ `swap #t innerHTML`, `while …`, a bare `[attr]` selector, a `parent` keyword, `you` as the event
33
+ target. The engine rejects those, or reads them as something else (`on click.prevent` listens for
34
+ an event named `click.prevent`). A test now parses every example on the engine. The repo's
35
+ `hyperfixi-developer` skill, generated from them, follows (`generate:skills` had been writing to
36
+ the wrong directory).
37
+ - **`@hyperfixi/core/metadata` describes 4.0.** `packageInfo.description` still said "Modern
38
+ hyperscript engine with fixi/htmx integration" and `compatibility` "~85% official _hyperscript",
39
+ both 3.x claims. `packageInfo.upstreamSuite` (`{ version, passed, total }`) now publishes the
40
+ engine's result on upstream _hyperscript's own test suite (1401 of 1467 in 0.9.93), and
41
+ `compatibility` is derived from it; `verify:reference` fails when it disagrees with
42
+ `packages/engine/upstream-suite/known-failures.json`. `ecosystem` drops "212 LLM examples".
43
+
44
+ ## [4.0.0] - 2026-10-04
45
+
46
+ `@hyperfixi/core`'s own hyperscript engine is gone: its root re-exports `@hyperfixi/engine`, and
47
+ its `hyperfixi.js` is the engine's 34 KB script-tag bundle where core's ~352 KB bundle used to be.
48
+ The engine follows upstream `_hyperscript`'s grammar and is gated by upstream's own test suite, so
49
+ scripts in core-only syntax need upstream's spelling (two additions stay: `new X()` and
50
+ `toggle <element>`). Core's other bundles, its htmx layer, the core-era plugin packages and the
51
+ AOT compiler retire: htmx 4 or fixi supply hypermedia attributes beside the engine, and
52
+ `@lokascript/hyperscript-adapter` runs hyperscript written in other languages. Every package moves
53
+ to 4.0.0 together. Everything under **Removed** is breaking, as is each **Changed** item marked ⚠;
54
+ [MIGRATION.md](MIGRATION.md) names each replacement.
55
+
56
+ ### Removed
57
+
58
+ - **Core's engine and its API** (#1368). These root exports had no engine counterpart:
59
+ `hyperscript` (`compile`, `compileSync`, `compileAsync`, `eval`, `validate`), `lokascript`,
60
+ `Runtime`, `RuntimeBase`, `createContext` and the element-scope helpers, `installPlugin`,
61
+ `getParserExtensionRegistry`, `setGlobal`, `DebugController`, `validatePartialContent` and its
62
+ helpers, `fromCoreAST` / `toCoreAST`. `parse` is now the engine's. The root is ESM-only, like the
63
+ engine: `dist/index.cjs` and the UMD `dist/index.min.js` (`LokaScriptCore`) are gone, though the
64
+ kept subpaths still ship CommonJS.
65
+ - **21 of core's 29 entry points**: `/commands`, `/expressions`, `/parser/*`, `/behaviors`,
66
+ `/bundle-generator`, `/registry/*`, `/lse`, and `/browser/` `hybrid-complete`, `hybrid-hx`,
67
+ `hybrid-hx-v4`, `multilingual`, `modular` (#1343–#1368). Instead of `/commands`, import the
68
+ grammar modules from the root: `import { toggle } from '@hyperfixi/core'`.
69
+ - **Core's browser bundles** (#1348, #1350, #1352, #1368): `hyperfixi-hx.js`,
70
+ `hyperfixi-hx-v4.js`, `hyperfixi-hybrid-complete.js`, `hyperfixi-multilingual.js`,
71
+ `hyperfixi-classic-i18n.js` and the code-split `hyperfixi.mjs`, plus every alias
72
+ (`hyperfixi-browser.js`, `hyperfixi-hybrid-hx.js`, each `lokascript-*.js`). Only
73
+ `hyperfixi.js` remains. CDN URLs pinned to 3.x keep working.
74
+ - **Core's embedded htmx layer** (#1342, #1348, #1350): `hx-*` and fixi `fx-*` on core's runtime,
75
+ `hx-live`, `sse-connect` / `sse-swap`, `ws-connect` / `ws-send`. Load htmx 4 or fixi beside the
76
+ engine. `hx-live` becomes the engine's `live` block, and SSE and WebSockets are htmx 4's
77
+ `hx-sse` / `hx-ws`.
78
+ - **Core-only members of `window.hyperfixi`**: `compile`, `compileSync`, `execute`, `setLocale`,
79
+ `evalLSENode`, `debugControl`, `semanticDebug`, the `hyperfixi:semantic-parse` event,
80
+ `hyperfixi:debug` logging, `_hyperscript.behaviors.resolve`, and the deprecated
81
+ `window.lokascript` (#1352, #1355).
82
+ - **Core-only hyperscript syntax** (#1346, #1363, #1365). The engine rejects `has` / `have`,
83
+ `prepend`, `copy`, `push url` / `replace url`, `process partials`, the `pseudo` keyword, prefix
84
+ `unless`, bare `beep`, `a ? b : c`, core's `swap <strategy> of X with Y`, `set @a to v on X`,
85
+ `.debounce(n)` / `.throttle(n)`, `my?.x` and `tell X to …`. A few forms still parse but no longer
86
+ do what they did, such as `toggle .a on this` and `go forward`. MIGRATION.md gives each form's
87
+ upstream spelling. The multilingual reader still accepts them all.
88
+ - **`@hyperfixi/core/multilingual`'s class API** (#1345, #1368): `MultilingualHyperscript` (with
89
+ `parseToAST`, `getAllTranslations`, …), `getMultilingual`, `multilingual`, `LanguageInfo`,
90
+ `schemaRoleInferrer`. The entry is three async functions: `parse`, `render`, `translate`.
91
+ - **The AOT compiler** (#1364). Its output ran only on a runtime that was never published.
92
+ `@lokascript/compilation-service` loses `compile()`, `POST /compile`, `CompileResponse`, the
93
+ compile cache (`SemanticCache`, `getCacheStats`, `clearCache`, `/cache`), `generateTests`'
94
+ `executionMode` and `generate()`'s `'js'` target. `generate()` now requires `target`
95
+ (`react` | `vue` | `svelte` | `intent-element`). `@hyperfixi/mcp-server` loses
96
+ `compile_hyperscript`, `execute_lse`, `lse_to_hyperscript`'s `compile` option and
97
+ `generate_tests`' `executionMode`, leaving 106 tools. The loop is now `validate_and_compile`
98
+ (name kept), then repair, then the hyperscript as is in `_="…"`.
99
+ - **`@hyperfixi/vite-plugin`'s compile mode** (#1360). `mode: 'compile'` warns and builds the
100
+ engine bundle. The exports `CompiledHandler`, `CompileOptions` and
101
+ `set/clear/hasSemanticParser` are removed.
102
+ - **Smaller removals.** `@hyperfixi/core/reference` loses `availability`, `BundleAvailability`
103
+ and `getCommandsByAvailability` (#1363). `@hyperfixi/speech` loses `speechPlugin`,
104
+ `speakCommand`, `askCommand` and `answerCommand`; the engine has `ask` / `answer` (#1358).
105
+ `@hyperfixi/intent-element` loses its `evalLSENode` fallback (#1356).
106
+ `@hyperfixi/patterns-reference` loses four rows that held only retired htmx attributes, and
107
+ `hx-live-with-mutator` is renamed `live-with-handler` (#1348). Of the never-published VS Code
108
+ extensions, the LokaScript one loses its debugger and the standalone `_hyperscript` one is
109
+ retired (#1361).
110
+
111
+ ### Deprecated
112
+
113
+ - **`@hyperfixi/reactivity`, `@hyperfixi/realtime`, `@hyperfixi/components`**: deprecated on npm,
114
+ last version 3.3.0 (#1358). Use the engine's built-in `live` / `when` / `bind`, htmx 4's
115
+ `hx-sse` / `hx-ws`, and upstream's component extension.
116
+ - **`@hyperfixi/types-browser`'s `LokaScriptCoreAPI`**: a deprecated alias of `HyperfixiAPI`
117
+ (#1356).
118
+
119
+ ### Changed
120
+
121
+ - ⚠ **`@hyperfixi/core` is the engine plus tooling** (#1368). The root is
122
+ `export * from '@hyperfixi/engine'` and `VERSION`: `register`, `everything` and each grammar
123
+ module, `api`, `parse`, `evaluate`, `processNode`, `boot`, and the AST types. The engine stays
124
+ external, so both packages share one grammar. Kept: `/multilingual`, `/ast-utils`,
125
+ `/reference`, `/metadata`, `/lsp-metadata`, `/browser`.
126
+ - ⚠ **`hyperfixi.js` (`@hyperfixi/core/browser`) is the engine's `hyperfixi-hs.js`**, byte for
127
+ byte: 34 KB gzipped instead of ~352 KB (#1355). `window.hyperfixi` is `window._hyperscript`,
128
+ upstream-shaped (`evaluate`, `parse`, `process`, `use`, `config`, `addSourceTransform`). Parse
129
+ errors are a `hyperscript:parse-error` event on the element plus a `console.error` (#1354).
130
+ - ⚠ **Runtime semantics follow upstream** wherever core's differed. `swap #a with #b` exchanges
131
+ the elements; `show` / `hide … with *opacity` honour the strategy; `tell` binds `you`, not `me`;
132
+ `set` on a selector sets every match; and `no ""` is true. MIGRATION.md has the full list.
133
+ - ⚠ **`@hyperfixi/vite-plugin` emits a bundle on `@hyperfixi/engine`** (#1343). There is one
134
+ tier: the grammar modules the scan finds. It weighs 17.9 KB gzipped for three commands (3.x lite:
135
+ 3.9 KB) and 34.4 KB for everything; 3.x fell back to 352 KB on `fetch`. htmx is not bundled.
136
+ Non-English scripts are translated as the engine reads them. `devFallback: 'everything'` is new,
137
+ and `'full'` / `'hybrid-complete'` mean the same. The plugin no longer depends on core.
138
+ - ⚠ **`@hyperfixi/behaviors` runs on the engine** (peer `@hyperfixi/engine`; upstream works too).
139
+ Each behavior is defined with `host.evaluate(source)` and written in upstream's idioms.
140
+ Toggleable's `target` parameter is now `targetEl`. `LokaScriptInstance` / `LokaScriptWindow`
141
+ are now `HyperscriptHost` / `HyperscriptWindow` (#1338).
142
+ - ⚠ **`@hyperfixi/speech` is an engine module** (#1358). Install it with
143
+ `register(...everything, speak)`. It is ESM-only and peers on the engine. `speak` takes
144
+ upstream's syntax (`speak <text> [with voice|rate|pitch|volume <x>]…`) and waits for the
145
+ utterance to end.
146
+ - ⚠ **The localized htmx vocabulary moved** from `@hyperfixi/core/vocab/htmx/{lang}.js` to
147
+ `@lokascript/htmx-adapter/vocab/{lang}.js`; the data is unchanged (#1349).
148
+ - **`@hyperfixi/intent-element`** renders an intent to English with the page's semantic bundle
149
+ and runs it through the host's `evaluate` (`NO_RENDERER` without one). It peers on
150
+ `@hyperfixi/engine` and `@lokascript/semantic` (#1339, #1356).
151
+ - **`@lokascript/semantic` writes upstream's spelling in English** for core-only forms (#1346).
152
+ For example, `has` becomes `matches`, `prepend` becomes `put … at start of`, and swap strategies
153
+ become `put … into` / `before` / `after`. This reaches `translate(…, 'en')`,
154
+ `render(node, 'en')`, both adapters and MCP `translate_to_english`. In `fromSemanticAST`, a
155
+ handler's `then` chain is now its body (#1359).
156
+ - **The language tools read the engine's parse** (#1356, #1359, #1365).
157
+ `@lokascript/language-server` and MCP `validate_hyperscript` report the engine's errors
158
+ (`source: 'engine'`). Hover and symbols come from `@lokascript/semantic`, and some long handlers
159
+ show fewer commands than in 3.x. Hyperscript mode flags exactly `new X()` and `toggle <element>`.
160
+ `@lokascript/compilation-service` rejects natural-language input the engine cannot read
161
+ (`ENGINE_PARSE_ERROR`), and its generated Playwright tests load `hyperfixi-hs.js`. The language
162
+ server, the MCP server and the compilation service now peer on `@hyperfixi/engine`.
163
+ - **MCP**: `get_bundle_config` recommends `hyperfixi-hs.js`, plus an `adapter` field for
164
+ non-English. `analyze_complexity` / `analyze_metrics` no longer count every comparison as a
165
+ decision. The `debug_*` tools point to `log` and `breakpoint`. `UNSUPPORTED_QUERY_LITERAL` is
166
+ gone (#1350, #1352, #1359, #1361, #1364).
167
+ - **`@hyperfixi/core/reference`, `/lsp-metadata`, `/metadata` describe the engine** (#1355,
168
+ #1363). They cover its 53 commands, adding `for`, `ask`, `answer` and `beep!`. Examples are in
169
+ upstream spelling and each is checked to parse; descriptions of core-only semantics (`pick`,
170
+ `beep`, `swap`) are fixed. `FEATURE_KEYWORDS` gains `install`, `when`, `live` and `bind`, and
171
+ `bundleInfo` holds one row.
172
+ - **Dependencies.** `@lokascript/i18n`, `@hyperfixi/testing-framework`, `@hyperfixi/vite-plugin`,
173
+ `@hyperfixi/behaviors`, `@hyperfixi/speech` and `@hyperfixi/intent-element` no longer depend
174
+ on core. Core drops `@lokascript/intent`, morphlex, tslib and its optional peers. Versions are
175
+ lockstep, so upgrade `@hyperfixi/*` and `@lokascript/*` together. `@hyperfixi/mcp-server`,
176
+ `@lokascript/framework` (tests) and `@hyperfixi/server-bridge` (tests) range on
177
+ `@lokascript/domains ^3.0.1`, the first domains release whose peers accept framework,
178
+ semantic and intent 4.x (`^3.1.0 || ^4.0.0`).
179
+
180
+ ### Added
181
+
182
+ - **`@hyperfixi/engine` exports `expr`**, so a grammar module can live in another package
183
+ (#1358).
184
+ - **`@hyperfixi/core/ast-utils`'s `withEnginePositions`** gives interchange nodes the source
185
+ spans of the engine's parse (#1359).
186
+
187
+ ### Fixed
188
+
189
+ - **`@hyperfixi/engine`** accepts upstream's `beep!` command (`beep! a, b`); 3.3.0 rejected it
190
+ (#1362).
191
+ - **`@hyperfixi/types-browser`** types `window.hyperfixi` / `window._hyperscript` as the engine's
192
+ `HyperfixiAPI`. 3.3.0 re-exported a `./globals` it never emitted, so `window.hyperfixi` was
193
+ untyped (#1356).
194
+ - **Semantic browser bundles** (#1351). Single-language and regional bundles (es, ja,
195
+ east-asian, …) registered no English, so the hyperscript adapter left scripts untranslated. They
196
+ now register it (+~2.2 KB gzipped) and export `translate`. The lite adapter finds any
197
+ `LokaScriptSemantic*` global.
198
+ - **`@lokascript/semantic`** (#1338, #1343, #1347). `/core` plus a language module no longer
199
+ throws "No patterns registered". `on click from (x or me)` parses in th and zh. A `js … end`
200
+ block no longer ends its handler in ja / ko / hi / tr / qu / bn. An options object's `}` is no
201
+ longer read as an SOV event name.
202
+ - **Language server and MCP** (#1359, #1363, #1365). `beep!` has hover docs. `set X to`
203
+ completions offer values. Hyperscript-mode errors land on the form, not line 0. Hover no longer
204
+ shows a second `on click`.
205
+
10
206
  ## [3.3.0] - 2026-10-02
11
207
 
12
208
  The first publication of `@hyperfixi/engine`, the hyperscript engine meant to replace the one
@@ -899,7 +1095,9 @@ _Synchronized version release. See git history for details._
899
1095
  - npm access token stored in GitHub Secrets
900
1096
  - 2FA recommended for npm organization
901
1097
 
902
- [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...HEAD
1098
+ [Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v4.0.1...HEAD
1099
+ [4.0.1]: https://github.com/codetalcott/hyperfixi/compare/v4.0.0...v4.0.1
1100
+ [4.0.0]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...v4.0.0
903
1101
  [3.3.0]: https://github.com/codetalcott/hyperfixi/compare/v3.2.0...v3.3.0
904
1102
  [3.2.0]: https://github.com/codetalcott/hyperfixi/compare/v3.1.0...v3.2.0
905
1103
  [3.1.0]: https://github.com/codetalcott/hyperfixi/compare/v3.0.0...v3.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "3.3.0",
3
+ "version": "4.0.1",
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 ../engine ../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 src/multilingual/shipped-sources-engine.test.ts src/multilingual/shipped-examples-execution.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.3.0",
61
- "@hyperfixi/patterns-reference": "^3.3.0",
62
- "@lokascript/compilation-service": "^3.3.0",
63
- "@lokascript/i18n": "^3.3.0",
64
- "@lokascript/semantic": "^3.3.0",
60
+ "@hyperfixi/patterns-reference": "^4.0.1",
61
+ "@lokascript/compilation-service": "^4.0.1",
62
+ "@lokascript/i18n": "^4.0.1",
63
+ "@lokascript/semantic": "^4.0.1",
65
64
  "diff": "^8.0.3",
66
65
  "esbuild": "^0.25.12",
67
66
  "happy-dom": "^20.14.5",
@@ -73,8 +72,8 @@
73
72
  "vite": "^8.3.1"
74
73
  },
75
74
  "devDependencies": {
76
- "@hyperfixi/engine": "^3.3.0",
77
- "@lokascript/hyperscript-adapter": "^3.3.0",
75
+ "@hyperfixi/engine": "^4.0.1",
76
+ "@lokascript/hyperscript-adapter": "^4.0.1",
78
77
  "@types/diff": "^5.0.0",
79
78
  "@types/node": "^26.6.3",
80
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);