eyeling 1.24.13 → 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.13",
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,9 +708,11 @@ 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,
664
714
  shareStatus: document.getElementById('share-status') ? String(document.getElementById('share-status').textContent || '') : '',
715
+ backgroundStatus: document.getElementById('background-status') ? String(document.getElementById('background-status').textContent || '') : '',
665
716
  href: String(window.location.href || ''),
666
717
  highlighted,
667
718
  };
@@ -758,6 +809,7 @@ ${JSON.stringify(last, null, 2)}`);
758
809
  `;
759
810
 
760
811
  // 1) Baseline smoke test: the default program runs to completion.
812
+ beginTest('playground runs the default Socrates program');
761
813
  await clickRun();
762
814
  const baseline = await waitForState(
763
815
  'default program completion',
@@ -769,9 +821,13 @@ ${JSON.stringify(last, null, 2)}`);
769
821
  );
770
822
  assert.ok(typeof baseline.output === 'string' && baseline.output.length > 0, 'Expected non-empty output');
771
823
  for (const [re, msg] of DEFAULT_PROGRAM_EXPECTS) assert.match(baseline.output, re, msg);
772
- 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();
773
828
 
774
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');
775
831
  await setProgram(syntaxErrorProgram);
776
832
  await clickRun();
777
833
  const syntaxErr = await waitForState(
@@ -783,9 +839,10 @@ ${JSON.stringify(last, null, 2)}`);
783
839
  assert.match(syntaxErr.output, /\n\^\s*$/m, 'Expected caret line in syntax error output');
784
840
  assert.equal(syntaxErr.highlighted[0].line, 3, 'Expected line 3 to be highlighted');
785
841
  assert.equal(syntaxErr.highlighted[0].text, '^', 'Expected highlighted line text to match the broken line');
786
- ok('playground shows syntax errors in Output and highlights the offending line');
842
+ endTest();
787
843
 
788
844
  // 3) Inference fuse output should be visible in the Output pane.
845
+ beginTest('playground clearly shows inference fuse output');
789
846
  await setProgram(fuseProgram);
790
847
  await clickRun();
791
848
  const fuse = await waitForState(
@@ -799,9 +856,10 @@ ${JSON.stringify(last, null, 2)}`);
799
856
  assert.match(fuse.output, /Inference fuse triggered\./i, 'Expected fuse message in Output');
800
857
  assert.match(fuse.output, /Fired rule:/i, 'Expected fired rule explanation in Output');
801
858
  assert.match(fuse.output, /Matched instance:/i, 'Expected matched instance in Output');
802
- ok('playground clearly shows inference fuse output');
859
+ endTest();
803
860
 
804
861
  // 4) log:outputString should render as clean text, not raw triples.
862
+ beginTest('playground renders log:outputString Markdown with Rendered/Markdown source tabs');
805
863
  await setProgram(outputStringProgram);
806
864
  await clickRun();
807
865
  const rendered = await waitForState(
@@ -818,6 +876,7 @@ ${JSON.stringify(last, null, 2)}`);
818
876
  /:report\s+log:outputString\s+"|# Derived triples/i,
819
877
  'Expected clean rendered output without raw triples',
820
878
  );
879
+ assert.equal(rendered.outputTabsHidden, false, 'Expected Markdown output tabs to be visible for log:outputString');
821
880
  assert.equal(rendered.renderedHidden, false, 'Expected rendered Markdown tab to be visible by default');
822
881
  assert.equal(rendered.sourceHidden, true, 'Expected Markdown source tab to be hidden by default');
823
882
  assert.equal(rendered.renderedTabSelected, true, 'Expected Rendered tab to be selected by default');
@@ -828,6 +887,7 @@ ${JSON.stringify(last, null, 2)}`);
828
887
 
829
888
  await clickOutputSourceTab();
830
889
  const sourceView = await getPlaygroundState();
890
+ assert.equal(sourceView.outputTabsHidden, false, 'Expected Markdown output tabs to stay visible in source view');
831
891
  assert.equal(sourceView.sourceTabSelected, true, 'Expected Markdown source tab to be selectable');
832
892
  assert.equal(sourceView.renderedHidden, true, 'Expected rendered Markdown panel to hide after selecting source');
833
893
  assert.equal(sourceView.sourceHidden, false, 'Expected source editor to show after selecting source');
@@ -836,17 +896,40 @@ ${JSON.stringify(last, null, 2)}`);
836
896
  await clickOutputRenderedTab();
837
897
  const renderedAgain = await getPlaygroundState();
838
898
  assert.equal(renderedAgain.renderedTabSelected, true, 'Expected Rendered tab to be selectable again');
839
- ok('playground renders log:outputString Markdown with Rendered/Markdown source tabs');
899
+ endTest();
840
900
 
841
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');
842
903
  assert.doesNotMatch(renderedAgain.href, /[?&](?:edit|program)=/, 'Expected live URL to avoid raw editor content');
843
904
  const compactShareUrl = await makeShareUrlInPage();
844
905
  assert.match(compactShareUrl, /[?&]state=/, 'Expected an on-demand compact state parameter');
845
906
  assert.doesNotMatch(compactShareUrl, /[?&](?:edit|program)=/, 'Expected share link to avoid raw edit/program params');
846
907
  assert.ok(compactShareUrl.length < playgroundUrl.length + encodeURIComponent(outputStringProgram).length, 'Expected compact share URL to be shorter than raw editor URL');
847
- ok('playground keeps the live URL short and creates compact share links on demand');
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();
848
930
 
849
- // 6) 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');
850
933
  await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3');
851
934
  await waitForState(
852
935
  'sudoku URL loaded with companion builtin',
@@ -867,14 +950,18 @@ ${JSON.stringify(last, null, 2)}`);
867
950
  );
868
951
  assert.match(sudoku.output, /Completed grid/i, 'Expected Sudoku rendered output');
869
952
  assert.match(sudoku.output, /unique valid Sudoku solution/i, 'Expected Sudoku builtin-backed result');
870
- ok('playground auto-loads a companion example builtin for URL-loaded Sudoku');
953
+ endTest();
871
954
 
872
955
  // Ensure no uncaught runtime exceptions.
956
+ beginTest('playground has no uncaught runtime exceptions');
873
957
  assert.equal(exceptions.length, 0, `Uncaught exceptions in playground.html: ${JSON.stringify(exceptions[0] || {})}`);
958
+ endTest();
874
959
 
875
960
  // Console errors are noisy and often indicate a broken UI.
876
961
  // (We suppress known noise like /favicon.ico on the server.)
962
+ beginTest('playground has no console errors');
877
963
  assert.equal(consoleErrors.length, 0, `Console errors in playground.html: ${JSON.stringify(consoleErrors[0] || {})}`);
964
+ endTest();
878
965
 
879
966
  // Cleanup.
880
967
  try {
@@ -883,9 +970,13 @@ ${JSON.stringify(last, null, 2)}`);
883
970
  } finally {
884
971
  await cleanup();
885
972
  }
973
+
974
+ printSummary();
886
975
  }
887
976
 
888
977
  main().catch((e) => {
978
+ if (!recordCurrentFailure()) nonTestFailure = true;
979
+ printSummary();
889
980
  fail(e && e.stack ? e.stack : String(e));
890
981
  process.exit(1);
891
982
  });