# sisudoc-ocda-writers
Sample **writers** over the object-centric document abstraction: functions
from the model to bytes.
**One writer set per language, in a directory named for the language.**
Five formats: `--text`, `--md`, `--html`, `--epub`, `--typst`.
| | |
|--------------------|---------|
| [`dlang/`](dlang/) | D |
| [`gleam/`](gleam/) | Gleam |
| `ruby/` | to come |
## What these are, and what they are not
They are **samples**, and they are **checked**. Spine is the output
pipeline; nothing here replaces it. What these demonstrate is that the
abstraction is sufficient: given a `.ssp` or a `.ocda.db` and nothing else,
a program of a few hundred lines reproduces spine's own output for that
format.
```
.ssp ─┐
├─> sisudoc-ocda-lib ─> the model ─> a writer ─> bytes
.ocda.db ─┘
```
**A writer is never told which serialisation it was given.** That is the
claim, and every writer here is run twice against every document to check
it: once from the `.ssp`, once from the `.ocda.db`, and the two outputs must
be identical to each other and to spine's.
## The oracle comes first
A writer with no oracle drifts, and "looks about right" is not a check. So
each writer is written **against a thing that can say no**:
| writer | oracle |
|-----------|--------------------------------------------------------------------------------------------------------------------------------------|
| `--text` | the words of the html writer's objects, as a multiset; every object number once; **D == Gleam byte for byte**. *Not* spine's bytes any more |
| `--md` | it is CommonMark; every `(#x)` resolves to an ``, no id twice; the words equal the html writer's objects; every OCN anchored |
| `--html` | it parses; every internal link resolves; the **objects' text** equals spine's scroll html |
| `--epub` | **epubcheck**, which read the specification so that the writer need not be trusted to have; and the same bytes from every input form |
| `--typst` | **typst itself**: a link to a label that does not exist is a compile error, so a silent compile means every internal link resolves |
and from every input form the library accepts, not only the two it started
with: a document renders identically from its `.ssp`, its `.ocda.db` and its
`.sisupod`.
**As at 2026-09-26**, over the 36 document-languages of the reference
collection:
`--text`, which is **no longer spine's bytes**, by decision (2026-09-27):
| | |
|---|---|
| the same bytes from every input form | **36 of 36** |
| **D == Gleam, byte for byte** | **36 of 36** |
| the words == the html writer's objects, as a multiset | **36 of 36** |
| every object number once, and no other | **36 of 36** |
| lines differing from `spine --text` | 131,474, **reported and not required** |
Byte identity was the first oracle and earned its place while there was
nothing else to check against; holding to it meant reproducing a dozen things
no one would choose, and the code said so a dozen times. What replaced it is
stricter where it matters. What changed in the output, and why each is better
rather than merely different, is in the head of `write/text`; the short list is
that a note is written once instead of twice, the author's markup comments and
the dummy headings are not written, an object with no text of its own writes
nothing (a `poem` shares its number with the verse before it), an image keeps
its caption, a table column is padded to its *display* width, and the table of
contents indents by ranked level rather than by spine's `indent_hang`.
`--md`, which is new (2026-09-27):
| | |
|---|---|
| the same bytes from every input form | **36 of 36** |
| **D == Gleam, byte for byte** | **36 of 36** |
| every `(#x)` resolves to an ``, and no id is defined twice | **36 of 36** |
| the words == the html writer's objects | **36 of 36** |
| every object number anchored, and no other | **36 of 36** |
The third is the markdown one: markdown has no anchor syntax, so the anchors
are the two pieces of raw html the writer emits, and a mistake in them is
invisible until a reader clicks.
**`--md` takes one option the others do not**, `--image-prefix `,
defaulting to `image`. A `.md` names an image by path, and the path depends on
where the `.md` is put, which is the caller's business and not the model's. The
writer emits a private-use marker for the directory and substitutes it once, at
the end, so nothing carries the constant through the call graph. spine, whose
markdown goes to `/markdown/` and shares the images with its html, passes
`../../image`; and **spine's `.md` is byte identical to what this produces with
that prefix, on all 36 document-languages.**
`--epub`:
| | |
|----------------------------------------|--------------------|
| epubcheck clean | **36 of 36** |
| from a `.ocda.db` == from a `.sisupod` | **35 of 35 pairs** |
| from a `.ssp` == from a `.ocda.db` | **36 of 36** |
| **D == Gleam, byte for byte** | **36 of 36** |
The third is the interesting one: a `.ssp` carries no images and a `.ocda.db`
and a `.sisupod` do, yet the epub is the same either way. It is the same
because the writer asks the library whether the source *can* give images
before it emits one, and a `.ssp` inside a pod can: the images are beside it,
at `media/image/`. So the three input forms are three ways to the same
publication rather than three publications.
(Thirty-five pairs rather than thirty-six, because spine names a `.sisupod`
after the pod directory and a `.ocda.db` after the document uid, and for one
document those differ, so it has no pair to compare.)
`--html`:
| | |
|--------------------------------------------------------------------|--------------|
| parses (`xmllint --html`) | **36 of 36** |
| every `href="#x"` resolves, no id twice, no heading outside h1..h6 | **36 of 36** |
| the objects' text == spine's scroll html | **36 of 36** |
| **D == Gleam, byte for byte** | **36 of 36** |
and so the two writers agree with each other, in two languages, without
either having been written from the other. That is the agreement that says
the model is enough.
`--typst`:
| | |
|----------------------------------------------------|--------------|
| `typst compile`, silently | **36 of 36** |
| from a `.ssp` == from a `.ocda.db` | **36 of 36** |
| the `.typ`'s text == the html writer's object text | **36 of 36** |
| every OCN on a page of the pdf | **36 of 36** |
| the same pdf twice, byte for byte | **36 of 36** |
| **D == Gleam, byte for byte** | **36 of 36** |
4,870 pages typeset. The first row is the strong one: **a link to a label
that does not exist is a typst compile error**, so a silent compile means
every internal link in the document resolves - object to note, note back to
object, table of contents, book index, every one. In html that took a checker
of its own.
The second is the same claim the epub makes, from the other side: a `.typ`
*names* its images rather than carrying them, so the two serialisations must
give the same bytes even though only one of them holds the image files.
The third is a **multiset** comparison and not a sequence one, because the
notes are in different places in the two outputs - at the point of reference
here, gathered at the end there - and what must agree is that the same words
are present the same number of times. Its two allowances are computed from
the `.ssp` object by object rather than allowed by pattern: what the `.typ` is
expected to lack (the endnotes section's heading and OCN, the table of
contents line that pointed at it, and each endnote's leading `N.`, which
typst generates from the note's own number) and what it is expected to *have*
that the html has not (see the note-84 paragraph below).
One allowance, with its reason in the sweep script rather than here: note 84
of `the_public_domain.james_boyle` is referenced from inside a verse and is
not gathered into the endnotes section, so nothing carries the anchor
`note_84`. **Spine's own html has the same dangling link.** It is an
abstraction-stage defect and affects every output.
And it affects them *unequally*, which the typst writer showed. An output
that gathers the notes at the end loses that note altogether, because the
reference has nowhere to go; a footnote at the point of reference never needed
the gathered copy, so the typst pdf has all 385 words of it. The sweep
computes that as an allowance from the `.ssp` rather than waving it through,
and the *direction* of the allowance is the finding.
## Running it
```sh
cd dlang
nix build --override-input ocda-lib path:../../sisudoc-ocda-lib
./result/bin/ocda-write --text .ssp
./result/bin/ocda-write --text .ocda.db en
./result/bin/ocda-write --typst .ocda.db en --out /.typ
typst compile --root /.typ /.pdf
```
`--typst` and `--epub` take `--out` because they write beside themselves:
the epub is a zip, and a `.typ` names its images by path, so `--out` also
writes the `image/` directory the `.typ` refers to.
Output goes to stdout, so the comparison is a pipeline:
```sh
./result/bin/ocda-write --text .ssp | cmp -
```
### the library, before it is published
`sisudoc-ocda-lib` is not yet at a public URL, so the flake declares the
URL it will have and the local copy is supplied on the command line with
`--override-input`. Nix accepts this without ever fetching the declared
url. When the library is published the override goes away.
## The pipeline
`pipeline/` holds fixed shell scripts that build every reader, library and
writer in both languages, run all of them over a directory of abstraction
artefacts, and put the output where it can be read.
```sh
ARTEFACTS= OUT= sh pipeline/run.sh
sh pipeline/one.sh md-g .ocda.db en # one tool, one document
```
They are not the checking - the sweeps above are - they are the run to do to
*see* all of it. `pipeline/README.md` is written as documentation and answers
"what do I type?".
## Byte identity was a means, and `--text` has outgrown it
**Byte identity with spine is a means, not the goal**, and `--text` no longer
uses it. It was the first oracle and it earned its place while there was
nothing else to check against; holding to it meant reproducing a dozen things
no one would choose:
- the table layout stripped trailing whitespace from the **whole accumulated
table** after every cell, not once per row, so a row ending in an empty
cell lost the newline before it;
- a column's width was the widest cell **in bytes** and the padding was
counted **in codepoints**, so a cell holding `©` was padded one place
further than one holding `c`;
- a table of contents entry's indent was `""`, `":"`, `"∴"` for the first
three levels and two spaces per level after that;
- and every note's text was in the file twice.
The four checks that replaced it are in "The oracle comes first" above, and
they are stricter where it matters. The divergence from `spine --text` is
**reported** by the sweep - 131,474 lines - so that it is visible without
anything depending on it.
**`--html` never used it either.** Html is where spine's output carries things
that are wrong rather than merely unusual, and a sample writer that copied them
would be teaching them. So the html writer is held to a different standard,
and a stricter one where it counts: it parses, every internal link
resolves, no id appears twice, and the objects' visible text equals spine's
exactly. What it does differently, each with the reason in the code:
| | |
|---|---|
| headings are `h1`..`h6` | spine writes `` for a level A heading, which is not an html element; a depth beyond six takes `h6` with `aria-level` |
| one charset declaration | spine writes `Content-Type: text/plain` for an html document, then a second `Content-Type` saying `text/html` |
| no placeholder metadata | spine writes `dc.publisher="Publisher"` and `dc.date.issued="year"` literally where it has no value |
| `span`, not `label`, for the object number | `