# 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 | `