Skip to content

Commit d0777a3

Browse files
docs(adr): ADR-0001 content engine — demote Pandoc, Djot prose / a2ml typed, boj substrate (#42)
## ADR-0001 (Proposed) — content engine direction Decision record capturing the Pandoc-demotion pivot, folding in the **boj substrate** and **Djot** conclusions from discussion. Proposed status — for ratification/reshaping, not silent landing. **The decision in one breath:** demote Pandoc to an authoring-time importer; adopt **Djot** as the canonical prose syntax (`jotdown` Rust parser now → verified Zig/Idris2 Djot core later, behind a stable AST); keep **a2ml** as the typed-data face (Djot replaces *CommonMark*, not a2ml); ship casket as a **boj capability cartridge** that *consumes* boj's verified MCP surface / Idris2 ABI / distribution / credential isolation instead of building its own (Emanote-style); and a **layered proof boundary** (Haskell cheap invariants, Idris2/Agda load-bearing, `FlexiText` as an `echo-types` `Echo`). **Why Djot is the keystone:** it shrinks the verifiable-parser target from *impossible* (CommonMark) to *finishable* (Djot, by the CommonMark/Pandoc author), reuses `jotdown` so we ship now, and its native attributes/divs/spans turn casket's a11y from string-rewrite hacks into first-class syntax. **Sequencing discipline (load-bearing):** ship the cheap differentiators first on today's engine (README truthing, register as a boj cartridge, `llms.txt`/per-page views off `descriptiles/`, wire `SixSCM`→`descriptiles/*.a2ml`) and demote Pandoc underneath them once the Djot/Idris2 core is proven — never gate the perishable agent-native window on the slow rewrite. Scope note: this is a decision doc only — no code/engine change in this PR. The first cheap slice (README truthing) follows as a separate PR. 🤖 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 c9d6b29 commit d0777a3

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
= ADR-0001: Content engine — demote Pandoc, Djot prose face, a2ml typed face, on the boj substrate
4+
:status: Proposed
5+
:date: 2026-06-23
6+
:deciders: @hyperpolymath
7+
:scope: casket-ssg · ddraig-ssg · poly-ssg · boj-server
8+
9+
== Status
10+
11+
*Proposed* — 2026-06-23.
12+
13+
== Context
14+
15+
casket re-exports Pandoc (`pandoc >= 3.0`) as its runtime content engine. Every
16+
one of Pandoc's worst properties here is independently evidenced, not theoretical:
17+
18+
* *#1 reason people leave Hakyll* (the estate's reference Haskell-SSG comparator):
19+
the Pandoc version-bump treadmill, ~1h cold CI builds, OOM-compiling-pandoc.
20+
* *Live red on `main` right now*: `src/CasketGnosis.hs:50` imports
21+
`Text.Pandoc.Options(HighlightMethod)`, which the resolved Pandoc no longer
22+
exports — an API-mismatch build break.
23+
* *Unverifiable*: a 100+-module dependency we can prove nothing about — directly
24+
contradicting casket's intended "verified / trustworthy output" positioning.
25+
* *Blocks the precompiled binary* (the answer to first-run friction): a
26+
Pandoc-linked static binary is large and slow to build.
27+
28+
Estate direction is settled around: *a2ml* as the canonical typed content/state
29+
format ("the way forward"); *Zig* for systems/FFI; *Rust/SPARK*, *Idris2* and
30+
*Agda* for correctness; and *boj-server* (Zig, Idris2-verified ABI `%default
31+
total`, credential-isolated) as the estate's primary MCP/capability server with
32+
on-demand cartridges. casket is already named as a boj capability
33+
(`standards/.github/workflows/boj-build.yml` → "Casket/ssg-mcp"). casket and
34+
ddraig currently overlap by each wrapping their own content path.
35+
36+
== Decision
37+
38+
. *Demote Pandoc* from runtime engine to an OPTIONAL, authoring-time importer
39+
(`casket import legacy.md -> .dj`). It is no longer a build/runtime dependency
40+
of the generator. Pandoc has a bidirectional Djot reader/writer, so the import
41+
pass is supported and clean.
42+
43+
. *Djot is the canonical prose syntax* (NOT CommonMark). Djot — by John
44+
MacFarlane, author of Pandoc and CommonMark — is purpose-built to be
45+
unambiguous and simple to parse, with *native attributes, fenced divs and
46+
bracketed spans* (`[text]{.class #id role=… aria-…}`, `::: {.sidebar}`). This
47+
turns casket's accessibility from string-rewrite hacks (`addThScope`,
48+
raw-HTML passthrough) into first-class syntax, and shrinks the "verifiable
49+
parser" target from *impossible* (CommonMark) to *finishable* (Djot).
50+
+
51+
*Stepping stone (implementation, not format):* adopt Djot now via an existing
52+
parser — `jotdown` (Rust, zero-dependency, complete) — behind a stable AST/event
53+
contract, and swap in a verified Zig/Idris2 Djot core *later* behind the same
54+
contract. Djot the format earns a *permanent* seat; only the implementation is
55+
transitional.
56+
57+
. *a2ml remains the typed-data / spec face.* Djot replaces CommonMark in the
58+
prose-parser slot; it does NOT replace a2ml. a2ml carries typed, validated
59+
content ("build fails on a wrong DNS record / missing governance section").
60+
Djot = prose + attributes; a2ml = typed structured data. They are
61+
complementary faces of one content model; conflating them is the one way this
62+
goes wrong.
63+
64+
. *boj-server is the substrate; casket is a cartridge.* casket does NOT build or
65+
maintain its own MCP server (the Emanote approach). It registers as a boj
66+
capability cartridge and *consumes* from boj: the verified agent/MCP surface,
67+
the proven Idris2 ABI (conform to it, do not reinvent it), cartridge
68+
distribution/discovery, and credential isolation for forge access. This makes
69+
casket's agent surface *formally verified and credential-isolated* — ahead of
70+
Emanote's embedded, dev-time, unverified MCP, not behind it.
71+
72+
. *Layered proof boundary.* Haskell carries the cheap in-language invariants
73+
(`FlexiText` smart constructor, GADT-tagged accessible HTML, newtype-validated
74+
content). Load-bearing proofs (validator totality, a2ml schema validity, the
75+
Djot grammar, the accessibility invariant) live in Idris2/Agda. The
76+
accessibility invariant is an `echo-types` `Echo f y := Σ (x:A),(f x ≡ y)` —
77+
`FlexiText` is its unverified Haskell shadow, and *ddraig owns the native
78+
proof*. The same loss-with-residue `Echo` models the Pandoc→Djot import
79+
(`import : Markdown -> Σ (d : Djot), (render d ≈ source)`): lossy import with a
80+
recoverable witness, rather than features silently discarded.
81+
82+
== Sequencing discipline (load-bearing)
83+
84+
The destination above is correct but the *content rewrite is slow*, and the
85+
agent-native window is perishable (Emanote already ships MCP). Therefore: *ship
86+
the cheap differentiators first on today's base, and demote Pandoc underneath
87+
them once the Djot/Idris2 core is proven — never gate the perishable wins on the
88+
slow rewrite.* Concretely, the agent surface (register as a boj cartridge) and
89+
machine-legible output (`llms.txt` + per-page views off `descriptiles/`) land
90+
early and cheap; Djot adoption rides an existing parser; the verified core lands
91+
last.
92+
93+
== Consequences
94+
95+
*Positive*
96+
97+
* Kills the treadmill / OOM / API-churn / live build-red; unblocks the
98+
precompiled static binary.
99+
* Removes the one unverifiable dependency — "verified" becomes honest.
100+
* Agent-native flips from casket's *weakest* leg to its *strongest*: a verified,
101+
credential-isolated surface no other SSG (Emanote included) offers.
102+
* Djot's clean AST + native attributes strengthen the a11y and typed-metadata
103+
legs and make Djot→a2ml mapping tractable.
104+
* Dissolves the casket↔ddraig overlap via a shared Djot/a2ml core, plausibly a
105+
shared verified boj capability.
106+
107+
*Negative / risk*
108+
109+
* The content pipeline (a2ml + Djot core + render) is still real work; boj gives
110+
the agent/dist/verify rails, not the content engine.
111+
* Djot is young (2022-) and niche; mitigated by Pandoc import for the Markdown
112+
world and by targeting the specs/governance/a11y/agent niche, not mass-market
113+
blogs.
114+
* Verifying even Djot is a project, not free; `jotdown` gets us *working* now,
115+
*verified* later.
116+
* New coupling to boj-server's health (another solo estate repo) — benign and
117+
aligned, but real.
118+
* a2ml prose ergonomics are poor; keep Djot as the prose face so "native" is not
119+
bypassed in practice.
120+
121+
== Alternatives considered
122+
123+
* *Keep Pandoc, fix the one-line highlighting break* — symptom only; leaves the
124+
treadmill/verifiability/binary problems. The one-line fix is folded onto the
125+
transitional importer path, not the engine.
126+
* *Small CommonMark core* — CommonMark is near-impossible to implement correctly
127+
and realistically un-verifiable for Idris2/Agda. Djot exists precisely to fix
128+
this.
129+
* *Prove casket in Haskell* (LiquidHaskell / Dependent Haskell) — swims upstream
130+
and duplicates the Idris2/Agda the estate already runs.
131+
* *Build casket's own MCP server (Emanote-style)* — duplicative and unverified;
132+
boj already provides a verified, shared, credential-isolated surface.
133+
134+
== Migration (staged)
135+
136+
. *Now / cheap (today's engine):* README truthing; register casket as a boj
137+
cartridge (banks the verified agent surface); emit `llms.txt` + per-page
138+
machine views from `descriptiles/`; wire `Gnosis.SixSCM` to read
139+
`descriptiles/*.a2ml` (it still reads the old flat `.scm`).
140+
. *Djot adoption:* parse via `jotdown` behind a stable AST; a2ml-native render
141+
path; Djot attributes carry a11y/metadata.
142+
. *Demote Pandoc:* `casket import` (Pandoc one-shot, Markdown→Djot); drop
143+
`pandoc` from the cabal's runtime deps; ship the precompiled static binary.
144+
. *Verify + share:* replace `jotdown` with a verified Zig/Idris2 Djot core behind
145+
the same AST; promote the core to a shared boj capability consumed by casket +
146+
ddraig.
147+
148+
== References
149+
150+
* `standards` — `SATELLITES.a2ml`, `immaculate-guide/IMMACULATE-GUIDE.adoc`
151+
(BoJ cartridges), `docs/decisions/ADR-002` (cartridge format),
152+
`.github/workflows/boj-build.yml` ("Casket/ssg-mcp").
153+
* Djot — https://github.com/jgm/djot ; `jotdown` (Rust) —
154+
https://github.com/hellux/jotdown ; Pandoc Djot support —
155+
https://github.com/jgm/djot/discussions/270
156+
* `hyperpolymath/echo-types` — loss-with-residue `Echo` formalism.

0 commit comments

Comments
 (0)