From 04cf2caa4392eaadd8775d13be5f846eaa50ab2f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 22:11:59 +0000 Subject: [PATCH] docs: README in example-led (Julia-package) form + new EXPLAINME deep-dive Reshape README.adoc into the canonical Julia-package idiom: clean badges, one-line pitch, a concrete runnable example up front, install, differentiators, implemented-vs-planned features, docs links, where-it-fits (poly-ssg/ddraig/boj), license. Lead with the real positioning (accessibility-first, metadata-driven; verified+agent-native as trajectory per ADR-0001) instead of the generic 'pure functional SSG'. Add EXPLAINME.adoc: developer deep-dive of the build pipeline, module map, accessibility invariants, content/metadata model, known gaps, and the ADR-0001 direction. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WN6nPWWe4dvXfoqBG6auWN --- EXPLAINME.adoc | 146 +++++++++++++++++++++++++++++++++++ README.adoc | 203 +++++++++++++++++++++++++++++-------------------- 2 files changed, 267 insertions(+), 82 deletions(-) create mode 100644 EXPLAINME.adoc diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc new file mode 100644 index 0000000..39922b4 --- /dev/null +++ b/EXPLAINME.adoc @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += EXPLAINME — casket architecture & invariants +:toc: left +:toclevels: 3 + +A developer deep-dive: how casket is built, what the Gnosis layer is, the +invariants it upholds, the known gaps, and the direction of travel. For the +user-facing overview see link:README.adoc[README]; for the engine decision see +link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]. + +== One paragraph + +casket is a Pandoc-based static site generator with a bespoke *Gnosis* layer. +The built executable (`CasketGnosis.hs`) discovers Markdown, runs each file +through Gnosis (metadata-driven substitution) and then Pandoc (Markdown → HTML), +fills a file-based template, and writes an accessible HTML tree plus collections, +tag pages, a sitemap and an Atom feed. Accessibility is enforced structurally, +not linted after the fact. + +== Build pipeline + +`buildSiteWithGnosis` (in `src/CasketGnosis.hs`) orchestrates one build: + +[source] +---- +loadAll6SCM .machine_readable ; metadata context (Gnosis.SixSCM) +loadSiteConfig (site.conf) ; {{site.*}} vars +findMarkdown ; recursive .md/.markdown discovery + └─ per page: renderPage + parseFrontmatter ; ----fenced key: value (tolerates SPDX comment) + mergeFrontmatter ctx fm ; front-matter overrides 6scm context + DAX.processTemplate ; {{#if}} / {{#for}} over the Markdown body + Render.renderWithBadges ; (:placeholder) → value / Shields badge + markdownToHtml (Pandoc) ; body + TOC, syntax highlighting + addThScope ; inject scope="col" on (WCAG 1.3.1) +filter drafts (unless --drafts) +buildNav ; from site.conf `nav` or derived +per page: writePage → loadLayout → fillTemplate → write +writeCollections ; date-sorted index per directory (≥2 dated) +writeTagPages ; /tags// + /tags/ index +writeSitemap ; writeFeed ; from site.baseurl +copyAssets ; writeHighlightCss ; copyPublic +---- + +Template substitution order in `fillTemplate` matters: `{{site.*}}` → +front-matter keys → standard vars (`{{nav}}`, `{{toc}}`, `{{title}}`, …) → +`{{content}}` *last*, so rendered body HTML is never re-processed. + +== Module map (`src/`) + +[cols="1,3"] +|=== +| Module | Responsibility + +| `CasketGnosis.hs` (`Main`) +| Build orchestration, CLI (`build` / `clean` / `--version`), frontmatter, + templates/partials, collections, tags, sitemap, Atom feed, assets. + +| `Gnosis.Types` +| `FlexiText` (visual + alt-text) and `Context`. Smart constructor `mkFlexiText` + returns `Left` on empty alt-text — the Tri-Guard accessibility primitive. + +| `Gnosis.Render` +| `(:placeholder)` rendering with optional `| filter` chains; `PlainText` or + `ShieldsIOBadge` mode. `renderWithBadges` is used in the build. + +| `Gnosis.DAX` +| Data-eXpression layer: `{{#if cond}}`, `{{#for x in list}}`, and value filters + (`thousands-separator`, `uppercase`, …). + +| `Gnosis.SixSCM` +| Loads the six descriptive metadata files into a `Context`. + *Known gap:* currently reads `.machine_readable/STATE.scm` … (the old flat + `.scm` layout) — it does **not** yet read `.machine_readable/descriptiles/*.a2ml`. + +| `Gnosis.SExp` +| Recursive-descent S-expression parser (for the legacy `.scm` metadata). + +| `Gnosis.Paxos` +| Timestamp-ballot last-write-wins merge for concurrent `STATE` updates by + automation. Aspirational; not on the render path. + +| `Casket.hs` +| *Legacy.* A standalone hand-rolled "HakyllLite" (own Markdown parser, no + Pandoc). **Not** in the cabal's build — `CasketGnosis.hs` is the real binary. + Slated for removal/relocation to `examples/`. +|=== + +== Invariants + +* *Alt-text is non-optional.* `FlexiText` pairs a visual with mandatory alt-text; + `mkFlexiText "" ""`-style empty alt-text is a `Left`, not output. Generated + badges cannot ship without alt text. +* *Tables are screen-reader navigable.* `addThScope` adds `scope="col"` to every + `` Pandoc emits that lacks one (never double-adds). +* *The default page is accessible.* The built-in fallback template ships + ``, a skip link, labelled `header`/`nav`/`main#main`/`footer` + landmarks, viewport meta, and `color-scheme: light dark`. +* *Body HTML is substituted last* so it is never re-templated. +* *Drafts are excluded* unless `--drafts` / `CASKET_DRAFTS=1`. + +== Content & metadata model + +* *Front-matter:* `---`-fenced `key: value`, tolerant of a leading SPDX HTML + comment. Arbitrary keys become `{{key}}` template vars. +* *Site config:* `site.conf`, simple `key = value`, exposed as `{{site.*}}`. +* *Gnosis context (descriptiles):* the six `.a2ml` files under + `.machine_readable/descriptiles/` are intended to feed `(:placeholder)` / + DAX. Wiring `Gnosis.SixSCM` to read `descriptiles/*.a2ml` (an `a2ml` reader) + is the first migration step (see below). + +== Known gaps (current state) + +* *Build is red on `main`* — `CasketGnosis.hs` imports + `Text.Pandoc.Options(HighlightMethod)`, which the resolved Pandoc no longer + exports. Retired by the ADR-0001 engine change, not patched in place. +* *Gnosis metadata is dormant against this repo* — `SixSCM` reads `.scm`, but the + metadata now lives at `descriptiles/*.a2ml`; until wired, `(:placeholder)` + resolves only against front-matter. +* *Two codebases* — `Casket.hs` (legacy) vs `CasketGnosis.hs` (built); only the + latter ships. +* *No incremental build, no dev server* — full rebuild each run. + +== Direction (ADR-0001) + +* *Demote Pandoc* to a one-shot authoring-time importer (Markdown → Djot). +* *Djot* as the canonical prose syntax (clean, attribute-bearing, far easier to + parse — and to verify — than CommonMark): `jotdown` (Rust) now, a verified + Zig/Idris2 Djot core later behind a stable AST. +* *a2ml* as the typed-data face (Djot replaces CommonMark, not a2ml). +* *boj cartridge* — consume boj's verified MCP/ABI/distribution/credential + isolation instead of building an MCP server. +* *Layered proofs* — Haskell carries cheap invariants; Idris2/Agda carry the + load-bearing ones. The accessibility invariant is an `echo-types` + `Echo f y := Σ (x:A),(f x ≡ y)` — `FlexiText` is its unverified shadow, and + ddraig owns the native proof. + +== Extending casket + +* *Templates:* drop `templates/.html`; select per page via `layout:` + front-matter; partials via `{{> name}}` from `templates/partials/`. +* *Theme:* override `assets/style.css`; highlight CSS is emitted to + `/assets/highlight.css`. +* *Verbatim files:* anything under `/public/` is copied to the output root + (`CNAME`, `.well-known/`, `robots.txt`, …). diff --git a/README.adoc b/README.adoc index c417381..93c3f12 100644 --- a/README.adoc +++ b/README.adoc @@ -2,128 +2,167 @@ // Copyright (c) Jonathan D.A. Jewell // SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -= casket-ssg += casket image:https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?logo=github[Sponsor,link="https://github.com/sponsors/hyperpolymath"] image:https://img.shields.io/badge/OpenSSF-Best_Practices-green?logo=opensourcesecurity[OpenSSF Best Practices,link="https://www.bestpractices.dev/en/projects/new?repo_url=https://github.com/hyperpolymath/casket-ssg"] -image:https://img.shields.io/badge/License-MPL_2.0--1.0-blue.svg[License: MPL-2.0,link="https://github.com/hyperpolymath/palimpsest-license"] -image:https://api.thegreenwebfoundation.org/greencheckimage/github.com[Green Web,link="https://www.thegreenwebfoundation.org/green-web-check/?url=github.com"] +image:https://img.shields.io/badge/License-MPL_2.0-blue.svg[License: MPL-2.0,link="https://github.com/hyperpolymath/casket-ssg/blob/main/LICENSE"] +image:https://img.shields.io/badge/a11y-WCAG--aware_by_default-success[Accessibility] +image:https://img.shields.io/badge/part_of-poly--ssg-indigo[poly-ssg,link="https://github.com/hyperpolymath/poly-ssg"] +image:https://img.shields.io/badge/RSR-compliant-gold[RSR Compliant,link="https://github.com/hyperpolymath/rhodium-standard-repositories"] - - - -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - - -:toc: auto +:toc: macro :toclevels: 2 -image:https://img.shields.io/badge/RSR-compliant-gold[RSR Compliant,link=https://github.com/hyperpolymath/rhodium-standard-repositories] - -**A Pandoc-based static site generator in Haskell, with an accessibility-first, metadata-driven rendering layer (Gnosis).** +**An accessibility-first, metadata-driven static site generator.** Markdown in, +an accessible website out — WCAG-aware output _by default_, plus a metadata layer +(*Gnosis*) that renders pages from typed project data. The Haskell member of +https://github.com/hyperpolymath/poly-ssg[poly-ssg]. [NOTE] ==== *Status: alpha.* The `build` / `clean` pipeline works and generates real sites. -Several items below are *planned, not yet implemented* — they are called out -explicitly so nothing here over-promises. The content engine's direction -(demote Pandoc; adopt a Djot prose core + `a2ml` typed content) is recorded in -link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]. +Items marked _Planned_ below are not yet implemented. The engine's direction — +demote Pandoc, adopt a Djot prose core + `a2ml` typed content on the +https://github.com/hyperpolymath/boj-server[boj] substrate — is recorded in +link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]. Architecture & +invariants: link:EXPLAINME.adoc[EXPLAINME]. ==== -== Who Is This For? +toc::[] -* **Haskell developers** who want a small, scriptable content-site generator -* **Accessibility-conscious authors** who want WCAG-aware output by default -* **poly-ssg / estate** users who want metadata-driven pages (the Gnosis layer) +== Example -== Why casket-ssg? +A minimal site is a tree of Markdown with front-matter: -=== Accessibility by construction +[source] +---- +content/ +├── index.md +└── posts/ + └── 2026-06-23-hello.md +---- + +[source,markdown] +---- +--- +title: Hello, world +date: 2026-06-23 +tags: [intro] +description: A first page built with casket. +--- +# Hello + +Markdown with **rich** formatting, tables, footnotes and highlighted code. +---- + +Build it: + +[source,bash] +---- +casket-ssg build ./content ./_site +---- -The built-in default theme is accessible by default — ``, a skip link, -labelled `header`/`nav`/`main`/`footer` landmarks, viewport meta, and a light/dark -`color-scheme`. casket also rewrites Pandoc's table headers to add `scope="col"` -(WCAG 1.3.1), and the Gnosis `FlexiText` type makes *empty alt-text a constructor -error* — generated badges cannot ship without alt text. +You get an *accessible* `_site/` with no configuration: -=== Metadata-driven rendering (Gnosis) +* `index.html` and `posts/2026-06-23-hello/index.html` rendered through an + accessible default theme (skip link, labelled landmarks, light/dark + `color-scheme`), with table headers auto-scoped for WCAG 1.3.1; +* a date-sorted collection index for `posts/`, and per-tag pages under `/tags/`; +* `sitemap.xml` and an Atom `feed.xml`. -An optional layer runs over the Markdown before Pandoc: `(:placeholder)` -substitution from a metadata context, `{{#if}}` / `{{#for}}` conditionals and loops -(DAX), and Shields.io badges generated from typed metadata. +No Node, no npm, no required config file. -=== Pandoc-based content (today) +== Installation -Rich Markdown via Pandoc — anchors, pipe tables, fenced code with syntax -highlighting, footnotes, task lists, strikethrough, smart punctuation. See -link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001] for the planned move to -a Djot core with Pandoc relegated to a one-shot importer. +casket is not yet on Hackage — build from source (GHC 9.4+, Cabal 3 or Stack 2): -== Quick Start +[source,bash] +---- +git clone https://github.com/hyperpolymath/casket-ssg +cd casket-ssg +stack build # or: cabal build +stack run casket-ssg -- --version # or put the built binary on PATH +---- -casket-ssg is not yet published to Hackage; build it from source. +CLI: [source,bash] ---- -# Build (Stack or Cabal) -stack build # or: cabal build +casket-ssg build [output-dir] # default output: _site +casket-ssg build --drafts # include draft: true pages (or CASKET_DRAFTS=1) +casket-ssg build --no-clean-urls # emit foo.html, not foo/index.html +casket-ssg clean [output-dir] +casket-ssg --version +---- -# Build a site: build [output-dir] (default output: _site) -stack run casket-ssg -- build ./content ./_site -# ...or, with the built binary on PATH: -casket-ssg build ./content ./_site +== What makes it different -# Include drafts (or set CASKET_DRAFTS=1) -casket-ssg build --drafts ./content +Accessibility by construction:: +The default theme is accessible by default, casket rewrites Pandoc table headers +to add `scope="col"`, and the Gnosis `FlexiText` type makes *empty alt-text a +constructor error* — generated badges cannot ship without alt text. Accessibility +is a property of the engine, not a checklist for the author. -# Emit foo.html instead of foo/index.html -casket-ssg build --no-clean-urls ./content +Metadata-driven (Gnosis):: +An optional layer renders pages from a typed metadata context: `(:placeholder)` +substitution, `{{#if}}` / `{{#for}}` conditionals and loops (DAX), and Shields.io +badges generated from typed values — sourced from the repo's +`.machine_readable/descriptiles/` data. -# Remove an output directory -casket-ssg clean ./_site +Verified & agent-native — the trajectory:: +casket is heading toward typed `a2ml` content (build fails on malformed data), a +formally-verifiable Djot core, and a verified agent surface as a +https://github.com/hyperpolymath/boj-server[boj] cartridge rather than an embedded +MCP server. See link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]. -# Version -casket-ssg --version ----- +Permissively licensed:: +*MPL-2.0* — build proprietary or differently-licensed sites on top, unlike +copyleft alternatives. == Features -*Implemented:* +*Implemented* -* **Recursive multi-page build** mirroring the source tree into the output -* **Frontmatter** — `title`, `date`, `description`, `layout`, `draft`, `tags`, - `slug`, plus arbitrary keys exposed as `{{key}}` template vars -* **Rich Markdown via Pandoc** — anchors, pipe tables, fenced/highlighted code, +* Recursive multi-page build mirroring the source tree +* Front-matter: `title`, `date`, `description`, `layout`, `draft`, `tags`, + `slug`, plus arbitrary keys as `{{key}}` template vars +* Rich Markdown via Pandoc — anchors, pipe tables, fenced/highlighted code, footnotes, task lists, strikethrough, smart punctuation -* **File templates** in `templates/` with per-page `layout` and partials - (`{{> name}}`), plus `{{nav}}`, `{{toc}}` -* **Site config** (`site.conf`, simple `key = value`) exposed as `{{site.*}}` -* **Collections** (a directory index sorted by date) and **per-tag pages** - (`/tags//`) -* **`sitemap.xml`** and an Atom **`feed.xml`** -* **Accessible default theme** + automatic `scope="col"` on table headers -* **Gnosis layer** — `(:placeholder)` rendering, `{{#if}}` / `{{#for}}` (DAX), - and `FlexiText` (alt-text-required) badge generation -* **Clean URLs** (`foo.md` → `foo/index.html`), toggleable with `--no-clean-urls` - -*Planned / not yet implemented* (do not assume these work today): +* File templates with per-page `layout`, partials (`{{> name}}`), `{{nav}}`, + `{{toc}}`, `{{site.*}}` (from `site.conf`) +* Collections (date-sorted directory index) and per-tag pages (`/tags//`) +* `sitemap.xml` + Atom `feed.xml` +* Accessible default theme + automatic `scope="col"` on table headers +* Gnosis layer — `(:placeholder)`, `{{#if}}`/`{{#for}}` (DAX), `FlexiText` badges +* Clean URLs, toggleable + +*Planned* (do not assume these work today — see ADR-0001) * `init` scaffolding and a `serve` / live-reload dev server -* Incremental rebuilds (the build is currently a full rebuild) -* Asset fingerprinting / hash-based cache-busting -* Schema-enforced frontmatter and compile-time template validation -* `a2ml`-native typed content and the Djot core (see - link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]) +* Incremental rebuilds; asset fingerprinting +* `a2ml`-native typed content + the Djot core; Pandoc relegated to an importer +* `boj` cartridge registration; `llms.txt` / per-page machine views -== Requirements +== Documentation -* GHC 9.4+ (CI builds with GHC 9.4.8) and Cabal 3.0+ or Stack 2.0+ -* Pandoc (pulled in as a library dependency) +* link:EXPLAINME.adoc[EXPLAINME] — architecture & invariants (developer deep-dive) +* link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001] — content-engine direction +* link:CONTRIBUTING.adoc[Contributing] · link:GOVERNANCE.adoc[Governance] -== Part of poly-ssg +== Where it fits + +* https://github.com/hyperpolymath/poly-ssg[*poly-ssg*] — the family of + language-native SSGs casket belongs to, unified via MCP. +* https://github.com/hyperpolymath/ddraig-ssg[*ddraig*] — the dependently-typed + (Idris2) sibling; casket's "proven twin" (casket validates, ddraig proves). +* https://github.com/hyperpolymath/boj-server[*boj-server*] — the estate's + verified MCP/capability server casket plugs into as a cartridge. + +== Requirements -casket-ssg is part of the https://github.com/hyperpolymath/poly-ssg[poly-ssg] family of language-native static site generators, unified through https://github.com/hyperpolymath/poly-ssg-mcp[MCP integration]. +* GHC 9.4+ (CI builds with 9.4.8), Cabal 3.0+ or Stack 2.0+ +* Pandoc (current library dependency; see ADR-0001 for the planned move off it) == License -MPL-2.0 +Code: *MPL-2.0* (link:LICENSE[LICENSE]). Prose documentation: `CC-BY-SA-4.0`.