Skip to content

Commit e597ff1

Browse files
docs: README in example-led (Julia-package) form + EXPLAINME deep-dive (#45)
## What The core documentation upgrade you asked for. **`README.adoc`** — reshaped into the canonical **Julia-package idiom** (the "Axiom.jl shape"): clean badges → one-line pitch → **a concrete runnable example up front** (source tree → `casket-ssg build` → accessible `_site/`) → install → differentiators → implemented-vs-planned features → docs links → where-it-fits → license. It now leads with the real positioning — *accessibility-first, metadata-driven; verified + agent-native as the trajectory* — instead of the generic "pure functional SSG in Haskell". Stays honest (the truthed implemented/planned split is preserved). **`EXPLAINME.adoc`** (new) — developer deep-dive: build pipeline, module map (`CasketGnosis` + the `Gnosis.*` set, and the legacy `Casket.hs`), accessibility invariants (FlexiText/Tri-Guard, `addThScope`, accessible default theme), content/metadata model, **known gaps** (Pandoc build break; `SixSCM` still on `.scm` not `descriptiles/*.a2ml`), and the ADR-0001 direction. ## Notes - Docs-only; inherits the two standing `main` reds (Pandoc build; Hypatia findings) — neither introduced here. - README "Axiom.jl shape" = the Julia-package README idiom (I couldn't find a specific `Axiom.jl` to mirror exactly; happy to re-shape if a particular one was meant). - Repo **description** and **topics** can't be set via the available tooling — proposed separately for you to apply in Settings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WN6nPWWe4dvXfoqBG6auWN --- _Generated by [Claude Code](https://claude.ai/code/session_01WN6nPWWe4dvXfoqBG6auWN)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9a3d923 commit e597ff1

2 files changed

Lines changed: 267 additions & 82 deletions

File tree

EXPLAINME.adoc

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= EXPLAINME — casket architecture & invariants
4+
:toc: left
5+
:toclevels: 3
6+
7+
A developer deep-dive: how casket is built, what the Gnosis layer is, the
8+
invariants it upholds, the known gaps, and the direction of travel. For the
9+
user-facing overview see link:README.adoc[README]; for the engine decision see
10+
link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001].
11+
12+
== One paragraph
13+
14+
casket is a Pandoc-based static site generator with a bespoke *Gnosis* layer.
15+
The built executable (`CasketGnosis.hs`) discovers Markdown, runs each file
16+
through Gnosis (metadata-driven substitution) and then Pandoc (Markdown → HTML),
17+
fills a file-based template, and writes an accessible HTML tree plus collections,
18+
tag pages, a sitemap and an Atom feed. Accessibility is enforced structurally,
19+
not linted after the fact.
20+
21+
== Build pipeline
22+
23+
`buildSiteWithGnosis` (in `src/CasketGnosis.hs`) orchestrates one build:
24+
25+
[source]
26+
----
27+
loadAll6SCM .machine_readable ; metadata context (Gnosis.SixSCM)
28+
loadSiteConfig (site.conf) ; {{site.*}} vars
29+
findMarkdown <input> ; recursive .md/.markdown discovery
30+
└─ per page: renderPage
31+
parseFrontmatter ; ----fenced key: value (tolerates SPDX comment)
32+
mergeFrontmatter ctx fm ; front-matter overrides 6scm context
33+
DAX.processTemplate ; {{#if}} / {{#for}} over the Markdown body
34+
Render.renderWithBadges ; (:placeholder) → value / Shields badge
35+
markdownToHtml (Pandoc) ; body + TOC, syntax highlighting
36+
addThScope ; inject scope="col" on <th> (WCAG 1.3.1)
37+
filter drafts (unless --drafts)
38+
buildNav ; from site.conf `nav` or derived
39+
per page: writePage → loadLayout → fillTemplate → write
40+
writeCollections ; date-sorted index per directory (≥2 dated)
41+
writeTagPages ; /tags/<tag>/ + /tags/ index
42+
writeSitemap ; writeFeed ; from site.baseurl
43+
copyAssets ; writeHighlightCss ; copyPublic
44+
----
45+
46+
Template substitution order in `fillTemplate` matters: `{{site.*}}` →
47+
front-matter keys → standard vars (`{{nav}}`, `{{toc}}`, `{{title}}`, …) →
48+
`{{content}}` *last*, so rendered body HTML is never re-processed.
49+
50+
== Module map (`src/`)
51+
52+
[cols="1,3"]
53+
|===
54+
| Module | Responsibility
55+
56+
| `CasketGnosis.hs` (`Main`)
57+
| Build orchestration, CLI (`build` / `clean` / `--version`), frontmatter,
58+
templates/partials, collections, tags, sitemap, Atom feed, assets.
59+
60+
| `Gnosis.Types`
61+
| `FlexiText` (visual + alt-text) and `Context`. Smart constructor `mkFlexiText`
62+
returns `Left` on empty alt-text — the Tri-Guard accessibility primitive.
63+
64+
| `Gnosis.Render`
65+
| `(:placeholder)` rendering with optional `| filter` chains; `PlainText` or
66+
`ShieldsIOBadge` mode. `renderWithBadges` is used in the build.
67+
68+
| `Gnosis.DAX`
69+
| Data-eXpression layer: `{{#if cond}}`, `{{#for x in list}}`, and value filters
70+
(`thousands-separator`, `uppercase`, …).
71+
72+
| `Gnosis.SixSCM`
73+
| Loads the six descriptive metadata files into a `Context`.
74+
*Known gap:* currently reads `.machine_readable/STATE.scm` … (the old flat
75+
`.scm` layout) — it does **not** yet read `.machine_readable/descriptiles/*.a2ml`.
76+
77+
| `Gnosis.SExp`
78+
| Recursive-descent S-expression parser (for the legacy `.scm` metadata).
79+
80+
| `Gnosis.Paxos`
81+
| Timestamp-ballot last-write-wins merge for concurrent `STATE` updates by
82+
automation. Aspirational; not on the render path.
83+
84+
| `Casket.hs`
85+
| *Legacy.* A standalone hand-rolled "HakyllLite" (own Markdown parser, no
86+
Pandoc). **Not** in the cabal's build — `CasketGnosis.hs` is the real binary.
87+
Slated for removal/relocation to `examples/`.
88+
|===
89+
90+
== Invariants
91+
92+
* *Alt-text is non-optional.* `FlexiText` pairs a visual with mandatory alt-text;
93+
`mkFlexiText "" ""`-style empty alt-text is a `Left`, not output. Generated
94+
badges cannot ship without alt text.
95+
* *Tables are screen-reader navigable.* `addThScope` adds `scope="col"` to every
96+
`<th>` Pandoc emits that lacks one (never double-adds).
97+
* *The default page is accessible.* The built-in fallback template ships
98+
`<html lang>`, a skip link, labelled `header`/`nav`/`main#main`/`footer`
99+
landmarks, viewport meta, and `color-scheme: light dark`.
100+
* *Body HTML is substituted last* so it is never re-templated.
101+
* *Drafts are excluded* unless `--drafts` / `CASKET_DRAFTS=1`.
102+
103+
== Content & metadata model
104+
105+
* *Front-matter:* `---`-fenced `key: value`, tolerant of a leading SPDX HTML
106+
comment. Arbitrary keys become `{{key}}` template vars.
107+
* *Site config:* `site.conf`, simple `key = value`, exposed as `{{site.*}}`.
108+
* *Gnosis context (descriptiles):* the six `.a2ml` files under
109+
`.machine_readable/descriptiles/` are intended to feed `(:placeholder)` /
110+
DAX. Wiring `Gnosis.SixSCM` to read `descriptiles/*.a2ml` (an `a2ml` reader)
111+
is the first migration step (see below).
112+
113+
== Known gaps (current state)
114+
115+
* *Build is red on `main`* — `CasketGnosis.hs` imports
116+
`Text.Pandoc.Options(HighlightMethod)`, which the resolved Pandoc no longer
117+
exports. Retired by the ADR-0001 engine change, not patched in place.
118+
* *Gnosis metadata is dormant against this repo* — `SixSCM` reads `.scm`, but the
119+
metadata now lives at `descriptiles/*.a2ml`; until wired, `(:placeholder)`
120+
resolves only against front-matter.
121+
* *Two codebases* — `Casket.hs` (legacy) vs `CasketGnosis.hs` (built); only the
122+
latter ships.
123+
* *No incremental build, no dev server* — full rebuild each run.
124+
125+
== Direction (ADR-0001)
126+
127+
* *Demote Pandoc* to a one-shot authoring-time importer (Markdown → Djot).
128+
* *Djot* as the canonical prose syntax (clean, attribute-bearing, far easier to
129+
parse — and to verify — than CommonMark): `jotdown` (Rust) now, a verified
130+
Zig/Idris2 Djot core later behind a stable AST.
131+
* *a2ml* as the typed-data face (Djot replaces CommonMark, not a2ml).
132+
* *boj cartridge* — consume boj's verified MCP/ABI/distribution/credential
133+
isolation instead of building an MCP server.
134+
* *Layered proofs* — Haskell carries cheap invariants; Idris2/Agda carry the
135+
load-bearing ones. The accessibility invariant is an `echo-types`
136+
`Echo f y := Σ (x:A),(f x ≡ y)` — `FlexiText` is its unverified shadow, and
137+
ddraig owns the native proof.
138+
139+
== Extending casket
140+
141+
* *Templates:* drop `templates/<layout>.html`; select per page via `layout:`
142+
front-matter; partials via `{{> name}}` from `templates/partials/`.
143+
* *Theme:* override `assets/style.css`; highlight CSS is emitted to
144+
`/assets/highlight.css`.
145+
* *Verbatim files:* anything under `<input>/public/` is copied to the output root
146+
(`CNAME`, `.well-known/`, `robots.txt`, …).

README.adoc

Lines changed: 121 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,128 +2,167 @@
22
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
44

5-
= casket-ssg
5+
= casket
66
image:https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?logo=github[Sponsor,link="https://github.com/sponsors/hyperpolymath"]
77
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"]
8-
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"]
9-
image:https://api.thegreenwebfoundation.org/greencheckimage/github.com[Green Web,link="https://www.thegreenwebfoundation.org/green-web-check/?url=github.com"]
8+
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"]
9+
image:https://img.shields.io/badge/a11y-WCAG--aware_by_default-success[Accessibility]
10+
image:https://img.shields.io/badge/part_of-poly--ssg-indigo[poly-ssg,link="https://github.com/hyperpolymath/poly-ssg"]
11+
image:https://img.shields.io/badge/RSR-compliant-gold[RSR Compliant,link="https://github.com/hyperpolymath/rhodium-standard-repositories"]
1012

11-
12-
13-
14-
image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"]
15-
16-
17-
:toc: auto
13+
:toc: macro
1814
:toclevels: 2
1915

20-
image:https://img.shields.io/badge/RSR-compliant-gold[RSR Compliant,link=https://github.com/hyperpolymath/rhodium-standard-repositories]
21-
22-
**A Pandoc-based static site generator in Haskell, with an accessibility-first, metadata-driven rendering layer (Gnosis).**
16+
**An accessibility-first, metadata-driven static site generator.** Markdown in,
17+
an accessible website out — WCAG-aware output _by default_, plus a metadata layer
18+
(*Gnosis*) that renders pages from typed project data. The Haskell member of
19+
https://github.com/hyperpolymath/poly-ssg[poly-ssg].
2320

2421
[NOTE]
2522
====
2623
*Status: alpha.* The `build` / `clean` pipeline works and generates real sites.
27-
Several items below are *planned, not yet implemented* — they are called out
28-
explicitly so nothing here over-promises. The content engine's direction
29-
(demote Pandoc; adopt a Djot prose core + `a2ml` typed content) is recorded in
30-
link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001].
24+
Items marked _Planned_ below are not yet implemented. The engine's direction —
25+
demote Pandoc, adopt a Djot prose core + `a2ml` typed content on the
26+
https://github.com/hyperpolymath/boj-server[boj] substrate — is recorded in
27+
link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001]. Architecture &
28+
invariants: link:EXPLAINME.adoc[EXPLAINME].
3129
====
3230

33-
== Who Is This For?
31+
toc::[]
3432

35-
* **Haskell developers** who want a small, scriptable content-site generator
36-
* **Accessibility-conscious authors** who want WCAG-aware output by default
37-
* **poly-ssg / estate** users who want metadata-driven pages (the Gnosis layer)
33+
== Example
3834

39-
== Why casket-ssg?
35+
A minimal site is a tree of Markdown with front-matter:
4036

41-
=== Accessibility by construction
37+
[source]
38+
----
39+
content/
40+
├── index.md
41+
└── posts/
42+
└── 2026-06-23-hello.md
43+
----
44+
45+
[source,markdown]
46+
----
47+
---
48+
title: Hello, world
49+
date: 2026-06-23
50+
tags: [intro]
51+
description: A first page built with casket.
52+
---
53+
# Hello
54+
55+
Markdown with **rich** formatting, tables, footnotes and highlighted code.
56+
----
57+
58+
Build it:
59+
60+
[source,bash]
61+
----
62+
casket-ssg build ./content ./_site
63+
----
4264

43-
The built-in default theme is accessible by default — `<html lang>`, a skip link,
44-
labelled `header`/`nav`/`main`/`footer` landmarks, viewport meta, and a light/dark
45-
`color-scheme`. casket also rewrites Pandoc's table headers to add `scope="col"`
46-
(WCAG 1.3.1), and the Gnosis `FlexiText` type makes *empty alt-text a constructor
47-
error* — generated badges cannot ship without alt text.
65+
You get an *accessible* `_site/` with no configuration:
4866

49-
=== Metadata-driven rendering (Gnosis)
67+
* `index.html` and `posts/2026-06-23-hello/index.html` rendered through an
68+
accessible default theme (skip link, labelled landmarks, light/dark
69+
`color-scheme`), with table headers auto-scoped for WCAG 1.3.1;
70+
* a date-sorted collection index for `posts/`, and per-tag pages under `/tags/`;
71+
* `sitemap.xml` and an Atom `feed.xml`.
5072

51-
An optional layer runs over the Markdown before Pandoc: `(:placeholder)`
52-
substitution from a metadata context, `{{#if}}` / `{{#for}}` conditionals and loops
53-
(DAX), and Shields.io badges generated from typed metadata.
73+
No Node, no npm, no required config file.
5474

55-
=== Pandoc-based content (today)
75+
== Installation
5676

57-
Rich Markdown via Pandoc — anchors, pipe tables, fenced code with syntax
58-
highlighting, footnotes, task lists, strikethrough, smart punctuation. See
59-
link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001] for the planned move to
60-
a Djot core with Pandoc relegated to a one-shot importer.
77+
casket is not yet on Hackage — build from source (GHC 9.4+, Cabal 3 or Stack 2):
6178

62-
== Quick Start
79+
[source,bash]
80+
----
81+
git clone https://github.com/hyperpolymath/casket-ssg
82+
cd casket-ssg
83+
stack build # or: cabal build
84+
stack run casket-ssg -- --version # or put the built binary on PATH
85+
----
6386

64-
casket-ssg is not yet published to Hackage; build it from source.
87+
CLI:
6588

6689
[source,bash]
6790
----
68-
# Build (Stack or Cabal)
69-
stack build # or: cabal build
91+
casket-ssg build <input-dir> [output-dir] # default output: _site
92+
casket-ssg build --drafts <input-dir> # include draft: true pages (or CASKET_DRAFTS=1)
93+
casket-ssg build --no-clean-urls <input-dir> # emit foo.html, not foo/index.html
94+
casket-ssg clean [output-dir]
95+
casket-ssg --version
96+
----
7097

71-
# Build a site: build <input-dir> [output-dir] (default output: _site)
72-
stack run casket-ssg -- build ./content ./_site
73-
# ...or, with the built binary on PATH:
74-
casket-ssg build ./content ./_site
98+
== What makes it different
7599

76-
# Include drafts (or set CASKET_DRAFTS=1)
77-
casket-ssg build --drafts ./content
100+
Accessibility by construction::
101+
The default theme is accessible by default, casket rewrites Pandoc table headers
102+
to add `scope="col"`, and the Gnosis `FlexiText` type makes *empty alt-text a
103+
constructor error* — generated badges cannot ship without alt text. Accessibility
104+
is a property of the engine, not a checklist for the author.
78105

79-
# Emit foo.html instead of foo/index.html
80-
casket-ssg build --no-clean-urls ./content
106+
Metadata-driven (Gnosis)::
107+
An optional layer renders pages from a typed metadata context: `(:placeholder)`
108+
substitution, `{{#if}}` / `{{#for}}` conditionals and loops (DAX), and Shields.io
109+
badges generated from typed values — sourced from the repo's
110+
`.machine_readable/descriptiles/` data.
81111

82-
# Remove an output directory
83-
casket-ssg clean ./_site
112+
Verified & agent-native — the trajectory::
113+
casket is heading toward typed `a2ml` content (build fails on malformed data), a
114+
formally-verifiable Djot core, and a verified agent surface as a
115+
https://github.com/hyperpolymath/boj-server[boj] cartridge rather than an embedded
116+
MCP server. See link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001].
84117

85-
# Version
86-
casket-ssg --version
87-
----
118+
Permissively licensed::
119+
*MPL-2.0* — build proprietary or differently-licensed sites on top, unlike
120+
copyleft alternatives.
88121

89122
== Features
90123

91-
*Implemented:*
124+
*Implemented*
92125

93-
* **Recursive multi-page build** mirroring the source tree into the output
94-
* **Frontmatter** — `title`, `date`, `description`, `layout`, `draft`, `tags`,
95-
`slug`, plus arbitrary keys exposed as `{{key}}` template vars
96-
* **Rich Markdown via Pandoc** — anchors, pipe tables, fenced/highlighted code,
126+
* Recursive multi-page build mirroring the source tree
127+
* Front-matter: `title`, `date`, `description`, `layout`, `draft`, `tags`,
128+
`slug`, plus arbitrary keys as `{{key}}` template vars
129+
* Rich Markdown via Pandoc — anchors, pipe tables, fenced/highlighted code,
97130
footnotes, task lists, strikethrough, smart punctuation
98-
* **File templates** in `templates/` with per-page `layout` and partials
99-
(`{{> name}}`), plus `{{nav}}`, `{{toc}}`
100-
* **Site config** (`site.conf`, simple `key = value`) exposed as `{{site.*}}`
101-
* **Collections** (a directory index sorted by date) and **per-tag pages**
102-
(`/tags/<tag>/`)
103-
* **`sitemap.xml`** and an Atom **`feed.xml`**
104-
* **Accessible default theme** + automatic `scope="col"` on table headers
105-
* **Gnosis layer** — `(:placeholder)` rendering, `{{#if}}` / `{{#for}}` (DAX),
106-
and `FlexiText` (alt-text-required) badge generation
107-
* **Clean URLs** (`foo.md` → `foo/index.html`), toggleable with `--no-clean-urls`
108-
109-
*Planned / not yet implemented* (do not assume these work today):
131+
* File templates with per-page `layout`, partials (`{{> name}}`), `{{nav}}`,
132+
`{{toc}}`, `{{site.*}}` (from `site.conf`)
133+
* Collections (date-sorted directory index) and per-tag pages (`/tags/<tag>/`)
134+
* `sitemap.xml` + Atom `feed.xml`
135+
* Accessible default theme + automatic `scope="col"` on table headers
136+
* Gnosis layer — `(:placeholder)`, `{{#if}}`/`{{#for}}` (DAX), `FlexiText` badges
137+
* Clean URLs, toggleable
138+
139+
*Planned* (do not assume these work today — see ADR-0001)
110140

111141
* `init` scaffolding and a `serve` / live-reload dev server
112-
* Incremental rebuilds (the build is currently a full rebuild)
113-
* Asset fingerprinting / hash-based cache-busting
114-
* Schema-enforced frontmatter and compile-time template validation
115-
* `a2ml`-native typed content and the Djot core (see
116-
link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001])
142+
* Incremental rebuilds; asset fingerprinting
143+
* `a2ml`-native typed content + the Djot core; Pandoc relegated to an importer
144+
* `boj` cartridge registration; `llms.txt` / per-page machine views
117145

118-
== Requirements
146+
== Documentation
119147

120-
* GHC 9.4+ (CI builds with GHC 9.4.8) and Cabal 3.0+ or Stack 2.0+
121-
* Pandoc (pulled in as a library dependency)
148+
* link:EXPLAINME.adoc[EXPLAINME] — architecture & invariants (developer deep-dive)
149+
* link:docs/decisions/ADR-0001-content-engine.adoc[ADR-0001] — content-engine direction
150+
* link:CONTRIBUTING.adoc[Contributing] · link:GOVERNANCE.adoc[Governance]
122151

123-
== Part of poly-ssg
152+
== Where it fits
153+
154+
* https://github.com/hyperpolymath/poly-ssg[*poly-ssg*] — the family of
155+
language-native SSGs casket belongs to, unified via MCP.
156+
* https://github.com/hyperpolymath/ddraig-ssg[*ddraig*] — the dependently-typed
157+
(Idris2) sibling; casket's "proven twin" (casket validates, ddraig proves).
158+
* https://github.com/hyperpolymath/boj-server[*boj-server*] — the estate's
159+
verified MCP/capability server casket plugs into as a cartridge.
160+
161+
== Requirements
124162

125-
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].
163+
* GHC 9.4+ (CI builds with 9.4.8), Cabal 3.0+ or Stack 2.0+
164+
* Pandoc (current library dependency; see ADR-0001 for the planned move off it)
126165

127166
== License
128167

129-
MPL-2.0
168+
Code: *MPL-2.0* (link:LICENSE[LICENSE]). Prose documentation: `CC-BY-SA-4.0`.

0 commit comments

Comments
 (0)