<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sisudoc-ocda-lib/dlang/src/ocda, branch orgify</title>
<subtitle>SiSU Spine: tools</subtitle>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/'/>
<entry>
<title>track</title>
<updated>2026-10-03T01:39:24+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-10-02T19:01:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=afcdcd2e99b5d06f8b3dd9d43b5fd2033e46b9ed'/>
<id>afcdcd2e99b5d06f8b3dd9d43b5fd2033e46b9ed</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>track</title>
<updated>2026-10-03T01:39:21+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-10-02T17:57:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=c80a06efd9a5b6f709d6d4eddb6d6ef78f6502f2'/>
<id>c80a06efd9a5b6f709d6d4eddb6d6ef78f6502f2</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>track</title>
<updated>2026-10-03T01:39:17+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-10-02T17:30:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=c458d2112ce91ccd55bcb2e5b6e7c6fc4ac083a3'/>
<id>c458d2112ce91ccd55bcb2e5b6e7c6fc4ac083a3</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>D: what a source holds, and what a database holds unread</title>
<updated>2026-10-03T01:38:45+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-29T23:36:53+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=a14ef39dc207955de13ba2b141a69236e356a521'/>
<id>a14ef39dc207955de13ba2b141a69236e356a521</id>
<content type='text'>
ocda.inventory answers two questions about the file rather than the
document. languages(path), for every form alike: the languages in the
manifest's order (which every .ssp records in @source languages), each
with the markup it was made from and the sha256 of its .ssp - stored
by the database, taken from the bytes for a .ssp or a pod, and the
same. carried(path), for a .ocda.db: what it holds besides the
abstraction, by role and storage.

And the database loader now counts what it holds and the model does
not. The text loader meets every property line and counts those it
does not keep; this one asks for columns by name and so never saw the
others, and reported that it had understood everything, which is the
silence the count exists to prevent. It counts them on the conditions
under which the .ssp writes the same property, and the report is now
the same from a .ssp, a .ocda.db and a .sisupod: 36 of 36.

The pod loader's archive and language-name helpers become package
visible for the inventory.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
ocda.inventory answers two questions about the file rather than the
document. languages(path), for every form alike: the languages in the
manifest's order (which every .ssp records in @source languages), each
with the markup it was made from and the sha256 of its .ssp - stored
by the database, taken from the bytes for a .ssp or a pod, and the
same. carried(path), for a .ocda.db: what it holds besides the
abstraction, by role and storage.

And the database loader now counts what it holds and the model does
not. The text loader meets every property line and counts those it
does not keep; this one asks for columns by name and so never saw the
others, and reported that it had understood everything, which is the
silence the count exists to prevent. It counts them on the conditions
under which the .ssp writes the same property, and the report is now
the same from a .ssp, a .ocda.db and a .sisupod: 36 of 36.

The pod loader's archive and language-name helpers become package
visible for the inventory.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>model 2.1: ocn_range, the verses a poem holds</title>
<updated>2026-10-03T01:38:40+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-29T18:07:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=597d73db60679b8ed13524f9dd3832bb7476038f'/>
<id>597d73db60679b8ed13524f9dd3832bb7476038f</id>
<content type='text'>
The format now states, for a container, the first and last ocn of
what it holds: .ocn_range in the .ssp, ocn_range_first and
ocn_range_last in the .ocda.db. A poem is the one container, and since
2.1 it carries ocn 0 and comes before its verses.

Both fields in the model, read by both loaders, in both languages, and
written by the dump. A 2.0 database has neither column, so each
loader asks sqlite whether it is there and reads a 2.0 file as having
no ranges, which is what 2.0 meant; checked on a published 2.0
database, D and Gleam giving the same dump.

Over the 36 document-languages written by spine at 2.1, the four
dumps (D and Gleam, .ssp and .ocda.db) are byte identical, 36 of 36.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The format now states, for a container, the first and last ocn of
what it holds: .ocn_range in the .ssp, ocn_range_first and
ocn_range_last in the .ocda.db. A poem is the one container, and since
2.1 it carries ocn 0 and comes before its verses.

Both fields in the model, read by both loaders, in both languages, and
written by the dump. A 2.0 database has neither column, so each
loader asks sqlite whether it is there and reads a 2.0 file as having
no ranges, which is what 2.0 meant; checked on a published 2.0
database, D and Gleam giving the same dump.

Over the 36 document-languages written by spine at 2.1, the four
dumps (D and Gleam, .ssp and .ocda.db) are byte identical, 36 of 36.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>images from the source, and the epub segment fields</title>
<updated>2026-10-03T01:38:33+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-26T13:47:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=6c14c162bdec65934fbda3b14295efcfc12d28d2'/>
<id>6c14c162bdec65934fbda3b14295efcfc12d28d2</id>
<content type='text'>
Two additions the epub writer needed and could not have without.

ocda.images: an image's bytes, from whichever input form the document came
from. The model names images, with digest, byte size and pixel size, and
does not hold them - which is right, an abstraction is not a bundle of
assets - so something has to know where they are, and the answer depends
on the input form rather than on the output. Hence the library.

  .ocda.db   the files table, role 'image', by name
  .sisupod   the archive entry whose path ends in /image/&lt;name&gt;
  .zip       the same
  .ssp       beside it: a pod puts the abstraction in media/abstraction/
             and its images in media/image/, so the path is documented
             rather than guessed

canGiveImages says whether a source can answer at all, because a writer
has to know before it emits an &lt;img&gt;: a manifest item whose file is absent
and an &lt;img&gt; whose file is not in the manifest are both epubcheck errors.
A .ssp outside a pod has nowhere to look, and that is absence rather than
error - the document is still readable.

Reading a blob needed sqlite3_column_blob rather than _column_text: an
image read as text stops at the first zero byte, and a png's ninth byte
is zero.

And two more model fields: segment_epub and segment_epub_is. The epub
segments at heading levels 1 to 4 where the html segments at level 4
alone, so the two are different files and the abstraction records both.
Nine of the 36 documents have images; the four-way agreement still holds
at 36 of 36 each way.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Two additions the epub writer needed and could not have without.

ocda.images: an image's bytes, from whichever input form the document came
from. The model names images, with digest, byte size and pixel size, and
does not hold them - which is right, an abstraction is not a bundle of
assets - so something has to know where they are, and the answer depends
on the input form rather than on the output. Hence the library.

  .ocda.db   the files table, role 'image', by name
  .sisupod   the archive entry whose path ends in /image/&lt;name&gt;
  .zip       the same
  .ssp       beside it: a pod puts the abstraction in media/abstraction/
             and its images in media/image/, so the path is documented
             rather than guessed

canGiveImages says whether a source can answer at all, because a writer
has to know before it emits an &lt;img&gt;: a manifest item whose file is absent
and an &lt;img&gt; whose file is not in the manifest are both epubcheck errors.
A .ssp outside a pod has nowhere to look, and that is absence rather than
error - the document is still readable.

Reading a blob needed sqlite3_column_blob rather than _column_text: an
image read as text stops at the first zero byte, and a png's ninth byte
is zero.

And two more model fields: segment_epub and segment_epub_is. The epub
segments at heading levels 1 to 4 where the html segments at level 4
alone, so the two are different files and the abstraction records both.
Nine of the 36 documents have images; the four-way agreement still holds
at 36 of 36 each way.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>two fields the model was missing, found by writing an html writer</title>
<updated>2026-10-03T01:38:29+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-26T04:50:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=8fd585f659c5dfc7c822cb8b7d66d2c5f9e901a1'/>
<id>8fd585f659c5dfc7c822cb8b7d66d2c5f9e901a1</id>
<content type='text'>
anchor_tags and heading_lev_anchor. Both are in the specification's
property table and neither was read, so they went to the unknown counts
and a writer over the library had no way to emit a link target.

The cost was visible the moment an html writer existed: 154 of its
internal links pointed at nothing, because a note reference points at
the endnote's anchor and the endnote's anchor is an anchor_tag.
heading_lev_anchor is the name a document's own cross references use,
1,854 of them in the reference collection.

The .ssp carries them as repeated .anchor_tag and as
.heading_lev_anchor; the database as the object_anchors child table and
the heading_lev_anchor column. Both loaders read both now, the dump
reports them, and the four-way agreement still holds: 36 of 36 each way.

One thing to watch when adding a column to the database query: the read
is by position, so a column inserted in the middle shifts every one
after it. Adding heading_lev_anchor after segment_html_is broke all 36
documents until it moved to the end of the select.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
anchor_tags and heading_lev_anchor. Both are in the specification's
property table and neither was read, so they went to the unknown counts
and a writer over the library had no way to emit a link target.

The cost was visible the moment an html writer existed: 154 of its
internal links pointed at nothing, because a note reference points at
the endnote's anchor and the endnote's anchor is an anchor_tag.
heading_lev_anchor is the name a document's own cross references use,
1,854 of them in the reference collection.

The .ssp carries them as repeated .anchor_tag and as
.heading_lev_anchor; the database as the object_anchors child table and
the heading_lev_anchor column. Both loaders read both now, the dump
reports them, and the four-way agreement still holds: 36 of 36 each way.

One thing to watch when adding a column to the database query: the read
is by position, so a column inserted in the middle shifts every one
after it. Adding heading_lev_anchor after segment_html_is broke all 36
documents until it moved to the end of the select.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>a third input form: the abstraction out of a pod archive</title>
<updated>2026-10-03T01:38:24+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-26T02:57:24+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=483c14bf6cc284710e06c9d27747e1ecf5d9466b'/>
<id>483c14bf6cc284710e06c9d27747e1ecf5d9466b</id>
<content type='text'>
A pod built with --pod2 carries the abstraction inside it, at
media/abstraction/&lt;uid&gt;.&lt;lang&gt;.ssp. So ocda.load gains .sisupod and .zip,
and because the dispatch is in one place a writer over the library gained
a third input form without being touched.

Over the reference collection a .sisupod gives the same model as the .ssp
beside it: 36 of 36 document-languages, byte identical dumps. And the
text writer, unchanged, renders a document from its .sisupod byte
identically to spine --text, 36 of 36.

Two small readers, neither of them a library: ocda.zstd declares the four
libzstd entry points needed to unwrap a frame, as ocda.sqlite declares
sqlite3; ocda.zip reads a central directory and one stored entry. Deflate
is refused rather than mishandled, since spine writes stored entries and
a library that says it read what it skipped is worse than one that says
it cannot. A frame that declares an expansion over 512 MiB is refused
before anything is allocated: a reader of an artefact it did not make
declines to be told how much memory to take.

The Gleam library reads the zip and not the frame. The only zstd on the
BEAM is a NIF that clones zstd from github in its own build, so it cannot
be built sandboxed without patching it. A .zip pod is read in both
languages, a .sisupod in D only, and the Gleam refusal says why and says
"zstd -d" rather than claiming the file is malformed. It is a packaging
limit, not anything about the format, and the only place the two
libraries differ in what they accept.

A pod without an abstraction is refused with a message naming the flag
that puts one in, because that is a fact about the pod.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
A pod built with --pod2 carries the abstraction inside it, at
media/abstraction/&lt;uid&gt;.&lt;lang&gt;.ssp. So ocda.load gains .sisupod and .zip,
and because the dispatch is in one place a writer over the library gained
a third input form without being touched.

Over the reference collection a .sisupod gives the same model as the .ssp
beside it: 36 of 36 document-languages, byte identical dumps. And the
text writer, unchanged, renders a document from its .sisupod byte
identically to spine --text, 36 of 36.

Two small readers, neither of them a library: ocda.zstd declares the four
libzstd entry points needed to unwrap a frame, as ocda.sqlite declares
sqlite3; ocda.zip reads a central directory and one stored entry. Deflate
is refused rather than mishandled, since spine writes stored entries and
a library that says it read what it skipped is worse than one that says
it cannot. A frame that declares an expansion over 512 MiB is refused
before anything is allocated: a reader of an artefact it did not make
declines to be told how much memory to take.

The Gleam library reads the zip and not the frame. The only zstd on the
BEAM is a NIF that clones zstd from github in its own build, so it cannot
be built sandboxed without patching it. A .zip pod is read in both
languages, a .sisupod in D only, and the Gleam refusal says why and says
"zstd -d" rather than claiming the file is malformed. It is a packaging
limit, not anything about the format, and the only place the two
libraries differ in what they accept.

A pod without an abstraction is refused with a message naming the flag
that puts one in, because that is a fact about the pod.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>ocda-dump, and the four disagreements it found</title>
<updated>2026-10-03T01:38:08+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-26T01:02:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=64571ec14438e85d740ede5810a6f3a5cca88fe3'/>
<id>64571ec14438e85d740ede5810a6f3a5cca88fe3</id>
<content type='text'>
The agreement check compared ten fields of each object. That was enough
to find the blank-first-line defect and not enough for anything
subtler, so this adds ocda-dump: the whole loaded model written out
canonically, every field, sorted dictionaries, sections in format
order, defaults omitted. It is an oracle, not an output format. Two
implementations of this model can now be compared with diff.

Run against the reference collection it found four disagreements
between the two loaders, none of which the ten-field check could see.

1. .identifier and .anchor. The .ssp emits a property only when it
   carries information, so the identifier is absent when it is simply
   the ocn and the anchor is absent when it is the segment name; the
   database stores both unconditionally. Over one document that is
   1,314 identifiers and 69 anchors. The loader now derives them, which
   is what the model means: an object's identifier is its ocn unless it
   says otherwise.

2. make and doc_has. The database has one metadata table where the
   text form has three blocks, and tells them apart by a key prefix.
   The loader now unprefixes them, so a writer asks doc.make["footer"]
   whichever serialisation it was handed and never
   doc.metadata["make.footer"].

3. The JSON columns were read by a character scan that stripped
   brackets and quotes, which is wrong the moment a value holds an
   escape: JSONValue.toString escapes "/" as "\/", so every heading
   containing a slash came back with a backslash in it. All 36
   document-languages differed on heading_ancestors_text. Now parsed as
   JSON.

4. translation.ocn_aligned is in the database and not in the .ssp,
   legitimately: it says whether the languages share one numbering, and
   a .ssp is one language and cannot know. It is a fact about the file
   rather than the document, so the loader leaves it out rather than
   letting a writer tell which serialisation it was given.

Metadata values are also stripped now, as the text loader strips them:
one topic_register value carried surrounding spaces in the database
that the text form cannot represent.

After all four: 36 of 36 document-languages give byte identical dumps
from their .ssp and from their .ocda.db.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The agreement check compared ten fields of each object. That was enough
to find the blank-first-line defect and not enough for anything
subtler, so this adds ocda-dump: the whole loaded model written out
canonically, every field, sorted dictionaries, sections in format
order, defaults omitted. It is an oracle, not an output format. Two
implementations of this model can now be compared with diff.

Run against the reference collection it found four disagreements
between the two loaders, none of which the ten-field check could see.

1. .identifier and .anchor. The .ssp emits a property only when it
   carries information, so the identifier is absent when it is simply
   the ocn and the anchor is absent when it is the segment name; the
   database stores both unconditionally. Over one document that is
   1,314 identifiers and 69 anchors. The loader now derives them, which
   is what the model means: an object's identifier is its ocn unless it
   says otherwise.

2. make and doc_has. The database has one metadata table where the
   text form has three blocks, and tells them apart by a key prefix.
   The loader now unprefixes them, so a writer asks doc.make["footer"]
   whichever serialisation it was handed and never
   doc.metadata["make.footer"].

3. The JSON columns were read by a character scan that stripped
   brackets and quotes, which is wrong the moment a value holds an
   escape: JSONValue.toString escapes "/" as "\/", so every heading
   containing a slash came back with a backslash in it. All 36
   document-languages differed on heading_ancestors_text. Now parsed as
   JSON.

4. translation.ocn_aligned is in the database and not in the .ssp,
   legitimately: it says whether the languages share one numbering, and
   a .ssp is one language and cannot know. It is a fact about the file
   rather than the document, so the loader leaves it out rather than
   letting a writer tell which serialisation it was given.

Metadata values are also stripped now, as the text loader strips them:
one topic_register value carried surrounding spaces in the database
that the text form cannot represent.

After all four: 36 of 36 document-languages give byte identical dumps
from their .ssp and from their .ocda.db.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
<entry>
<title>style: restore the comment arrows, double-encoded by an edit</title>
<updated>2026-10-03T01:38:04+00:00</updated>
<author>
<name>Ralph Amissah</name>
<email>ralph.amissah@gmail.com</email>
</author>
<published>2026-09-26T00:49:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sisudoc.org/projects/sisudoc-ocda-lib/commit/?id=8a3a66e3f0ff4f031c2918f4501b6a05ab05a06f'/>
<id>8a3a66e3f0ff4f031c2918f4501b6a05ab05a06f</id>
<content type='text'>
The /+ ↓ +/ arrows had each of their three UTF-8 bytes re-encoded as
UTF-8 in turn, by an in-place edit that read the file as latin-1. 13
comments in load_ssp.d. Comment text only; the parser is unchanged and
the agreement check still passes.

(assisted by Claude-Code)
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The /+ ↓ +/ arrows had each of their three UTF-8 bytes re-encoded as
UTF-8 in turn, by an in-place edit that read the file as latin-1. 13
comments in load_ssp.d. Comment text only; the parser is unchanged and
the agreement check still passes.

(assisted by Claude-Code)
</pre>
</div>
</content>
</entry>
</feed>
