@hyperfixi/testing-framework 3.2.0 → 3.3.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 +41 -1
- package/package.json +10 -9
- package/src/multilingual/engine-parser-parity.test.ts +67 -0
- package/src/multilingual/shipped-examples-execution.test.ts +76 -1
- package/src/multilingual/shipped-examples-execution.ts +73 -3
- package/src/multilingual/shipped-sources-engine.test.ts +106 -0
- package/src/multilingual/shipped-sources-validity.ts +65 -0
- package/src/multilingual/validators/execution-validator.ts +5 -3
- package/src/multilingual/value-matrix.accepted.test.ts +3 -0
- package/src/multilingual/value-matrix.ts +52 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
The first publication of `@hyperfixi/engine`, the hyperscript engine meant to replace the one
|
|
13
|
+
in `@hyperfixi/core`, and the example gallery moved onto it.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`@hyperfixi/engine` is published.** A hyperscript engine written against upstream
|
|
18
|
+
`_hyperscript`'s source, with upstream's own test suite as the acceptance oracle (1,401 of
|
|
19
|
+
1,467 tests; the 66 known failures are upstream's internal API, sockets and workers). It
|
|
20
|
+
ships `dist/hyperfixi-hs.js`, a script-tag bundle of hyperscript and nothing else, 34.1 KB
|
|
21
|
+
gzipped, which 36 of the repository's example pages now load instead of `hyperfixi.js`. It
|
|
22
|
+
keeps two forms upstream lacks, `new X(...)` and `toggle <element>`; the other hyperfixi-only
|
|
23
|
+
forms are not in it. It is typed (`tsc --strict`, no `any`), built from modules, and exposes
|
|
24
|
+
upstream's public API shape plus one hook, `addSourceTransform`, which
|
|
25
|
+
`@lokascript/hyperscript-adapter` uses to run the 24 languages on it.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Examples and docs are written in upstream `_hyperscript`'s spelling** wherever upstream has
|
|
30
|
+
one: `put Y into X` for the `swap` strategies, `debounced at 300ms`, `matches`, `set X's @a`,
|
|
31
|
+
`increment #count's textContent` (core counted in a bare `#count`; upstream does not),
|
|
32
|
+
`on mutation of childList from #x`, `my offsetLeft` in place of `measure x`. The multilingual
|
|
33
|
+
reader still accepts the old forms; the renderer writes upstream's. Corpus rows follow.
|
|
34
|
+
- **`@lokascript/semantic` renders `repeat for x in xs index i`** (was `with index`) and
|
|
35
|
+
`tell X show` (was `tell X to show`), upstream's spellings.
|
|
36
|
+
- **The examples' bundle loader** takes a per-page default (`data-default="hs"`), and `?bundle=hs`
|
|
37
|
+
switches any page to the engine bundle.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`@hyperfixi/core`**: `toggle *display of X` parsed and then threw.
|
|
42
|
+
- **Hybrid bundles (`hyperfixi-hx.js`)**: `increment` / `decrement` of a possessive wrote only
|
|
43
|
+
style properties; `increment #count's textContent` evaluated to the text and threw on
|
|
44
|
+
`querySelectorAll('0')`. They now write the property.
|
|
45
|
+
- **`@lokascript/semantic`**: a template-literal URL in a Korean handler was read as a custom
|
|
46
|
+
event name.
|
|
47
|
+
|
|
10
48
|
## [3.2.0] - 2026-09-30
|
|
11
49
|
|
|
12
50
|
A correctness release for multilingual hyperscript. The semantic parser behind
|
|
@@ -861,7 +899,9 @@ _Synchronized version release. See git history for details._
|
|
|
861
899
|
- npm access token stored in GitHub Secrets
|
|
862
900
|
- 2FA recommended for npm organization
|
|
863
901
|
|
|
864
|
-
[Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.
|
|
902
|
+
[Unreleased]: https://github.com/codetalcott/hyperfixi/compare/v3.3.0...HEAD
|
|
903
|
+
[3.3.0]: https://github.com/codetalcott/hyperfixi/compare/v3.2.0...v3.3.0
|
|
904
|
+
[3.2.0]: https://github.com/codetalcott/hyperfixi/compare/v3.1.0...v3.2.0
|
|
865
905
|
[3.1.0]: https://github.com/codetalcott/hyperfixi/compare/v3.0.0...v3.1.0
|
|
866
906
|
[2.10.0]: https://github.com/codetalcott/hyperfixi/compare/v2.9.0...v2.10.0
|
|
867
907
|
[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.
|
|
3
|
+
"version": "3.3.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 ../core ../aot-compiler ../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-validity.test.ts src/multilingual/shipped-sources-engine.test.ts src/multilingual/shipped-examples-execution.test.ts"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"hyperscript",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"author": "LokaScript Contributors",
|
|
58
58
|
"license": "MIT",
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@hyperfixi/core": "^3.
|
|
61
|
-
"@hyperfixi/patterns-reference": "^3.
|
|
62
|
-
"@lokascript/compilation-service": "^3.
|
|
63
|
-
"@lokascript/i18n": "^3.
|
|
64
|
-
"@lokascript/semantic": "^3.
|
|
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",
|
|
65
65
|
"diff": "^8.0.3",
|
|
66
66
|
"esbuild": "^0.25.12",
|
|
67
67
|
"happy-dom": "^20.14.5",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"vite": "^8.3.1"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@
|
|
76
|
+
"@hyperfixi/engine": "^3.3.0",
|
|
77
|
+
"@lokascript/hyperscript-adapter": "^3.3.0",
|
|
77
78
|
"@types/diff": "^5.0.0",
|
|
78
79
|
"@types/node": "^26.6.3",
|
|
79
80
|
"@vitest/coverage-v8": "^5.0.0",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine parser parity: the canonical-validity gates' strings on `@hyperfixi/engine`.
|
|
3
|
+
*
|
|
4
|
+
* Every English string those gates put to upstream `hyperscript.org`'s parser — each corpus
|
|
5
|
+
* row, its English re-render, and the English render of every authored translation — is put
|
|
6
|
+
* to the new engine's parser as well. The two must agree on whether it parses. The engine is
|
|
7
|
+
* meant to replace core's, and the multilingual product reaches an engine as English text, so
|
|
8
|
+
* a string upstream reads and the engine rejects (or the reverse) is a translation that works
|
|
9
|
+
* on one host and not the other.
|
|
10
|
+
*
|
|
11
|
+
* Unlike the foreign canonical-validity gate, this needs no fresh `populate`: it compares two
|
|
12
|
+
* parsers on the SAME strings, so a stale patterns.db changes which strings are asked, not
|
|
13
|
+
* whether the parsers agree. It always runs.
|
|
14
|
+
*
|
|
15
|
+
* On a failure, run one string on both engines:
|
|
16
|
+
* `npx tsx packages/engine/tools/probe.mts '<source>'`.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
19
|
+
import {
|
|
20
|
+
checkCorpusRenderValidity,
|
|
21
|
+
loadCanonicalParser,
|
|
22
|
+
type CanonicalValidate,
|
|
23
|
+
} from './canonical-validity';
|
|
24
|
+
import { checkForeignRenderValidity } from './foreign-canonical-validity';
|
|
25
|
+
|
|
26
|
+
describe('engine parser parity (R4 strings on @hyperfixi/engine)', () => {
|
|
27
|
+
const asked = new Set<string>();
|
|
28
|
+
const disagreements: string[] = [];
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
const { api, everything, register } = await import('@hyperfixi/engine');
|
|
32
|
+
register(...everything);
|
|
33
|
+
const upstream = await loadCanonicalParser();
|
|
34
|
+
const engine: CanonicalValidate = source => {
|
|
35
|
+
try {
|
|
36
|
+
return api.parse(source).errors.map(error => error.message);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return ['threw: ' + (e instanceof Error ? e.message.split('\n')[0] : String(e))];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// Upstream's verdict is the one handed back, so the gates' own logic is unchanged.
|
|
42
|
+
const both: CanonicalValidate = source => {
|
|
43
|
+
const up = upstream(source);
|
|
44
|
+
if (!asked.has(source)) {
|
|
45
|
+
asked.add(source);
|
|
46
|
+
const mine = engine(source);
|
|
47
|
+
if ((up.length === 0) !== (mine.length === 0)) {
|
|
48
|
+
disagreements.push(
|
|
49
|
+
`${JSON.stringify(source)}\n upstream: ${up[0]?.split('\n')[0] ?? 'accepts'}` +
|
|
50
|
+
`\n engine: ${mine[0]?.split('\n')[0] ?? 'accepts'}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return up;
|
|
55
|
+
};
|
|
56
|
+
await checkCorpusRenderValidity({ validate: both });
|
|
57
|
+
await checkForeignRenderValidity({ validate: both });
|
|
58
|
+
}, 300_000);
|
|
59
|
+
|
|
60
|
+
it('asks about a real corpus (sanity: patterns.db has rows)', () => {
|
|
61
|
+
expect(asked.size).toBeGreaterThan(100);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('the two parsers agree on every string', () => {
|
|
65
|
+
expect(disagreements).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* 3. no allowlisted key has silently converged (stale entries must be
|
|
18
18
|
* removed so the list only ever ratchets down).
|
|
19
19
|
*
|
|
20
|
+
* The same three run for the ENGINE LANE: `@hyperfixi/engine`, the engine meant
|
|
21
|
+
* to replace core's, against the same oracle on every handler upstream accepts
|
|
22
|
+
* (`allowedEngineDivergences` in the same baseline). It is the gate for a
|
|
23
|
+
* handler that parses on both engines and runs differently, which the
|
|
24
|
+
* parse-level list in shipped-sources-engine.test.ts cannot see.
|
|
25
|
+
*
|
|
20
26
|
* To update after an intentional change: re-run and regenerate
|
|
21
27
|
* `baselines/shipped-examples-execution.json` (the allowlist key embeds a
|
|
22
28
|
* source hash, so FIXING a handler changes its key and assertion 3 forces the
|
|
@@ -54,6 +60,14 @@ interface AllowlistDoc {
|
|
|
54
60
|
excerpt: string;
|
|
55
61
|
reason: string;
|
|
56
62
|
}>;
|
|
63
|
+
/** The engine lane: `@hyperfixi/engine` against upstream. */
|
|
64
|
+
allowedEngineDivergences: Array<{
|
|
65
|
+
key: string;
|
|
66
|
+
file: string;
|
|
67
|
+
event: string;
|
|
68
|
+
excerpt: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
}>;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
const baselinePath = path.resolve(
|
|
@@ -62,12 +76,17 @@ const baselinePath = path.resolve(
|
|
|
62
76
|
);
|
|
63
77
|
const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
|
|
64
78
|
const allowed = new Set(allowlist.allowedDivergences.map(e => e.key));
|
|
79
|
+
const allowedOnEngine = new Set(allowlist.allowedEngineDivergences.map(e => e.key));
|
|
80
|
+
|
|
81
|
+
/** One sweep, shared by both lanes' tests. */
|
|
82
|
+
let sweeping: Promise<ExecutionParityResult> | undefined;
|
|
83
|
+
const sweep = (): Promise<ExecutionParityResult> => (sweeping ??= runShippedExamplesExecution());
|
|
65
84
|
|
|
66
85
|
describe('shipped-examples execution gate', () => {
|
|
67
86
|
let result: ExecutionParityResult;
|
|
68
87
|
|
|
69
88
|
beforeAll(async () => {
|
|
70
|
-
result = await
|
|
89
|
+
result = await sweep();
|
|
71
90
|
|
|
72
91
|
// Visibility, not assertions: what the sweep could not compare, and why.
|
|
73
92
|
// A silently shrinking denominator is this gate's own blind spot.
|
|
@@ -84,6 +103,12 @@ describe('shipped-examples execution gate', () => {
|
|
|
84
103
|
for (const [r, n] of [...reasons].sort((a, b) => b[1] - a[1])) {
|
|
85
104
|
console.log(`[shipped-examples-execution] skip ×${n}: ${r}`);
|
|
86
105
|
}
|
|
106
|
+
const onEngine = result.engineCompared;
|
|
107
|
+
console.log(
|
|
108
|
+
`[shipped-examples-execution] engine lane: compared=${onEngine.length} ` +
|
|
109
|
+
`(vacuous=${onEngine.filter(c => c.vacuous).length}) ` +
|
|
110
|
+
`diverging=${onEngine.filter(c => !c.match).length}`
|
|
111
|
+
);
|
|
87
112
|
}, 240_000);
|
|
88
113
|
|
|
89
114
|
it('walks pages and compares handlers (sanity: extraction and both engines working)', () => {
|
|
@@ -142,6 +167,56 @@ describe('shipped-examples execution gate', () => {
|
|
|
142
167
|
});
|
|
143
168
|
});
|
|
144
169
|
|
|
170
|
+
describe('shipped-examples execution gate: @hyperfixi/engine against upstream', () => {
|
|
171
|
+
let result: ExecutionParityResult;
|
|
172
|
+
beforeAll(async () => {
|
|
173
|
+
result = await sweep();
|
|
174
|
+
}, 240_000);
|
|
175
|
+
|
|
176
|
+
it('compares handlers on the engine (sanity: the lane ran and mostly agrees)', () => {
|
|
177
|
+
expect(result.engineCompared.length).toBeGreaterThan(90);
|
|
178
|
+
const realMatches = result.engineCompared.filter(c => c.match && !c.vacuous).length;
|
|
179
|
+
expect(realMatches).toBeGreaterThan(60);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('has no NEW divergence from upstream outside the allowlist', () => {
|
|
183
|
+
const unexpected = result.engineCompared.filter(c => !c.match && !allowedOnEngine.has(c.key));
|
|
184
|
+
expect(
|
|
185
|
+
unexpected,
|
|
186
|
+
unexpected.length
|
|
187
|
+
? `\nShipped handlers whose DOM effect on @hyperfixi/engine DIVERGES from the hyperscript.org ` +
|
|
188
|
+
`engine\n(the engine follows upstream's source: fix the engine, or allowlist with a reason ` +
|
|
189
|
+
`under allowedEngineDivergences):\n` +
|
|
190
|
+
unexpected
|
|
191
|
+
.map(
|
|
192
|
+
f =>
|
|
193
|
+
` [${f.key}]\n` +
|
|
194
|
+
` "${f.excerpt}"\n` +
|
|
195
|
+
` engine : ${JSON.stringify(f.engineEffects).slice(0, 300)}\n` +
|
|
196
|
+
` upstream: ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
|
|
197
|
+
)
|
|
198
|
+
.join('\n') +
|
|
199
|
+
`\n\nOne source on both, on one page: npx tsx packages/engine/tools/probe.mts '<source>'`
|
|
200
|
+
: ''
|
|
201
|
+
).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('has no stale allowlist entries', () => {
|
|
205
|
+
const stillDiverging = new Set(result.engineCompared.filter(c => !c.match).map(c => c.key));
|
|
206
|
+
const stale = allowlist.allowedEngineDivergences
|
|
207
|
+
.map(e => e.key)
|
|
208
|
+
.filter(k => !stillDiverging.has(k));
|
|
209
|
+
expect(
|
|
210
|
+
stale,
|
|
211
|
+
stale.length
|
|
212
|
+
? `\nThese handlers no longer diverge on the engine (or were edited: the key embeds a\n` +
|
|
213
|
+
`source hash). Remove them from allowedEngineDivergences in\n` +
|
|
214
|
+
`baselines/shipped-examples-execution.json:\n ${stale.join('\n ')}`
|
|
215
|
+
: ''
|
|
216
|
+
).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
145
220
|
describe('harness pieces', () => {
|
|
146
221
|
it('extracts the trigger event from the leading on-clause', () => {
|
|
147
222
|
expect(triggerEventOf('on click add .a to me')).toBe('click');
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* before/after snapshot around it. See runHandlerOnEngine for why isolation is
|
|
36
36
|
* worth its cost.
|
|
37
37
|
*
|
|
38
|
+
* ## The engine lane
|
|
39
|
+
* `@hyperfixi/engine` is meant to replace core's engine, so the same sweep
|
|
40
|
+
* runs it against the same oracle: every handler upstream accepts (whether or
|
|
41
|
+
* not core compiles it clean) is executed on upstream and on the engine, and
|
|
42
|
+
* the two effect signatures are compared (`engineCompared`). The parse-level
|
|
43
|
+
* list of what the engine rejects (shipped-sources-engine.test.ts) cannot see
|
|
44
|
+
* a handler that parses on both and runs differently; this can. A handler
|
|
45
|
+
* upstream accepts and the engine rejects is compared too, and diverges.
|
|
46
|
+
*
|
|
38
47
|
* ## Node-only
|
|
39
48
|
* Imports the real `hyperscript.org` build off disk and swaps jsdom globals
|
|
40
49
|
* per handler execution (both engines resolve `document` lazily through
|
|
@@ -144,6 +153,18 @@ export interface ComparedHandler extends ShippedHandler {
|
|
|
144
153
|
excerpt: string;
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
/** One handler run on upstream and on `@hyperfixi/engine`. */
|
|
157
|
+
export interface EngineComparedHandler extends ShippedHandler {
|
|
158
|
+
key: string;
|
|
159
|
+
event: string;
|
|
160
|
+
engineEffects: string[];
|
|
161
|
+
upstreamEffects: string[];
|
|
162
|
+
match: boolean;
|
|
163
|
+
/** Both signatures empty: not evidence of parity (see ComparedHandler.vacuous). */
|
|
164
|
+
vacuous: boolean;
|
|
165
|
+
excerpt: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
147
168
|
export interface ExecutionParityResult {
|
|
148
169
|
/** Pages walked. */
|
|
149
170
|
pages: number;
|
|
@@ -153,6 +174,8 @@ export interface ExecutionParityResult {
|
|
|
153
174
|
compared: ComparedHandler[];
|
|
154
175
|
/** Handlers excluded, each with its reason. */
|
|
155
176
|
skipped: SkippedHandler[];
|
|
177
|
+
/** Handlers upstream accepts, executed on upstream and on `@hyperfixi/engine`. */
|
|
178
|
+
engineCompared: EngineComparedHandler[];
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
/** Stable key for one handler execution. */
|
|
@@ -219,6 +242,10 @@ export interface Engines {
|
|
|
219
242
|
hyperfixiInstall(source: string, el: Element): Promise<void>;
|
|
220
243
|
/** Install a handler on an element via the upstream engine. */
|
|
221
244
|
upstreamInstall(el: Element): void;
|
|
245
|
+
/** `@hyperfixi/engine`'s parse errors for a source; [] = it parses. */
|
|
246
|
+
engineErrors(source: string): string[];
|
|
247
|
+
/** Install a handler on an element via `@hyperfixi/engine`. */
|
|
248
|
+
engineInstall(el: Element): void;
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
/**
|
|
@@ -311,7 +338,20 @@ export async function initEngines(): Promise<Engines> {
|
|
|
311
338
|
processNode(el: Node): void;
|
|
312
339
|
};
|
|
313
340
|
|
|
341
|
+
const engine = await import('@hyperfixi/engine');
|
|
342
|
+
engine.register(...engine.everything);
|
|
343
|
+
|
|
314
344
|
return {
|
|
345
|
+
engineErrors(source) {
|
|
346
|
+
try {
|
|
347
|
+
return engine.api.parse(source).errors.map(e => e.message.split('\n')[0] ?? '');
|
|
348
|
+
} catch (e) {
|
|
349
|
+
return ['threw: ' + (e as Error).message.split('\n')[0]];
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
engineInstall(el) {
|
|
353
|
+
engine.api.processNode(el);
|
|
354
|
+
},
|
|
315
355
|
compileClean(source) {
|
|
316
356
|
try {
|
|
317
357
|
const r = core.hyperscript.compileSync(source);
|
|
@@ -425,6 +465,7 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
425
465
|
|
|
426
466
|
const compared: ComparedHandler[] = [];
|
|
427
467
|
const skipped: SkippedHandler[] = [];
|
|
468
|
+
const engineCompared: EngineComparedHandler[] = [];
|
|
428
469
|
let pages = 0;
|
|
429
470
|
let handlers = 0;
|
|
430
471
|
|
|
@@ -438,6 +479,9 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
438
479
|
handlers += pageHandlers.length;
|
|
439
480
|
|
|
440
481
|
const eligible: Array<ShippedHandler & { event: string }> = [];
|
|
482
|
+
// The engine lane's denominator: dispatchable, deterministic, and upstream
|
|
483
|
+
// accepts it. Core's verdict is not part of it.
|
|
484
|
+
const oracle: Array<ShippedHandler & { event: string }> = [];
|
|
441
485
|
for (const h of pageHandlers) {
|
|
442
486
|
if (!h.event) {
|
|
443
487
|
skipped.push({ ...h, reason: 'not an `on <event>` handler' });
|
|
@@ -452,6 +496,8 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
452
496
|
skipped.push({ ...h, reason: disq.reason });
|
|
453
497
|
continue;
|
|
454
498
|
}
|
|
499
|
+
const upstreamErrs = engines.upstreamErrors(h.source);
|
|
500
|
+
if (upstreamErrs.length === 0) oracle.push(h as ShippedHandler & { event: string });
|
|
455
501
|
if (!engines.compileClean(h.source)) {
|
|
456
502
|
skipped.push({
|
|
457
503
|
...h,
|
|
@@ -459,14 +505,18 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
459
505
|
});
|
|
460
506
|
continue;
|
|
461
507
|
}
|
|
462
|
-
const upstreamErrs = engines.upstreamErrors(h.source);
|
|
463
508
|
if (upstreamErrs.length > 0) {
|
|
464
509
|
skipped.push({ ...h, reason: `upstream rejects it (no oracle): ${upstreamErrs[0]}` });
|
|
465
510
|
continue;
|
|
466
511
|
}
|
|
467
512
|
eligible.push(h as ShippedHandler & { event: string });
|
|
468
513
|
}
|
|
469
|
-
if (eligible.length === 0) continue;
|
|
514
|
+
if (eligible.length === 0 && oracle.length === 0) continue;
|
|
515
|
+
// Upstream's signatures from the core lane serve the engine lane too when
|
|
516
|
+
// the two lanes install the same handlers on the page.
|
|
517
|
+
const sameHandlers =
|
|
518
|
+
eligible.length === oracle.length && eligible.every((h, i) => h === oracle[i]);
|
|
519
|
+
const upstreamRun = new Map<ShippedHandler, string[]>();
|
|
470
520
|
|
|
471
521
|
// Both engines log runtime errors to the console during dispatch
|
|
472
522
|
// (COMMAND FAILED etc.). That is expected data here — the effect
|
|
@@ -488,6 +538,7 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
488
538
|
const theirs = await runHandlerOnEngine(html, eligible, h, (_hh, el) =>
|
|
489
539
|
engines.upstreamInstall(el)
|
|
490
540
|
);
|
|
541
|
+
upstreamRun.set(h, theirs);
|
|
491
542
|
compared.push({
|
|
492
543
|
...h,
|
|
493
544
|
key: keyFor(h),
|
|
@@ -498,6 +549,25 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
498
549
|
excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
499
550
|
});
|
|
500
551
|
}
|
|
552
|
+
for (const h of oracle) {
|
|
553
|
+
const rejected = engines.engineErrors(h.source)[0];
|
|
554
|
+
const theirs =
|
|
555
|
+
(sameHandlers && upstreamRun.get(h)) ||
|
|
556
|
+
(await runHandlerOnEngine(html, oracle, h, (_hh, el) => engines.upstreamInstall(el)));
|
|
557
|
+
const ours =
|
|
558
|
+
rejected !== undefined
|
|
559
|
+
? [`<engine rejects the source: ${rejected}>`]
|
|
560
|
+
: await runHandlerOnEngine(html, oracle, h, (_hh, el) => engines.engineInstall(el));
|
|
561
|
+
engineCompared.push({
|
|
562
|
+
...h,
|
|
563
|
+
key: keyFor(h),
|
|
564
|
+
engineEffects: ours,
|
|
565
|
+
upstreamEffects: theirs,
|
|
566
|
+
match: JSON.stringify(ours) === JSON.stringify(theirs),
|
|
567
|
+
vacuous: ours.length === 0 && theirs.length === 0,
|
|
568
|
+
excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
501
571
|
} finally {
|
|
502
572
|
console.log = saved.log;
|
|
503
573
|
console.warn = saved.warn;
|
|
@@ -507,5 +577,5 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
507
577
|
}
|
|
508
578
|
}
|
|
509
579
|
|
|
510
|
-
return { pages, handlers, compared, skipped };
|
|
580
|
+
return { pages, handlers, compared, skipped, engineCompared };
|
|
511
581
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped sources on `@hyperfixi/engine`: what replacing core's engine would break.
|
|
3
|
+
*
|
|
4
|
+
* `packages/engine` is meant to replace the engine in `packages/core`. Every
|
|
5
|
+
* hyperscript source this repository ships (`examples/` and the doc trees, the
|
|
6
|
+
* validity gate's collection) that core compiles clean is put to the engine's
|
|
7
|
+
* parser. The ones it rejects are listed in
|
|
8
|
+
* `baselines/shipped-sources-engine.json`, each with the reason it is still
|
|
9
|
+
* there. Three assertions, as the sibling gates:
|
|
10
|
+
* 1. sanity: sources were found and the engine reads most of them;
|
|
11
|
+
* 2. no NEW rejected source outside the list. A page or a doc example
|
|
12
|
+
* written in syntax only core has would break when core's engine goes;
|
|
13
|
+
* write upstream's spelling (`packages/engine/README.md` lists the forms
|
|
14
|
+
* that were considered and not kept), or list it with a reason;
|
|
15
|
+
* 3. no stale entry: a listed source the engine now reads, or one that was
|
|
16
|
+
* edited (the key embeds a hash of the source), must be removed, so the
|
|
17
|
+
* list only shrinks. It is empty when core's engine can be replaced
|
|
18
|
+
* without breaking a shipped page.
|
|
19
|
+
*
|
|
20
|
+
* This is a PARSE-level gate. A source can parse on both engines and run
|
|
21
|
+
* differently; the engine lane of `shipped-examples-execution.test.ts` is the
|
|
22
|
+
* gate for that.
|
|
23
|
+
*
|
|
24
|
+
* To see the list by form, and beside upstream's verdict:
|
|
25
|
+
* `npx tsx packages/engine/tools/shipped-sources.mts`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { JSDOM } from 'jsdom';
|
|
32
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
33
|
+
import {
|
|
34
|
+
checkShippedSourcesOnEngine,
|
|
35
|
+
type CompileForValidity,
|
|
36
|
+
type ShippedSourcesOnEngineResult,
|
|
37
|
+
} from './shipped-sources-validity';
|
|
38
|
+
|
|
39
|
+
interface RejectedDoc {
|
|
40
|
+
rejected: Array<{ key: string; file: string; error: string; excerpt: string; reason: string }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const baselinePath = path.resolve(
|
|
44
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
45
|
+
'../../baselines/shipped-sources-engine.json'
|
|
46
|
+
);
|
|
47
|
+
const listed = JSON.parse(readFileSync(baselinePath, 'utf8')) as RejectedDoc;
|
|
48
|
+
const allowed = new Set(listed.rejected.map(e => e.key));
|
|
49
|
+
|
|
50
|
+
describe('shipped sources on @hyperfixi/engine', () => {
|
|
51
|
+
let result: ShippedSourcesOnEngineResult;
|
|
52
|
+
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
const core = (await import('@hyperfixi/core')) as unknown as {
|
|
55
|
+
hyperscript: { compileSync: CompileForValidity };
|
|
56
|
+
};
|
|
57
|
+
const { api, everything, register } = await import('@hyperfixi/engine');
|
|
58
|
+
register(...everything);
|
|
59
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
60
|
+
result = checkShippedSourcesOnEngine(
|
|
61
|
+
code => core.hyperscript.compileSync(code),
|
|
62
|
+
code => {
|
|
63
|
+
try {
|
|
64
|
+
return api.parse(code).errors.map(error => error.message);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return ['threw: ' + (e instanceof Error ? e.message : String(e))];
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[2]
|
|
70
|
+
);
|
|
71
|
+
}, 120_000);
|
|
72
|
+
|
|
73
|
+
it('finds the shipped sources and the engine reads most of them (sanity)', () => {
|
|
74
|
+
expect(result.coreClean).toBeGreaterThan(200);
|
|
75
|
+
expect(result.engineAccepts).toBeGreaterThan(200);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('has no NEW shipped source the engine rejects outside the list', () => {
|
|
79
|
+
const unexpected = result.rejections.filter(r => !allowed.has(r.key));
|
|
80
|
+
expect(
|
|
81
|
+
unexpected,
|
|
82
|
+
unexpected.length
|
|
83
|
+
? `\nShipped sources core compiles and @hyperfixi/engine rejects (write upstream's spelling,\n` +
|
|
84
|
+
`or list it in baselines/shipped-sources-engine.json with a reason):\n` +
|
|
85
|
+
unexpected
|
|
86
|
+
.map(r => ` [${r.key}]\n "${r.excerpt}"\n -> ${r.error}`)
|
|
87
|
+
.join('\n') +
|
|
88
|
+
`\n\nOne source on the engine and on upstream, side by side:\n` +
|
|
89
|
+
` npx tsx packages/engine/tools/probe.mts '<source>'`
|
|
90
|
+
: ''
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('has no stale entries (a source the engine now reads must be removed, so the list only shrinks)', () => {
|
|
95
|
+
const stillRejected = new Set(result.rejections.map(r => r.key));
|
|
96
|
+
const stale = listed.rejected.map(e => e.key).filter(key => !stillRejected.has(key));
|
|
97
|
+
expect(
|
|
98
|
+
stale,
|
|
99
|
+
stale.length
|
|
100
|
+
? `\nThese listed sources are no longer rejected (read by the engine now, or edited: the key\n` +
|
|
101
|
+
`embeds a hash of the source). Remove them from baselines/shipped-sources-engine.json:\n ` +
|
|
102
|
+
stale.join('\n ')
|
|
103
|
+
: ''
|
|
104
|
+
).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -231,3 +231,68 @@ export function checkShippedSourcesValidity(
|
|
|
231
231
|
|
|
232
232
|
return { checked: sources.length, clean, findings };
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
/** A shipped source `packages/core` compiles clean and `@hyperfixi/engine` rejects. */
|
|
236
|
+
export interface EngineRejection extends ShippedSource {
|
|
237
|
+
/** Stable key: file plus a hash of the source, as the validity gate's. */
|
|
238
|
+
key: string;
|
|
239
|
+
/** The engine's first parse error, first line. */
|
|
240
|
+
error: string;
|
|
241
|
+
excerpt: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ShippedSourcesOnEngineResult {
|
|
245
|
+
/** Distinct (file, source) pairs core compiles clean. */
|
|
246
|
+
coreClean: number;
|
|
247
|
+
/** Of those, the ones the engine parses. */
|
|
248
|
+
engineAccepts: number;
|
|
249
|
+
rejections: EngineRejection[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* What replacing core's engine with `@hyperfixi/engine` would break in the
|
|
254
|
+
* material we ship: every source core compiles clean, put to the engine's
|
|
255
|
+
* parser. `engineErrors` returns the engine's parse errors ([] = it parses).
|
|
256
|
+
*
|
|
257
|
+
* The denominator is core-clean sources on purpose. A source core itself
|
|
258
|
+
* rejects or recovers from is already broken where it is shipped, and is the
|
|
259
|
+
* validity gate's business; the question here is only what would STOP working.
|
|
260
|
+
*/
|
|
261
|
+
export function checkShippedSourcesOnEngine(
|
|
262
|
+
compile: CompileForValidity,
|
|
263
|
+
engineErrors: (code: string) => string[],
|
|
264
|
+
doc: Parameters<typeof extractHyperscriptFromMarkup>[0],
|
|
265
|
+
opts?: { roots?: string[]; repoRoot?: string }
|
|
266
|
+
): ShippedSourcesOnEngineResult {
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
const rejections: EngineRejection[] = [];
|
|
269
|
+
let coreClean = 0;
|
|
270
|
+
let engineAccepts = 0;
|
|
271
|
+
|
|
272
|
+
for (const s of collectShippedSources(doc, opts)) {
|
|
273
|
+
const key = keyFor(s.file, s.source);
|
|
274
|
+
if (seen.has(key)) continue;
|
|
275
|
+
seen.add(key);
|
|
276
|
+
let clean = false;
|
|
277
|
+
try {
|
|
278
|
+
const result = compile(s.source);
|
|
279
|
+
clean = result.ok && (result.errors ?? []).length === 0;
|
|
280
|
+
} catch {
|
|
281
|
+
/* not core-clean */
|
|
282
|
+
}
|
|
283
|
+
if (!clean) continue;
|
|
284
|
+
coreClean++;
|
|
285
|
+
const errors = engineErrors(s.source);
|
|
286
|
+
if (errors.length === 0) {
|
|
287
|
+
engineAccepts++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
rejections.push({
|
|
291
|
+
...s,
|
|
292
|
+
key,
|
|
293
|
+
error: (errors[0] ?? '').split('\n')[0] ?? '',
|
|
294
|
+
excerpt: s.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return { coreClean, engineAccepts, rejections };
|
|
298
|
+
}
|
|
@@ -72,9 +72,11 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
72
72
|
// patterns whose en reference now parses (the if/else conditional fold) and
|
|
73
73
|
// executes (propertyAccess evaluator; matches/exists/is-empty condition
|
|
74
74
|
// forms). Probed through this validator: each produces a non-empty,
|
|
75
|
-
// deterministic effect signature. `unless-condition`
|
|
76
|
-
// deliberately NOT folded
|
|
77
|
-
// its flat parse
|
|
75
|
+
// deterministic effect signature. `unless-condition` stayed out while it was
|
|
76
|
+
// core's prefix `unless` (deliberately NOT folded: see
|
|
77
|
+
// semantic-parser.tryParseConditionalBlock; its flat parse errored at
|
|
78
|
+
// runtime). The row is `if I do not match …` since 2026-10-01 and has not
|
|
79
|
+
// been probed for this subset.
|
|
78
80
|
'if-condition',
|
|
79
81
|
'if-matches',
|
|
80
82
|
'if-exists',
|
|
@@ -23,6 +23,9 @@ describe('value matrix: accepted pairs', () => {
|
|
|
23
23
|
known!.reason
|
|
24
24
|
);
|
|
25
25
|
expect(acceptedReason('increment|#a.textContent', ['it', 'it/up'])).toBe(ambiguity!.reason);
|
|
26
|
+
expect(acceptedReason('increment|#a.textContent', ['it', 'it/up', 'it/eng'])).toBe(
|
|
27
|
+
ambiguity!.reason
|
|
28
|
+
);
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
it('leaves it open when one failing lane is not', () => {
|
|
@@ -49,7 +49,12 @@
|
|
|
49
49
|
* `render(parse_en(src), L)`, compiled with `{ language: L }`;
|
|
50
50
|
* - `<L>/up` the same translation through `@lokascript/hyperscript-adapter`
|
|
51
51
|
* (`preprocess`, back to English) on upstream: the multilingual
|
|
52
|
-
* product for original _hyperscript users
|
|
52
|
+
* product for original _hyperscript users;
|
|
53
|
+
* - `eng` the English source on `@hyperfixi/engine`, the engine that
|
|
54
|
+
* replaces core's;
|
|
55
|
+
* - `<L>/eng` the adapter's English (the string the `/up` lane runs) on
|
|
56
|
+
* that engine: where it differs from `<L>/up`, the two ENGINES
|
|
57
|
+
* differ, since they were given the same text.
|
|
53
58
|
*
|
|
54
59
|
* A (cell, lane) pair FAILS when its result differs from the oracle's.
|
|
55
60
|
*
|
|
@@ -69,7 +74,9 @@
|
|
|
69
74
|
*
|
|
70
75
|
* A translation that loses a loop's condition can loop forever. Core caps a
|
|
71
76
|
* loop at 10,000 iterations; upstream has no cap and blocks the thread, so
|
|
72
|
-
* every upstream run gets an evaluation budget (see EVAL_BUDGET).
|
|
77
|
+
* every upstream run gets an evaluation budget (see EVAL_BUDGET). The new
|
|
78
|
+
* engine has no cap and no hook for one: a string that spent the budget on
|
|
79
|
+
* upstream is not run on it, and its `/eng` lane reads `✗budget` too.
|
|
73
80
|
*
|
|
74
81
|
* Node-only: imports the real `hyperscript.org` build off disk and needs the
|
|
75
82
|
* node vitest environment (see the shipped-examples gate for why).
|
|
@@ -733,7 +740,8 @@ export const FOREIGN_LANGUAGES = [
|
|
|
733
740
|
export const LANES: readonly string[] = [
|
|
734
741
|
'en',
|
|
735
742
|
'en-rt',
|
|
736
|
-
|
|
743
|
+
'eng',
|
|
744
|
+
...FOREIGN_LANGUAGES.flatMap(language => [language, `${language}/up`, `${language}/eng`]),
|
|
737
745
|
];
|
|
738
746
|
|
|
739
747
|
/**
|
|
@@ -743,10 +751,14 @@ export const LANES: readonly string[] = [
|
|
|
743
751
|
*/
|
|
744
752
|
const EVAL_BUDGET = 20_000;
|
|
745
753
|
|
|
746
|
-
/**
|
|
747
|
-
interface
|
|
754
|
+
/** What a lane needs of a host that reads scripts off attributes: upstream, or the new engine. */
|
|
755
|
+
interface ScriptHost {
|
|
748
756
|
parse(source: string): { errors?: Array<{ message: string }> } | undefined;
|
|
749
757
|
processNode(element: Element): void;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** The upstream surface this uses (`hyperscript.org`'s ESM default export). */
|
|
761
|
+
interface UpstreamEngine extends ScriptHost {
|
|
750
762
|
internals: {
|
|
751
763
|
runtime: { unifiedEval(parseElement: unknown, context: unknown): unknown };
|
|
752
764
|
};
|
|
@@ -809,6 +821,9 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
809
821
|
const require = createRequire(import.meta.url);
|
|
810
822
|
const esm = require.resolve('hyperscript.org').replace(/[^/\\]+$/, '_hyperscript.esm.js');
|
|
811
823
|
const upstream: UpstreamEngine = (await import(pathToFileURL(esm).href)).default;
|
|
824
|
+
const engineModule = await import('@hyperfixi/engine');
|
|
825
|
+
engineModule.register(...engineModule.everything);
|
|
826
|
+
const engine: ScriptHost = engineModule.api;
|
|
812
827
|
|
|
813
828
|
// The evaluation budget: every upstream evaluation goes through unifiedEval.
|
|
814
829
|
const runtime = upstream.internals.runtime;
|
|
@@ -862,20 +877,21 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
862
877
|
};
|
|
863
878
|
const read = (): string => document.getElementById('out')?.textContent ?? '✗no #out';
|
|
864
879
|
|
|
865
|
-
/** Run English on
|
|
866
|
-
const
|
|
867
|
-
const errors =
|
|
880
|
+
/** Run English on a host that reads the script off the button. */
|
|
881
|
+
const onHost = (host: ScriptHost, source: string): string => {
|
|
882
|
+
const errors = host.parse(source)?.errors ?? [];
|
|
868
883
|
if (errors.length) return `✗parse: ${errors[0]?.message.split('\n')[0] ?? ''}`;
|
|
869
884
|
const button = reset();
|
|
870
885
|
button.setAttribute('_', source);
|
|
871
886
|
evaluations = 0;
|
|
872
887
|
const before = reportedErrors;
|
|
873
|
-
|
|
888
|
+
host.processNode(button);
|
|
874
889
|
click(button);
|
|
875
890
|
if (evaluations > EVAL_BUDGET) return '✗budget';
|
|
876
891
|
const got = read();
|
|
877
892
|
return reportedErrors > before && got === '∅' ? '✗threw' : got;
|
|
878
893
|
};
|
|
894
|
+
const onUpstream = (source: string): string => onHost(upstream, source);
|
|
879
895
|
|
|
880
896
|
/** Install a compiled handler on hyperfixi, click, and settle. */
|
|
881
897
|
const onHyperfixi = async (ast: Ast): Promise<string> => {
|
|
@@ -914,6 +930,7 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
914
930
|
lanes['en-rt'] = await guard(() =>
|
|
915
931
|
english ? onUpstream(render(english, 'en')) : '✗untranslatable'
|
|
916
932
|
);
|
|
933
|
+
lanes.eng = await guard(() => onHost(engine, cell.source));
|
|
917
934
|
|
|
918
935
|
for (const language of FOREIGN_LANGUAGES) {
|
|
919
936
|
if (cell.skip?.includes(language)) continue;
|
|
@@ -926,6 +943,7 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
926
943
|
if (code === null) {
|
|
927
944
|
lanes[language] = '✗untranslatable';
|
|
928
945
|
lanes[`${language}/up`] = '✗untranslatable';
|
|
946
|
+
lanes[`${language}/eng`] = '✗untranslatable';
|
|
929
947
|
continue;
|
|
930
948
|
}
|
|
931
949
|
const translated = code;
|
|
@@ -934,7 +952,15 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
934
952
|
if (!compiled.ok || !compiled.ast) return '✗compile';
|
|
935
953
|
return onHyperfixi(compiled.ast);
|
|
936
954
|
});
|
|
937
|
-
|
|
955
|
+
// The adapter's English, once, for both hosts.
|
|
956
|
+
const adapted = await guard(() => preprocess(translated, language));
|
|
957
|
+
const up = (lanes[`${language}/up`] = adapted.startsWith('✗')
|
|
958
|
+
? adapted
|
|
959
|
+
: await guard(() => onUpstream(adapted)));
|
|
960
|
+
lanes[`${language}/eng`] =
|
|
961
|
+
adapted.startsWith('✗') || up === '✗budget'
|
|
962
|
+
? up
|
|
963
|
+
: await guard(() => onHost(engine, adapted));
|
|
938
964
|
}
|
|
939
965
|
return result;
|
|
940
966
|
},
|
|
@@ -966,8 +992,9 @@ export async function runValueMatrix(cells: readonly MatrixCell[]): Promise<Cell
|
|
|
966
992
|
|
|
967
993
|
export interface BaselineEntry {
|
|
968
994
|
/**
|
|
969
|
-
* The failing lanes, space-separated, in LANES order, with
|
|
970
|
-
* `*direct` for all 23 languages on hyperfixi, `*up` for all 23 on upstream
|
|
995
|
+
* The failing lanes, space-separated, in LANES order, with three shorthands:
|
|
996
|
+
* `*direct` for all 23 languages on hyperfixi, `*up` for all 23 on upstream,
|
|
997
|
+
* `*eng` for all 23 on the new engine.
|
|
971
998
|
*/
|
|
972
999
|
lanes: string;
|
|
973
1000
|
/** Where the loss sits, for reading the burn-down (see familyOf); not asserted. */
|
|
@@ -989,6 +1016,7 @@ export interface ValueMatrixBaseline {
|
|
|
989
1016
|
|
|
990
1017
|
const DIRECT_LANES: readonly string[] = FOREIGN_LANGUAGES;
|
|
991
1018
|
const ADAPTER_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/up`);
|
|
1019
|
+
const ENGINE_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/eng`);
|
|
992
1020
|
|
|
993
1021
|
/** Lanes (in LANES order) as a baseline string, with a shorthand for each full group. */
|
|
994
1022
|
export function compressLanes(lanes: readonly string[]): string {
|
|
@@ -996,14 +1024,17 @@ export function compressLanes(lanes: readonly string[]): string {
|
|
|
996
1024
|
const out: string[] = [];
|
|
997
1025
|
const direct = DIRECT_LANES.every(l => set.has(l));
|
|
998
1026
|
const adapter = ADAPTER_LANES.every(l => set.has(l));
|
|
1027
|
+
const engine = ENGINE_LANES.every(l => set.has(l));
|
|
999
1028
|
for (const lane of LANES) {
|
|
1000
1029
|
if (!set.has(lane)) continue;
|
|
1001
1030
|
if (direct && DIRECT_LANES.includes(lane)) continue;
|
|
1002
1031
|
if (adapter && ADAPTER_LANES.includes(lane)) continue;
|
|
1032
|
+
if (engine && ENGINE_LANES.includes(lane)) continue;
|
|
1003
1033
|
out.push(lane);
|
|
1004
1034
|
}
|
|
1005
1035
|
if (direct) out.push('*direct');
|
|
1006
1036
|
if (adapter) out.push('*up');
|
|
1037
|
+
if (engine) out.push('*eng');
|
|
1007
1038
|
return out.join(' ');
|
|
1008
1039
|
}
|
|
1009
1040
|
|
|
@@ -1013,6 +1044,7 @@ export function expandLanes(text: string): string[] {
|
|
|
1013
1044
|
for (const token of text.split(' ').filter(Boolean)) {
|
|
1014
1045
|
if (token === '*direct') out.push(...DIRECT_LANES);
|
|
1015
1046
|
else if (token === '*up') out.push(...ADAPTER_LANES);
|
|
1047
|
+
else if (token === '*eng') out.push(...ENGINE_LANES);
|
|
1016
1048
|
else out.push(token);
|
|
1017
1049
|
}
|
|
1018
1050
|
return out;
|
|
@@ -1031,7 +1063,10 @@ export function failingLanes(result: CellResult): string[] {
|
|
|
1031
1063
|
* translation inherits the loss;
|
|
1032
1064
|
* - `translation` some foreign lanes, on both engines;
|
|
1033
1065
|
* - `direct-path` hyperfixi's foreign lanes only;
|
|
1034
|
-
* - `adapter` upstream's foreign lanes only
|
|
1066
|
+
* - `adapter` upstream's foreign lanes only;
|
|
1067
|
+
* - `engine` the new engine differs from upstream on the same text:
|
|
1068
|
+
* its English run, or a language's `/eng` lane without
|
|
1069
|
+
* its `/up` lane.
|
|
1035
1070
|
*
|
|
1036
1071
|
* Several can hold at once; they are joined in that order.
|
|
1037
1072
|
*/
|
|
@@ -1048,6 +1083,9 @@ export function familyOf(lanes: readonly string[]): string {
|
|
|
1048
1083
|
if (direct.length > both.length && !set.has('en')) parts.push('direct-path');
|
|
1049
1084
|
if (adapter.length > both.length) parts.push('adapter');
|
|
1050
1085
|
}
|
|
1086
|
+
if (set.has('eng') || FOREIGN_LANGUAGES.some(l => set.has(`${l}/eng`) !== set.has(`${l}/up`))) {
|
|
1087
|
+
parts.push('engine');
|
|
1088
|
+
}
|
|
1051
1089
|
return parts.join('+') || 'none';
|
|
1052
1090
|
}
|
|
1053
1091
|
|
|
@@ -1082,7 +1120,7 @@ export const ACCEPTED: ReadonlyArray<{
|
|
|
1082
1120
|
'increment|#a.textContent + 2',
|
|
1083
1121
|
'increment|#a.textContent as Int',
|
|
1084
1122
|
],
|
|
1085
|
-
lanes: 'it it/up',
|
|
1123
|
+
lanes: 'it it/up it/eng',
|
|
1086
1124
|
reason:
|
|
1087
1125
|
'ambiguity: it `di` is both `by` and `of`, so `incrementare i di #a.textContent` also says `increment i of #a.textContent`',
|
|
1088
1126
|
},
|