Spec explorer data endpoint, spec file finder, browser render test (failing)

- Add spec-explorer-data-by-slug helper with _SPEC_SLUG_MAP
- _find_spec_file searches spec/, web/, shared/sx/ref/ directories
- defpage specs-explore-page uses :data for server-side data fetch
- test_evaluator_renders_in_browser: failing test for client-side rendering
  (client re-evaluates defpage content, find-spec unavailable — pre-existing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 17:36:21 +00:00
parent 71c2003a60
commit fac97883f9
6 changed files with 100 additions and 15 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -40,7 +40,7 @@
;; ----------------------------------------------------------------------- ;; -----------------------------------------------------------------------
(~docs/section :title "Tiers" :id "tiers" (~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" (div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm" (table :class "w-full text-left text-sm"
@@ -275,7 +275,7 @@
(ul :class "list-disc pl-5 text-stone-700 space-y-1" (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.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/(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.")) (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" (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"

View File

@@ -159,18 +159,35 @@ def _reference_data(slug: str) -> dict:
return build_reference_data(slug, raw, detail_keys) return build_reference_data(slug, raw, detail_keys)
def _read_spec_file(filename: str) -> str: def _find_spec_file(filename: str) -> str | None:
"""Read a spec .sx file from the ref directory. Pure I/O — metadata lives in .sx.""" """Find a spec .sx file across spec/, web/, shared/sx/ref/ directories."""
import os import os
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") base = os.path.join(os.path.dirname(__file__), "..", "..")
if not os.path.isdir(ref_dir): search_dirs = [
ref_dir = "/app/shared/sx/ref" os.path.join(base, "spec"),
filepath = os.path.join(ref_dir, filename) 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: try:
with open(filepath, encoding="utf-8") as f: with open(filepath, encoding="utf-8") as f:
return f.read() return f.read()
except FileNotFoundError: except (FileNotFoundError, TypeError):
return ";; spec file not found" return ";; spec file not found: " + filename
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -332,7 +349,7 @@ def _collect_symbols(expr) -> set[str]:
_SPEC_SLUG_MAP = { _SPEC_SLUG_MAP = {
"parser": ("parser.sx", "Parser", "Tokenization and parsing"), "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"), "primitives": ("primitives.sx", "Primitives", "Built-in pure functions"),
"render": ("render.sx", "Renderer", "Three rendering modes"), "render": ("render.sx", "Renderer", "Three rendering modes"),
"special-forms": ("special-forms.sx", "Special Forms", "Special form dispatch"), "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 return None
# Read the raw source # Read the raw source
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") filepath = _find_spec_file(filename)
if not os.path.isdir(ref_dir): if not filepath:
ref_dir = "/app/shared/sx/ref" return None
filepath = os.path.join(ref_dir, filename)
try: try:
with open(filepath, encoding="utf-8") as f: with open(filepath, encoding="utf-8") as f:
source = f.read() source = f.read()

View File

@@ -491,6 +491,15 @@ class TestSpecExplorer:
assert "define" in body, "Should contain define forms from spec" assert "define" in body, "Should contain define forms from spec"
assert "eval-expr" in body, "Should contain eval-expr from evaluator 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) # Key doc pages (smoke tests)