luv

Workshop wiki

wiki-site.org

The wiki as a static site

What this page is #BNH91U

The wiki is a set of Org files, and Org is pleasant to write in Emacs but awkward to read anywhere else. This page records how the wiki becomes a browsable static site, why the build is expressed in ASDF, and what the Org-subset reader deliberately does and does not understand. It is a design page for luv-wiki, the Lisp system in wiki/org.lisp, wiki/html.lisp, and wiki/asdf.lisp, and for the corpus system luv-wiki-site in luv-wiki-site.asd whose components are the pages themselves.

The rendered site lives at mbrock.github.io/luv once the Pages workflow in wiki.yml has run; locally, make wiki writes it to build/wiki/. Besides the pages it carries a figure index and, per #58SVCM, the whole source of the repository's four primary system families as browsable dexp pages.

Why ASDF, and how the wiki slots into it #RYHRUL

ASDF describes itself as a tool that "plays a role like make or ant" for Lisp systems, and it warns that it is not a general installer. Underneath, though, its model is not Lisp-specific: a component has input files, an operation applied to a component has output files, perform does the work, and the plan is a dependency graph over (operation, component) actions with timestamps. The manual invites extension by subclassing operation and component and defining perform, input-files, output-files, and component-depends-on methods. cffi-toolchain uses exactly this to compile .c files: a c-file component whose compile-op outputs an object file.

The wiki fits the same shape, and more naturally than a fresh script would:

Two ASDF details are worth remembering. defsystem-depends-on cannot name a system defined in the same .asd file (ASDF reports a circular define-op), so the tooling is the primary system luv-wiki in its own file and the corpus is the primary luv-wiki-site system in another. Class names in a defsystem can be given as strings such as "luv.wiki:org-file", which ASDF reads in its own package; that avoids interning anything into ASDF's package, which the manual asks extensions not to do.

Capture recipes are source; captured media is a cache #IVRWI8

Pictures that carry design evidence should be reproducible without making Git the video store. capture-specification is the semantic boundary: a stable name, wiki figure ID, media kind, extension, description, and an ordinary named renderer function. define-capture is only a thin definition-site macro over that object and function. The body may assemble a gazetteer world, borrow an atelier camera flight, or set up a focused agent interaction; it still ends in the renderer which already owns those things.

The deterministic filename joins figure and recipe, for example Y7X7WK-construction-proposal-still.png. A bare [[capture:Y7X7WK-construction-proposal-still.png]] paragraph transcludes it from media/; MP4 and WebM links become native, muted, looping video figures. The HTML build does not require the bytes to exist. This is important: prose and recipes remain cheap and portable, while a local build without the artifact has an honest missing image rather than silently substituting stale evidence.

capture-op makes (asdf:make :luv/showcase) render the registered set into build/wiki/media/, which is already under the ignored build/ tree. Each run also writes a small manifest naming the source revision and every figure, recipe, kind, and filename. scripts/captures list discovers the set and scripts/captures render can select a few recipes during iteration. Captures announce progress because shader compilation, readback, and video encoding are real GPU work, not a reason for a silent build.

Execution placement is deliberately outside this graph. A GPU worker runs the same command through SSH, but its hostname and checkout path are deployment configuration, never recipe data. Git annex is likewise a publication cache, not part of capture identity: the web host explicitly obtains each annexed file before Caddy serves it. A pointer alone is never mistaken for a browser artifact.

defclass capture-specification capture-specification.lisp:9
defclasscapture-specification
name:initarg:name:readercapture-specification-name
figure-id:initarg:figure-id:readercapture-specification-figure-id
kind:initarg:kind:readercapture-specification-kind
description:initarg:description:readercapture-specification-description
extension:initarg:extension:readercapture-specification-extension
layout:initarg:layout:initform:landscape:readercapture-specification-layout
renderer:initarg:renderer:readercapture-specification-renderer
:documentation

A named recipe for one generated wiki image or video.

The semantic metadata is inspectable and retained in source; renderer is the ordinary named function installed by define-capture. Rendered bytes live under the capture output directory and do not belong in Git. #IVRWI8

defun prepare-capture-web-media capture-specification.lisp:177
defunprepare-capture-web-media
specificationpathname

Create the small public-index derivative for captured media at pathname.

Image originals get 480w and 768w WebPs whenever those are true downscales. Films get a card-sized WebP poster from their first frame. Originals remain untouched and retain their stable capture identity. #IVRWI8

multiple-value-bind
declare
ignoreheight
ecase
capture-specification-kindspecification
:image
dolist
when
>widthresponsive-width
write-capture-web-imagepathname
capture-responsive-image-pathnamepathnameresponsive-width
responsive-width
defclass showcase-page showcase.lisp:9
defclassshowcase-page
directory:initarg:directory:readershowcase-page-directory
media-path:initarg:media-path:readershowcase-page-media-path
:documentation

The generated capture catalog mounted beside externally served media.

DIRECTORY contains the capture manifest. MEDIA-PATH is the public static url subtree whose files are served by the deployment edge. #IVRWI8

DONE Make wiki media executable without committing it #S6I9C2

Intent. Establish the smallest end-to-end path from a figure-linked Lisp recipe to ignored image or video bytes and a wiki transclusion, while leaving machine selection and artifact publication open.

Evidence. Two #Y7X7WK recipes construct the same inert six-block wall and gnome on a hidden luvcraft canvas. One captures the final focused camera and one records its four-second orbit. The registry has live-redefinition and stable-path tests; the wiki renderer tests image and video transclusion; and the generated manifest binds both artifacts to their source revision.

Done when. A recipe is inspectable and selectable, ASDF can render the set, the wiki can show both media kinds without tracking their bytes, and no source file names a GPU host.

DONE Publish generated media out of band #0BGP3P

Intent. Put durable capture bytes somewhere the static site can fetch without growing the ordinary source clone, and run expensive recipes on a configured GPU executor.

Evidence. The local contract in #IVRWI8 supplies deterministic paths and a source-revision manifest. scripts/showcase now gives that contract an ordinary two-machine deployment:

origin/main -> chapel:~/luv-showcase          -> capture recipes
                 |                              |
                 `-> ~/luv-showcase-media      -> annex content
                                  |                |
                                  `----------> swa:/var/www/luv/showcase
origin/main --------------------------------> swa:/srv/luv

The separate showcase-media branch contains tiny annex pointers and the plain-text manifest, so normal source history stays light. Chapel is the authoritative annex content store. SWA fast-forwards both branches, obtains the bytes directly from Chapel, verifies that no referenced artifact is missing, and only then restarts the web service. Caddy's existing /video/ static route exposes those files while the Lisp web application reads the manifest and renders /showcase/. This split keeps byte serving out of the character-oriented HTTP implementation.

The common tailnet layout is deliberately boring and recorded as overrideable .envrc defaults: hosts chapel and swa, source checkouts ~/luv-showcase and /srv/luv, and media checkouts ~/luv-showcase-media and /var/www/luv/showcase. A differently named host or checkout only needs the corresponding LUV_SHOWCASE_* environment value. The whole trip is:

make showcase-publish

For diagnosis or iteration, showcase-bootstrap establishes clones and annex remotes, showcase-render only captures and publishes media, showcase-deploy only obtains it and restarts the service, and showcase-status reports both machines and the public page. Each remote checkout must be clean and can advance only by fast-forward; a human edit on a deployment machine stops publication instead of being overwritten.

Operational evidence. On 21 August 2026 Chapel rendered source 35ef9bafd893 through its Radeon Vulkan device, committed capture manifest aaba121 on showcase-media, and served both annex keys to SWA over its explicit pinned git-annex-shell. https://luv.swa.sh/showcase/ then returned the manifest-backed page; Caddy returned image/png for the 960x640 still and video/mp4 for the 960x640, 24 fps, four-second H.264 orbit. A cold service restart became healthy on its sixth one-second connection attempt.

Done when. The public showcase has been exercised end to end, including real image and video responses.

IDEA Give historical captures an annex retention policy #9H2KVT

Intent. Decide which historical annex keys remain reachable when recipes or figures change, without turning the ordinary source clone into a media archive.

Evidence. The initial publication keeps current keys in Chapel and SWA and keeps their metadata on the git-annex branch. That is sufficient for current showcase publication but deliberately says nothing yet about pruning superseded captures or retaining named milestones.

Done when. Publication has an explicit, inspectable rule for current, milestone, and unreferenced capture keys, plus a recovery story if Chapel is rebuilt.

The Org subset the reader understands #NS35X9

The reader is not a general Org parser. It reads the subset the wiki actually uses, chosen by surveying the pages: #+title, headings with property drawers, paragraphs, bulleted and numbered lists with wrapped and blank-separated items, example and src blocks, simple tables with an optional header rule, TeX math as $x^2$ or \(...\) inline and \[...\] or $$...$$ on display (drawn by KaTeX, loaded only when a page has math), Mermaid diagrams in #+begin_src mermaid blocks (likewise), bracket links with file:, https:, id:, lisp:, capture: or no scheme, a bare file: link to an image in the wiki directory as an inline image (a paragraph of only that link becomes a figure, and a paragraph of only a lisp: link becomes the definition it names), a bare capture: image or video as ignored generated media under build/wiki/media/, the light #ABC123 mention, and the six emphasis markers with Org's pre- and post-character rules. Anything else stays paragraph text, so an unfamiliar construct can never fail the build; it can only render plainly.

Emphasis rules follow Org: a marker opens only after whitespace or an opening bracket and closes only before whitespace or punctuation, so 2 * 3, foo_bar, and ../src/ are text. Verbatim and code keep their contents raw. Emphasis may cross one line break but not a blank line. Headings whose title starts with one of the work-mark words of #W3K9MK carry that word as a keyword and render with a status badge.

The element model is CLOS: document, heading, paragraph, plain-list, list-item, example-block, src-block, and table at the block level; emphasis, link, and mention inline, with plain strings for text. render-html is a generic function over those classes, so a new element is one class and one method. reference-id is the small protocol that lets both mentions and id: links count as references when the site indexes backlinks.

The reader was written rather than borrowed. deepfire's cl-org-mode (2018, built on parser-combinators) parses headline, drawer, and block structure but leaves section bodies as text and has no inline layer, links, lists, or HTML output, so it would have supplied perhaps a hundred lines and cost two dependencies and a second parser for the inside of sections. Its layered raw-AST/dressed-object design is a good precedent for the shape used here.

Spinneret as the HTML layer #VRKEYI

Spinneret's with-html compiles keyword forms to tags and passes the value of every other body form to its exported generic spinneret:html. Three consequences shaped the renderer:

Mentions carry their target with them: each page embeds a small card per figure it mentions (title, page, status, opening prose) and per definition it links (kind, name, lambda list, file and line, docstring), and a few lines of script in site.js show the card as a top-layer popover beside the link on hover, focus, or a first tap, placed below the link or above it when the viewport is short. The same script loads KaTeX and Mermaid from a CDN only for pages that carry math or diagrams. The site stays static and the links stay plain links.

Each page is one HTML file: a header linking to the index and the page's Org source on GitHub, an <h1> from #+title, and a <section> per heading whose id is the figure ID, so page.html#ABC123 is the figure's stable address. Mentions become links with the target's title as a tooltip; dangling ones are struck through. Each figure that is mentioned elsewhere ends with a "Mentioned in" line, the disposable backlink index that #F2N8VX anticipates. A figures.html page lists every figure by page with its work-mark status.

DONE Render the wiki as a static site from ASDF #QH1ONS

Intent: make the wiki browsable outside Emacs without a second source of truth, and express the build as an ASDF operation so it composes with the rest of the project.

Evidence: (asdf:make :luv-wiki-site) renders all pages, figures.html, and the stylesheet into build/wiki/; a second make wiki rewrites nothing and touching a page re-renders. The corpus loads with no dangling mentions. luv-wiki/test covers the reader, the site index, the rendered HTML, and the render-op output placement.

Done when: the six tests pass and the site builds from a fresh SBCL. Publishing is the next mark.

The wiki from a shell: scripts/wiki #GB4FT0

Agents and people who want the table of contents, the work marks, one figure's text, or the places a figure is mentioned should not need Emacs or a browser. scripts/wiki is a small executable built with ASDF's program-op (system luv-wiki/cli, entry point luv.wiki.cli:main, build/wiki-cli), rebuilt by its wrapper when the tooling changed and run inside the Nix shell. It reads the same objects the site renders — pages through the Org reader, code through the Eclector index — so what it prints agrees with the site. Commands: toc, marks, figure, page, mentions, defs, dangling, ids, build; help lists them.

Three details of the executable are worth recording. The Nix sbcl wrapper supplies CL_SOURCE_REGISTRY and ASDF_OUTPUT_TRANSLATIONS to SBCL processes only, so a dumped image would look for compiled files in the wrong place; wiki/build.lisp captures that environment before the dump and main re-establishes it. The build command registers luv.asd, luvcraft.asd, luv-wiki.asd, and luv-wiki-site.asd before asking ASDF to render, so the source browser sees every component tree without loading the GPU, game, or native systems. make wiki uses that command rather than a partial fresh image that knows only the two wiki ASD files.

DONE Serve the site from GitHub Pages #SIWPDS

Intent: publish build/wiki/ at mbrock.github.io/luv on every push that touches the wiki or its tooling.

Evidence: wiki.yml installs Nix, restores the closure with hestia (chunked GitHub Actions cache; about a minute warm, replacing the sunsetting magic-nix-cache), runs make wiki, and deploys with the Pages actions. Lisp and ASD changes trigger the workflow because they can change the derived source browser even when no Org page changed. Pages had to be enabled once by hand with gh api -X POST repos/mbrock/luv/pages -f build_type=workflow; the workflow token cannot create the site. The index and source pages are reachable at the public URL.

Done when: the workflow has succeeded once and the index page is reachable at the public URL. Both hold.

A fluid dual system: code contributes to the wiki #F6B3NI

The wiki does not want to become Lisp forms; Org in Emacs is the better writing surface, and the reader already gives the corpus to Lisp as objects. The interesting direction is the reverse flow: the code contributing to the discourse where that is more natural than prose, with less text and more references and examples. Two facts make that cheap.

Figure IDs are already valid, readable symbol names, and index.org anticipated a WIKI package of them (#F2N8VX). And code already mentions figures: the interpret-quantity-specification docstring in arithmetic/semantics.lisp cites #PLRP3A and the tests in arithmetic/tests.lisp cite #LNRY72 as the table they check. Nothing yet sees those mentions. Three rungs, cheapest first:

The 2021 "chronicle" experiment, dummy macros over dated todo, hmm, done entries so xref finds notes embedded in source, is the same shape as work marks with the status words renamed; what it adds is the reminder that symbols in a package are the identity mechanism and that macroexpansion is free cross-referencing.

The model a McCLIM browser wants #AGTIO4

The loaded documents on the org-file components are already the browser's data source. What the element model still lacks is small and each piece is motivated by a browser affordance:

The live loop is then: edit Org in Emacs, (asdf:load-system :luv-wiki-site) re-reads only the changed page, the browser redisplays. Mentions become presentations that jump on click and offer backlinks on right-click; the shader lab's presentation types could accept a figure to attach it to a definition.

Code views: a dexp renderer for Common Lisp #N06GKK

Source links that leave the page for GitHub are a poor reading experience, especially on a phone. The wiki should show code in place, and the way it should show it comes from wisp's structure editor, whose "dexp" renderer draws Lisp as nested boxes rather than text with indentation:

The mechanism is small and, since flexbox does the work, responsive for free. Every cons is a div.list with display: flex; flex-wrap: wrap and a small column gap; atoms are inline spans. Forms flow horizontally while they fit and wrap when they do not, so there is no line-breaking logic anywhere. The parentheses are the box's own left and right borders with rounded corners, so a wrapped list becomes a bracket around a paragraph of code, and depth is visible as nesting rather than indentation. Structure is carried in attributes, data-callee on the list and data-package and data-symbol-name on symbols, and CSS does the rest: body forms of defun or let take full width so they stack; quote, backquote, and unquote hide their symbol and paint ', `, and , with ::before; keywords get their colon the same way; the current package's prefix is hidden; the head of a list is coloured by :first-child with no separate classification pass. Ordinary prose text and code then share one flowing layout, which is what a wiki wants.

Two things change in the Common Lisp version. Dexp renders sexp data, so comments and original formatting are gone; the wiki shows source definitions and needs a reader that keeps them, see #SKNK2N. And dexp keeps per-operator layout in CSS selectors that cannot see method qualifiers or lambda-list keywords; the wiki renderer should assign roles in Lisp, using the same table the parinfer checker already keeps in parinfer/implementation.lisp (*binding-form-heads*, *clause-form-heads*, child-role), and emit class"head"= or class"body"= so the stylesheet needs only .list > .body { width: 100% }. Everything else transfers verbatim: side borders as parentheses, quote glyphs, keyword colons, home-package hiding, head colouring, and ; comments as full-width children.

The same renderer serves three places: the "Referenced from" backlinks of #PF39II, where a figure's code references expand in place; a lisp: link protocol, [[lisp:luv.arithmetic:interpret-quantity-specification]], that resolves against a definition index and shows the definition; and the wiki's own #+begin_src lisp blocks, which are read and drawn structurally with the <pre> as fallback whenever reading fails. A paragraph consisting only of a lisp: link becomes the definition itself, drawn from luv's source at build time:

defun interpret-quantity-specification semantics.lisp:1010
defuninterpret-quantity-specification
derivedinterpretation

Give a compatible anonymous derived specification an explicit meaning.

This is a semantic interpretation, never a numerical unit conversion. An already named quantity may only retain its name; anonymous derived results may acquire one when their dimension, exact unit, and tensor order agree with interpretation. Character must agree too, with one deliberate exception: a signed difference may be interpreted as an absolute. That is the explicit promotion the affine algebra otherwise never performs — the author asserts the amount is non-negative, and no lowering checks it (#PLRP3A). Points never cross to or from the other characters here.

unless
andderived
or
null
quantity-specification-namederived
eq
quantity-specification-namederived
quantity-specification-nameinterpretation
dimension=
quantity-specification-dimensionderived
quantity-specification-dimensioninterpretation
unit-expression=
quantity-specification-unitderived
quantity-specification-unitinterpretation
=
quantity-specification-tensor-orderderived
quantity-specification-tensor-orderinterpretation
let
from
quantity-specification-characterderived
to
quantity-specification-characterinterpretation
or
eqfromto
and
eqfrom:difference
eqto:absolute
quantity-operation-error'interpret
listderivedinterpretation
:incompatible-interpretation
interpretation

Inside a definition, #ID mentions in strings and comments are links to their figures, so the loop closes: the docstring above points back at #PLRP3A, and that figure lists this definition under "Referenced from code".

Layouts are objects chosen by operator and role #BGIM3I

The first renderer assigned layout with a table of head counts and a growing set of role strings and special cases; let bindings drawn as a two-column grid, loop keywords starting rows, and lambda lists whose sublists are (var default) clauses made that untenable. The design that replaced it follows clos-design: a layout is an object, and a list is drawn by the layout that its operator and the role its parent gave it select together (dispatch on the relationship, not on one participant).

Hand-written layouts are only hints for the standard forms whose lambda lists say too little (let, cond, defmethod, loop). For everything else, luv's own macros above all, the layout is derived from the real lambda list, the way SLY's slynk-indentation derives indentation from macro arglists: scripts/wiki introspect loads the systems, resolves every operator symbol the corpus uses against its file's in-package, records its kind and sb-introspect:function-lambda-list, and writes arglists.sexp, one readable form per operator, keyed by home package (2151 operators today). From such a lambda list a derived-layout knows that what precedes &body is the head, that a destructuring pattern in a position makes that argument a clause (with-block-content-storage ((domain palette indices) chunk &body body)), that &key marks where pairs begin, and, from SBCL's own parameter names, that BINDINGS, DEFINITIONS, or SLOT-SPECS mean a binding grid and LAMBDA-LIST or VARS a lambda list. The file is committed, so the site renders without loading luv, and it is among render-op's inputs; run the pass again after adding or changing macros.

Two rules apply across layouts. A trailing run of :keyword value arguments is drawn pair by pair in a .pair container that never breaks between key and value; where the pairs are body forms, as in the options of a definer, each pair is a row with the keys aligned, so define-unit and define-quantity read as small tables. And an operator known only by its def prefix is inspected rather than assumed: a list in second position makes it a lambda form, anything else a name followed by options.

The stylesheet knows only roles: a list with body forms is .stacked, a grid of rows with the head in a .head span (#993QQQ); .bindings is a CSS grid, its .clause children subgrids spanning two columns; a .clauses form is a two-column table of .stacked-clause rows (#4175NC); .rest a cell, .pair an inline unit, .break a zero-height row break before a row-start. The rules live beside the layouts, as style definitions (#UODI74). Adding a form's layout is one define-layout line or one class with two methods, and nothing else changes.

DONE Boxes hug their widest row #993QQQ

Intent. In the rendered source pages a nested body form stretches to its parent's full width (.body is flex-basis: 100%), and worse, a wrapping flex box's intrinsic width is the single-line sum of its children, so even an argument box like a multi-line (lambda …) comes out far wider than its tallest row. Closing parentheses pile up at the right margin, far from what they close. This costs more readability than any operator-specific layout, and it needs no operator knowledge to fix.

Evidence. Injecting CSS into the built hal/shader/language.lisp page: when a list with body forms becomes a grid of rows (a .head flex row holding the operator and head arguments, then one row per body form) with width: fit-content; max-width: 100%, every box hugs its widest row and the page reads like a pretty-printer's output — (unless operands (error …)) is exactly as wide as its error row. Row gaps and baseline alignment survive unchanged.

Done. The drawing routine now works in two passes: layout-items groups a list's children into items (a comment, a child with its role, or a keyword pair), and render-children-with-roles draws them; when any item is a body form the list is stacked, its head items in a .head span and every body item a row (a comment just before the first body form is a row too). The stacked style group draws it as a one-column grid with width: fit-content. On the shader-expression and streaming pages every box hugs its widest row at every depth; (unless operands (error …)) is as wide as its error row, a (lambda …) argument as wide as its longest body row, and top-level definitions still start at the left margin. What remains of the flex-wrap intrinsic-width problem is inside flow boxes: a function call holding a stacked argument, like (every (lambda …) types) as a cond test, is still sized as if its children stood on one line, so the argument after the lambda floats right of a gap; that wants the call layout of #ACX8LK.

defstruct (item (:constructor make-item (kind role node &key value comments index previous))) dexp.lisp:447

Drawing

The children of a list are first grouped into ITEMs -- a comment, one child with its role, or a :keyword value pair -- and then drawn. A list with body items is stacked: its head items go in a .head row and each body item is a row of its own, so the box is a grid of rows and hugs its widest row instead of stretching to the parent (#993QQQ).

defstruct
item
:constructormake-item
kindrolenode&keyvaluecommentsindexprevious

One drawn unit of a list: KIND is :comment, :child, or :pair. ROLE is the role the layout gave it (a pair takes its value's role); node the child or the key; VALUE and COMMENTS the pair's value and the comments between key and value; INDEX and PREVIOUS the argument index and previous argument, for docstring position.

kindrolenodevaluecommentsindexprevious
define-style stacked style.lisp:634
define-stylestacked

A list with body forms is a grid of rows: the .head row, then one row per body form, so the box hugs its widest row instead of stretching to its parent (#993QQQ).

".lisp .list.stacked":widthfit-content:max-width100%
"& > .head"
"& > .body":flex-basisauto:max-width100%
"& > .list.body":widthfit-content

DONE Clauses as tables: cond, case, typecase, handler-case #4175NC

Intent. A cond clause today stacks its test and its result as two identical rows, so (null vectors) and (find-shader-type :float) look like siblings and the reader cannot tell test from consequent. What the let grid did for bindings (#BGIM3I) should be done for clauses: a two-column table, key or test in the first column, result in the second, aligned across all clauses of the form.

Evidence. The same page with a two-column subgrid on clauses-layout bodies: (null vectors) → (find-shader-type :float), (every …) → (first vectors), t → (error …) line up, and a clause with several body forms stacks them in the second column. A tall test keeps the result on its first baseline. Nothing operator-specific is needed beyond what clauses-layout already marks, and every luv macro whose lambda list says &body clauses or cases gets it through the derived layout.

Done. clauses-layout carries a clause-head-count, 1 for cond, case, typecase and their variants, 2 for handler-case and restart-case; each clause is drawn by a stacked-clause-layout with that head count (and, for the two-element key, the lambda list marked as one) as a .head cell and a .rest cell of stacked body forms. The clauses style group makes the form a two-column grid, fit-content(50%) for the keys and the remainder for the bodies, each clause a subgrid row. On the source pages cond reads as test → result with the results aligned, handler-case as error () → "<unprintable>", and defstruct, which already used the clauses layout, became a slot table with names in one column and defaults and options in the other for free. Tests cover a cond with a multi-form clause and a handler-case. Alongside, the data-callee attribute is now decided by layout-callee-p, a generic with methods on the clause, grid, and lambda-list layouts, which also stopped lambda lists from claiming their first parameter as a callee.

defmethod render-layout dexp.lisp:570
defmethodrender-layout

A COND-style clause, one row of its parent's clause table (#4175NC): the key or test in a .head cell, then the body forms stacked in a .rest cell.

let
multiple-value-bind
headrows
spinneret:with-html
:span.head
dolist
itemhead
whenrows
:span.rest
dolist
itemrows
define-style clauses style.lisp:649
define-styleclauses

COND, CASE, HANDLER-CASE and other clause forms are tables (#4175NC): each clause a row with its key or test in the first column and its body forms stacked in the second, aligned across the clauses.

".lisp .list.clauses":grid-template-columns
minmax01fr
:column-gap0.7em
"& > .head""& > .comment"
:grid-columnspan2
"& > .stacked-clause":widthauto
"& > .head"
"& > .rest":row-gap0.05em:min-width0
"& > .rest > .body":flex-basisauto:max-width100%
"& > .rest > .list.body":widthfit-content

DONE The stylesheet as Lisp definitions #UODI74

Intent. wiki/style.css is 850 lines of hand-written CSS that the renderer's roles must agree with by convention. luv already compiles several small languages from S-expressions (#C6E3LA the shader language, #SQC5JN the arithmetic language, MSL); the stylesheet should be one more: rules written as Lisp definitions in the wiki system, compiled to CSS at build time, so a layout class and its rules can live side by side, be re-evaluated in the running image, and be read by the same tools as the rest of the site.

Evidence. The CSS in use needs only a small vocabulary: selectors with nesting and &, properties with keyword, string, number, and list values, var() and a handful of functions (clamp, minmax, repeat, color-mix), one @media query for dark mode and one for narrow screens. A compiler for that is a page of Lisp.

Done. wiki/css.lisp is the compiler, the package luv.css, in three parts. A reader syntax, luv.css:syntax (a named readtable that makes the digits and the hyphen read a token), reads CSS quantities and references as objects: 0.85rem and 94% are dimension instances, 0.72rem/1 a slash, --ink a variable-reference; no number-and-unit symbols are interned. A syntax tree of CLOS instances -- style, rule, at-rule, declaration, selector-list of complex-selector strings, and the value classes -- is built by the rule and define-style macros, whose bodies alternate :property keywords with values: bare symbols are CSS words (grid, solid), strings are written verbatim, and lists are Lisp, so (clamp 1rem 4vw 3rem) calls the CSS function and (gutter) or (palette :dark) call the sheet's own helpers, whose values, declarations, or rules join the enclosing rule (add-item dispatches on what a form returned). Nested rules combine with the parent, & standing for it, groups as lists of strings crossed with groups -- nothing is ever split on a comma -- and (:media "query" …) is an at-rule, hoisted around what it wraps. write-css is the text backend, a generic over the tree, so another backend can walk the same objects. wiki/style.lisp, package luv.wiki.style, holds the sheet: the palette is a table of light and dark values that (palette scheme) turns into declarations, the mark and definition-kind colours are loops, and the layout rows the dexp boxes share (flowing-row, stacked-rows, subgrid-row) are functions returning declarations. The generated style.css was diffed against the previous one: identical apart from the --fg=/--bg= aliases and one repeated font stack, both removed, and groups with equal declarations merged.

TODO Pairs, loop tables, and layouts from arglists alone #ACX8LK

Intent. Smaller layout wins in the order they pay: keyword pairs on rows aligned by a subgrid instead of a guessed min-width for the key; loop as a keyword table, each clause a row of keyword and rest as Emacs indents it; and function calls whose arguments wrap continuing under the first argument, which would also size a call holding a stacked argument by its rows rather than by one imagined line. Done already: setf, setq, psetf, psetq draw as a place/value table when they have more than one pair (pairs-layout; a single pair stays inline), and symbol names no longer break at hyphens.

Underneath, fold derived-layout into constructors of the existing layout classes so that a table entry and an introspected lambda list produce the same objects — (bindings &body body) a bindings-layout, (keyform &body cases) a clauses-layout — and every refinement above applies to both; extend the parameter-name heuristics with PLACE, KEYFORM, TEST-FORM, SPECIALIZED-LAMBDA-LIST, OPTIONS; then trim the hand table to true overrides. Alongside, the renderer's remaining typecase and typep on layout classes (lambda-list-of, docstring-position-p, layout-pairs-index, the data-callee test) become generics with methods on the layouts, and child-role answers a placement and a child layout rather than a space-separated role string that role-p parses back, per clos-design.

Done when. Each item lands as its own small commit with the source pages checked in the browser; the layout table is shorter than today; luv-wiki/test passes.

Reading source without loading it: Eclector #SKNK2N

The site is built by luv-wiki, which does not and should not load luv itself; the Vulkan and SDL systems have no business in a documentation build. So definitions must be read from files without interning their symbols or evaluating anything. Eclector is the ally here: a portable Common Lisp reader whose client protocol lets interpret-symbol return a token (package prefix, name, and whether the package is the file's current one) instead of interning, whose parse-result protocol yields source ranges for every object, and which reports skipped input, comments and #+ branches included, as results too. It reads #', #p, #(, ,@, and reader conditionals correctly, which a hand-rolled tokenizer would not; parinfer's tokenizer is deliberately a repair heuristic and stays that.

Reading with Eclector gives a definition index beside the figure index: for every cl-source-file in the registered luv systems' component trees, each top-level form with its kind, name, qualifiers or specializers, file, line range, comment text, and the #ID mentions inside it. Figures ask it who references them; lisp: links ask it where a name is defined; the McCLIM browser can present it directly. Reader conditionals and #. are rendered as visible tokens, never evaluated.

Platform availability belongs to ASDF's :if-feature metadata, not to a #+darwin conditional around the defsystem form. ASDF therefore registers the Objective-C and Metal systems and constructs their component trees on Linux, while their compile and load actions remain invalid there. The wiki deliberately walks those complete trees rather than ASDF's active sub-components, so a Linux documentation build renders the Darwin sources without loading them. Individual files such as the Cocoa canvas helper use the same component-level :if-feature convention.

Dexp on McCLIM #CEON0R

The token tree from #SKNK2N is a model, like the Org elements, and can be drawn by more than one backend. In McCLIM the natural rendering is the same idea in a different medium: a list is an output record with a rounded border, its children laid out by formatting-item-list or a small flow layout, symbols are presentations of a lisp-symbol type with translators to Edit Definition and to the wiki figures that mention them. Since McCLIM is meant to become a significant part of the 3D world's own interface, a dexp view of live definitions inside luvcraft is not far-fetched. As with the wiki browser, the HTML backend should come first and the CLIM one should drive the (render backend element) refactor of #AGTIO4.

Browsing the whole source from the wiki #58SVCM

Once every source file is read into nodes, showing all of it is one more render. Each Common Lisp file owned by the five root ASD files -- luv, luvcraft, mcluv, luv-wiki, and luv-wiki-site -- becomes a page under source/, drawn entirely as dexp boxes: a table of its definitions on top, every top-level form anchored by its starting line (#L956), the file's in-package hiding that package's prefix, comment blocks kept as one box each. source.html lists the files by system; on a wide screen a source page keeps a sidebar of the systems and their files on the left, the current file's system open, and the status bar reads as a breadcrumb trail (Source › system › file; on wiki pages Pages › title). The reference graph runs both ways through these pages: any symbol that names a definition anywhere in the corpus links to that definition (a defining form is preferred over a method), "Referenced from" summaries and lisp: links point at the local page with GitHub as a secondary , and #ID mentions in docstrings and comments point back at the wiki. Source pages use a wider column than prose, and since dexp needs no monospace alignment the boxes are set in the system sans-serif.

Docstrings, :documentation strings, any multi-line string, and comment blocks are not code and are not drawn as code: their content goes through the same reader as wiki prose and comes out as paragraphs and lists set in the prose face inside the box, with #ID mentions and verbatim markup live. One rule differs from Org: in code prose *name* without spaces is a special variable and reads as code, not bold.

This is the reading surface for keeping up with the codebase: the pages are regenerated by render-op whenever a source file changes, since the files are among its inputs.

DONE Browse the source as dexp pages #2KK966

Intent: make the whole codebase readable from the rendered wiki, per #58SVCM, so that following a figure into code and code back into figures never leaves the site.

Evidence: wiki/source.lisp adds source-file objects (path, system, text, nodes, definitions), render-source-page, and render-source-index; render-op on the system lists source.html and one page per file among its output-files. find-named-definition backs symbol links; the mesher page alone carries a few hundred. Adjacent comment lines are merged into one node, and clause lists (let bindings, defclass slots, lambda lists, cond clauses) do not colour their first element as an operator. The canonical make wiki path registers all five root ASD files before rendering, and the Pages workflow watches Lisp and ASD changes, so a clean build cannot silently collapse the index to the wiki tooling alone.

Done when: every source file has a page reachable from the site header, definitions are anchored and linked from symbols, and code references open locally rather than on GitHub. All hold.

DONE The site shows code references to figures #PF39II

Intent: the first rung of #F6B3NI. Build the definition index of #SKNK2N during render-op on the system and render a "Referenced from" line per figure naming the definitions that mention it, each expandable in place to the definition drawn by the dexp renderer of #N06GKK, with the GitHub line link as the secondary action, so #PLRP3A and #LNRY72 pay off.

Evidence: render-op on the system reads the cl-source-file components of every registered luv and luvcraft system (67 files, about 2500 definitions, under a second) and every page's input-files include them, so a source edit re-renders the site. #PLRP3A now shows interpret-quantity-specification and #LNRY72 shows the define-quantity :light-level form that follows its section comment, each as a <details> whose summary is kind, name, and file:line source link and whose body is the definition as dexp boxes; the #ID in the docstring links back. [[lisp:name]] links resolve against the same index. Dangling code mentions are warned about like dangling wiki mentions and are covered by a test; luv-wiki's own sources are excluded because their docstrings use placeholder IDs.

Done when: those two figures show their referencing definitions inline on the site, the index and renderer are covered by tests, and a dangling code mention is reported like a dangling wiki mention. All three hold.

DONE Read luv sources with Eclector into a definition index #YFDCSB

Intent: the reader of #SKNK2N as part of luv-wiki: an Eclector client that produces a token tree with source ranges and comments without interning, and a definition index over the cl-source-file components ASDF lists.

Evidence: wiki/lisp.lisp. source-reader subclasses Eclector's parse-result-client; interpret-symbol returns a token with the package prefix as written, quote-like macros and #. return marker lists that make-expression-result turns into lisp-prefix nodes, feature expressions are evaluated by name against *features* with the skipped branch kept as a lisp-conditional from its source text, and comments arrive through make-skipped-input-result as lisp-comment nodes, inside forms as well as between them. Every luv source file reads without error; the index names interpret-quantity-specification at arithmetic/semantics.lisp:956 with its #PLRP3A mention, and no package or symbol is created (the tests check).

Done when: every luv source file reads without error, the index names interpret-quantity-specification with its line range and its #PLRP3A mention, and a reader conditional appears as a token rather than a choice. All hold; the last is a lisp-conditional node with sign, feature, and form.

DONE The dexp HTML renderer #Z50KYF

Intent: render a token tree from #YFDCSB as dexp boxes per #N06GKK, with roles assigned in Lisp and painted by the wiki stylesheet, and use it for #+begin_src lisp blocks first, since the wiki already has those.

Evidence: wiki/dexp.lisp and the .lisp rules in style.css. A list is a flex-wrapping div.list with side borders; roles come from the layout objects of #BGIM3I and are emitted as operator, body, bindings, lambda-list, clause, and row-start classes, comments not counting as arguments. The stylesheet does .body { flex-basis: 100% } and paints operators, keywords, strings, numbers, comments, and prefixes. All lisp source blocks in the wiki now render structurally, readable at phone width; input that reads to nothing falls back to <pre>. The screenshot above is not yet reproduced from luv's code, but the same shapes are visible in the definitions the figures now show; a luv-flavoured version can replace it later.

Done when: the source blocks of the wiki render structurally and readably on a phone-width viewport, fall back to <pre> on read failure, and the screenshot above can be reproduced from luv's own code. The first two hold; the third is left as a nice-to-have.

TODO A wiki browser frame in mcluv #NPZWLR

Intent: a McCLIM application frame over the loaded luv-wiki-site documents, with figure and mention presentations, page navigation, and backlinks; the exercise that tells us what the model in #AGTIO4 is missing.

Evidence: none yet; the shader lab in shader-lab.lisp shows the presentation-type pattern to follow.

Done when: every wiki page can be read in the frame, clicking a mention jumps to its figure, and re-loading luv-wiki-site after an Org edit redisplays.

WAIT A WIKI package of figure symbols #MAE5RT

Intent: the second rung of #F6B3NI: intern figure IDs as external symbols of a WIKI package at load-op, attach the heading, and offer wiki:ref for xref-visible references from code.

Evidence: waits on #NPZWLR, which should show what accepting or invoking a figure symbol ought to do before the notation is fixed.

Done when: 'wiki:LNRY72 names a figure at the REPL, (wiki:ref LNRY72) is found by xref, and the site's "Referenced from" line uses the compile-time table when available.

IDEA Structural associations through the MOP #D0NQ54

Beyond textual mentions, a metaclass mixin or class option could make the association between a class or generic function and its wiki figures structural: (defclass ... (:metaclass documented-class) (:figures LNRY72)) or a wiki:figures slot on a metaobject, so the browser and the site can ask an object which figures explain it and a figure which definitions realize it. clos-design's rule applies: use the MOP only when the definition itself carries the meaning. Vague on purpose until the package and browser exist.

IDEA Source-anchored file links #MJQDRA

Org file: links with a ::(defun name search string presently point at the file on GitHub without the search. A later render step could resolve the search to a line number at build time, since the source is in the same tree.