@hyperfixi/testing-framework 3.2.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.
- package/CHANGELOG.md +203 -1
- package/package.json +9 -9
- package/src/agent-bench/README.md +48 -1
- package/src/agent-bench/agent-bench.test.ts +7 -0
- package/src/agent-bench/harness.ts +39 -50
- package/src/multilingual/README.md +30 -11
- package/src/multilingual/bare-render-fidelity.ts +1 -1
- package/src/multilingual/cli.ts +5 -3
- package/src/multilingual/direct-path-shapes-gate.ts +56 -0
- package/src/multilingual/direct-path-shapes.1.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.2.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.3.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.cases.json +2621 -0
- package/src/multilingual/direct-path-shapes.ts +459 -0
- package/src/multilingual/engine-parser-parity.test.ts +67 -0
- package/src/multilingual/orchestrator.ts +2 -1
- package/src/multilingual/render-fidelity.ts +1 -1
- package/src/multilingual/shipped-examples-execution.test.ts +44 -33
- package/src/multilingual/shipped-examples-execution.ts +55 -75
- package/src/multilingual/shipped-sources-engine.test.ts +106 -0
- package/src/multilingual/shipped-sources-localized.test.ts +87 -0
- package/src/multilingual/shipped-sources-validity.ts +127 -74
- package/src/multilingual/validators/execution-validator.test.ts +81 -23
- package/src/multilingual/validators/execution-validator.ts +140 -94
- package/src/multilingual/validators/parse-validator.ts +7 -20
- package/src/multilingual/value-matrix.accepted.test.ts +8 -11
- package/src/multilingual/value-matrix.isolation.test.ts +7 -6
- package/src/multilingual/value-matrix.ts +72 -86
- package/src/multilingual/shipped-sources-validity.test.ts +0 -95
|
@@ -5,33 +5,46 @@
|
|
|
5
5
|
* parse that EXECUTES wrongly: the session-5 probe found translations whose
|
|
6
6
|
* parse looks plausible but whose runtime behavior diverges from the English
|
|
7
7
|
* reference — a dropped destination role lands the effect on `me` instead of
|
|
8
|
-
* `#menu`, a
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
8
|
+
* `#menu`, a quoted literal keeps its quotes in the DOM. This validator
|
|
9
|
+
* executes a CURATED subset of corpus patterns in jsdom and compares the
|
|
10
|
+
* resulting DOM effects against the English reference's effects.
|
|
11
|
+
*
|
|
12
|
+
* What runs where (since Phase C2 of the engine cutover): the en reference is
|
|
13
|
+
* the English source on upstream `_hyperscript` (the oracle, as for the value
|
|
14
|
+
* matrix and the shipped-examples gate); a translation is its foreign text
|
|
15
|
+
* AS WRITTEN on `@hyperfixi/engine` with `@lokascript/hyperscript-adapter`'s
|
|
16
|
+
* plugin installed and `lang` on the element — the product's multilingual
|
|
17
|
+
* path, where the engine translates the script as it reads it. Until C2 both
|
|
18
|
+
* ran on core's runtime through `parseSemantic → buildAST` (the direct path,
|
|
19
|
+
* which retires with core's parser). Measured before the switch, on a fresh
|
|
20
|
+
* populate: the engine matched upstream on all 49 en references, and all
|
|
21
|
+
* 1,127 translations matched on the engine; core's R2 path differed from
|
|
22
|
+
* upstream on 13 en references (core-only `show`/`toggle @attr`/`with
|
|
23
|
+
* *opacity`/`swap` semantics), which is why the oracle is upstream.
|
|
13
24
|
*
|
|
14
25
|
* Effect signature: a before/after diff of every element's classes, attributes,
|
|
15
|
-
* inline style, and leaf text
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* (
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* behavior diverges in its effects regardless.
|
|
26
|
+
* inline style, and leaf text, plus (here only) the document order of the
|
|
27
|
+
* id'd elements when it changed — upstream's `swap #a with #b` exchanges the
|
|
28
|
+
* two elements and changes nothing else. Selectors/classes/attribute names are
|
|
29
|
+
* code (not translated), so signatures are directly comparable across
|
|
30
|
+
* languages. A pattern scores 1 when its signature EXACTLY matches the en
|
|
31
|
+
* reference's (binary — extra effects are as wrong as missing ones). Errors
|
|
32
|
+
* the host reports are recorded for diagnostics but are NOT part of the match;
|
|
33
|
+
* a script that damages behavior diverges in its effects regardless.
|
|
23
34
|
*
|
|
24
|
-
* Determinism: a fresh JSDOM document
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* effects are excluded (no usable reference — same semantics as R0/R1).
|
|
35
|
+
* Determinism: a fresh JSDOM document per execution, no network, no timers
|
|
36
|
+
* beyond a fixed settle window. Patterns whose en reference errors or produces
|
|
37
|
+
* no effects are excluded (no usable reference — same semantics as R0/R1).
|
|
28
38
|
*/
|
|
29
39
|
|
|
40
|
+
import { createRequire } from 'node:module';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { pathToFileURL } from 'node:url';
|
|
30
43
|
import { JSDOM } from 'jsdom';
|
|
31
|
-
import { parseSemantic, buildAST } from '@lokascript/semantic';
|
|
32
44
|
import { getAllPatterns, getTranslationsByLanguage } from '@hyperfixi/patterns-reference';
|
|
33
45
|
import type { LanguageCode } from '../types';
|
|
34
46
|
import { snapshot, diffSnapshots } from '../effect-signature';
|
|
47
|
+
import { installGlobals } from '../shipped-examples-execution';
|
|
35
48
|
|
|
36
49
|
/**
|
|
37
50
|
* The curated execution subset: simple, deterministic, fixture-friendly
|
|
@@ -72,9 +85,11 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
72
85
|
// patterns whose en reference now parses (the if/else conditional fold) and
|
|
73
86
|
// executes (propertyAccess evaluator; matches/exists/is-empty condition
|
|
74
87
|
// forms). Probed through this validator: each produces a non-empty,
|
|
75
|
-
// deterministic effect signature. `unless-condition`
|
|
76
|
-
// deliberately NOT folded
|
|
77
|
-
// its flat parse
|
|
88
|
+
// deterministic effect signature. `unless-condition` stayed out while it was
|
|
89
|
+
// core's prefix `unless` (deliberately NOT folded: see
|
|
90
|
+
// semantic-parser.tryParseConditionalBlock; its flat parse errored at
|
|
91
|
+
// runtime). The row is `if I do not match …` since 2026-10-01 and has not
|
|
92
|
+
// been probed for this subset.
|
|
78
93
|
'if-condition',
|
|
79
94
|
'if-matches',
|
|
80
95
|
'if-exists',
|
|
@@ -228,7 +243,7 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
228
243
|
* The SAME fixture (plus the same per-pattern setup) is used for the en
|
|
229
244
|
* reference and every translation, so signatures are comparable.
|
|
230
245
|
*/
|
|
231
|
-
const FIXTURE_HTML = `<!DOCTYPE html><html><body>
|
|
246
|
+
export const FIXTURE_HTML = `<!DOCTYPE html><html><body>
|
|
232
247
|
<div class="card"><button id="btn">Click</button></div>
|
|
233
248
|
<div id="item"></div>
|
|
234
249
|
<div id="menu"></div>
|
|
@@ -252,7 +267,7 @@ const FIXTURE_HTML = `<!DOCTYPE html><html><body>
|
|
|
252
267
|
</body></html>`;
|
|
253
268
|
|
|
254
269
|
/** Per-pattern fixture preconditions (applied identically for every language). */
|
|
255
|
-
const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
|
|
270
|
+
export const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
|
|
256
271
|
// `remove .highlight from me` needs the class present to have an effect.
|
|
257
272
|
'remove-class-basic': doc => doc.getElementById('btn')!.classList.add('highlight'),
|
|
258
273
|
// `show #modal` needs the modal hidden.
|
|
@@ -280,6 +295,19 @@ const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
|
|
|
280
295
|
out.textContent = '5';
|
|
281
296
|
doc.getElementById('btn')!.before(out);
|
|
282
297
|
},
|
|
298
|
+
// `if #modal exists show #modal …` changes nothing on upstream while #modal
|
|
299
|
+
// is visible (core's `show` added a `show` class, which is what the
|
|
300
|
+
// reference used to record). Hidden, the `show` is observable.
|
|
301
|
+
'if-exists': doc => {
|
|
302
|
+
(doc.getElementById('modal') as HTMLElement).style.display = 'none';
|
|
303
|
+
},
|
|
304
|
+
// `toggle .open on closest .accordion-item` throws on upstream when there is
|
|
305
|
+
// no such ancestor (core fell back to `me`), and the second command never
|
|
306
|
+
// runs. #btn inside an item, as on a real accordion header.
|
|
307
|
+
'accordion-toggle': doc => doc.querySelector('.card')!.classList.add('accordion-item'),
|
|
308
|
+
// `toggle .open on next .panel` throws on upstream when nothing follows
|
|
309
|
+
// (core fell back to `me`), so the reference errored and was excluded.
|
|
310
|
+
'toggle-aria-expanded': doc => doc.getElementById('panel')!.classList.add('panel'),
|
|
283
311
|
};
|
|
284
312
|
|
|
285
313
|
/**
|
|
@@ -292,11 +320,11 @@ const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
|
|
|
292
320
|
* reference and every translation (it is harness-supplied, not translated), so
|
|
293
321
|
* effect signatures stay comparable across languages.
|
|
294
322
|
*/
|
|
295
|
-
interface PatternTrigger {
|
|
323
|
+
export interface PatternTrigger {
|
|
296
324
|
readonly event: string;
|
|
297
325
|
readonly detail?: unknown;
|
|
298
326
|
}
|
|
299
|
-
const PATTERN_TRIGGER: Record<string, PatternTrigger> = {
|
|
327
|
+
export const PATTERN_TRIGGER: Record<string, PatternTrigger> = {
|
|
300
328
|
// `on success put event.detail.message into #sr-announce set @role to "alert"
|
|
301
329
|
// on #sr-announce` — a custom event carrying the announcement text. The fixed
|
|
302
330
|
// message lands in #sr-announce's text; role=alert is set on the same node.
|
|
@@ -309,18 +337,27 @@ export interface ExecutionResult {
|
|
|
309
337
|
language: LanguageCode;
|
|
310
338
|
/** Sorted effect lines (see snapshot/diff). Empty on error or no effect. */
|
|
311
339
|
effects: string[];
|
|
312
|
-
/**
|
|
340
|
+
/** Errors the host reported (parse or runtime), when any. */
|
|
313
341
|
error?: string;
|
|
314
342
|
/** 1 = exact signature match vs the en reference, 0 = mismatch. Undefined for en itself or when no usable reference exists. */
|
|
315
343
|
executionFidelity?: number;
|
|
316
344
|
}
|
|
317
345
|
|
|
318
|
-
// Effect-signature machinery (serializeElement/snapshot/diffSnapshots)
|
|
319
|
-
// ../effect-signature.ts
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
346
|
+
// Effect-signature machinery (serializeElement/snapshot/diffSnapshots) lives in
|
|
347
|
+
// ../effect-signature.ts, shared with the shipped-examples execution gate so the
|
|
348
|
+
// two can never disagree about what a DOM effect is.
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Where an execution runs: `engine` is `@hyperfixi/engine` with the
|
|
352
|
+
* multilingual plugin (English or any language); `upstream` is
|
|
353
|
+
* `hyperscript.org`, the oracle, which runs English only.
|
|
354
|
+
*/
|
|
355
|
+
export type ExecutionHost = 'engine' | 'upstream';
|
|
356
|
+
|
|
357
|
+
/** What this needs of a host that reads scripts off attributes. */
|
|
358
|
+
interface ScriptHost {
|
|
359
|
+
processNode(element: Element): void;
|
|
360
|
+
}
|
|
324
361
|
|
|
325
362
|
/** How long to let the dispatched handler settle (ms). The subset contains no
|
|
326
363
|
* waits/transitions/fetches, so this only needs to drain micro/macrotasks. */
|
|
@@ -329,42 +366,53 @@ const SETTLE_MS = 20;
|
|
|
329
366
|
/** Per-execution hard timeout — a hung execution must not hang the sweep. */
|
|
330
367
|
const EXECUTION_TIMEOUT_MS = 5000;
|
|
331
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Both hosts, loaded once per process: the engine's grammar is a module
|
|
371
|
+
* singleton, and `register` re-runs every module against it.
|
|
372
|
+
*/
|
|
373
|
+
let hostsLoading: Promise<Record<ExecutionHost, ScriptHost>> | null = null;
|
|
374
|
+
function loadHosts(): Promise<Record<ExecutionHost, ScriptHost>> {
|
|
375
|
+
hostsLoading ??= (async () => {
|
|
376
|
+
const engine = await import('@hyperfixi/engine');
|
|
377
|
+
engine.register(...engine.everything);
|
|
378
|
+
const { hyperscriptI18n } = await import('@lokascript/hyperscript-adapter');
|
|
379
|
+
engine.api.use(hyperscriptI18n());
|
|
380
|
+
const require = createRequire(import.meta.url);
|
|
381
|
+
const esm = path.join(path.dirname(require.resolve('hyperscript.org')), '_hyperscript.esm.js');
|
|
382
|
+
const upstream = (await import(pathToFileURL(esm).href)).default as ScriptHost;
|
|
383
|
+
return { engine: engine.api, upstream };
|
|
384
|
+
})();
|
|
385
|
+
return hostsLoading;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The ids of id'd elements in document order — what `swap` changes. */
|
|
389
|
+
function idOrder(document: Document): string {
|
|
390
|
+
return Array.from(document.body.querySelectorAll('[id]'), el => el.id).join(',');
|
|
391
|
+
}
|
|
392
|
+
|
|
332
393
|
/**
|
|
333
394
|
* R2 execution validator. `initialize()` MUST complete before `execute()`:
|
|
334
|
-
* it bootstraps jsdom globals and only then loads
|
|
335
|
-
*
|
|
395
|
+
* it bootstraps jsdom globals and only then loads the hosts, whose module
|
|
396
|
+
* evaluation touches DOM constructors.
|
|
336
397
|
*/
|
|
337
398
|
export class ExecutionValidator {
|
|
338
|
-
private
|
|
339
|
-
Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
|
|
340
|
-
createContext: (el: HTMLElement) => unknown;
|
|
341
|
-
} | null = null;
|
|
399
|
+
private hosts: Record<ExecutionHost, ScriptHost> | null = null;
|
|
342
400
|
|
|
343
|
-
/** Errors
|
|
344
|
-
*
|
|
345
|
-
|
|
401
|
+
/** Errors the hosts report during the current execution: parse and runtime
|
|
402
|
+
* errors go to console.error, and an async handler's rejection to the
|
|
403
|
+
* process. */
|
|
404
|
+
private hostErrors: string[] = [];
|
|
346
405
|
private trapInstalled = false;
|
|
347
406
|
|
|
348
407
|
async initialize(): Promise<void> {
|
|
349
|
-
if (this.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// evaluates `Element` at module load). The per-execution fresh document
|
|
353
|
-
// replaces these for each run.
|
|
354
|
-
this.installGlobals(new JSDOM(FIXTURE_HTML));
|
|
355
|
-
|
|
356
|
-
const core = await import('@hyperfixi/core');
|
|
357
|
-
this.core = {
|
|
358
|
-
Runtime: core.Runtime as unknown as new () => {
|
|
359
|
-
execute(ast: unknown, ctx: unknown): Promise<unknown>;
|
|
360
|
-
},
|
|
361
|
-
createContext: core.createContext as unknown as (el: HTMLElement) => unknown,
|
|
362
|
-
};
|
|
408
|
+
if (this.hosts) return;
|
|
409
|
+
installGlobals(new JSDOM(FIXTURE_HTML));
|
|
410
|
+
this.hosts = await loadHosts();
|
|
363
411
|
|
|
364
412
|
if (!this.trapInstalled) {
|
|
365
413
|
process.on('unhandledRejection', (reason: unknown) => {
|
|
366
414
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
367
|
-
this.
|
|
415
|
+
this.hostErrors.push(msg.split('\n')[0] ?? msg);
|
|
368
416
|
});
|
|
369
417
|
this.trapInstalled = true;
|
|
370
418
|
}
|
|
@@ -374,25 +422,17 @@ export class ExecutionValidator {
|
|
|
374
422
|
return 'ExecutionValidator';
|
|
375
423
|
}
|
|
376
424
|
|
|
377
|
-
private installGlobals(dom: JSDOM): void {
|
|
378
|
-
const g = globalThis as Record<string, unknown>;
|
|
379
|
-
g.window = dom.window;
|
|
380
|
-
g.document = dom.window.document;
|
|
381
|
-
g.Event = dom.window.Event;
|
|
382
|
-
g.CustomEvent = dom.window.CustomEvent;
|
|
383
|
-
g.HTMLElement = dom.window.HTMLElement;
|
|
384
|
-
g.Element = dom.window.Element;
|
|
385
|
-
g.Node = dom.window.Node;
|
|
386
|
-
g.MutationObserver = dom.window.MutationObserver;
|
|
387
|
-
g.getComputedStyle = dom.window.getComputedStyle;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
425
|
/**
|
|
391
|
-
* Execute one pattern
|
|
426
|
+
* Execute one pattern's source in a fresh fixture and return its effect
|
|
392
427
|
* signature. Never throws; failures come back as `error`.
|
|
393
428
|
*/
|
|
394
|
-
async execute(
|
|
395
|
-
|
|
429
|
+
async execute(
|
|
430
|
+
codeExampleId: string,
|
|
431
|
+
code: string,
|
|
432
|
+
lang: string,
|
|
433
|
+
host: ExecutionHost = 'engine'
|
|
434
|
+
): Promise<ExecutionResult> {
|
|
435
|
+
const run = this.executeInner(codeExampleId, code, lang, host);
|
|
396
436
|
const timeout = new Promise<ExecutionResult>(resolve =>
|
|
397
437
|
setTimeout(
|
|
398
438
|
() =>
|
|
@@ -411,40 +451,44 @@ export class ExecutionValidator {
|
|
|
411
451
|
private async executeInner(
|
|
412
452
|
codeExampleId: string,
|
|
413
453
|
code: string,
|
|
414
|
-
lang: string
|
|
454
|
+
lang: string,
|
|
455
|
+
host: ExecutionHost
|
|
415
456
|
): Promise<ExecutionResult> {
|
|
416
|
-
if (!this.
|
|
457
|
+
if (!this.hosts) await this.initialize();
|
|
417
458
|
const base: ExecutionResult = {
|
|
418
459
|
codeExampleId,
|
|
419
460
|
language: lang as LanguageCode,
|
|
420
461
|
effects: [],
|
|
421
462
|
};
|
|
463
|
+
if (host === 'upstream' && lang !== 'en') {
|
|
464
|
+
return { ...base, error: 'upstream runs English only' };
|
|
465
|
+
}
|
|
422
466
|
|
|
423
467
|
const dom = new JSDOM(FIXTURE_HTML);
|
|
424
|
-
|
|
468
|
+
installGlobals(dom);
|
|
425
469
|
const document = dom.window.document;
|
|
426
470
|
PATTERN_SETUP[codeExampleId]?.(document);
|
|
427
471
|
const btn = document.getElementById('btn')!;
|
|
428
|
-
|
|
472
|
+
// The plugin reads the language off the closest `lang`, as on a page; on
|
|
473
|
+
// <html>, outside the snapshot, so it is no part of any signature.
|
|
474
|
+
if (lang !== 'en') document.documentElement.setAttribute('lang', lang);
|
|
475
|
+
btn.setAttribute('_', code);
|
|
476
|
+
this.hostErrors = [];
|
|
429
477
|
|
|
430
|
-
//
|
|
431
|
-
// multi-language sweep that is
|
|
478
|
+
// Both hosts log every failing script via console — across a full
|
|
479
|
+
// multi-language sweep that is noise; keep only the errors, as diagnostics.
|
|
432
480
|
const saved = { log: console.log, warn: console.warn, error: console.error };
|
|
433
|
-
console.log = console.warn =
|
|
481
|
+
console.log = console.warn = () => {};
|
|
482
|
+
console.error = (...args: unknown[]) => {
|
|
483
|
+
const first = args.map(String).join(' ').split('\n')[0] ?? '';
|
|
484
|
+
this.hostErrors.push(first);
|
|
485
|
+
};
|
|
434
486
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return { ...base, error: `parse failed (confidence ${parsed.confidence?.toFixed(2)})` };
|
|
438
|
-
}
|
|
439
|
-
const built = buildAST(parsed.node);
|
|
440
|
-
if (!built.ast) return { ...base, error: 'buildAST returned no AST' };
|
|
441
|
-
|
|
442
|
-
const runtime = new this.core!.Runtime();
|
|
443
|
-
const ctx = this.core!.createContext(btn as unknown as HTMLElement);
|
|
444
|
-
// Installs the event handler (or runs bare commands immediately).
|
|
445
|
-
await runtime.execute(built.ast, ctx);
|
|
487
|
+
this.hosts![host].processNode(btn);
|
|
488
|
+
await new Promise(r => setTimeout(r, 0));
|
|
446
489
|
|
|
447
490
|
const before = snapshot(document);
|
|
491
|
+
const orderBefore = idOrder(document);
|
|
448
492
|
const trigger = PATTERN_TRIGGER[codeExampleId];
|
|
449
493
|
const triggerEvent =
|
|
450
494
|
trigger?.detail !== undefined
|
|
@@ -452,11 +496,13 @@ export class ExecutionValidator {
|
|
|
452
496
|
: new dom.window.Event(trigger?.event ?? 'click', { bubbles: true });
|
|
453
497
|
btn.dispatchEvent(triggerEvent);
|
|
454
498
|
await new Promise(r => setTimeout(r, SETTLE_MS));
|
|
455
|
-
const after = snapshot(document);
|
|
456
499
|
|
|
457
|
-
const effects = diffSnapshots(before,
|
|
458
|
-
|
|
459
|
-
|
|
500
|
+
const effects = diffSnapshots(before, snapshot(document));
|
|
501
|
+
const orderAfter = idOrder(document);
|
|
502
|
+
if (orderAfter !== orderBefore) effects.push(`order[${orderAfter}]`);
|
|
503
|
+
effects.sort();
|
|
504
|
+
if (this.hostErrors.length > 0) {
|
|
505
|
+
return { ...base, effects, error: this.hostErrors.join('; ') };
|
|
460
506
|
}
|
|
461
507
|
return { ...base, effects };
|
|
462
508
|
} catch (e: unknown) {
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Parse Validator - Validates hyperscript parsing across languages
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { MultilingualHyperscript } from '@hyperfixi/core/multilingual';
|
|
6
5
|
import type { SemanticNode } from '@lokascript/semantic';
|
|
7
|
-
import { fillSchemaDefaults } from '@lokascript/semantic';
|
|
6
|
+
import { fillSchemaDefaults, parseSemantic } from '@lokascript/semantic';
|
|
8
7
|
import type { PatternTranslation, ParseResult, Validator } from '../types';
|
|
9
8
|
import {
|
|
10
9
|
collectActions,
|
|
@@ -19,21 +18,12 @@ import {
|
|
|
19
18
|
* Validates that hyperscript patterns parse correctly using the semantic parser.
|
|
20
19
|
*/
|
|
21
20
|
export class ParseValidator implements Validator<ParseResult[]> {
|
|
22
|
-
private ml: MultilingualHyperscript;
|
|
23
|
-
private initialized = false;
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
this.ml = new MultilingualHyperscript();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
21
|
/**
|
|
30
|
-
* Initialize the validator
|
|
22
|
+
* Initialize the validator. Nothing to load: the semantic parser is a
|
|
23
|
+
* static import (it was reached through core's MultilingualHyperscript,
|
|
24
|
+
* whose `parse` was this same call, until Phase C2).
|
|
31
25
|
*/
|
|
32
|
-
async initialize(): Promise<void> {
|
|
33
|
-
if (this.initialized) return;
|
|
34
|
-
await this.ml.initialize();
|
|
35
|
-
this.initialized = true;
|
|
36
|
-
}
|
|
26
|
+
async initialize(): Promise<void> {}
|
|
37
27
|
|
|
38
28
|
/**
|
|
39
29
|
* Get validator name
|
|
@@ -65,11 +55,8 @@ export class ParseValidator implements Validator<ParseResult[]> {
|
|
|
65
55
|
const startTime = performance.now();
|
|
66
56
|
|
|
67
57
|
try {
|
|
68
|
-
// Parse the hyperscript
|
|
69
|
-
const semanticNode = (
|
|
70
|
-
pattern.hyperscript,
|
|
71
|
-
pattern.language
|
|
72
|
-
)) as SemanticNode;
|
|
58
|
+
// Parse the hyperscript. Confidence is not filtered here.
|
|
59
|
+
const semanticNode = parseSemantic(pattern.hyperscript, pattern.language).node;
|
|
73
60
|
|
|
74
61
|
if (!semanticNode) {
|
|
75
62
|
return {
|
|
@@ -16,36 +16,33 @@ import {
|
|
|
16
16
|
import { loadBaseline } from './value-matrix-gate';
|
|
17
17
|
|
|
18
18
|
describe('value matrix: accepted pairs', () => {
|
|
19
|
-
const [
|
|
19
|
+
const [ambiguity] = ACCEPTED;
|
|
20
20
|
|
|
21
21
|
it('tags a cell whose failing lanes are all accepted', () => {
|
|
22
|
-
expect(acceptedReason('
|
|
23
|
-
|
|
24
|
-
);
|
|
25
|
-
expect(acceptedReason('increment|#a.textContent', ['it', 'it/up'])).toBe(ambiguity!.reason);
|
|
22
|
+
expect(acceptedReason('increment|#a.textContent', ['it/up'])).toBe(ambiguity!.reason);
|
|
23
|
+
expect(acceptedReason('increment|#a.textContent', ['it/up', 'it/eng'])).toBe(ambiguity!.reason);
|
|
26
24
|
});
|
|
27
25
|
|
|
28
26
|
it('leaves it open when one failing lane is not', () => {
|
|
29
|
-
expect(acceptedReason('increment|#a.textContent', ['it', 'es'])).toBeUndefined();
|
|
30
|
-
expect(acceptedReason('put|the textContent of #a as Int', ['en', 'es/up'])).toBeUndefined();
|
|
27
|
+
expect(acceptedReason('increment|#a.textContent', ['it/up', 'es/up'])).toBeUndefined();
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
it('a regenerated baseline carries the reason, and counts the pairs', () => {
|
|
34
31
|
const result = (lanes: Record<string, string>): CellResult => ({
|
|
35
32
|
id: 'increment|#a.textContent',
|
|
36
33
|
want: '7',
|
|
37
|
-
lanes: {
|
|
34
|
+
lanes: { eng: '7', 'es/up': '7', ...lanes },
|
|
38
35
|
});
|
|
39
|
-
const kept = baselineFrom([result({ it: '1', 'it/
|
|
36
|
+
const kept = baselineFrom([result({ 'it/up': '1', 'it/eng': '1' })], '');
|
|
40
37
|
expect(kept.entries['increment|#a.textContent']?.accepted).toBe(ambiguity!.reason);
|
|
41
38
|
expect(kept.accepted).toBe(2);
|
|
42
|
-
const open = baselineFrom([result({ it: '1', es: '1' })], '');
|
|
39
|
+
const open = baselineFrom([result({ 'it/up': '1', 'es/up': '1' })], '');
|
|
43
40
|
expect(open.entries['increment|#a.textContent']?.accepted).toBeUndefined();
|
|
44
41
|
expect(open.accepted).toBe(0);
|
|
45
42
|
});
|
|
46
43
|
|
|
47
44
|
it('accepts only the cells it names', () => {
|
|
48
|
-
expect(acceptedReason('put
|
|
45
|
+
expect(acceptedReason('put|#a.textContent', ['it/up'])).toBeUndefined();
|
|
49
46
|
});
|
|
50
47
|
|
|
51
48
|
it('names cells the generator makes, and the baseline lists them as accepted', () => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The value matrix runs every (cell, lane) pair in one process, so no run may
|
|
3
3
|
* see another's writes. Two ways one did:
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - a run writes a window global (`increment n`); `@hyperfixi/core` also kept
|
|
5
|
+
* global variables in a Map every context shared, so one lane's write made
|
|
6
|
+
* every later run read `n` as 7 (that lane left with core's engine in C4,
|
|
7
|
+
* and the reset still restores every global);
|
|
7
8
|
* - a lane can remove the body (tr read `increment i by 2 * 2` as
|
|
8
9
|
* `increment *`), and the next run's reset then threw.
|
|
9
10
|
*
|
|
@@ -41,9 +42,9 @@ describe('value matrix: runs are isolated', () => {
|
|
|
41
42
|
cell('writes', 'on click increment n then put n into #out')
|
|
42
43
|
);
|
|
43
44
|
const after = await engines.runCell(read);
|
|
44
|
-
expect(writes.lanes.
|
|
45
|
+
expect(writes.lanes.eng).toBe('7');
|
|
45
46
|
expect(before.want).toBe('6');
|
|
46
|
-
expect(before.lanes.
|
|
47
|
+
expect(before.lanes.eng).toBe('6');
|
|
47
48
|
expect(after.lanes).toEqual(before.lanes);
|
|
48
49
|
}, 120_000);
|
|
49
50
|
|
|
@@ -51,7 +52,7 @@ describe('value matrix: runs are isolated', () => {
|
|
|
51
52
|
const before = await engines.runCell(read);
|
|
52
53
|
await engines.runCell(cell('removes', 'on click remove document.body'));
|
|
53
54
|
const after = await engines.runCell(read);
|
|
54
|
-
expect(before.lanes.
|
|
55
|
+
expect(before.lanes.eng).toBe('6');
|
|
55
56
|
expect(after.lanes).toEqual(before.lanes);
|
|
56
57
|
}, 120_000);
|
|
57
58
|
});
|