diff --git a/.claude/projects/-root-rose-ash/memory/feedback_sx_navigation.md b/.claude/projects/-root-rose-ash/memory/feedback_sx_navigation.md new file mode 100644 index 0000000..060763c --- /dev/null +++ b/.claude/projects/-root-rose-ash/memory/feedback_sx_navigation.md @@ -0,0 +1,17 @@ +--- +name: SX navigation single-source-of-truth +description: Navigation must be defined once in nav-data.sx — no fragment URLs, no duplicated case statements, use make-page-fn for convention-based routing +type: feedback +--- + +Never use fragment URLs (#anchors) in the SX docs nav system. Every navigable item must have its own Lisp URL. + +**Why:** Fragment URLs don't work with the SX URL routing system — fragments are client-side only and never reach the server, so nav resolution can't identify the current page. + +**How to apply:** +- `nav-data.sx` is the single source of truth for all navigation labels, hrefs, summaries, and hierarchy +- `page-functions.sx` uses `make-page-fn` (convention-based) or `slug->component` to derive component names from slugs — no hand-written case statements for simple pages +- Overview/index pages should generate link lists from nav-data variables (e.g. `reactive-examples-nav-items`) rather than hardcoding URLs +- To add a new simple page: add nav item to nav-data.sx, create the component file. That's it — the naming convention handles routing. +- Pages that need server-side data fetching (reference, spec, test, bootstrapper, isomorphism) still use custom functions with explicit case clauses +- Legacy Python nav lists in `content/pages.py` have been removed — nav-data.sx is canonical diff --git a/hosts/ocaml/browser/test_page_render.js b/hosts/ocaml/browser/test_page_render.js new file mode 100644 index 0000000..2acfe6f --- /dev/null +++ b/hosts/ocaml/browser/test_page_render.js @@ -0,0 +1,18 @@ +const path = require("path"); +const fs = require("fs"); +require(path.join(__dirname, "../_build/default/browser/sx_browser.bc.js")); +require(path.join(__dirname, "sx-platform.js")); +const K = globalThis.SxKernel; +for (const n of ["signals","deps","page-helpers","router","adapter-html"]) + K.loadSource(fs.readFileSync(path.join(__dirname,`../../../web/${n}.sx`),"utf8")); +K.loadSource(fs.readFileSync("/tmp/comp_defs.txt","utf8")); + +const pageSx = fs.readFileSync("/tmp/page_sx.txt","utf8"); +const parsed = K.parse(pageSx); +const html = K.renderToHtml(parsed[0]); +if (typeof html === "string" && !html.startsWith("Error:")) { + console.log("SUCCESS! Rendered", html.length, "chars of HTML"); + console.log("Preview:", html.substring(0, 200)); +} else { + console.log("Error:", html); +} diff --git a/hosts/ocaml/browser/test_signals.js b/hosts/ocaml/browser/test_signals.js new file mode 100644 index 0000000..495ced2 --- /dev/null +++ b/hosts/ocaml/browser/test_signals.js @@ -0,0 +1,25 @@ +const path = require("path"); +const fs = require("fs"); +require(path.join(__dirname, "../_build/default/browser/sx_browser.bc.js")); +require(path.join(__dirname, "sx-platform.js")); +const K = globalThis.SxKernel; +for (const n of ["signals","deps","page-helpers","router","adapter-html"]) + K.loadSource(fs.readFileSync(path.join(__dirname,`../../../web/${n}.sx`),"utf8")); + +// Test signal basics +const tests = [ + '(signal 42)', + '(let ((s (signal 42))) (deref s))', + '(let ((s (signal 42))) (reset! s 100) (deref s))', + '(let ((s (signal 10))) (swap! s (fn (v) (* v 2))) (deref s))', + '(let ((s (signal 0))) (computed (fn () (+ (deref s) 1))))', + '(let ((idx (signal 0))) (let ((c (computed (fn () (+ (deref idx) 10))))) (deref c)))', +]; + +for (const t of tests) { + const r = K.eval(t); + const s = JSON.stringify(r); + console.log(`${t.substring(0,60)}`); + console.log(` => ${s && s.length > 100 ? s.substring(0,100) + '...' : s}`); + console.log(); +} diff --git a/sx/sx/plans/runtime-slicing.sx b/sx/sx/plans/runtime-slicing.sx index 58cc8d8..5a87086 100644 --- a/sx/sx/plans/runtime-slicing.sx +++ b/sx/sx/plans/runtime-slicing.sx @@ -40,7 +40,7 @@ ;; ----------------------------------------------------------------------- (~docs/section :title "Tiers" :id "tiers" - (p "Four tiers, matching the " (a :href "/sx/(geography.(reactive.plan))" :class "text-violet-700 underline" "reactive islands") " levels:") + (p "Four tiers, matching the " (a :href "/sx/(geography.(reactive.(reactive-design)))" :class "text-violet-700 underline" "reactive islands") " levels:") (div :class "overflow-x-auto rounded border border-stone-200 mb-4" (table :class "w-full text-left text-sm" @@ -275,7 +275,7 @@ (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li (a :href "/sx/(etc.(plan.environment-images))" :class "text-violet-700 underline" "Environment Images") " — tiered images are smaller. An L0 image omits the parser, evaluator, and most primitives.") (li (a :href "/sx/(etc.(plan.content-addressed-components))" :class "text-violet-700 underline" "Content-Addressed Components") " — component CID resolution is L3-only. L0 pages don't resolve components client-side.") - (li (a :href "/sx/(geography.(reactive.plan))" :class "text-violet-700 underline" "Reactive Islands") " — L2 tier is defined by island presence. The signal runtime is the L1→L2 delta.") + (li (a :href "/sx/(geography.(reactive.(reactive-design)))" :class "text-violet-700 underline" "Reactive Islands") " — L2 tier is defined by island presence. The signal runtime is the L1→L2 delta.") (li (a :href "/sx/(etc.(plan.isomorphic-architecture))" :class "text-violet-700 underline" "Isomorphic Architecture") " — client-side page rendering is L3. Most pages don't need it.")) (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2" diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 772c75e..086b3d3 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -159,18 +159,35 @@ def _reference_data(slug: str) -> dict: return build_reference_data(slug, raw, detail_keys) -def _read_spec_file(filename: str) -> str: - """Read a spec .sx file from the ref directory. Pure I/O — metadata lives in .sx.""" +def _find_spec_file(filename: str) -> str | None: + """Find a spec .sx file across spec/, web/, shared/sx/ref/ directories.""" import os - ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") - if not os.path.isdir(ref_dir): - ref_dir = "/app/shared/sx/ref" - filepath = os.path.join(ref_dir, filename) + base = os.path.join(os.path.dirname(__file__), "..", "..") + search_dirs = [ + os.path.join(base, "spec"), + os.path.join(base, "web"), + os.path.join(base, "shared", "sx", "ref"), + "/app/spec", + "/app/web", + "/app/shared/sx/ref", + ] + for d in search_dirs: + path = os.path.join(d, filename) + if os.path.isfile(path): + return path + return None + + +def _read_spec_file(filename: str) -> str: + """Read a spec .sx file. Pure I/O — metadata lives in .sx.""" + filepath = _find_spec_file(filename) + if not filepath: + return ";; spec file not found: " + filename try: with open(filepath, encoding="utf-8") as f: return f.read() - except FileNotFoundError: - return ";; spec file not found" + except (FileNotFoundError, TypeError): + return ";; spec file not found: " + filename # --------------------------------------------------------------------------- @@ -332,7 +349,7 @@ def _collect_symbols(expr) -> set[str]: _SPEC_SLUG_MAP = { "parser": ("parser.sx", "Parser", "Tokenization and parsing"), - "evaluator": ("eval.sx", "Evaluator", "Tree-walking evaluation"), + "evaluator": ("evaluator.sx", "Evaluator", "CEK machine evaluator"), "primitives": ("primitives.sx", "Primitives", "Built-in pure functions"), "render": ("render.sx", "Renderer", "Three rendering modes"), "special-forms": ("special-forms.sx", "Special Forms", "Special form dispatch"), @@ -374,10 +391,9 @@ def _spec_explorer_data(filename: str, title: str = "", desc: str = "") -> dict return None # Read the raw source - ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") - if not os.path.isdir(ref_dir): - ref_dir = "/app/shared/sx/ref" - filepath = os.path.join(ref_dir, filename) + filepath = _find_spec_file(filename) + if not filepath: + return None try: with open(filepath, encoding="utf-8") as f: source = f.read() diff --git a/sx/tests/test_demos.py b/sx/tests/test_demos.py index cb1dc74..f62eaca 100644 --- a/sx/tests/test_demos.py +++ b/sx/tests/test_demos.py @@ -491,6 +491,15 @@ class TestSpecExplorer: assert "define" in body, "Should contain define forms from spec" assert "eval-expr" in body, "Should contain eval-expr from evaluator spec" + def test_evaluator_renders_in_browser(self, page: Page): + """Spec explorer should render correctly in the browser, not show 'not found'.""" + nav(page, "(language.(spec.(explore.evaluator)))") + page.wait_for_timeout(3000) + content = page.locator("#main-panel").text_content() or "" + assert "not found" not in content.lower(), \ + f"Page shows 'not found' instead of spec content: {content[:200]}" + expect(page.locator("#main-panel")).to_contain_text("Evaluator", timeout=5000) + # --------------------------------------------------------------------------- # Key doc pages (smoke tests)