@hyperfixi/testing-framework 2.5.0 → 2.6.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.
@@ -83,6 +83,12 @@ export interface TestConfig {
83
83
  /** Enable regression comparison */
84
84
  regression?: boolean;
85
85
 
86
+ /** Path to the committed baseline JSON (for --regression / --save-baseline). */
87
+ baselinePath?: string;
88
+
89
+ /** Run the suite, then write results as the baseline (--save-baseline). */
90
+ saveBaseline?: boolean;
91
+
86
92
  /** Minimum confidence threshold for semantic parsing */
87
93
  confidenceThreshold?: number;
88
94
 
@@ -108,6 +114,56 @@ export interface ParseResult {
108
114
  parser?: 'semantic' | 'traditional';
109
115
  error?: string;
110
116
  duration: number; // ms
117
+
118
+ /**
119
+ * Distinct command actions present anywhere in the parsed node tree
120
+ * (event-handler + nested body/statements), excluding the structural
121
+ * `compound` wrapper. Used to compute structural `fidelity` against the
122
+ * English reference parse. Set by the validator.
123
+ */
124
+ actionSignature?: string[];
125
+
126
+ /**
127
+ * R0-precision — multiset of command actions (duplicates preserved). Lets a
128
+ * *duplicated* spurious action (a phantom `toggle` ahead of a real one) be
129
+ * counted, which the deduped `actionSignature` absorbs. Set by the validator.
130
+ */
131
+ actionMultisetSignature?: string[];
132
+
133
+ /**
134
+ * R1 — role-level signature: `action.role:valueType` for every command node
135
+ * in the tree (`add.patient:selector`, `put.destination:reference`).
136
+ * Cross-language comparison is by role name + value TYPE, never the value
137
+ * string (which is legitimately translated). Set by the validator.
138
+ */
139
+ roleSignature?: string[];
140
+
141
+ /**
142
+ * Structural fidelity vs the English reference parse, in [0, 1]: the fraction
143
+ * of the English parse's distinct actions also present in this language's
144
+ * parse (recall). `1` = same command structure; low values flag a *degenerate*
145
+ * pass (parses non-null, but lost most of the source's commands). Computed
146
+ * cross-language after all parses complete; `undefined` when there is no
147
+ * usable English reference or this parse failed.
148
+ */
149
+ fidelity?: number;
150
+
151
+ /**
152
+ * R0-precision vs the English reference, in [0, 1]: the fraction of *this*
153
+ * parse's actions justified by the English reference (multiset-aware). Falls
154
+ * below 1.0 when the parse/render adds commands the source never had — the
155
+ * phantom-injection signal recall cannot see. `undefined` when this parse has
156
+ * no actions or there is no usable English reference.
157
+ */
158
+ precision?: number;
159
+
160
+ /**
161
+ * R1 — role fidelity vs the English reference, in [0, 1]: the fraction of the
162
+ * English parse's role-signature entries also present here. Catches a parse
163
+ * that finds the right commands with the WRONG roles (swapped
164
+ * patient/destination executes wrongly while action-fidelity scores 1.0).
165
+ */
166
+ roleFidelity?: number;
111
167
  }
112
168
 
113
169
  /**
@@ -136,6 +192,56 @@ export interface LanguageResults {
136
192
  parseRate: number;
137
193
  avgConfidence: number;
138
194
 
195
+ /**
196
+ * Mean structural `fidelity` over successful parses that have an English
197
+ * reference (see ParseResult.fidelity). `undefined` for English itself.
198
+ */
199
+ avgFidelity?: number | undefined;
200
+
201
+ /**
202
+ * R0-precision — mean `precision` over successful parses with an English
203
+ * reference. Recorded + ratcheted alongside avgFidelity: a drop means a parser
204
+ * change started injecting phantom/spurious commands (invisible to recall).
205
+ */
206
+ avgPrecision?: number | undefined;
207
+
208
+ /**
209
+ * R1 — mean `roleFidelity` over successful parses with an English reference.
210
+ * Recorded + ratcheted in the baseline; the burn-down is deliberately NOT part
211
+ * of the parsing-track goal (see CORRECTNESS_RELIABILITY_PLAN.md §8).
212
+ */
213
+ avgRoleFidelity?: number | undefined;
214
+
215
+ /**
216
+ * R2 — mean executionFidelity over the curated execution subset (see
217
+ * validators/execution-validator.ts): 1 per pattern whose jsdom DOM-effect
218
+ * signature exactly matches the en reference's, else 0. Recorded + ratcheted
219
+ * in the baseline; the burn-down is NOT part of any current session goal
220
+ * (CORRECTNESS_RELIABILITY_PLAN.md §8).
221
+ */
222
+ avgExecutionFidelity?: number | undefined;
223
+
224
+ /**
225
+ * R2 — curated-subset pattern IDs whose execution diverged from the en
226
+ * reference (error, no effect, or different DOM-effect signature).
227
+ */
228
+ executionFailures?: string[] | undefined;
229
+
230
+ /**
231
+ * Pattern IDs that pass (non-null) but parse *degenerately* — fidelity below
232
+ * the harness threshold (lost most of the English command structure). These
233
+ * are real-but-shallow passes the parse-rate metric alone can't surface.
234
+ */
235
+ degeneratePasses?: string[] | undefined;
236
+
237
+ /**
238
+ * Pattern IDs that pass with fidelity in [0.5, 1.0) — *lossy* passes that parse
239
+ * non-null and clear the degenerate floor but still silently drop ≥1 command vs
240
+ * the English reference. ~7× more common than degenerate passes; the avgFidelity
241
+ * / faithful→lossy ratchet (R0) makes these visible and non-regressable.
242
+ */
243
+ lossyPasses?: string[] | undefined;
244
+
139
245
  /** Duration in ms */
140
246
  duration: number;
141
247
 
@@ -188,6 +294,20 @@ export interface Baseline {
188
294
  parseFailure: number;
189
295
  parseRate: number;
190
296
  avgConfidence: number;
297
+ /** Mean structural fidelity vs the English reference parse (see ParseResult.fidelity). */
298
+ avgFidelity?: number | undefined;
299
+ /** R0-precision — mean precision vs the English reference (see ParseResult.precision). */
300
+ avgPrecision?: number | undefined;
301
+ /** R1 — mean role fidelity vs the English reference (see ParseResult.roleFidelity). */
302
+ avgRoleFidelity?: number | undefined;
303
+ /** R2 — mean execution fidelity over the curated execution subset (see LanguageResults.avgExecutionFidelity). */
304
+ avgExecutionFidelity?: number | undefined;
305
+ /** R2 — curated-subset pattern IDs whose execution diverged from the en reference. */
306
+ executionFailures?: string[] | undefined;
307
+ /** Pattern IDs that pass but parse degenerately (fidelity below threshold). */
308
+ degeneratePasses?: string[] | undefined;
309
+ /** Pattern IDs that pass *lossily* (fidelity in [0.5, 1.0) — drop ≥1 command). */
310
+ lossyPasses?: string[] | undefined;
191
311
  bundleSize: number | undefined;
192
312
  /** Pattern-level results for detailed tracking */
193
313
  patterns: Record<string, { success: boolean; confidence: number | undefined }> | undefined;
@@ -211,9 +331,35 @@ export interface RegressionResult {
211
331
  language: LanguageCode;
212
332
  parseRateDelta: number; // % change
213
333
  avgConfidenceDelta: number; // absolute change
334
+ /** Absolute change in avgFidelity (current − baseline). Negative = correctness drop. */
335
+ avgFidelityDelta: number;
336
+ /** R0-precision — absolute change in avgPrecision (current − baseline). 0 when either side lacks data. Negative = phantom commands introduced. */
337
+ avgPrecisionDelta: number;
338
+ /** R1 — absolute change in avgRoleFidelity (current − baseline). 0 when either side lacks data. */
339
+ avgRoleFidelityDelta: number;
340
+ /** R2 — absolute change in avgExecutionFidelity (current − baseline). 0 when either side lacks data. */
341
+ avgExecutionFidelityDelta: number;
342
+ /**
343
+ * R2 — curated-subset patterns that executed faithfully in the baseline but
344
+ * diverge now. Empty when the baseline carries no execution data yet (never
345
+ * retro-flags); the precise per-pattern execution ratchet.
346
+ */
347
+ newExecutionFailures: string[];
214
348
  bundleSizeDelta: number | undefined; // % change
215
349
  newFailures: string[]; // pattern IDs
216
350
  newSuccesses: string[]; // pattern IDs
351
+ /**
352
+ * Patterns that were a *faithful* pass in the baseline but are now a
353
+ * *degenerate* pass (fidelity dropped below threshold) — a fidelity
354
+ * regression. Empty when the baseline has no fidelity data yet.
355
+ */
356
+ newDegeneratePasses: string[];
357
+ /**
358
+ * Patterns that were a *faithful* (fidelity 1.0) pass in the baseline but are now
359
+ * a *lossy* pass (0.5 ≤ fid < 1.0) — a silent command-drop the degenerate ratchet
360
+ * misses. Empty when the baseline has no lossy data yet (never retro-flags).
361
+ */
362
+ newLossyPasses: string[];
217
363
  status: 'improved' | 'regressed' | 'unchanged';
218
364
  }
219
365
 
@@ -0,0 +1,412 @@
1
+ /**
2
+ * R2 — execution smoke validator locks (session 5).
3
+ *
4
+ * Locks three things:
5
+ * 1. The curated subset's MEMBERSHIP — expanding/shrinking it recalibrates
6
+ * every language's avgExecutionFidelity, so it must be a deliberate,
7
+ * baseline-regenerating change, never an accident.
8
+ * 2. The en references EXECUTE: parse → buildAST → runtime.execute installs
9
+ * the click handler in jsdom and dispatch produces a non-empty,
10
+ * deterministic DOM-effect signature (the foundation every language is
11
+ * scored against).
12
+ * 3. Known cross-language behavior: a faithful translation reproduces the en
13
+ * signature exactly; a translation that drops the destination role lands
14
+ * its effect on `me` and MUST diverge (the failure class R2 exists to
15
+ * catch — R0/R1 score some of these 1.0).
16
+ */
17
+
18
+ import { describe, it, expect, beforeAll } from 'vitest';
19
+ import { ExecutionValidator, EXECUTION_SUBSET, loadExecutionSubset } from './execution-validator';
20
+
21
+ describe('R2 execution subset (lock)', () => {
22
+ it('contains exactly the 47 curated patterns', () => {
23
+ // Changing this list recalibrates avgExecutionFidelity for every language.
24
+ // If you expand the subset, regenerate the baseline (--save-baseline) in
25
+ // the SAME PR and update this lock.
26
+ expect([...EXECUTION_SUBSET].sort()).toEqual(
27
+ [
28
+ 'add-class-basic',
29
+ 'add-class-to-other',
30
+ 'remove-class-basic',
31
+ 'remove-class-from-all',
32
+ 'toggle-class-basic',
33
+ 'toggle-class-on-other',
34
+ 'put-content-basic',
35
+ 'show-element',
36
+ 'hide-element',
37
+ 'increment-counter',
38
+ 'decrement-counter',
39
+ 'modal-open',
40
+ 'tabs-basic',
41
+ 'set-text-possessive-dot',
42
+ 'set-inner-html-possessive-dot',
43
+ 'set-style',
44
+ 'closest-ancestor',
45
+ // Session-8 expansion wave 1 (multi-command): see the eligibility
46
+ // probe notes in execution-validator.ts for the candidates excluded
47
+ // because their en reference doesn't execute.
48
+ 'tabs-content',
49
+ 'accordion-exclusive',
50
+ // Session-9 expansion wave 2 (control-flow): en if/else now folds to a
51
+ // conditional and executes (matches/exists/is-empty condition forms +
52
+ // the propertyAccess runtime evaluator). unless-condition stays out
53
+ // (unless is deliberately not folded).
54
+ 'if-condition',
55
+ 'if-matches',
56
+ 'if-exists',
57
+ 'modal-close-backdrop',
58
+ // Session-9 expansion wave 2b (@attr family): the semantic value
59
+ // converter / expression parser now emit canonical attributeAccess
60
+ // nodes, so these en references execute. set-text-basic stays out
61
+ // (jsdom doesn't implement innerText — harness limitation).
62
+ 'set-attribute',
63
+ 'toggle-visibility',
64
+ 'tabs-aria',
65
+ // Session-9 expansion wave 2c: halt-the-event patient preserved by the
66
+ // semantic halt mapper (handler continues); make element literals carry
67
+ // their markup on `raw`. make-toast-element stays out (positional
68
+ // `put it at end of body` — the dropdown-toggle positional gap).
69
+ 'halt-propagation',
70
+ 'make-element',
71
+ // Session-9 expansion wave 2d: `next .sel` folds to a positional call
72
+ // expression. make-toast-element still out (at-end-of positional put).
73
+ 'dropdown-toggle',
74
+ // Session-10 expansion wave 3: positional-phrase patients — the
75
+ // pattern matcher captures `closest <sel>` / `the next <sel>`, so
76
+ // `hide closest .modal` parses and executes (PATTERN_SETUP gives #btn
77
+ // a .modal ancestor).
78
+ 'modal-close-button',
79
+ // Session-10 expansion wave 3b: the at-end-of positional put — en
80
+ // `at end of`/`at start of` patterns + the parseBodyWithClauses
81
+ // end-noun guard + putMapper manner + core multi-word modifier keys
82
+ // + contextReference body. The last R2 candidate excluded for an
83
+ // unusable en reference is now in.
84
+ 'make-toast-element',
85
+ // Expansion wave 4 (the deferred S1 follow-up): the first non-click,
86
+ // event-reading cell. buildEventHandler binds the custom `success`
87
+ // event (carried as an expression role) instead of defaulting to click;
88
+ // PATTERN_TRIGGER dispatches a CustomEvent whose detail.message the
89
+ // handler reads. set @role on the #sr-announce scope landed in S1.
90
+ 'announce-screen-reader',
91
+ // Session-12 expansion wave 5: `remove me` (bare self-removal).
92
+ 'remove-element',
93
+ // Session-13 expansion wave 6: six wave-5 "worklist" candidates that, when
94
+ // re-grounded against a freshly populated patterns.db, match the en effect in
95
+ // ALL 23 languages (the wave-5 divergence counts were measured against a stale
96
+ // committed db — see the wave-6 note in execution-validator.ts). multiple-events,
97
+ // put-after, put-before remain out (real cross-language execution divergences).
98
+ 'next-element',
99
+ 'toggle-aria-expanded',
100
+ 'set-opacity',
101
+ 'set-transform',
102
+ 'accordion-toggle',
103
+ 'caret-var-on-target',
104
+ // Session-13 expansion wave 7: multiple-events (`on click or
105
+ // keypress[...] toggle .active`), the third wave-5 worklist divergence,
106
+ // now fixed by the semantic or-clause excision pre-pass + ja `または`→or
107
+ // tokenizer fix + hi/bn OR_KEYWORDS. All 23 langs match the en click effect.
108
+ 'multiple-events',
109
+ // Session-14 expansion wave 8: put-before / put-after (`put X before/after me`),
110
+ // the last two wave-5 worklist divergences. Position word captured as `manner`
111
+ // across SVO/SOV (#516) + handcrafted VSO put-event patterns for ar/tl/uk.
112
+ 'put-before',
113
+ 'put-after',
114
+ // Wave 9 (R2-coverage sweep): three patterns whose en reference runs with
115
+ // a clean deterministic signature and already match in all 23 languages —
116
+ // pure coverage, no parser/dict fix. See the wave-9 note in
117
+ // execution-validator.ts (the *opacity hide/show strategies are
118
+ // synchronous, not the excluded async transition family).
119
+ 'chained-access-possessive-dot',
120
+ 'hide-with-transition',
121
+ 'show-with-transition',
122
+ // Wave 10 (SOV literal-role-extraction arc, PRs #560/#561): the two R2
123
+ // blockers — the fronted append content literal (bogus `event` role in
124
+ // the body-clause marker lookup) and the trailing bare increment amount
125
+ // (unconsumed by every fused event pattern, defaulted to 1) — now
126
+ // captured in all 23 languages. See the wave-10 note in
127
+ // execution-validator.ts.
128
+ 'append-content',
129
+ 'increment-by-amount',
130
+ ].sort()
131
+ );
132
+ });
133
+ });
134
+
135
+ describe('R2 execution validator (lock)', () => {
136
+ const validator = new ExecutionValidator();
137
+
138
+ beforeAll(async () => {
139
+ await validator.initialize();
140
+ });
141
+
142
+ it('en reference executes: on click add .highlight to me', async () => {
143
+ const res = await validator.execute('add-class-basic', 'on click add .highlight to me', 'en');
144
+ expect(res.error).toBeUndefined();
145
+ expect(res.effects).toEqual(['Δ#btn cls[highlight] attr[id=btn] style[] text[Click]']);
146
+ });
147
+
148
+ it('is deterministic: two runs of the same pattern produce identical signatures', async () => {
149
+ const a = await validator.execute('toggle-class-basic', 'on click toggle .active', 'en');
150
+ const b = await validator.execute('toggle-class-basic', 'on click toggle .active', 'en');
151
+ expect(a.effects).toEqual(b.effects);
152
+ expect(a.effects.length).toBeGreaterThan(0);
153
+ });
154
+
155
+ it('a faithful translation reproduces the en signature exactly (es)', async () => {
156
+ const en = await validator.execute('add-class-basic', 'on click add .highlight to me', 'en');
157
+ const es = await validator.execute('add-class-basic', 'en clic agregar .highlight a yo', 'es');
158
+ expect(es.effects).toEqual(en.effects);
159
+ });
160
+
161
+ it('a dropped destination role diverges (the R2 failure class)', async () => {
162
+ // en targets #menu; a translation that loses the destination acts on `me`
163
+ // (#btn) instead — parse-level fidelity can score this 1.0, execution
164
+ // cannot. Locks that the effect signature keys by target element.
165
+ const en = await validator.execute(
166
+ 'toggle-class-on-other',
167
+ 'on click toggle .open on #menu',
168
+ 'en'
169
+ );
170
+ const meInstead = await validator.execute(
171
+ 'toggle-class-on-other',
172
+ 'on click toggle .open',
173
+ 'en'
174
+ );
175
+ expect(en.effects.join()).toContain('#menu');
176
+ expect(meInstead.effects.join()).toContain('#btn');
177
+ expect(meInstead.effects).not.toEqual(en.effects);
178
+ });
179
+
180
+ it('wave-2 control-flow en references execute with non-empty signatures', async () => {
181
+ // The else-branch fires (btn has no .active): conditional fold + branch
182
+ // selection are live, not just parsing.
183
+ const ifCond = await validator.execute(
184
+ 'if-condition',
185
+ 'on click if I match .active then remove .active else add .active end',
186
+ 'en'
187
+ );
188
+ expect(ifCond.error).toBeUndefined();
189
+ expect(ifCond.effects.join()).toContain('#btn cls[active');
190
+
191
+ // The PATTERN_SETUP makes #btn the backdrop, so the condition is true and
192
+ // hide produces the effect.
193
+ const backdrop = await validator.execute(
194
+ 'modal-close-backdrop',
195
+ 'on click if target matches .modal-backdrop hide .modal-backdrop end',
196
+ 'en'
197
+ );
198
+ expect(backdrop.error).toBeUndefined();
199
+ expect(backdrop.effects.length).toBeGreaterThan(0);
200
+ });
201
+
202
+ it('the announce-screen-reader cell executes via its custom-event trigger', async () => {
203
+ // First non-click cell: `on success put event.detail.message into
204
+ // #sr-announce set @role to "alert" on #sr-announce`. PATTERN_TRIGGER
205
+ // dispatches a `success` CustomEvent carrying detail.message; the handler
206
+ // writes that text into #sr-announce and sets role=alert on it. Locks both
207
+ // the per-cell trigger wiring and the two-line effect signature.
208
+ const res = await validator.execute(
209
+ 'announce-screen-reader',
210
+ 'on success put event.detail.message into #sr-announce set @role to "alert" on #sr-announce',
211
+ 'en'
212
+ );
213
+ expect(res.error).toBeUndefined();
214
+ expect(res.effects).toEqual([
215
+ 'Δ#sr-announce cls[] attr[id=sr-announce,role=alert] style[] text[Saved successfully]',
216
+ ]);
217
+ });
218
+
219
+ it('a click trigger does not fire the success-only handler (trigger isolation)', async () => {
220
+ // The default click dispatch must NOT execute a handler bound to a custom
221
+ // event — proves PATTERN_TRIGGER actually routes the event name, rather than
222
+ // the handler firing on any event.
223
+ const clickOnly = await validator.execute(
224
+ 'toggle-class-basic', // a click cell, but force the wrong source text:
225
+ 'on success add .x to me',
226
+ 'en'
227
+ );
228
+ // No PATTERN_TRIGGER for toggle-class-basic → click dispatched → success
229
+ // handler never runs → empty signature.
230
+ expect(clickOnly.effects).toEqual([]);
231
+ });
232
+
233
+ it('wave-6 en references execute with their locked signatures', async () => {
234
+ // The six wave-6 additions. Each en reference must produce a non-empty,
235
+ // deterministic signature against the existing fixture (the foundation every
236
+ // language is scored against). next/closest positionals fall back to `me`
237
+ // when no match exists in the fixture; set *opacity/*transform write inline
238
+ // style; caret-var-on-target clears #btn text (undefined `^count`).
239
+ const cases: ReadonlyArray<[string, string, string[]]> = [
240
+ [
241
+ 'next-element',
242
+ 'on click add .highlight to next <li/>',
243
+ ['Δli[9] cls[active highlight items] attr[] style[] text[]'],
244
+ ],
245
+ [
246
+ 'toggle-aria-expanded',
247
+ 'on click toggle @aria-expanded on me toggle .open on next .panel',
248
+ ['Δ#btn cls[open] attr[aria-expanded=,id=btn] style[] text[Click]'],
249
+ ],
250
+ [
251
+ 'set-opacity',
252
+ 'on click set my *opacity to 0.5',
253
+ ['Δ#btn cls[] attr[id=btn] style[opacity: 0.5;] text[Click]'],
254
+ ],
255
+ [
256
+ 'set-transform',
257
+ 'on click set my *transform to "rotate(45deg)"',
258
+ ['Δ#btn cls[] attr[id=btn] style[transform: rotate(45deg);] text[Click]'],
259
+ ],
260
+ [
261
+ 'accordion-toggle',
262
+ 'on click toggle .open on closest .accordion-item toggle @aria-expanded',
263
+ ['Δ#btn cls[open] attr[aria-expanded=,id=btn] style[] text[Click]'],
264
+ ],
265
+ [
266
+ 'caret-var-on-target',
267
+ 'on click put ^count on #host into me',
268
+ ['Δ#btn cls[] attr[id=btn] style[] text[]'],
269
+ ],
270
+ ];
271
+ for (const [id, code, expected] of cases) {
272
+ const res = await validator.execute(id, code, 'en');
273
+ expect(res.error, `${id} errored: ${res.error}`).toBeUndefined();
274
+ expect(res.effects, `${id} signature`).toEqual(expected);
275
+ }
276
+ });
277
+
278
+ it('a localized translation reproduces the wave-6 en signature (ms next-element)', async () => {
279
+ // This is the wave-5 worklist's recorded ms "divergence": it only appeared
280
+ // because the committed db carried an UNTRANSLATED `to next` (a stale snapshot).
281
+ // A fresh populate localizes it (`ke seterusnya`), and it executes identically
282
+ // to en — the reason next-element joins the subset with no parser/dict fix.
283
+ const en = await validator.execute(
284
+ 'next-element',
285
+ 'on click add .highlight to next <li/>',
286
+ 'en'
287
+ );
288
+ const ms = await validator.execute(
289
+ 'next-element',
290
+ 'apabila click tambah .highlight ke seterusnya <li/>',
291
+ 'ms'
292
+ );
293
+ expect(ms.error, `ms errored: ${ms.error}`).toBeUndefined();
294
+ expect(ms.effects).toEqual(en.effects);
295
+ });
296
+
297
+ it('wave-7 multiple-events: en + a translation toggle .active on click', async () => {
298
+ // `on click or keypress[...] toggle .active` — the multi-event handler. R2
299
+ // dispatches click only, so both events bind but only the click effect is
300
+ // measured: .active toggled onto #btn. A translation whose `or`-clause used to
301
+ // become a phantom command (it `o`) or mangle (ko) now reproduces it exactly.
302
+ const en = await validator.execute(
303
+ 'multiple-events',
304
+ 'on click or keypress[key=="Enter"] toggle .active',
305
+ 'en'
306
+ );
307
+ expect(en.error).toBeUndefined();
308
+ expect(en.effects).toEqual(['Δ#btn cls[active] attr[id=btn] style[] text[Click]']);
309
+ const it = await validator.execute(
310
+ 'multiple-events',
311
+ 'su clic o keypress[key=="Enter"] commutare .active',
312
+ 'it'
313
+ );
314
+ expect(it.error, `it errored: ${it.error}`).toBeUndefined();
315
+ expect(it.effects).toEqual(en.effects);
316
+ });
317
+
318
+ it('wave-8 put-before/after: en inserts the <p> at the right offset; a VSO translation matches', async () => {
319
+ // `put "<p>New</p>" before/after me` — the position word must land the content
320
+ // before/after #btn (not inside it). before → +p[1] (preceding #btn's parent
321
+ // diff), after → +p[2]. ar (VSO) used to drop the position (fused VSO event
322
+ // pattern consumed قبل/بعد as a plain destination marker → inserted INTO me);
323
+ // the handcrafted put-event-ar-vso-before/after now captures manner, so ar
324
+ // reproduces the en effect exactly.
325
+ for (const [id, enCode, arCode] of [
326
+ ['put-before', 'on click put "<p>New</p>" before me', 'ضع "<p>New</p>" قبل أنا عند نقر'],
327
+ ['put-after', 'on click put "<p>New</p>" after me', 'ضع "<p>New</p>" بعد أنا عند نقر'],
328
+ ] as const) {
329
+ const en = await validator.execute(id, enCode, 'en');
330
+ expect(en.error, `en ${id} errored: ${en.error}`).toBeUndefined();
331
+ expect(en.effects.join(), `en ${id} should insert a <p>`).toContain('text[New]');
332
+ const ar = await validator.execute(id, arCode, 'ar');
333
+ expect(ar.error, `ar ${id} errored: ${ar.error}`).toBeUndefined();
334
+ expect(ar.effects, `ar ${id} must reproduce en`).toEqual(en.effects);
335
+ }
336
+ });
337
+
338
+ it('wave-9 en references execute with their locked signatures', async () => {
339
+ // The three wave-9 additions. Each en reference must produce a non-empty,
340
+ // deterministic signature against the existing fixture. The `*opacity`
341
+ // hide/show STRATEGIES are synchronous (no timer): hide writes display:none
342
+ // + a data-original-display marker on #btn; show adds the visibility class
343
+ // on #modal. chained-access-possessive-dot writes the parent (.card) display.
344
+ const cases: ReadonlyArray<[string, string, string[]]> = [
345
+ [
346
+ 'chained-access-possessive-dot',
347
+ 'on click set my.parentElement.style.display to "none"',
348
+ ['Δdiv[0] cls[card] attr[] style[display: none;] text[]'],
349
+ ],
350
+ [
351
+ 'hide-with-transition',
352
+ 'on click hide me with *opacity',
353
+ ['Δ#btn cls[] attr[data-original-display=,id=btn] style[display: none;] text[Click]'],
354
+ ],
355
+ [
356
+ 'show-with-transition',
357
+ 'on click show #modal with *opacity',
358
+ ['Δ#modal cls[show] attr[id=modal] style[] text[]'],
359
+ ],
360
+ ];
361
+ for (const [id, code, expected] of cases) {
362
+ const res = await validator.execute(id, code, 'en');
363
+ expect(res.error, `${id} errored: ${res.error}`).toBeUndefined();
364
+ expect(res.effects, `${id} signature`).toEqual(expected);
365
+ }
366
+ });
367
+
368
+ it('wave-10 en references execute with their locked signatures; SOV translations match', async () => {
369
+ // The two SOV literal-role-extraction blockers (PRs #560/#561). The en
370
+ // signatures anchor on the wave-10 fixture elements (appended last, so all
371
+ // pre-existing document-order indices are preserved). The ja translations
372
+ // lock the exact failure class each fix closed: append's fronted content
373
+ // literal (was a bogus `event` role → runtime "append requires content")
374
+ // and increment's trailing bare amount (was dropped → +1 instead of +10).
375
+ const cases: ReadonlyArray<[string, string, string, string]> = [
376
+ [
377
+ 'append-content',
378
+ 'on click append "<li>Item</li>" to #list',
379
+ '"<li>Item</li>" を クリック で 末尾追加 #list に',
380
+ 'text[Item]',
381
+ ],
382
+ [
383
+ 'increment-by-amount',
384
+ 'on click increment #score by 10',
385
+ '#score を クリック で 増加 10',
386
+ 'text[10]',
387
+ ],
388
+ ];
389
+ for (const [id, enCode, jaCode, marker] of cases) {
390
+ const en = await validator.execute(id, enCode, 'en');
391
+ expect(en.error, `en ${id} errored: ${en.error}`).toBeUndefined();
392
+ expect(en.effects.join(), `en ${id} signature must carry ${marker}`).toContain(marker);
393
+ const ja = await validator.execute(id, jaCode, 'ja');
394
+ expect(ja.error, `ja ${id} errored: ${ja.error}`).toBeUndefined();
395
+ expect(ja.effects, `ja ${id} must reproduce en`).toEqual(en.effects);
396
+ }
397
+ });
398
+
399
+ it('a parse failure comes back as an error, never a throw', async () => {
400
+ const res = await validator.execute('bogus', 'completely unparseable gibberish', 'en');
401
+ expect(res.error).toBeDefined();
402
+ expect(res.effects).toEqual([]);
403
+ });
404
+
405
+ it('loadExecutionSubset returns en source for every curated pattern', async () => {
406
+ const sources = await loadExecutionSubset(['en']);
407
+ const en = sources.get('en')!;
408
+ for (const id of EXECUTION_SUBSET) {
409
+ expect(en.get(id), `missing en source for ${id}`).toBeTruthy();
410
+ }
411
+ });
412
+ });