Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .claude/skills/library/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ js/library/
client.js the index and entry files fetched, verified and kept offline.
submit.js the submission document, the pre-check, the issue URL.
requests.js import-free. automata-studio:// links queued until boot ends.
article.js import-free. An essay's Markdown → HTML: the dialect, facts,
figures, footnotes. Shared by the website build and the view.
article.js An essay's Markdown → HTML: markdown-it and the library's
plugins (facts, figures, math, callouts, [[links]]), the HTML
allowlist and the tag balancer. Shared by the website build,
the view and the editor; the app loads it lazily.
article-figures.js import-free. The figures an essay draws from a machine's
standard code: space-time diagrams, growth charts.
essay.js what an essay may ask of the index: {{facts}}, figure
arguments (bounded), lib: links, and the build's warnings.
arguments (bounded), lib: links. Light: no markdown-it.
essay-check.js the build's warnings for an essay (imports the renderer).
essay-guide.md how to write an essay: the documentation, in the essay format.
essay-guide.js renders it, each example beside what it becomes.
essay-draw.js the view's figures, drawn in essay-figures.worker.js.
js/library-ui.js the view: discover, browse, entry pages, collections,
My Library, submit, about.
Expand Down Expand Up @@ -113,11 +118,15 @@ StateMate reaches the library two ways: `/library [words]`, and the `search_libr

**A machine or a collection can carry an essay**: a Markdown file beside it (`machines/…/bb5.md` beside `bb5.automaton`, `collections/busy-beavers.md` beside its `.json`). Its own file, so prose is diffed and reviewed as prose, and editing it never changes the machine's bytes, hash or id. The index lists it as `essay: { path, hash, minutes }` on the entry or collection (`essayRef` in index-model.js refuses a path that could not be one — it ends up in a fetch URL); `writeLibrary` serves it beside its machine, and only when the machine was published.

- **The dialect is [js/library/article.js](js/library/article.js)**, small on purpose: `##`/`###` (a `#` is a section too — a page has one title, the entry's), paragraphs, lists, quotes, tables, fenced code, `$…$`/`$$…$$`, `[^n]` notes, links to `http(s)`, `#` or `lib:<id>`. Code spans and math are set aside **before** anything else, and everything is escaped before any tag is written: nothing an author writes reaches the page as markup. Anything it cannot answer stays visible and is returned in `warnings`.
- **The Markdown is all of it**, parsed by markdown-it in [js/library/article.js](js/library/article.js): CommonMark, GitHub's tables, strikethrough, task lists and bare-URL links, footnotes, definition lists, `==mark==`, `~sub~`/`^sup^`, typographic quotes — plus Obsidian's `> [!note] Title` callouts (`-`/`+` fold them) and `[[id|text]]` links, `$…$`/`$$…$$`/`\(…\)`/`\[…\]` math left intact for KaTeX (a price like `$5 and $10` is not math — Pandoc's rule), `{{facts}}` and `:::` figures. The shallowest heading becomes an h2, since the page's h1 is the entry's title, so `#` and `##` essays read the same; YAML front matter (`---` … `---` at the top, as Obsidian writes it) is dropped.
- **Safety is the renderer's, not the author's.** Text is escaped by markdown-it; `validateLink` is opened only so that a core rule (`essay_links`) can judge every link — written or linkified — and **report** the ones it drops: a link must be `http(s)`, `mailto`, `#` or `lib:`, an image `https://` (no trackers over plain HTTP, no local files). Raw HTML passes only as **bare tags from `HTML_OK`** — `<kbd>`, `<details>`, `<sub>` and friends, with no attributes — and anything else is shown as the text it is. Then **`balanceHtml`** closes what the author left open and drops closing tags with nothing to close: on the website the essay is written into the page's own markup, and a stray `</section>` would otherwise close the page's. Anything the renderer cannot answer stays visible and is returned in `warnings`.
- **The one promise holds inside the prose.** `{{steps}}`, `{{ones}}`, `{{cells}}`, `{{states}}`, `{{transitions}}`, `{{size}}`, `{{standard}}`, `{{title}}` — and `{{steps <id>}}` for another entry — are read off the index by `essayFacts` (essay.js), the analysis that earned the badges, never typed. They are set in the mono with a dotted rule so a reader can tell the machine's answers from the author's words. A name is looked up with `Object.hasOwn`: `{{constructor}}` is not a fact.
- **Figures are drawn from the machine**: `::: spacetime steps= h=`, `::: growth y=log scale=linear`, `::: diagram id=`, `::: machines <id> <id> …`, each followed by its caption and a closing `:::`. `essayFigureSpec` parses and **bounds** the arguments (200,000 steps for a space-time diagram; a growth chart runs to the halt under 10⁸) for both faces. The website draws them at build time; the view draws them in a worker (essay-draw.js — one worker, started on first use, results kept by input so going back does not rerun BB(5)), falling back to drawing in place where there is no Worker. A space-time diagram draws the head's path only when every step has its row: sampled, it joins positions many steps apart into a zigzag the machine never made.
- **The build checks every essay** against what is published (`essayWarnings`): an unknown fact, a figure the machine cannot draw, a `lib:` link to nothing, a note never written. They are **warnings** on the entry's report — the PR comment — not errors: a sentence to fix is no reason to unpublish a machine that passed.
- **An essay is credited as its machine is** (`guard.mjs`): it is Markdown, with no credit of its own, so the machine's author or a maintainer may write it and nobody else may attach one to their machine; an essay with no machine beside it is refused. A collection's essay is the maintainers', like the collection. Essays arrive by pull request — the issue form carries the short write-up (`meta.library.readme`), which an essay replaces on the page.
- **An essay is credited as its machine is** (`guard.mjs`): it is Markdown, with no credit of its own, so the machine's author or a maintainer may write it and nobody else may attach one to their machine; an essay with no machine beside it is refused. A collection's essay is the maintainers', like the collection.
- **Essays are written in the app, or sent by pull request.** The Submit page has an **essay editor** (`essayEditor` in library-ui.js): Write / Split / Preview — Obsidian's split, the Markdown beside the page it makes, scrolled together — with `Ctrl/⌘ E` toggling reading, **Open .md file…** (or a file dropped on the panes; `readEssayFile` takes Markdown or plain text up to 1 MB, dropping a BOM and CRLFs), **Save as .md**, and Undo for a draft a file replaced. The preview is the real renderer with the real answers: facts from this machine's analysis (`essaySelf`, run once per machine, with placeholders for a title or account the form does not have yet — the analysis gives no facts without them), figures from the worker, `[[links]]` checked against the index, and the warnings listed under it. A step count the dialog's budget cannot reach shows as a pending `…`, counted by CI. The draft is kept per machine in localStorage (`essayDraft`/`saveEssayDraft`), and an author sending back their own entry starts from its published essay.
- **The documentation is [js/library/essay-guide.md](js/library/essay-guide.md)**, written in the essay format itself: every ```` ```example ```` block is shown as its source beside what the real renderer makes of it (`renderGuide` in essay-guide.js, with BB(2) — the index's entry, or `GUIDE_SAMPLE` — as the machine the examples are about). The website builds it into `/writing/` (`writingPage` in site.mjs, linked from Submit and the footer); the editor's **Guide** button shows it in the preview's place, loaded with Vite's `?raw` (a test swaps the loader: Node has no `?raw`). [tests/essay.test.js](tests/essay.test.js) holds it to the code: it renders with **no warnings**, and it names every `ESSAY_FACTS` fact, every figure, every `CALLOUT_ALIAS` kind, every `HTML_OK` tag and the real limits — a feature added without its documentation fails there.
- **An essay travels inside the submitted document** (`meta.library.essay`, `buildSubmissionDoc`), not in the issue form's address: the form's URL is capped at `ISSUE_URL_MAX` and a few paragraphs would fill it, while the document rides the compressed share link — or the clipboard, when that is too long, as the machine already did. `issue-to-entry.mjs` takes it out (the form's own **Essay** field wins, for a form filled in by hand), refuses one past `ARTICLE_MAX_CHARS`, writes it beside the machine as `.md` — which the submission workflow's `machines/**` picks up — and leaves the machine file without it. An update that brings no essay keeps the published one. The Essay field is **last** in the form on purpose: `parseIssueForm` starts each section once, so a `### Licence` heading inside an essay, coming after the real one, stays in the essay.
- **In the view** (`essaySection` in library-ui.js) the essay is fetched by `fetchEssayText` — the same verified, cached path as a machine's file, so a tampered download is refused and an essay read once reads offline. Two things differ from the website because the essay shares a document with the app: **every id is prefixed** (`lib-essay-`, via `idPrefix`), and **in-page links are handled in the view** — a footnote or a `lib:` link (written as `#library=`/`#collection=`) must not write the address bar's hash, which carries share links. Figures and plates go in as empty `data-essay-slot` nodes and are filled once the markup is in place. Anything scrolled to carries `scroll-margin-top`, or the sticky status bar covers it.

[tests/essay.test.js](tests/essay.test.js) pins the dialect, the escaping, the fact lookup, the figure bounds and the index's `essayRef`; [tests/library.test.js](tests/library.test.js) the build, the report's warnings, the guard, the website's page and the view (a tampered essay is refused, ids are prefixed, the essay comes before Behaviour and replaces the notes).
Expand Down
69 changes: 69 additions & 0 deletions css/library.css
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,71 @@
.lib-essay-body .essay-notes { margin-top: 2.2em; padding-top: 14px; border-top: 1px solid var(--border); font-size: .96rem; line-height: 1.5; color: var(--text2); }
.lib-essay-body .essay-notes ol { margin: 0; padding-left: 1.3em; }
.lib-essay-body .lib-plates { margin: 0; }
/* The writing guide's examples: the Markdown, and what it becomes, side by side. */
.lib-essay-body .essay-example { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; margin: 1.1em 0 1.6em; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.lib-essay-body .essay-example > .essay-example-src { margin: 0; border: 0; border-right: 1px solid var(--border); border-radius: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.lib-essay-body .essay-example-out { padding: 12px 16px; min-width: 0; font-size: .96em; }
.lib-essay-body .essay-example-out > :first-child { margin-top: 0; }
.lib-essay-body .essay-example-out > :last-child { margin-bottom: 0; }
.lib-essay-body .essay-example-out > p:first-child::first-letter { float: none; font-size: inherit; padding: 0; }
.lib-essay-body .essay-example-out .essay-fig { margin: .4em 0; }
.lib-essay-body hr { border: 0; border-top: 1px solid var(--border2); margin: 2em 0; }
.lib-essay-body mark { background: color-mix(in srgb, var(--gold) 28%, transparent); color: inherit; padding: 0 2px; border-radius: 2px; }
.lib-essay-body kbd { font-family: var(--mono); font-size: .72em; padding: 1px 6px; border: 1px solid var(--border2); border-bottom-width: 2px; border-radius: 4px; background: var(--lib-well); }
.lib-essay-body del, .lib-essay-body s { color: var(--text2); }
.lib-essay-body dl { margin: 0 0 1em; }
.lib-essay-body dt { font-weight: 600; }
.lib-essay-body dd { margin: 0 0 .6em 1.3em; color: var(--text2); }
.lib-essay-body img.essay-img { display: block; max-width: 100%; height: auto; margin: 1.2em auto; border-radius: 6px; border: 1px solid var(--border); }
.lib-essay-body li.task-item { list-style: none; margin-left: -1.3em; }
.lib-essay-body .task-box { margin: 0 .45em 0 0; vertical-align: middle; accent-color: var(--h, var(--accent)); }
.lib-essay-body details:not(.callout) { margin: 1em 0; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; }
.lib-essay-body details:not(.callout) > summary { cursor: pointer; color: var(--text2); }
/* Obsidian's callouts: a tinted panel, a titled head, the kind's colour on its edge. */
.lib-essay-body .callout { --c: var(--accent); margin: 1.3em 0; border: 1px solid color-mix(in srgb, var(--c) 30%, var(--border)); border-left: 3px solid var(--c); border-radius: 6px; background: color-mix(in srgb, var(--c) 6%, transparent); }
.lib-essay-body .callout-title { padding: 8px 14px; font-family: var(--sans); font-size: .82rem; font-weight: 600; letter-spacing: .01em; color: var(--c); }
.lib-essay-body summary.callout-title { cursor: pointer; }
.lib-essay-body .callout-body { padding: 0 14px 4px; font-size: .96em; }
.lib-essay-body .callout-body > :last-child { margin-bottom: .7em; }
.lib-essay-body .callout-tip, .lib-essay-body .callout-success { --c: var(--green); }
.lib-essay-body .callout-warning, .lib-essay-body .callout-question { --c: var(--gold); }
.lib-essay-body .callout-danger, .lib-essay-body .callout-failure, .lib-essay-body .callout-bug { --c: var(--red); }
.lib-essay-body .callout-example, .lib-essay-body .callout-abstract { --c: var(--violet); }
.lib-essay-body .callout-quote { --c: var(--text3); }
.lib-essay-body .fact.is-pending { font-size: 1em; color: var(--text3); border-bottom-style: dashed; }
.lib-essay-body .board td.center, .lib-essay-body .board th.center { text-align: center; }

/* ── the essay editor ──
Write, Split or Preview: Obsidian's split, the Markdown beside the page it
makes. The preview is the listing's own essay styles, so what it shows is
what will be published. A file dragged over the panes lights their edge. */
.lib-essay-field { gap: 8px; }
.lib-md-editor { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.lib-md-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; }
.lib-md-modes { display: inline-flex; border: 1px solid var(--border2); border-radius: 6px; overflow: hidden; }
#v-library .lib-md-mode { all: unset; cursor: pointer; padding: 4px 12px; font-family: var(--mono); font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text2); }
#v-library .lib-md-mode + .lib-md-mode { border-left: 1px solid var(--border2); }
#v-library .lib-md-mode.is-on { background: var(--accent-soft, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--accent); }
#v-library .lib-md-mode:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.lib-md-files { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px; }
.lib-md-status { font-family: var(--mono); font-size: .64rem; color: var(--text3); }
.lib-md-panes { display: grid; gap: 0; height: min(64vh, 620px); min-height: 280px; border: 1px solid var(--border2); border-radius: 8px; overflow: hidden; resize: vertical; transition: border-color var(--transition-base); }
.lib-md-panes.is-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.lib-md-panes.is-write, .lib-md-panes.is-preview { grid-template-columns: minmax(0, 1fr); }
.lib-md-panes.is-write .lib-md-preview, .lib-md-panes.is-preview .lib-md-input { display: none; }
.lib-md-guide { display: none; overflow: auto; padding: 16px 22px 28px; max-width: none; font-size: 1rem; }
.lib-md-panes.has-guide .lib-md-preview { display: none; }
.lib-md-panes.has-guide .lib-md-guide { display: block; }
.lib-md-guide .essay-example { grid-template-columns: minmax(0, 1fr); }
.lib-md-guide .essay-example > .essay-example-src { border-right: 0; border-bottom: 1px solid var(--border); }
#v-library .lib-md-modes .lib-md-guide-btn { border-left: 1px solid var(--border2); }
.lib-md-panes.is-drop { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
#v-library .lib-md-input { box-sizing: border-box; width: 100%; height: 100%; min-height: 0; margin: 0; padding: 14px 16px; border: 0; border-radius: 0; resize: none; font-family: var(--mono) !important; font-size: .82rem; line-height: 1.65; tab-size: 2; background: var(--lib-well); color: var(--text); }
.lib-md-panes.is-split .lib-md-input { border-right: 1px solid var(--border2); }
.lib-md-preview { overflow: auto; padding: 16px 22px 28px; max-width: none; font-size: 1.06rem; background: var(--bg2, transparent); }
.lib-md-preview > p:first-child::first-letter { float: none; font-size: inherit; padding: 0; }
.lib-md-warnings { margin: 0; padding: 8px 12px 8px 28px; border: 1px solid color-mix(in srgb, var(--gold) 35%, var(--border)); border-radius: 6px; font-size: .78rem; line-height: 1.5; color: var(--text2); }
.lib-md-warnings[hidden] { display: none; }

@media (max-width: 1100px) {
.lib-mast.has-frontis { grid-template-columns: minmax(0, 1fr); }
Expand All @@ -566,6 +631,10 @@
.lib-essay-grid { grid-template-columns: minmax(0, 1fr); gap: 20px; }
.lib-essay-toc { position: static; }
.lib-essay-toc ol { flex-direction: row; flex-wrap: wrap; gap: 6px 18px; }
.lib-md-panes.is-split { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }
.lib-md-panes.is-split .lib-md-input { border-right: 0; border-bottom: 1px solid var(--border2); }
.lib-essay-body .essay-example { grid-template-columns: minmax(0, 1fr); }
.lib-essay-body .essay-example > .essay-example-src { border-right: 0; border-bottom: 1px solid var(--border); }
}

@media (max-width: 900px) {
Expand Down
Loading
Loading