@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
@@ -42,14 +42,12 @@
42
42
  * Each cell runs its English source on the real `hyperscript.org` engine: that
43
43
  * result is the ORACLE. Then, on the same fixture:
44
44
  *
45
- * - `en` hyperfixi's English path (core's parser and runtime);
46
45
  * - `en-rt` semantic's English parse rendered back to English, on upstream:
47
46
  * where it fails, every translation inherits the loss;
48
- * - `<L>` each of 23 languages on hyperfixi's direct path —
49
- * `render(parse_en(src), L)`, compiled with `{ language: L }`;
50
- * - `<L>/up` the same translation through `@lokascript/hyperscript-adapter`
51
- * (`preprocess`, back to English) on upstream: the multilingual
52
- * product for original _hyperscript users;
47
+ * - `<L>/up` each of 23 languages — `render(parse_en(src), L)` — through
48
+ * `@lokascript/hyperscript-adapter` (`preprocess`, back to
49
+ * English) on upstream: the multilingual product for original
50
+ * _hyperscript users;
53
51
  * - `eng` the English source on `@hyperfixi/engine`, the engine that
54
52
  * replaces core's;
55
53
  * - `<L>/eng` the adapter's English (the string the `/up` lane runs) on
@@ -58,6 +56,15 @@
58
56
  *
59
57
  * A (cell, lane) pair FAILS when its result differs from the oracle's.
60
58
  *
59
+ * A `<L>` lane ran each translation on core's direct path (compiled with
60
+ * `{ language: L }`) until Phase C2 of the engine cutover, which made text the
61
+ * multilingual interchange. It retired after the measurement the plan asked
62
+ * for: on the committed baseline no cell failed `<L>/eng` while passing `<L>`
63
+ * (the only foreign failures, three Italian `di` cells, failed all three).
64
+ * An `en` lane ran the English source on `@hyperfixi/core`'s parser and runtime
65
+ * until Phase C4, when it left with core's engine; its only failures were the
66
+ * eight accepted cells of core's `the X of Y as T` reading, which `eng` passes.
67
+ *
61
68
  * ## The ratchet
62
69
  * `baselines/value-matrix.json` lists every failing pair, per cell. The gate
63
70
  * fails on a failing pair it does not list AND on a listed pair that now
@@ -69,14 +76,13 @@
69
76
  * One jsdom window for the whole run; each lane resets `<body>` and the
70
77
  * globals, installs the handler on a fresh button, clicks it, and reads
71
78
  * `#out`. Variables are window globals (see GLOBALS), so no source needs a
72
- * `set` prefix. Upstream runs synchronously; hyperfixi's handler settles
73
- * within one macrotask.
79
+ * `set` prefix. Both engines run synchronously unless the source waits.
74
80
  *
75
- * A translation that loses a loop's condition can loop forever. Core caps a
76
- * loop at 10,000 iterations; upstream has no cap and blocks the thread, so
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.
81
+ * A translation that loses a loop's condition can loop forever. Upstream has
82
+ * no cap and blocks the thread, so every upstream run gets an evaluation
83
+ * budget (see EVAL_BUDGET). The new engine has no cap and no hook for one: a
84
+ * string that spent the budget on upstream is not run on it, and its `/eng`
85
+ * lane reads `✗budget` too.
80
86
  *
81
87
  * Node-only: imports the real `hyperscript.org` build off disk and needs the
82
88
  * node vitest environment (see the shipped-examples gate for why).
@@ -738,12 +744,14 @@ export const FOREIGN_LANGUAGES = [
738
744
 
739
745
  /** Every lane, in report order. */
740
746
  export const LANES: readonly string[] = [
741
- 'en',
742
747
  'en-rt',
743
748
  'eng',
744
- ...FOREIGN_LANGUAGES.flatMap(language => [language, `${language}/up`, `${language}/eng`]),
749
+ ...FOREIGN_LANGUAGES.flatMap(language => [`${language}/up`, `${language}/eng`]),
745
750
  ];
746
751
 
752
+ /** The lanes a cell runs per language it does not skip. */
753
+ export const LANES_PER_LANGUAGE = 2;
754
+
747
755
  /**
748
756
  * Upstream evaluations allowed per run. A cell uses well under a hundred; a
749
757
  * loop whose condition a translation lost uses them all and stops, where it
@@ -787,8 +795,8 @@ export interface MatrixEngines {
787
795
  *
788
796
  * Until `close()`, the console is silenced and unhandled rejections are
789
797
  * trapped: thousands of lanes fail by design, both engines report a failure
790
- * on the console, and hyperfixi's handler is an async listener, so an error
791
- * in it rejects a promise nobody holds. The process's own rejection
798
+ * on the console, and a handler that waits on something runs on after its
799
+ * click, so an error in it rejects a promise nobody holds. The process's own rejection
792
800
  * listeners (vitest's, under test) are set aside for the run and restored.
793
801
  */
794
802
  export async function initMatrixEngines(): Promise<MatrixEngines> {
@@ -815,7 +823,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
815
823
  };
816
824
  process.on('unhandledRejection', trap);
817
825
 
818
- const { hyperscript } = await import('@hyperfixi/core');
819
826
  const { parseSemantic, render } = await import('@lokascript/semantic');
820
827
  const { preprocess } = await import('@lokascript/hyperscript-adapter');
821
828
  const require = createRequire(import.meta.url);
@@ -836,13 +843,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
836
843
 
837
844
  const window = dom.window;
838
845
  const document = window.document;
839
- type Ast = Parameters<typeof hyperscript.execute>[0];
840
-
841
- // hyperfixi keeps its global variables in one Map that every context shares,
842
- // and writes a window global there (`increment n`). It outlives the run, and
843
- // it shadows window's copy, so every later lane would read the write.
844
- const coreGlobals = hyperscript.createContext().globals;
845
- const coreGlobalsAtStart = new Map(coreGlobals);
846
846
  const headAtStart = document.head.innerHTML;
847
847
  const names = collidingNames();
848
848
 
@@ -859,8 +859,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
859
859
  html.innerHTML = `<head>${headAtStart}</head><body></body>`;
860
860
  }
861
861
  document.body.innerHTML = FIXTURE;
862
- coreGlobals.clear();
863
- for (const [name, value] of coreGlobalsAtStart) coreGlobals.set(name, value);
864
862
  for (const [name, make] of Object.entries(GLOBALS)) {
865
863
  const value = make();
866
864
  Reflect.set(window, name, value);
@@ -893,15 +891,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
893
891
  };
894
892
  const onUpstream = (source: string): string => onHost(upstream, source);
895
893
 
896
- /** Install a compiled handler on hyperfixi, click, and settle. */
897
- const onHyperfixi = async (ast: Ast): Promise<string> => {
898
- const button = reset();
899
- await hyperscript.execute(ast, hyperscript.createContext(button));
900
- click(button);
901
- await new Promise(resolve => setTimeout(resolve, 0));
902
- return read();
903
- };
904
-
905
894
  const guard = async (run: () => Promise<string> | string): Promise<string> => {
906
895
  try {
907
896
  return await run();
@@ -920,12 +909,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
920
909
  }
921
910
  const lanes = result.lanes;
922
911
 
923
- lanes.en = await guard(async () => {
924
- const compiled = hyperscript.compileSync(cell.source);
925
- if (!compiled.ok || !compiled.ast) return '✗compile';
926
- return onHyperfixi(compiled.ast);
927
- });
928
-
929
912
  const english = parseSemantic(cell.source, 'en').node;
930
913
  lanes['en-rt'] = await guard(() =>
931
914
  english ? onUpstream(render(english, 'en')) : '✗untranslatable'
@@ -941,17 +924,11 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
941
924
  code = null;
942
925
  }
943
926
  if (code === null) {
944
- lanes[language] = '✗untranslatable';
945
927
  lanes[`${language}/up`] = '✗untranslatable';
946
928
  lanes[`${language}/eng`] = '✗untranslatable';
947
929
  continue;
948
930
  }
949
931
  const translated = code;
950
- lanes[language] = await guard(async () => {
951
- const compiled = await hyperscript.compile(translated, { language });
952
- if (!compiled.ok || !compiled.ast) return '✗compile';
953
- return onHyperfixi(compiled.ast);
954
- });
955
932
  // The adapter's English, once, for both hosts.
956
933
  const adapted = await guard(() => preprocess(translated, language));
957
934
  const up = (lanes[`${language}/up`] = adapted.startsWith('✗')
@@ -992,9 +969,8 @@ export async function runValueMatrix(cells: readonly MatrixCell[]): Promise<Cell
992
969
 
993
970
  export interface BaselineEntry {
994
971
  /**
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.
972
+ * The failing lanes, space-separated, in LANES order, with two shorthands:
973
+ * `*up` for all 23 languages on upstream, `*eng` for all 23 on the new engine.
998
974
  */
999
975
  lanes: string;
1000
976
  /** Where the loss sits, for reading the burn-down (see familyOf); not asserted. */
@@ -1014,7 +990,6 @@ export interface ValueMatrixBaseline {
1014
990
  entries: Record<string, BaselineEntry>;
1015
991
  }
1016
992
 
1017
- const DIRECT_LANES: readonly string[] = FOREIGN_LANGUAGES;
1018
993
  const ADAPTER_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/up`);
1019
994
  const ENGINE_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/eng`);
1020
995
 
@@ -1022,17 +997,14 @@ const ENGINE_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${lan
1022
997
  export function compressLanes(lanes: readonly string[]): string {
1023
998
  const set = new Set(lanes);
1024
999
  const out: string[] = [];
1025
- const direct = DIRECT_LANES.every(l => set.has(l));
1026
1000
  const adapter = ADAPTER_LANES.every(l => set.has(l));
1027
1001
  const engine = ENGINE_LANES.every(l => set.has(l));
1028
1002
  for (const lane of LANES) {
1029
1003
  if (!set.has(lane)) continue;
1030
- if (direct && DIRECT_LANES.includes(lane)) continue;
1031
1004
  if (adapter && ADAPTER_LANES.includes(lane)) continue;
1032
1005
  if (engine && ENGINE_LANES.includes(lane)) continue;
1033
1006
  out.push(lane);
1034
1007
  }
1035
- if (direct) out.push('*direct');
1036
1008
  if (adapter) out.push('*up');
1037
1009
  if (engine) out.push('*eng');
1038
1010
  return out.join(' ');
@@ -1042,8 +1014,7 @@ export function compressLanes(lanes: readonly string[]): string {
1042
1014
  export function expandLanes(text: string): string[] {
1043
1015
  const out: string[] = [];
1044
1016
  for (const token of text.split(' ').filter(Boolean)) {
1045
- if (token === '*direct') out.push(...DIRECT_LANES);
1046
- else if (token === '*up') out.push(...ADAPTER_LANES);
1017
+ if (token === '*up') out.push(...ADAPTER_LANES);
1047
1018
  else if (token === '*eng') out.push(...ENGINE_LANES);
1048
1019
  else out.push(token);
1049
1020
  }
@@ -1058,12 +1029,10 @@ export function failingLanes(result: CellResult): string[] {
1058
1029
  /**
1059
1030
  * Where a cell's failure sits, from which lanes fail:
1060
1031
  *
1061
- * - `core` core's English run differs from upstream's;
1062
1032
  * - `semantic-en` semantic's English parse loses it (`en-rt`), so every
1063
1033
  * translation inherits the loss;
1064
- * - `translation` some foreign lanes, on both engines;
1065
- * - `direct-path` hyperfixi's foreign lanes only;
1066
- * - `adapter` upstream's foreign lanes only;
1034
+ * - `translation` some foreign lanes on upstream: the adapter's English
1035
+ * for that language says something else;
1067
1036
  * - `engine` the new engine differs from upstream on the same text:
1068
1037
  * its English run, or a language's `/eng` lane without
1069
1038
  * its `/up` lane.
@@ -1073,16 +1042,8 @@ export function failingLanes(result: CellResult): string[] {
1073
1042
  export function familyOf(lanes: readonly string[]): string {
1074
1043
  const set = new Set(lanes);
1075
1044
  const parts: string[] = [];
1076
- if (set.has('en')) parts.push('core');
1077
1045
  if (set.has('en-rt')) parts.push('semantic-en');
1078
- else {
1079
- const direct = FOREIGN_LANGUAGES.filter(l => set.has(l));
1080
- const adapter = FOREIGN_LANGUAGES.filter(l => set.has(`${l}/up`));
1081
- const both = direct.filter(l => adapter.includes(l));
1082
- if (both.length) parts.push('translation');
1083
- if (direct.length > both.length && !set.has('en')) parts.push('direct-path');
1084
- if (adapter.length > both.length) parts.push('adapter');
1085
- }
1046
+ else if (FOREIGN_LANGUAGES.some(l => set.has(`${l}/up`))) parts.push('translation');
1086
1047
  if (set.has('eng') || FOREIGN_LANGUAGES.some(l => set.has(`${l}/eng`) !== set.has(`${l}/up`))) {
1087
1048
  parts.push('engine');
1088
1049
  }
@@ -1101,26 +1062,13 @@ export const ACCEPTED: ReadonlyArray<{
1101
1062
  lanes: string;
1102
1063
  reason: string;
1103
1064
  }> = [
1104
- {
1105
- cells: [
1106
- ...['put', 'set', 'while', 'times', 'increment'].map(
1107
- p => `${p}|the textContent of #a as Int`
1108
- ),
1109
- // The same difference with a reference owner (PR 108); upstream's
1110
- // `window as Int` is null, and in a loop bound both read 0 iterations.
1111
- ...['put', 'set', 'increment'].map(p => `${p}|the scrollY of window as Int`),
1112
- ],
1113
- lanes: 'en *direct',
1114
- reason:
1115
- 'known difference: core converts the property, upstream the target (core/docs/UPSTREAM-KNOWN-DIFFS.md)',
1116
- },
1117
1065
  {
1118
1066
  cells: [
1119
1067
  'increment|#a.textContent',
1120
1068
  'increment|#a.textContent + 2',
1121
1069
  'increment|#a.textContent as Int',
1122
1070
  ],
1123
- lanes: 'it it/up it/eng',
1071
+ lanes: 'it/up it/eng',
1124
1072
  reason:
1125
1073
  'ambiguity: it `di` is both `by` and `of`, so `incrementare i di #a.textContent` also says `increment i of #a.textContent`',
1126
1074
  },
@@ -1,95 +0,0 @@
1
- /**
2
- * Shipped-sources validity gate (see shipped-sources-validity.ts for the why).
3
- *
4
- * Compiles every hyperscript source we ship in `examples/` and the doc trees,
5
- * and ratchets on the recovers-with-errors state (`ok: true` with a non-empty
6
- * `errors`). Three assertions, matching the canonical-validity gates:
7
- * 1. sanity — sources were actually found and mostly compile clean;
8
- * 2. no NEW recovering source appears outside the committed allowlist;
9
- * 3. no allowlisted key has silently become clean (stale entries must be
10
- * removed so the list only ever shrinks).
11
- *
12
- * To update after an intentional fix: re-run and rewrite
13
- * `baselines/shipped-sources-validity.json`. Note the allowlist key embeds a
14
- * hash of the source, so FIXING a source changes its key — the entry goes
15
- * stale and assertion 3 makes removing it mandatory.
16
- */
17
-
18
- import { readFileSync } from 'node:fs';
19
- import { fileURLToPath } from 'node:url';
20
- import path from 'node:path';
21
- import { JSDOM } from 'jsdom';
22
- import { describe, it, expect, beforeAll } from 'vitest';
23
- import {
24
- checkShippedSourcesValidity,
25
- type ShippedSourcesResult,
26
- type CompileForValidity,
27
- } from './shipped-sources-validity';
28
-
29
- interface AllowlistDoc {
30
- allowedRecovered: Array<{
31
- key: string;
32
- file: string;
33
- error: string;
34
- upstream: string;
35
- reason: string;
36
- }>;
37
- }
38
-
39
- const baselinePath = path.resolve(
40
- path.dirname(fileURLToPath(import.meta.url)),
41
- '../../baselines/shipped-sources-validity.json'
42
- );
43
- const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
44
- const allowed = new Set(allowlist.allowedRecovered.map(e => e.key));
45
-
46
- describe('shipped-sources validity gate', () => {
47
- let result: ShippedSourcesResult;
48
-
49
- beforeAll(async () => {
50
- // Import core through its built entry, the same surface a consumer gets.
51
- const core = (await import('@hyperfixi/core')) as unknown as {
52
- hyperscript: { compileSync: CompileForValidity };
53
- };
54
- const dom = new JSDOM('<!doctype html><html><body></body></html>');
55
- result = checkShippedSourcesValidity(
56
- code => core.hyperscript.compileSync(code),
57
- dom.window.document as unknown as Parameters<typeof checkShippedSourcesValidity>[1]
58
- );
59
- }, 120_000);
60
-
61
- it('finds and compiles the shipped sources (sanity: trees walked, extraction working)', () => {
62
- // Guards the silent-zero failure mode: a broken walk or extractor would
63
- // make every other assertion vacuously pass.
64
- expect(result.checked).toBeGreaterThan(100);
65
- expect(result.clean).toBeGreaterThan(100);
66
- });
67
-
68
- it('has no NEW source that parses with recovered errors outside the allowlist', () => {
69
- const unexpected = result.findings.filter(f => !allowed.has(f.key));
70
- expect(
71
- unexpected,
72
- unexpected.length
73
- ? `\nNew shipped sources that parse ok:true WITH errors (fix the source, or allowlist with a reason):\n` +
74
- unexpected
75
- .map(f => ` [${f.key}]\n "${f.excerpt}"\n -> ${f.error}`)
76
- .join('\n') +
77
- `\n\nAsk the real hyperscript.org engine for a second opinion before deciding:\n` +
78
- `upstream REJECTING means the source is malformed; upstream ACCEPTING means this is a hyperfixi parser defect.\n` +
79
- `See loadCanonicalParser() in canonical-validity.ts.`
80
- : ''
81
- ).toEqual([]);
82
- });
83
-
84
- it('has no stale allowlist entries (a now-clean source must be removed so the list ratchets down)', () => {
85
- const stillFailing = new Set(result.findings.map(f => f.key));
86
- const stale = allowlist.allowedRecovered.map(e => e.key).filter(key => !stillFailing.has(key));
87
- expect(
88
- stale,
89
- stale.length
90
- ? `\nThese allowlisted sources no longer recover-with-errors (fixed, or edited — the key embeds a source hash).\n` +
91
- `Remove them from baselines/shipped-sources-validity.json:\n ${stale.join('\n ')}`
92
- : ''
93
- ).toEqual([]);
94
- });
95
- });