@hyperfixi/testing-framework 3.3.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 (28) hide show
  1. package/CHANGELOG.md +162 -0
  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
@@ -3,25 +3,25 @@
3
3
  * why and the execution model).
4
4
  *
5
5
  * Executes every eligible `_="…"` handler shipped in `examples/**` on BOTH
6
- * hyperfixi and the real `hyperscript.org` engine, in jsdom, and ratchets on
7
- * divergence of their DOM effect signatures. Upstream is the behavioral
8
- * oracle — the same role R4 gives it for validity. This is the gate that would
9
- * have caught the #785 defect (a conditional body running unconditionally on a
10
- * shipped page) on BEHAVIOR: every parse-level gate stayed green while it
11
- * shipped.
6
+ * `@hyperfixi/engine` and the real `hyperscript.org` engine, in jsdom, and
7
+ * ratchets on divergence of their DOM effect signatures. Upstream is the
8
+ * behavioral oracle — the same role R4 gives it for validity. This is the gate
9
+ * that would have caught the #785 defect (a conditional body running
10
+ * unconditionally on a shipped page) on BEHAVIOR: every parse-level gate stayed
11
+ * green while it shipped. It is also the gate for a handler that parses on both
12
+ * engines and runs differently, which the parse-level list in
13
+ * shipped-sources-engine.test.ts cannot see.
12
14
  *
13
15
  * Assertions, matching the shipped-sources gate:
14
16
  * 1. sanity — pages walked, handlers extracted, comparisons actually ran
15
17
  * (guards the silent-zero failure mode);
16
- * 2. no NEW divergence appears outside the committed allowlist;
18
+ * 2. no NEW divergence appears outside the committed allowlist
19
+ * (`allowedEngineDivergences`);
17
20
  * 3. no allowlisted key has silently converged (stale entries must be
18
21
  * removed so the list only ever ratchets down).
19
22
  *
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.
23
+ * (Until Phase C4 the same three also ran for a lane on `@hyperfixi/core`'s
24
+ * runtime, against its own allowlist. It left with core's engine.)
25
25
  *
26
26
  * To update after an intentional change: re-run and regenerate
27
27
  * `baselines/shipped-examples-execution.json` (the allowlist key embeds a
@@ -37,8 +37,8 @@
37
37
  * (happy-dom) the DOM constructors already exist on globalThis, the harness's
38
38
  * globals bootstrap refuses to overwrite what it does not own, and both
39
39
  * engines then bind happy-dom's constructors — every instanceof against a
40
- * jsdom element fails and every hyperfixi signature comes back empty
41
- * (measured: 0 real matches under happy-dom vs 74 under node).
40
+ * jsdom element fails and every signature comes back empty
41
+ * (measured on core's lane: 0 real matches under happy-dom vs 74 under node).
42
42
  */
43
43
 
44
44
  import { readFileSync } from 'node:fs';
@@ -53,14 +53,7 @@ import {
53
53
  } from './shipped-examples-execution';
54
54
 
55
55
  interface AllowlistDoc {
56
- allowedDivergences: Array<{
57
- key: string;
58
- file: string;
59
- event: string;
60
- excerpt: string;
61
- reason: string;
62
- }>;
63
- /** The engine lane: `@hyperfixi/engine` against upstream. */
56
+ /** `@hyperfixi/engine` against upstream. */
64
57
  allowedEngineDivergences: Array<{
65
58
  key: string;
66
59
  file: string;
@@ -75,18 +68,13 @@ const baselinePath = path.resolve(
75
68
  '../../baselines/shipped-examples-execution.json'
76
69
  );
77
70
  const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
78
- const allowed = new Set(allowlist.allowedDivergences.map(e => e.key));
79
71
  const allowedOnEngine = new Set(allowlist.allowedEngineDivergences.map(e => e.key));
80
72
 
81
- /** One sweep, shared by both lanes' tests. */
82
- let sweeping: Promise<ExecutionParityResult> | undefined;
83
- const sweep = (): Promise<ExecutionParityResult> => (sweeping ??= runShippedExamplesExecution());
84
-
85
- describe('shipped-examples execution gate', () => {
73
+ describe('shipped-examples execution gate: @hyperfixi/engine against upstream', () => {
86
74
  let result: ExecutionParityResult;
87
75
 
88
76
  beforeAll(async () => {
89
- result = await sweep();
77
+ result = await runShippedExamplesExecution();
90
78
 
91
79
  // Visibility, not assertions: what the sweep could not compare, and why.
92
80
  // A silently shrinking denominator is this gate's own blind spot.
@@ -95,26 +83,21 @@ describe('shipped-examples execution gate', () => {
95
83
  const r = s.reason.split(':')[0] ?? s.reason;
96
84
  reasons.set(r, (reasons.get(r) ?? 0) + 1);
97
85
  }
98
- const vacuous = result.compared.filter(c => c.vacuous).length;
86
+ const onEngine = result.engineCompared;
99
87
  console.log(
100
88
  `[shipped-examples-execution] pages=${result.pages} handlers=${result.handlers} ` +
101
- `compared=${result.compared.length} (vacuous=${vacuous}) skipped=${result.skipped.length}`
89
+ `compared=${onEngine.length} (vacuous=${onEngine.filter(c => c.vacuous).length}) ` +
90
+ `diverging=${onEngine.filter(c => !c.match).length} skipped=${result.skipped.length}`
102
91
  );
103
92
  for (const [r, n] of [...reasons].sort((a, b) => b[1] - a[1])) {
104
93
  console.log(`[shipped-examples-execution] skip ×${n}: ${r}`);
105
94
  }
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
- );
112
95
  }, 240_000);
113
96
 
114
97
  it('walks pages and compares handlers (sanity: extraction and both engines working)', () => {
115
- // Floors well below current values (48 / 261 / 122 / 52) but far above
116
- // zero: a broken walk, extractor, or engine bootstrap fails loudly here
117
- // instead of making assertions 2-3 vacuously pass.
98
+ // Floors well below current values but far above zero: a broken walk,
99
+ // extractor, or engine bootstrap fails loudly here instead of making
100
+ // assertions 2-3 vacuously pass.
118
101
  //
119
102
  // Calibrated against the git-TRACKED corpus — the sweep ignores untracked
120
103
  // examples/ dirs, so these numbers are the same on every machine and in
@@ -122,59 +105,9 @@ describe('shipped-examples execution gate', () => {
122
105
  // gitignored dirs present and failed every clean checkout — #862.)
123
106
  expect(result.pages).toBeGreaterThan(35);
124
107
  expect(result.handlers).toBeGreaterThan(200);
125
- expect(result.compared.length).toBeGreaterThan(90);
108
+ expect(result.engineCompared.length).toBeGreaterThan(90);
126
109
  // Vacuous (empty-vs-empty) pairs are NOT parity evidence — the floor is on
127
110
  // real, non-empty signature matches.
128
- const realMatches = result.compared.filter(c => c.match && !c.vacuous).length;
129
- expect(realMatches).toBeGreaterThan(40);
130
- });
131
-
132
- it('has no NEW divergence from upstream outside the allowlist', () => {
133
- const unexpected = result.compared.filter(c => !c.match && !allowed.has(c.key));
134
- expect(
135
- unexpected,
136
- unexpected.length
137
- ? `\nShipped handlers whose DOM effect DIVERGES from the hyperscript.org engine ` +
138
- `(fix the behavior, or allowlist with a family reason):\n` +
139
- unexpected
140
- .map(
141
- f =>
142
- ` [${f.key}]\n` +
143
- ` "${f.excerpt}"\n` +
144
- ` hyperfixi: ${JSON.stringify(f.hyperfixiEffects).slice(0, 300)}\n` +
145
- ` upstream : ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
146
- )
147
- .join('\n') +
148
- `\n\nTriage guidance: an EMPTY hyperfixi signature with a non-empty upstream one usually\n` +
149
- `means hyperfixi silently dropped behavior (the #785 class). The reverse often means a\n` +
150
- `deliberate hyperfixi extension or a jsdom limitation on the upstream side — check the\n` +
151
- `existing family reasons in baselines/shipped-examples-execution.json before adding a new one.`
152
- : ''
153
- ).toEqual([]);
154
- });
155
-
156
- it('has no stale allowlist entries (a now-converged handler must be removed so the list ratchets down)', () => {
157
- const stillDiverging = new Set(result.compared.filter(c => !c.match).map(c => c.key));
158
- const stale = allowlist.allowedDivergences.map(e => e.key).filter(k => !stillDiverging.has(k));
159
- expect(
160
- stale,
161
- stale.length
162
- ? `\nThese allowlisted handlers no longer diverge (fixed, or edited — the key embeds a\n` +
163
- `source hash; or no longer eligible, in which case the coverage loss should be deliberate).\n` +
164
- `Remove them from baselines/shipped-examples-execution.json:\n ${stale.join('\n ')}`
165
- : ''
166
- ).toEqual([]);
167
- });
168
- });
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
111
  const realMatches = result.engineCompared.filter(c => c.match && !c.vacuous).length;
179
112
  expect(realMatches).toBeGreaterThan(60);
180
113
  });
@@ -196,12 +129,14 @@ describe('shipped-examples execution gate: @hyperfixi/engine against upstream',
196
129
  ` upstream: ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
197
130
  )
198
131
  .join('\n') +
199
- `\n\nOne source on both, on one page: npx tsx packages/engine/tools/probe.mts '<source>'`
132
+ `\n\nTriage guidance: an EMPTY engine signature with a non-empty upstream one usually\n` +
133
+ `means the engine silently dropped behavior (the #785 class).\n` +
134
+ `One source on both, on one page: npx tsx packages/engine/tools/probe.mts '<source>'`
200
135
  : ''
201
136
  ).toEqual([]);
202
137
  });
203
138
 
204
- it('has no stale allowlist entries', () => {
139
+ it('has no stale allowlist entries (a now-converged handler must be removed so the list ratchets down)', () => {
205
140
  const stillDiverging = new Set(result.engineCompared.filter(c => !c.match).map(c => c.key));
206
141
  const stale = allowlist.allowedEngineDivergences
207
142
  .map(e => e.key)
@@ -209,9 +144,10 @@ describe('shipped-examples execution gate: @hyperfixi/engine against upstream',
209
144
  expect(
210
145
  stale,
211
146
  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 ')}`
147
+ ? `\nThese handlers no longer diverge on the engine (fixed, or edited — the key embeds a\n` +
148
+ `source hash; or no longer eligible, in which case the coverage loss should be deliberate).\n` +
149
+ `Remove them from allowedEngineDivergences in baselines/shipped-examples-execution.json:\n ` +
150
+ stale.join('\n ')
215
151
  : ''
216
152
  ).toEqual([]);
217
153
  });
@@ -9,24 +9,27 @@
9
9
  *
10
10
  * This gate executes the handlers we actually ship. For each `_="…"` attribute
11
11
  * in `examples/**`, the full page is loaded into two jsdom instances — one
12
- * processed by hyperfixi (parse + a fresh Runtime with the element as context,
13
- * the same shape the browser attribute processor uses), one by the real
14
- * `hyperscript.org` engine (`processNode`) — the handler's trigger event is
15
- * dispatched, and the resulting DOM effect signatures (../effect-signature.ts,
16
- * shared with the R2 execution ratchet) are diffed against each other.
17
- * Upstream plays the role R4 gives it for validity: the oracle. A divergence
18
- * means hyperfixi's runtime behavior differs from upstream's ON A SHIPPED
19
- * PAGE — exactly the #785/#786 failure mode, caught on behavior instead of by
20
- * luck.
12
+ * processed by `@hyperfixi/engine`, one by the real `hyperscript.org` engine
13
+ * (`processNode` on both) — the handler's trigger event is dispatched, and the
14
+ * resulting DOM effect signatures (../effect-signature.ts, shared with the R2
15
+ * execution ratchet) are diffed against each other. Upstream plays the role R4
16
+ * gives it for validity: the oracle. A divergence means the engine's runtime
17
+ * behavior differs from upstream's ON A SHIPPED PAGE — exactly the #785/#786
18
+ * failure mode, caught on behavior instead of by luck. The parse-level list of
19
+ * what the engine rejects (shipped-sources-engine.test.ts) cannot see a handler
20
+ * that parses on both and runs differently; this can. A handler upstream
21
+ * accepts and the engine rejects is compared too, and diverges.
22
+ *
23
+ * (Until Phase C4 a second lane ran each handler on `@hyperfixi/core`'s runtime
24
+ * against the same oracle. It left with core's engine; its ten allowlisted
25
+ * divergences were core's.)
21
26
  *
22
27
  * ## Fair denominator (mirrors R4)
23
- * A handler is only compared when BOTH engines accept its source: hyperfixi
24
- * must compile it clean (`ok: true`, no recovered errors — recovering sources
25
- * are the shipped-sources gate's business) and upstream must parse it (a
26
- * hyperfixi-only extension has no oracle). It must also be deterministically
27
- * executable in jsdom: triggered by a dispatchable event, free of network /
28
- * timer / navigation constructs. Every exclusion is recorded with a reason —
29
- * the skip list is part of the result, never silent (`no silent caps`).
28
+ * A handler is only compared when upstream parses it (an engine-only form, such
29
+ * as `new X()`, has no oracle). It must also be deterministically executable in
30
+ * jsdom: triggered by a dispatchable event, free of network / timer /
31
+ * navigation constructs. Every exclusion is recorded with a reason — the skip
32
+ * list is part of the result, never silent (`no silent caps`).
30
33
  *
31
34
  * ## Execution model
32
35
  * Per handler and engine: a FRESH jsdom of the whole page, every eligible
@@ -35,15 +38,6 @@
35
38
  * before/after snapshot around it. See runHandlerOnEngine for why isolation is
36
39
  * worth its cost.
37
40
  *
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
- *
47
41
  * ## Node-only
48
42
  * Imports the real `hyperscript.org` build off disk and swaps jsdom globals
49
43
  * per handler execution (both engines resolve `document` lazily through
@@ -135,12 +129,12 @@ export interface SkippedHandler extends ShippedHandler {
135
129
  reason: string;
136
130
  }
137
131
 
138
- /** One compared handler: both engines ran it; signatures either match or not. */
139
- export interface ComparedHandler extends ShippedHandler {
132
+ /** One handler run on upstream and on `@hyperfixi/engine`; signatures either match or not. */
133
+ export interface EngineComparedHandler extends ShippedHandler {
140
134
  /** Stable key: file + source hash + event. Fixing a source changes its key. */
141
135
  key: string;
142
136
  event: string;
143
- hyperfixiEffects: string[];
137
+ engineEffects: string[];
144
138
  upstreamEffects: string[];
145
139
  match: boolean;
146
140
  /**
@@ -153,25 +147,11 @@ export interface ComparedHandler extends ShippedHandler {
153
147
  excerpt: string;
154
148
  }
155
149
 
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
-
168
150
  export interface ExecutionParityResult {
169
151
  /** Pages walked. */
170
152
  pages: number;
171
153
  /** Handlers found (before eligibility). */
172
154
  handlers: number;
173
- /** Handlers executed on both engines. */
174
- compared: ComparedHandler[];
175
155
  /** Handlers excluded, each with its reason. */
176
156
  skipped: SkippedHandler[];
177
157
  /** Handlers upstream accepts, executed on upstream and on `@hyperfixi/engine`. */
@@ -234,12 +214,8 @@ export function extractHandlers(file: string, html: string): ShippedHandler[] {
234
214
 
235
215
  /** Minimal engine surfaces, injected by `initEngines`. */
236
216
  export interface Engines {
237
- /** hyperfixi compile check (fair-denominator side 1). */
238
- compileClean(source: string): boolean;
239
- /** upstream parse check (fair-denominator side 2). Returns error strings; [] = valid. */
217
+ /** upstream parse check (the fair denominator). Returns error strings; [] = valid. */
240
218
  upstreamErrors(source: string): string[];
241
- /** Install a handler on an element via hyperfixi (the public eval surface). */
242
- hyperfixiInstall(source: string, el: Element): Promise<void>;
243
219
  /** Install a handler on an element via the upstream engine. */
244
220
  upstreamInstall(el: Element): void;
245
221
  /** `@hyperfixi/engine`'s parse errors for a source; [] = it parses. */
@@ -315,22 +291,6 @@ export async function initEngines(): Promise<Engines> {
315
291
  installRejectionTrap();
316
292
  installGlobals(new JSDOM('<!doctype html><html><body></body></html>'));
317
293
 
318
- // Install goes through parse + a FRESH Runtime per handler with the element
319
- // as context — the same shape the browser attribute processor uses, and the
320
- // same shape the R2 validator proved stable across per-page document swaps.
321
- // (The api singleton `hyperscript.eval` binds document-dependent state at
322
- // first use, which is correct in a browser — document identity never changes
323
- // within a realm — but silently resolves later PAGES' selectors against the
324
- // first page here. Measured: a two-page probe no-opped page 2's toggle.)
325
- const core = (await import('@hyperfixi/core')) as unknown as {
326
- hyperscript: {
327
- compileSync(code: string): { ok: boolean; errors?: Array<{ message: string }> };
328
- };
329
- parse(code: string): { success: boolean; node?: unknown };
330
- Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
331
- createContext(el: HTMLElement): unknown;
332
- };
333
-
334
294
  const require = createRequire(import.meta.url);
335
295
  const esm = path.join(path.dirname(require.resolve('hyperscript.org')), '_hyperscript.esm.js');
336
296
  const hs = (await import(pathToFileURL(esm).href)).default as {
@@ -352,14 +312,6 @@ export async function initEngines(): Promise<Engines> {
352
312
  engineInstall(el) {
353
313
  engine.api.processNode(el);
354
314
  },
355
- compileClean(source) {
356
- try {
357
- const r = core.hyperscript.compileSync(source);
358
- return r.ok && (r.errors ?? []).length === 0;
359
- } catch {
360
- return false;
361
- }
362
- },
363
315
  upstreamErrors(source) {
364
316
  try {
365
317
  return (hs.parse(source)?.errors ?? []).map(e => e.message);
@@ -367,14 +319,6 @@ export async function initEngines(): Promise<Engines> {
367
319
  return ['threw: ' + (e as Error).message.split('\n')[0]];
368
320
  }
369
321
  },
370
- async hyperfixiInstall(source, el) {
371
- const parsed = core.parse(source);
372
- if (!parsed.success || !parsed.node) {
373
- throw new Error('parse failed at install (eligibility should have caught this)');
374
- }
375
- const runtime = new core.Runtime();
376
- await runtime.execute(parsed.node, core.createContext(el as HTMLElement));
377
- },
378
322
  upstreamInstall(el) {
379
323
  hs.processNode(el);
380
324
  },
@@ -452,7 +396,7 @@ async function runHandlerOnEngine(
452
396
  /**
453
397
  * The sweep: walk the git-tracked `examples/**` corpus, extract handlers,
454
398
  * apply the fair-denominator filters (each skip reasoned), and execute every
455
- * eligible handler on both engines.
399
+ * eligible handler on upstream and on the engine.
456
400
  */
457
401
  export async function runShippedExamplesExecution(opts?: {
458
402
  roots?: string[];
@@ -463,7 +407,6 @@ export async function runShippedExamplesExecution(opts?: {
463
407
  const roots = opts?.roots ?? DEFAULT_ROOTS;
464
408
  const engines = opts?.engines ?? (await initEngines());
465
409
 
466
- const compared: ComparedHandler[] = [];
467
410
  const skipped: SkippedHandler[] = [];
468
411
  const engineCompared: EngineComparedHandler[] = [];
469
412
  let pages = 0;
@@ -478,9 +421,7 @@ export async function runShippedExamplesExecution(opts?: {
478
421
  pages++;
479
422
  handlers += pageHandlers.length;
480
423
 
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.
424
+ // The denominator: dispatchable, deterministic, and upstream accepts it.
484
425
  const oracle: Array<ShippedHandler & { event: string }> = [];
485
426
  for (const h of pageHandlers) {
486
427
  if (!h.event) {
@@ -497,26 +438,13 @@ export async function runShippedExamplesExecution(opts?: {
497
438
  continue;
498
439
  }
499
440
  const upstreamErrs = engines.upstreamErrors(h.source);
500
- if (upstreamErrs.length === 0) oracle.push(h as ShippedHandler & { event: string });
501
- if (!engines.compileClean(h.source)) {
502
- skipped.push({
503
- ...h,
504
- reason: 'hyperfixi does not compile it clean (shipped-sources gate territory)',
505
- });
506
- continue;
507
- }
508
441
  if (upstreamErrs.length > 0) {
509
442
  skipped.push({ ...h, reason: `upstream rejects it (no oracle): ${upstreamErrs[0]}` });
510
443
  continue;
511
444
  }
512
- eligible.push(h as ShippedHandler & { event: string });
445
+ oracle.push(h as ShippedHandler & { event: string });
513
446
  }
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[]>();
447
+ if (oracle.length === 0) continue;
520
448
 
521
449
  // Both engines log runtime errors to the console during dispatch
522
450
  // (COMMAND FAILED etc.). That is expected data here — the effect
@@ -531,29 +459,11 @@ export async function runShippedExamplesExecution(opts?: {
531
459
  const noop = () => {};
532
460
  console.log = console.warn = console.error = console.debug = noop;
533
461
  try {
534
- for (const h of eligible) {
535
- const ours = await runHandlerOnEngine(html, eligible, h, (hh, el) =>
536
- engines.hyperfixiInstall(hh.source, el)
537
- );
538
- const theirs = await runHandlerOnEngine(html, eligible, h, (_hh, el) =>
539
- engines.upstreamInstall(el)
540
- );
541
- upstreamRun.set(h, theirs);
542
- compared.push({
543
- ...h,
544
- key: keyFor(h),
545
- hyperfixiEffects: ours,
546
- upstreamEffects: theirs,
547
- match: JSON.stringify(ours) === JSON.stringify(theirs),
548
- vacuous: ours.length === 0 && theirs.length === 0,
549
- excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
550
- });
551
- }
552
462
  for (const h of oracle) {
553
463
  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)));
464
+ const theirs = await runHandlerOnEngine(html, oracle, h, (_hh, el) =>
465
+ engines.upstreamInstall(el)
466
+ );
557
467
  const ours =
558
468
  rejected !== undefined
559
469
  ? [`<engine rejects the source: ${rejected}>`]
@@ -577,5 +487,5 @@ export async function runShippedExamplesExecution(opts?: {
577
487
  }
578
488
  }
579
489
 
580
- return { pages, handlers, compared, skipped, engineCompared };
490
+ return { pages, handlers, skipped, engineCompared };
581
491
  }
@@ -1,21 +1,24 @@
1
1
  /**
2
- * Shipped sources on `@hyperfixi/engine`: what replacing core's engine would break.
2
+ * Shipped sources on `@hyperfixi/engine`: every English source must parse.
3
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
4
+ * Every hyperscript source this repository ships in English (`examples/` and
5
+ * the doc trees; a source under a non-English `lang` is the localized gate's)
6
+ * is put to the engine's parser. The ones it rejects are listed in
8
7
  * `baselines/shipped-sources-engine.json`, each with the reason it is still
9
8
  * there. Three assertions, as the sibling gates:
10
9
  * 1. sanity: sources were found and the engine reads most of them;
11
10
  * 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;
11
+ * written in syntax the engine lacks (core 3.x's own forms among it)
12
+ * does not run; write upstream's spelling (`packages/engine/README.md`
13
+ * lists the forms that were considered and not kept), or list it with a
14
+ * reason;
15
15
  * 3. no stale entry: a listed source the engine now reads, or one that was
16
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.
17
+ * list only shrinks. It has been empty since the history pages moved to
18
+ * `call history.pushState` (Phase B1).
19
+ *
20
+ * Until Phase C4 the denominator was the sources `@hyperfixi/core` compiled
21
+ * clean (see `checkShippedSourcesOnEngine`).
19
22
  *
20
23
  * This is a PARSE-level gate. A source can parse on both engines and run
21
24
  * differently; the engine lane of `shipped-examples-execution.test.ts` is the
@@ -32,7 +35,6 @@ import { JSDOM } from 'jsdom';
32
35
  import { describe, it, expect, beforeAll } from 'vitest';
33
36
  import {
34
37
  checkShippedSourcesOnEngine,
35
- type CompileForValidity,
36
38
  type ShippedSourcesOnEngineResult,
37
39
  } from './shipped-sources-validity';
38
40
 
@@ -51,14 +53,10 @@ describe('shipped sources on @hyperfixi/engine', () => {
51
53
  let result: ShippedSourcesOnEngineResult;
52
54
 
53
55
  beforeAll(async () => {
54
- const core = (await import('@hyperfixi/core')) as unknown as {
55
- hyperscript: { compileSync: CompileForValidity };
56
- };
57
56
  const { api, everything, register } = await import('@hyperfixi/engine');
58
57
  register(...everything);
59
58
  const dom = new JSDOM('<!doctype html><html><body></body></html>');
60
59
  result = checkShippedSourcesOnEngine(
61
- code => core.hyperscript.compileSync(code),
62
60
  code => {
63
61
  try {
64
62
  return api.parse(code).errors.map(error => error.message);
@@ -66,13 +64,15 @@ describe('shipped sources on @hyperfixi/engine', () => {
66
64
  return ['threw: ' + (e instanceof Error ? e.message : String(e))];
67
65
  }
68
66
  },
69
- dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[2]
67
+ dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[1]
70
68
  );
71
69
  }, 120_000);
72
70
 
73
71
  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);
72
+ expect(result.checked).toBeGreaterThan(300);
73
+ expect(result.engineAccepts).toBeGreaterThan(300);
74
+ // Written in another language: the localized gate's, not this one's.
75
+ expect(result.localized).toBeGreaterThan(10);
76
76
  });
77
77
 
78
78
  it('has no NEW shipped source the engine rejects outside the list', () => {
@@ -80,7 +80,7 @@ describe('shipped sources on @hyperfixi/engine', () => {
80
80
  expect(
81
81
  unexpected,
82
82
  unexpected.length
83
- ? `\nShipped sources core compiles and @hyperfixi/engine rejects (write upstream's spelling,\n` +
83
+ ? `\nShipped sources @hyperfixi/engine rejects (write upstream's spelling,\n` +
84
84
  `or list it in baselines/shipped-sources-engine.json with a reason):\n` +
85
85
  unexpected
86
86
  .map(r => ` [${r.key}]\n "${r.excerpt}"\n -> ${r.error}`)
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shipped sources written in another language, through the multilingual path.
3
+ *
4
+ * Both shipped-sources gates hand every `_` to an English parser, which
5
+ * rejects a script written in Spanish, so they skip it — and nothing else
6
+ * looked. On a page such a script runs through `@lokascript/hyperscript-
7
+ * adapter`, which translates it under its element's `lang`. This gate does
8
+ * the same and requires English that upstream `_hyperscript` and
9
+ * `@hyperfixi/engine` both parse (Phase C2c; fifteen attributes today: the
10
+ * `live` blocks of examples/hx-v4-i18n/live-multilang.html, the example in
11
+ * docs/BROWSER_BUNDLES.md's "Hyperscript in another language", and the
12
+ * multilingual section of packages/core/docs/EXAMPLES.md).
13
+ *
14
+ * @vitest-environment node
15
+ * Required: the hosts and the collector need real jsdom documents.
16
+ */
17
+ import { createRequire } from 'node:module';
18
+ import { pathToFileURL } from 'node:url';
19
+ import { describe, it, expect, beforeAll } from 'vitest';
20
+ import { JSDOM } from 'jsdom';
21
+ import { preprocess } from '@lokascript/hyperscript-adapter';
22
+ import { collectLocalizedSources, type LocalizedSource } from './shipped-sources-validity';
23
+ import { installGlobals } from './shipped-examples-execution';
24
+
25
+ interface ParseHost {
26
+ parse(source: string): { errors?: Array<{ message: string }> } | undefined;
27
+ }
28
+
29
+ describe('shipped sources written in another language', () => {
30
+ const sources: LocalizedSource[] = collectLocalizedSources();
31
+ let upstream: ParseHost;
32
+ let engine: ParseHost;
33
+
34
+ beforeAll(async () => {
35
+ installGlobals(new JSDOM('<!doctype html><html><body></body></html>'));
36
+ const require = createRequire(import.meta.url);
37
+ const esm = require.resolve('hyperscript.org').replace(/[^/\\]+$/, '_hyperscript.esm.js');
38
+ upstream = (await import(pathToFileURL(esm).href)).default as ParseHost;
39
+ const engineModule = await import('@hyperfixi/engine');
40
+ engineModule.register(...engineModule.everything);
41
+ engine = engineModule.api;
42
+ });
43
+
44
+ it('finds them (a page that moves them changes this count on purpose)', () => {
45
+ expect(sources.map(s => `${s.file} [${s.lang}]`)).toEqual([
46
+ 'examples/hx-v4-i18n/live-multilang.html [es]',
47
+ 'examples/hx-v4-i18n/live-multilang.html [ja]',
48
+ 'examples/hx-v4-i18n/live-multilang.html [ar]',
49
+ // The engine + adapter example in "Hyperscript in another language" (C-R3).
50
+ 'docs/BROWSER_BUNDLES.md [ja]',
51
+ // "Multilingual Examples". Marked with `lang` in C4b: core's English parser had
52
+ // kept them out of the engine gate; the engine gate now reads everything else.
53
+ 'packages/core/docs/EXAMPLES.md [es]',
54
+ 'packages/core/docs/EXAMPLES.md [ja]',
55
+ 'packages/core/docs/EXAMPLES.md [ar]',
56
+ 'packages/core/docs/EXAMPLES.md [es]',
57
+ 'packages/core/docs/EXAMPLES.md [ja]',
58
+ 'packages/core/docs/EXAMPLES.md [ko]',
59
+ 'packages/core/docs/EXAMPLES.md [es]',
60
+ 'packages/core/docs/EXAMPLES.md [fr]',
61
+ 'packages/core/docs/EXAMPLES.md [de]',
62
+ 'packages/core/docs/EXAMPLES.md [es]',
63
+ 'packages/core/docs/EXAMPLES.md [pt]',
64
+ ]);
65
+ });
66
+
67
+ it('each translates to English upstream and the engine both parse', () => {
68
+ const failures: string[] = [];
69
+ for (const s of sources) {
70
+ let english: string;
71
+ try {
72
+ english = preprocess(s.source, s.lang);
73
+ } catch (e) {
74
+ failures.push(`${s.file} [${s.lang}] threw: ${(e as Error).message}`);
75
+ continue;
76
+ }
77
+ const first = (host: ParseHost) => host.parse(english)?.errors?.[0]?.message.split('\n')[0];
78
+ const problem =
79
+ english === s.source
80
+ ? 'not translated'
81
+ : (first(upstream) && `upstream: ${first(upstream)}`) ||
82
+ (first(engine) && `engine: ${first(engine)}`);
83
+ if (problem) failures.push(`${s.file} [${s.lang}] ${problem}\n ${s.source}\n → ${english}`);
84
+ }
85
+ expect(failures).toEqual([]);
86
+ });
87
+ });