eyeling 1.24.12 → 1.24.14

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/HANDBOOK.md CHANGED
@@ -3682,7 +3682,7 @@ That is exactly the sort of explanation that N3, and Eyeling in particular, can
3682
3682
 
3683
3683
  ## Appendix I — The Eyeling Playground
3684
3684
 
3685
- The **Eyeling Playground** is the browser-based front end for experimenting with Eyeling without a local install or command-line workflow. It is meant for teaching, quick debugging, live demos, and shareable reasoning examples. Rather than treating reasoning as an offline batch process, the playground makes it interactive: users can edit N3 directly in the browser, load remote N3 from a URL, run reasoning, inspect streamed output, and share the current state through a link.
3685
+ The **Eyeling Playground** is the browser-based front end for experimenting with Eyeling without a local install or command-line workflow. It is meant for teaching, quick debugging, live demos, and shareable reasoning examples. Rather than treating reasoning as an offline batch process, the playground makes it interactive: users can edit N3 directly in the browser, load remote N3 from a URL, run reasoning, inspect streamed or rendered output, autosave local state, and create a compact share link when needed.
3686
3686
 
3687
3687
  This appendix explains what the playground is for, how it is structured, and why it matters in practice.
3688
3688
 
@@ -3696,7 +3696,8 @@ The playground exists to lower that initial friction. It lets a user:
3696
3696
  - edit or paste a small N3 program,
3697
3697
  - run reasoning immediately,
3698
3698
  - inspect output and errors in place,
3699
- - and share the exact setup with a URL.
3699
+ - autosave local work between reloads,
3700
+ - and copy a compact share link when the setup should be shared.
3700
3701
 
3701
3702
  That makes the playground useful not only for newcomers, but also for experienced users who want a fast feedback loop for small examples.
3702
3703
 
@@ -3714,6 +3715,8 @@ A key recent addition is **background knowledge mode**. When enabled, the N3 loa
3714
3715
 
3715
3716
  That separation is helpful both pedagogically and practically. It mirrors real reasoning work, where a user often reasons _over_ a fixed body of data rather than constantly rewriting it.
3716
3717
 
3718
+ For repository examples, the playground also follows the same sidecar-input convention as the example test runner. When a loaded URL looks like `.../examples/name.n3`, the playground probes for `.../examples/input/name.trig`. If that companion TriG file exists, it is loaded automatically as background evidence and the run uses RDF/TriG compatibility mode, matching the command-line `-r` behavior used by `npm run test:examples`.
3719
+
3717
3720
  ### I.3 Execution behavior
3718
3721
 
3719
3722
  The playground is designed to feel responsive even when reasoning is not trivial. To do that, it uses a browser execution model that can run inference in a worker rather than blocking the main UI thread. Output is then surfaced back into the page.
@@ -3728,6 +3731,8 @@ This matters because the playground is not just a text box plus a submit button.
3728
3731
 
3729
3732
  The output behavior also adapts to the kind of N3 program being run. In some cases the natural result is a streamed list of derived triples. In others, such as programs using output-oriented constructs like `log:outputString`, a rendered text result is more appropriate. The playground supports both styles.
3730
3733
 
3734
+ For Markdown-oriented `log:outputString` examples, the output pane has two views: a rendered Markdown view and a Markdown source view. The rendered view is selected by default when the output is text intended for presentation, while the source view keeps the exact generated Markdown available for copying, inspection, or comparison.
3735
+
3731
3736
  ### I.4 Error handling and explainability
3732
3737
 
3733
3738
  For an interactive reasoning environment, error behavior matters almost as much as successful output. The playground therefore gives particular attention to syntax and runtime feedback.
@@ -3741,21 +3746,21 @@ The playground also exposes two configuration toggles that are especially useful
3741
3746
 
3742
3747
  Together these choices make the playground better suited to live explanation, teaching, and debugging than a minimal browser wrapper would be.
3743
3748
 
3744
- ### I.5 Shareable state through URLs
3749
+ ### I.5 Local state and compact share links
3750
+
3751
+ The playground deliberately separates ordinary editing from link sharing.
3745
3752
 
3746
- One of the most practical features of the playground is that its state can be encoded in the page URL.
3753
+ During normal use, the live browser URL is kept short. Editor content and UI state are autosaved in `localStorage`, so reloading the page can restore local work without continuously rewriting the address bar with a large encoded N3 program.
3747
3754
 
3748
- The canonical query parameters are:
3755
+ When a user does want a portable link, the **Copy share link** button creates one on demand:
3749
3756
 
3750
- - `edit` sets the editor content,
3751
- - `url` fills the URL field,
3752
- - `loadbg` determines whether the URL should be loaded as background knowledge,
3753
- - `proofcomments` — initializes the proof-comments checkbox,
3754
- - `httpsderef` — initializes the HTTPS dereferencing checkbox.
3757
+ - unedited examples that were loaded from a URL can be shared as short `?url=...` links,
3758
+ - edited programs are shared with a compact compressed `?state=...` payload,
3759
+ - default option values are omitted from that payload to keep links small.
3755
3760
 
3756
- This makes the playground particularly strong for tutorials and demos. A link can specify not just a program, but a whole configuration: an imported resource, whether it belongs in background knowledge, a small editable overlay, and the relevant runtime toggles.
3761
+ This keeps everyday use pleasant while preserving the important tutorial and issue-reporting workflow: a link can still capture the imported resource, the local editable overlay, background-knowledge mode, proof-comments mode, and HTTPS-dereferencing mode.
3757
3762
 
3758
- Older hash-based links are still accepted as a fallback, but new state updates are written using query parameters because they scale better as the UI grows beyond a single editor field.
3763
+ For compatibility, older `?edit=`, `?program=`, `?url=`, compact `?state=`, and hash-based links are still accepted when opened. The old `/demo` entry point is also kept as a redirect to the canonical `/playground` page.
3759
3764
 
3760
3765
  ### I.6 What the playground is good for
3761
3766
 
@@ -3775,7 +3780,7 @@ For short reasoning tasks, the playground can be a faster debugging surface than
3775
3780
 
3776
3781
  #### I.6.4 Sharing examples
3777
3782
 
3778
- A single link can capture enough context for another person to reproduce an example quickly. This is valuable in issue reports, discussions, teaching material, and public-facing demonstrations.
3783
+ A copied share link can capture enough context for another person to reproduce an example quickly, without forcing the live browser URL to carry the full editor content during normal use. This is valuable in issue reports, discussions, teaching material, and public-facing demonstrations.
3779
3784
 
3780
3785
  ### I.7 Limits of the playground
3781
3786
 
@@ -3787,7 +3792,7 @@ In short: the playground is best thought of as a compact interactive front end f
3787
3792
 
3788
3793
  ### I.8 Why it matters
3789
3794
 
3790
- The Eyeling Playground shows that N3 reasoning can be made substantially more approachable without flattening the underlying logic into a toy interface. A relatively small set of features — an editor, a URL loader, background knowledge mode, responsive execution, proof toggles, and shareable query parameters — is enough to support serious educational and exploratory work.
3795
+ The Eyeling Playground shows that N3 reasoning can be made substantially more approachable without flattening the underlying logic into a toy interface. A relatively small set of features — an editor, a URL loader, background knowledge mode, responsive execution, proof toggles, rendered Markdown output, local autosave, and compact share links — is enough to support serious educational and exploratory work.
3791
3796
 
3792
3797
  That is the main value of the playground. It gives Eyeling a public-facing, browser-native environment where reasoning is not hidden behind setup overhead, and where examples can move easily between author, teacher, student, and reviewer.
3793
3798
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.12",
3
+ "version": "1.24.14",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -22,8 +22,18 @@ const TTY = process.stdout.isTTY;
22
22
  const C = TTY
23
23
  ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
24
24
  : { g: '', r: '', y: '', dim: '', n: '' };
25
+ const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
26
+
27
+ const TOTAL_TESTS = 11;
28
+ const idxWidth = String(TOTAL_TESTS).length;
29
+ let passed = 0;
30
+ let failed = 0;
31
+ let currentTest = null;
32
+ let nonTestFailure = false;
33
+ const suiteStart = Date.now();
34
+
25
35
  function ok(msg) {
26
- console.log(`${C.g}OK ${C.n} ${msg}`);
36
+ console.log(`${C.g}OK${C.n} ${msg}`);
27
37
  }
28
38
  function info(msg) {
29
39
  console.log(`${C.y}==${C.n} ${msg}`);
@@ -31,6 +41,36 @@ function info(msg) {
31
41
  function fail(msg) {
32
42
  console.error(`${C.r}FAIL${C.n} ${msg}`);
33
43
  }
44
+ function beginTest(msg) {
45
+ currentTest = { msg, start: Date.now() };
46
+ }
47
+ function endTest() {
48
+ const tc = currentTest;
49
+ if (!tc) return;
50
+ const idx = String(passed + failed + 1).padStart(idxWidth, '0');
51
+ ok(`${idx} ${tc.msg} ${msTag(Date.now() - tc.start)}`);
52
+ passed += 1;
53
+ currentTest = null;
54
+ }
55
+ function recordCurrentFailure() {
56
+ const tc = currentTest;
57
+ if (!tc) return false;
58
+ const idx = String(passed + failed + 1).padStart(idxWidth, '0');
59
+ fail(`${idx} ${tc.msg} ${msTag(Date.now() - tc.start)}`);
60
+ failed += 1;
61
+ currentTest = null;
62
+ return true;
63
+ }
64
+ function printSummary() {
65
+ console.log('');
66
+ const suiteMs = Date.now() - suiteStart;
67
+ info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
68
+ if (failed === 0 && !nonTestFailure) {
69
+ ok(`All playground tests passed (${passed}/${TOTAL_TESTS})`);
70
+ } else {
71
+ fail(`Some playground tests failed (${passed}/${TOTAL_TESTS})`);
72
+ }
73
+ }
34
74
 
35
75
  function guessContentType(p) {
36
76
  const ext = path.extname(p).toLowerCase();
@@ -534,6 +574,10 @@ async function main() {
534
574
  'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/builtin/sudoku.js',
535
575
  { ct: 'application/javascript', body: localSudokuBuiltin },
536
576
  ],
577
+ [
578
+ 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/input/sudoku.trig',
579
+ { code: 404, ct: 'text/plain', body: 'not found' },
580
+ ],
537
581
  ]);
538
582
 
539
583
  async function getText(url) {
@@ -551,13 +595,17 @@ async function main() {
551
595
  });
552
596
  }
553
597
 
598
+ beginTest('clean /playground URL serves the playground');
554
599
  const cleanRes = await getText(cleanPlaygroundUrl);
555
600
  assert.equal(cleanRes.statusCode, 200, 'clean /playground URL should serve the playground');
556
601
  assert.match(cleanRes.body, /Eyeling N3 Playground/, 'clean /playground URL should load the playground');
602
+ endTest();
557
603
 
604
+ beginTest('legacy /demo URL serves the redirect page');
558
605
  const legacyRes = await getText(legacyDemoUrl);
559
606
  assert.equal(legacyRes.statusCode, 200, 'legacy /demo URL should serve redirect page');
560
607
  assert.match(legacyRes.body, /playground/, 'legacy /demo URL should point to the playground');
608
+ endTest();
561
609
 
562
610
  await cdp.send(
563
611
  'Fetch.enable',
@@ -582,7 +630,7 @@ async function main() {
582
630
  'Fetch.fulfillRequest',
583
631
  {
584
632
  requestId: p.requestId,
585
- responseCode: 200,
633
+ responseCode: hit.code || 200,
586
634
  responseHeaders: [
587
635
  { name: 'Content-Type', value: `${hit.ct}; charset=utf-8` },
588
636
  { name: 'Cache-Control', value: 'no-store' },
@@ -647,6 +695,7 @@ async function main() {
647
695
  })
648
696
  : [];
649
697
  const renderedPanel = document.getElementById('output-rendered');
698
+ const outputTabs = document.querySelector('.output-tabs');
650
699
  const renderedTab = document.getElementById('output-rendered-tab');
651
700
  const sourceTab = document.getElementById('output-source-tab');
652
701
  const sourceWrapper = document.getElementById('output-source');
@@ -659,8 +708,12 @@ async function main() {
659
708
  renderedHtml: renderedPanel ? String(renderedPanel.innerHTML || '') : '',
660
709
  renderedHidden: renderedPanel ? !!renderedPanel.hidden : true,
661
710
  sourceHidden: sourceWrapper ? sourceWrapper.classList.contains('markdown-source-hidden') : true,
711
+ outputTabsHidden: outputTabs ? !!outputTabs.hidden : true,
662
712
  renderedTabSelected: renderedTab ? renderedTab.getAttribute('aria-selected') === 'true' : false,
663
713
  sourceTabSelected: sourceTab ? sourceTab.getAttribute('aria-selected') === 'true' : false,
714
+ shareStatus: document.getElementById('share-status') ? String(document.getElementById('share-status').textContent || '') : '',
715
+ backgroundStatus: document.getElementById('background-status') ? String(document.getElementById('background-status').textContent || '') : '',
716
+ href: String(window.location.href || ''),
664
717
  highlighted,
665
718
  };
666
719
  })()`)) || { status: '', output: '', highlighted: [] }
@@ -709,6 +762,10 @@ async function main() {
709
762
  })()`);
710
763
  }
711
764
 
765
+ async function makeShareUrlInPage() {
766
+ return await evalInPage(`window.__eyelingPlaygroundMakeShareUrl()`);
767
+ }
768
+
712
769
  async function loadUrlIntoEditor(url) {
713
770
  const payload = JSON.stringify(String(url));
714
771
  await evalInPage(`(() => {
@@ -752,6 +809,7 @@ ${JSON.stringify(last, null, 2)}`);
752
809
  `;
753
810
 
754
811
  // 1) Baseline smoke test: the default program runs to completion.
812
+ beginTest('playground runs the default Socrates program');
755
813
  await clickRun();
756
814
  const baseline = await waitForState(
757
815
  'default program completion',
@@ -763,9 +821,13 @@ ${JSON.stringify(last, null, 2)}`);
763
821
  );
764
822
  assert.ok(typeof baseline.output === 'string' && baseline.output.length > 0, 'Expected non-empty output');
765
823
  for (const [re, msg] of DEFAULT_PROGRAM_EXPECTS) assert.match(baseline.output, re, msg);
766
- ok('playground runs the default Socrates program');
824
+ assert.equal(baseline.outputTabsHidden, true, 'Expected plain Turtle output to hide Markdown tabs');
825
+ assert.equal(baseline.renderedHidden, true, 'Expected plain Turtle output to skip rendered Markdown panel');
826
+ assert.equal(baseline.sourceHidden, false, 'Expected plain Turtle output to show source directly');
827
+ endTest();
767
828
 
768
829
  // 2) N3 syntax errors should be shown in Output and highlight the offending line.
830
+ beginTest('playground shows syntax errors in Output and highlights the offending line');
769
831
  await setProgram(syntaxErrorProgram);
770
832
  await clickRun();
771
833
  const syntaxErr = await waitForState(
@@ -777,9 +839,10 @@ ${JSON.stringify(last, null, 2)}`);
777
839
  assert.match(syntaxErr.output, /\n\^\s*$/m, 'Expected caret line in syntax error output');
778
840
  assert.equal(syntaxErr.highlighted[0].line, 3, 'Expected line 3 to be highlighted');
779
841
  assert.equal(syntaxErr.highlighted[0].text, '^', 'Expected highlighted line text to match the broken line');
780
- ok('playground shows syntax errors in Output and highlights the offending line');
842
+ endTest();
781
843
 
782
844
  // 3) Inference fuse output should be visible in the Output pane.
845
+ beginTest('playground clearly shows inference fuse output');
783
846
  await setProgram(fuseProgram);
784
847
  await clickRun();
785
848
  const fuse = await waitForState(
@@ -793,9 +856,10 @@ ${JSON.stringify(last, null, 2)}`);
793
856
  assert.match(fuse.output, /Inference fuse triggered\./i, 'Expected fuse message in Output');
794
857
  assert.match(fuse.output, /Fired rule:/i, 'Expected fired rule explanation in Output');
795
858
  assert.match(fuse.output, /Matched instance:/i, 'Expected matched instance in Output');
796
- ok('playground clearly shows inference fuse output');
859
+ endTest();
797
860
 
798
861
  // 4) log:outputString should render as clean text, not raw triples.
862
+ beginTest('playground renders log:outputString Markdown with Rendered/Markdown source tabs');
799
863
  await setProgram(outputStringProgram);
800
864
  await clickRun();
801
865
  const rendered = await waitForState(
@@ -812,6 +876,7 @@ ${JSON.stringify(last, null, 2)}`);
812
876
  /:report\s+log:outputString\s+"|# Derived triples/i,
813
877
  'Expected clean rendered output without raw triples',
814
878
  );
879
+ assert.equal(rendered.outputTabsHidden, false, 'Expected Markdown output tabs to be visible for log:outputString');
815
880
  assert.equal(rendered.renderedHidden, false, 'Expected rendered Markdown tab to be visible by default');
816
881
  assert.equal(rendered.sourceHidden, true, 'Expected Markdown source tab to be hidden by default');
817
882
  assert.equal(rendered.renderedTabSelected, true, 'Expected Rendered tab to be selected by default');
@@ -822,6 +887,7 @@ ${JSON.stringify(last, null, 2)}`);
822
887
 
823
888
  await clickOutputSourceTab();
824
889
  const sourceView = await getPlaygroundState();
890
+ assert.equal(sourceView.outputTabsHidden, false, 'Expected Markdown output tabs to stay visible in source view');
825
891
  assert.equal(sourceView.sourceTabSelected, true, 'Expected Markdown source tab to be selectable');
826
892
  assert.equal(sourceView.renderedHidden, true, 'Expected rendered Markdown panel to hide after selecting source');
827
893
  assert.equal(sourceView.sourceHidden, false, 'Expected source editor to show after selecting source');
@@ -830,15 +896,49 @@ ${JSON.stringify(last, null, 2)}`);
830
896
  await clickOutputRenderedTab();
831
897
  const renderedAgain = await getPlaygroundState();
832
898
  assert.equal(renderedAgain.renderedTabSelected, true, 'Expected Rendered tab to be selectable again');
833
- ok('playground renders log:outputString Markdown with Rendered/Markdown source tabs');
899
+ endTest();
900
+
901
+ // 5) Normal editing should not keep rewriting the browser URL with raw N3 content.
902
+ beginTest('playground keeps the live URL short and creates compact share links on demand');
903
+ assert.doesNotMatch(renderedAgain.href, /[?&](?:edit|program)=/, 'Expected live URL to avoid raw editor content');
904
+ const compactShareUrl = await makeShareUrlInPage();
905
+ assert.match(compactShareUrl, /[?&]state=/, 'Expected an on-demand compact state parameter');
906
+ assert.doesNotMatch(compactShareUrl, /[?&](?:edit|program)=/, 'Expected share link to avoid raw edit/program params');
907
+ assert.ok(compactShareUrl.length < playgroundUrl.length + encodeURIComponent(outputStringProgram).length, 'Expected compact share URL to be shorter than raw editor URL');
908
+ endTest();
909
+
910
+ // 6) URL-loaded examples should auto-load matching examples/input/<stem>.trig and run in RDF/TriG mode.
911
+ beginTest('playground auto-loads companion TriG sidecars and uses RDF/TriG mode');
912
+ await loadUrlIntoEditor(`${started.baseUrl}/examples/smoke-arithmetic.n3`);
913
+ const smokeLoaded = await waitForState(
914
+ 'smoke-arithmetic URL loaded with companion TriG input',
915
+ (st) => /companion RDF\/TriG input/i.test(String(st.status || '')) && /input\/smoke-arithmetic\.trig/i.test(String(st.backgroundStatus || '')),
916
+ 20000,
917
+ );
918
+ assert.match(smokeLoaded.backgroundStatus, /smoke-arithmetic\.trig/i, 'Expected companion TriG sidecar in background status');
919
+ await clickRun();
920
+ const smoke = await waitForState(
921
+ 'URL-loaded smoke-arithmetic example completion with sidecar input',
922
+ (st) =>
923
+ String(st.status || '')
924
+ .trim()
925
+ .startsWith('Done') && /product = 42/i.test(String(st.output || '')),
926
+ 30000,
927
+ );
928
+ assert.match(smoke.output, /product = 42/i, 'Expected result derived from companion TriG evidence');
929
+ endTest();
834
930
 
835
- // 5) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
931
+ // 7) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
932
+ beginTest('playground auto-loads a companion example builtin for URL-loaded Sudoku');
836
933
  await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3');
837
934
  await waitForState(
838
935
  'sudoku URL loaded with companion builtin',
839
936
  (st) => /loaded n3 into the editor and loaded its example builtin/i.test(String(st.status || '')),
840
937
  20000,
841
938
  );
939
+ const urlLoadedShareUrl = await makeShareUrlInPage();
940
+ assert.match(urlLoadedShareUrl, /[?&]url=/, 'Expected URL-loaded examples to share as a short url= link');
941
+ assert.doesNotMatch(urlLoadedShareUrl, /[?&]state=/, 'Expected unedited URL-loaded examples to avoid state payloads');
842
942
  await clickRun();
843
943
  const sudoku = await waitForState(
844
944
  'URL-loaded Sudoku example completion',
@@ -850,14 +950,18 @@ ${JSON.stringify(last, null, 2)}`);
850
950
  );
851
951
  assert.match(sudoku.output, /Completed grid/i, 'Expected Sudoku rendered output');
852
952
  assert.match(sudoku.output, /unique valid Sudoku solution/i, 'Expected Sudoku builtin-backed result');
853
- ok('playground auto-loads a companion example builtin for URL-loaded Sudoku');
953
+ endTest();
854
954
 
855
955
  // Ensure no uncaught runtime exceptions.
956
+ beginTest('playground has no uncaught runtime exceptions');
856
957
  assert.equal(exceptions.length, 0, `Uncaught exceptions in playground.html: ${JSON.stringify(exceptions[0] || {})}`);
958
+ endTest();
857
959
 
858
960
  // Console errors are noisy and often indicate a broken UI.
859
961
  // (We suppress known noise like /favicon.ico on the server.)
962
+ beginTest('playground has no console errors');
860
963
  assert.equal(consoleErrors.length, 0, `Console errors in playground.html: ${JSON.stringify(consoleErrors[0] || {})}`);
964
+ endTest();
861
965
 
862
966
  // Cleanup.
863
967
  try {
@@ -866,9 +970,13 @@ ${JSON.stringify(last, null, 2)}`);
866
970
  } finally {
867
971
  await cleanup();
868
972
  }
973
+
974
+ printSummary();
869
975
  }
870
976
 
871
977
  main().catch((e) => {
978
+ if (!recordCurrentFailure()) nonTestFailure = true;
979
+ printSummary();
872
980
  fail(e && e.stack ? e.stack : String(e));
873
981
  process.exit(1);
874
982
  });