aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sisudoc
Commit message (Collapse)AuthorAgeFilesLines
* abstraction: format 2.0, source.digestsRalph Amissah2026-09-223-4/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | format 2.0, source.digest covers every markup file, the sha256 over one line per markup file, "<sha256> <filename>", sorted by filename. source.digest was the sha256 of the master file as read, before any insert. This is right for a .sst, which is the whole document. For a .ssm with inserts .ssi (containing the substantive part of a documents text) this is close to meaningless. Considered change of meaning (rather than an addition) and given the major part of the format version with it: 1.1 becomes 2.0, in the .ssp header line and in the database's schema.version row together. A 1.x reader refuses a 2.0 artefact, which is what that check is for. - Sorted, so the value does not depend on the order a filesystem hands back a directory. - By filename (rather than by path), so it does not depend on where the pod sits, which is the property the pod rebuild comparison rests on: a pod unzipped elsewhere must still give the same abstraction. Within one language every markup file lives in one directory, so a filename identifies it. Each line is checkable on its own against a digests.txt line or a files row, which the single value was not. The whole is reproducible with sha256sum and sort, and was verified that way for a one file document and for a twenty file one. An unreadable file is named in the digest rather than skipped. The parse has failed elsewhere by then, and a digest that quietly left a file out would claim the document is something it is not. The reference abstractions are regenerated. Two lines change in each and no others: the version, and the digest. (assisted by Claude-Code)
* pod: generate the po4a configuration, --po4a-cfgRalph Amissah2026-09-223-7/+214
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A document's po4a configuration is derived rather than kept. Spine already knows both halves without looking: the manifest says which languages, and the markup says which files, through the << lines the parser follows. Derived, it cannot disagree with the document, where a hand-kept file eventually does: a language added to the manifest and forgotten in the Makefile is a translation nobody updates. It prints, rather than writing. Spine does not write into a source tree, and the file belongs beside the catalogues in the pod it describes: spine --po4a-cfg <pod> > <pod>/tools/po4a/po4a.cfg Three things the Makefile it replaces knew, which had to be found by running po4a rather than by reading: --keep 0 is the difference between regenerating a document and destroying it. po4a will not write a translated file below a completeness threshold, and that threshold defaults to 80%. A partly translated document is the normal state of one being worked on, and sisu covers it: an untranslated string falls back to the source text. Measured at the default, 181 of live-manual's 189 translated files would be discarded, and nine languages would collapse to eight surviving files, reading as the document reverting to its source language rather than as a failure. index.html.in is not markup and no << line names it, so a configuration built from the manifest and the insert list alone drops it, and with it index.html.in.pot and its nine .po files at the next regeneration. It is found by asking whether the source language has one. neverwrap is not set, and the measurement is recorded beside the code: setting it turns 889 of 1126 strings fuzzy in four languages, and po4a does not use a fuzzy translation, so Catalan's coverage would fall from about 46% to about 12%. Tidy formatting is not worth that. The run-complete banner is suppressed for this action. The configuration goes to stdout and the banner would otherwise be part of the file, which is what the round trips already avoid by leaving early; this one prints from inside output processing and cannot. (assisted by Claude-Code)
* pod: carry tools/po4a, the translation working setRalph Amissah2026-09-221-0/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pod now carries its catalogues, so a translation can be updated rather than only retyped: providing the .pot and .po (the means of maintaining it). A directory walk, where every other thing the pod writer carries is named. A catalogue set is whatever the translator has, and enumerating .pot and .po files would mean the writer deriving the document's languages a second time, from a second place, to say what it already reads from the manifest. Done once, on the last language, as the markup blocks are. The tree is not language specific, so a pass for each language would re-read all of it, 5.5 MB ten times over for live-manual, into an archive the next pass overwrites. Every name is checked before it is carried, by the rule the pod reader applies to an archive it is handed. The writer is reading a directory someone else may have filled, which is the reason the reader checks. The cost is small, and it is the whole argument for compressing the archive as a stream rather than per member: uncompressed content 6972418 -> 13167489 +89% the .sisupod 692942 -> 891388 +28.6% 5.5 MB of catalogues cost 198 KB, because the .po files largely repeat markup already in the archive and whole-stream compression sees it. Per-member deflate could not. (assisted by Claude-Code)
* pod: set entry limit for the writer and readerRalph Amissah2026-09-223-1/+77
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Both now take the number from one constant now set at 10,000, the writer checks it before the archive is written. Previously the number set for the pod reader was less than 500 members. The writer had no limit, (so spine could write a pod it would then refuse to read, reporting too many entries: a good file that looks corrupt, and only for a document with enough parts). 500 was set when a pod held markup and images. A pod carrying translation catalogues has a different arithmetic, and the count follows from how many files a document is made of times how many languages it has, not from how large the document is: the_wealth_of_networks 213,405 words, 1 file per language 12 languages -> 49 entries live-manual 24,724 words, 20 files per language 12 languages -> 506 entries The big book is not what runs into this; the modular manual is. live-manual stood two languages from an unreadable artefact. 10,000 covers a hundred-insert manual in thirty languages, about 6,200 entries, with room. It is deliberately well clear of any real document rather than snug above the largest one known: the two errors are not comparable, since too low refuses a legitimate document with a message that reads as corruption, while too high defers to a size cap a moment later. The count is the weakest of the three guards and is not what bounds resource use; the per entry and total size caps do that, both before a byte is written. Refusing also removes any archive an earlier language left. The writer runs once per language and only the last pass holds every language, so it is the last that goes over, and the passes before it wrote smaller archives that passed. Without this, refusal left a pod missing a language: an artefact that reads perfectly well and is wrong. A missing file is an error someone notices. The arithmetic is recorded beside the constant so the next person can re-derive the number rather than guess at it. (assisted by Claude-Code)
* pod: write <doc>.sisupod (single zstd frame)Ralph Amissah2026-09-224-9/+55
| | | | | | | | | | | | | | | | | | | | | | | | | | | | The pod archive spine writes is now named .sisupod and carries the same archive inside one zstd frame. The reader has accepted both since the previous commit, so every pod already published stays readable and this changes only what is written. free_culture 2863504 -> 1100180 live-manual 6972418 -> 692942 10.06x The wrap is at the pod call site and not in createZipFile, which also writes every epub and every odt. Best compression from whole-stream (rather than per-member compression). (for live-manual per-member deflate manages about 3x compared to 10x for whole pod content compression). The suffix is named as pod_archive_suffix. --pod-compression sets the level, 19 by default: a pod is written once and fetched many times (692942 bytes at 19, 837703 at 9, 2054365 at 1). (A value that is not a number warns and the default is used). Output built from a written .sisupod is byte identical to output built from the markup it came from. (assisted by Claude-Code)
* pod: read when zstd-wrappedRalph Amissah2026-09-226-10/+230
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | sisupod using zstd compression recognised by first bytes. <doc>.sisupod: spine reads one before it writes one, so when the name changes (from .zip to .ssiupod) every pod already published stays readable. A .sisupod is one zstd frame wrapping the archive spine already builds. The reader unwraps the bytes and hands the same archive to the same parser, so every guard downstream is untouched: entry names, per-entry and total size, path depth, escape and symlinks. Which container it is comes from the first four bytes (rather than the name). A pod published as a plain .zip reads as it always did, a .sisupod reads, and either one renamed reads too. The suffix is still recognised, both spellings, for the argument and for a url. libzstd is declared rather than bound: provides the whole surface of fifteen extern C prototypes (there is nothing to generate and no upstream tree to track, which is the arrangement sqlite3 already has). dub links it with "libs": [ "zstd" ]; nix needs zstd.out rather than zstd, whose default output is the binaries and carries no library at all. A frame declares its uncompressed size in its own header, and for a pod fetched over https that number is attacker controlled. The declared size is checked against a ceiling before a buffer is asked for, a frame that will not declare one is refused, and what comes out is checked against what was promised. The ceiling is the extraction limit the archive reader already applies, so the two bounds agree. Measured: output built from a .sisupod is byte identical to output built from the same pod's .zip, 359 files over text, html, epub, odt and .ssp for three documents, live-manual's ten languages included. A truncated frame is refused and the document skipped. free_culture 2863504 -> 1099736 2.60x the_wealth_of_networks 4294022 -> 1172649 3.66x live-manual 6972418 -> 675970 10.31x (assisted by Claude-Code)
* odt: office:meta from the document, not the runRalph Amissah2026-09-223-72/+88
| | | | | | | | | | | | | | | | | | | | | | | | | Three faults in four lines of the odt metadata. meta:creation-date and dc:date were taken from generated_time, the run's own clock, so two runs over unchanged markup produced different odt bytes. They now carry the document's own instant, so an odt is a function of its source. That was the last clock stamp in any writer: every output format is now reproducible. generated_time is a human-readable run stamp, of the form "2026-9-17 [38/3] 18:39:35", with unpadded numbers and an iso week in brackets. It is not a valid xsd:dateTime, which is what ODF asks for in both of those fields, so what was written there was malformed as well as unstable. dc:language now carries the document's own language tag. The instant is doc_matters.modified_utc, moved there from epub3.d in this commit: epub3 needs the same value for its dcterms:modified, and one definition cannot drift from the other. generated_time now has no caller and is left in place. (assisted by Claude-Code)
* metadata: use document's languageRalph Amissah2026-09-221-2/+6
| | | | | | | Every metadata page body element carried lang="en" xml:lang="en" as a literal. It now carries the document's own language tag. (assisted by Claude-Code)
* epub: dcterms:modified timestamp, reproducibilityRalph Amissah2026-09-221-3/+62
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | timestamp dcterms:modified from the document, not the clock EPUB3 requires exactly one dcterms:modified on the package It is now read in order: SOURCE_DATE_EPOCH where the environment sets it, which is the reproducible-builds convention and lets a build pin every artefact of one run; then the document's own date.modified; then date.published. The clock remains the last resort, for a document that says nothing about when it is from. A document date is loose markup. "1991", "2006-03" and "2021-12-00" all occur in the sample set, and a month or day of 00 is not a date. A missing or zero part becomes 01, and a day out of range for its month falls back to the first, so what is written is always a valid CCYY-MM-DDThh:mm:ssZ. Over the sample set two consecutive runs now produce all 36 epubs byte identical, and epubcheck reports no change: 0 fatal, 0 errors. (previously taken from the clock at build time. Two runs over unchanged markup therefore produced different epub bytes, so no comparison of epub output could tell a real change from the seconds between two builds. It also meant an epub was a function of when it was built rather than of what it was built from.) (assisted by Claude-Code)
* conf: read conf/document_make, drop the dr_ prefixRalph Amissah2026-09-223-8/+8
| | | | | | | Spine now looks for conf/document_make, and bundles that name. (the dr_ prefix was a sisu-era name to distinguish it) (assisted by Claude-Code)
* xml: markup language attributes take a bcp 47 tagRalph Amissah2026-09-223-10/+25
| | | | | | | | | | | | | | | | Spine names a language as the directory under media/text/ is named, which for a region is the posix form with an underscore, pt_BR. A lang, xml:lang or dc:language value is a BCP 47 (RFC 3066) tag and wants a hyphen, pt-BR. Spine put its own code there unchanged, so a document in a region language produced an invalid attribute in every one of its xhtml files: 138 epubcheck errors for a document of ten segments and a navigation file. src.language_tag is the same language as a tag. The attributes and the dc metadata now read it. Output paths keep src.language, since a path is named after the directory, not after the tag. (assisted by Claude-Code)
* pod digests: multilingual document digests sortedRalph Amissah2026-09-221-7/+16
| | | | | | | | | | | | | | Fixes for three faults in the insert (.ssi) digests. The key was the language captured from whichever path resolved the insert. It did not change across the loop over languages, so every language's inserts were filed under one language. The key is now the language being digested, as the digest of the primary file above it already did. - digest is taken after the check that the file exists. - digest is now taken inside the guard. - non-pod path now correctly keyed on the document's own language. (assisted by Claude-Code)
* downloads: honour --allow-downloadsRalph Amissah2026-09-221-1/+11
| | | | | | | | | The flag has existed since a url argument could be fetched and nothing ever read it, so every url was fetched whether or not the flag was given. A refused url is now dropped from the arguments, which is what a failed download already did. (assisted by Claude-Code)
* ocda db: before writing check carried file namesRalph Amissah2026-09-222-1/+230
| | | | | | | | | | | | | | | | | | | | bugfix: content image names are now checked in a pass of their own, before anything is created or written, and one bad name refuses every image in the artefact rather than skipping the one, which is what the zip reader does with a zip. The write then re-checks containment. (prior to this an image name read out of a .ocda.db was written without checks, and such a database can be fetched over https. A name could climb with "..", and being handed to chainPath an absolute name dropped everything before it, so "/x" did not land under the image directory at all. The zip reader has guarded against this since it was written; the database reader did not). carried_names.d holds both rules: a bare filename, as an image is carried, and a relative path, for the markup and conf a database is to carry. (assisted by Claude-Code)
* markup: keep a utf-8 bom out of the yaml headerRalph Amissah2026-09-211-1/+14
| | | | | | | | | | | | | | | | | | | A file beginning with a utf-8 byte order mark had the mark read as part of its first yaml key, so "title:" arrived as "title:" and every value under that key was dropped silently: the document kept its text and lost its title. Downstream that showed as an empty dc:title and an empty <title> element in the epub, which epubcheck reports as two errors. The mark is now kept out of the header and body split, and so out of the parse. It is not stripped when the file is read, because that text is what source.digest is taken over and the digest names the file as it sits on disk. One document in the sample set begins with a mark. It is left as it is: it is the case this guards against. (assisted by Claude-Code)
* images alt text, and epub says what it isRalph Amissah2026-09-147-31/+313
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four things, not visible in an error count: - Images had no alt text, even where markup carried the text, and the field alt text is present, incorrectly used in code; fixed as the alt. { sm_tux.png 64x80 "Gnu/Linux - a better way" }image an image with nothing to say for itself gets alt="" - Images with no dimensions were incorrectly written width="0" height="0", fixed - Accessibility metadata, which EPUB Accessibility 1.1 requires: schema:accessMode, accessibilityFeature and accessibilityHazard, plus accessModeSufficient and accessibilitySummary, which epubcheck does not test for; used by - readers looking for a book it can use, - anyone distributing into the EU. Every value is derived from the document, not asserted: a document with no images says so, one whose images all carry alt text claims alternativeText and textual sufficiency, one where an image says nothing claims neither. Nothing claims WCAG conformance, which would be a claim about an evaluation that has not happened. - DPUB-ARIA roles beside the structure spine already knew about. doc-toc, doc-endnotes, doc-bibliography, doc-glossary and doc-index on the sections, doc-noteref on a note reference and doc-footnote on the note it points at. "doc_endnotes" as a class name means something to a stylesheet and nothing to a reading system. Not doc-biblioentry: DPUB- ARIA 1.1 deprecated it. These are plain ARIA, so the html gets them too. Also dealt with here: - Spine makes certain sections such as endnotes, bookindex, ... and a heading that has a name for itself cannot share the same name, if it does the name is now disambiguated, disambiguated. (disambiguation similar to that used for a repeated anchor tag) - Endnote anchors fixed to use "id" (being one unique tag in the document that is target of every reference to it)instead of obsolete form (they were <a name="note_1">. "name" on an <a>). Book index markers keep "name": the same marker sits on every object an entry names, 296 times in one document, and those are markers of where a term occurs rather than distinct targets. (assisted by Claude-Code)
* epub and html: no errors left over the sample setRalph Amissah2026-09-147-73/+318
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | epubcheck 5.3.0 over the 35 sample epubs went from 1302 remaining errors to none. The six causes for those remaining errors fixed here. most of the count was one of them: - Tables (1123 errors, 86%) where the writer emitted obsolete attributes, removed in 2014; table also now in div instead of a paragraph. also fixed: - Navigation (115). Every level 4 heading's nav entry ended "#0", the ocn of an object that has no ocn and so no id. - Duplicate ids (37). (from three unrelated sources). - The publication identifier (27 warnings). dc:identifier was a hardcoded hex string, not a UUID, and the same one in all 35 files, so two documents in one library collided. It is now UUID v5 over the document's own uid: a real RFC 4122 identifier, stable across builds and distinct per document. - Image manifest entries (15). media-type was "image/" plus the file extension, giving "image/jpg", which is not a media type; epubcheck reads it as a foreign resource and wants a fallback. There is now a mapping, which also covers svg and webp. Item ids came from the file basename, and "2bits_02_01-100.png" gave an id starting with a digit, which is not an XML name; ids are now prefixed and sanitised. - An ocn resolving to no segment (9). A poem block and its first verse share one ocn; the verses are written into a segment and the block itself is not. Also dropped from the package document: an xmlns:xsi that nothing used, and a prefix declaration for the rendition vocabulary, which is a reserved prefix and needed no declaring. The package now carries xml:lang. test-epub-validity.sh now allows no fatal and no error. Two NAV-011 warnings remain and are described in it. Three reference .ssp files change, all of them the anchor tag disambiguation above. (assisted by Claude-Code)
* regex: confine url within an endnote (delimiter)Ralph Amissah2026-09-141-4/+4
| | | | | | | in error regex permitted a url to go past an endnote close delimiter, fixed (assisted by Claude-Code)
* epub, html: a named anchor, and subtitle fixesRalph Amissah2026-09-142-16/+15
| | | | | | | | fixes related to: - named anchor id - subtitles, emit only where they exist (assisted by Claude-Code)
* epub, html: correctly identify code block as blockRalph Amissah2026-09-142-10/+11
| | | | | | | | - wrapper set to <div>, with class unchanged, and the six stylesheets select div.code where they selected p.code. - invented element <codeline> removed (assisted by Claude-Code)
* epub, html: reference issues fixesRalph Amissah2026-09-142-18/+10
| | | | | | | | - _part_eof.xhtml was declared in the manifest and never written. - image_sys/bullet_09.png was referenced as a background image, a throwback to the Ruby version of sisu, never used here. (assisted by Claude-Code)
* epub, html: document heads and ids, fixesRalph Amissah2026-09-142-6/+13
| | | | | | | One document head per file, and ids must be unique. No remaining fatal errors in the epub in the marup sample set. (assisted by Claude-Code)
* epub, html, db: use of elements & attributes, fixRalph Amissah2026-09-143-120/+121
| | | | | | | fixed, four things used that XHTML does not have. (search db sisudoc-spine-search-cgi results share styling) (assisted by Claude-Code)
* epub: mimetype entry comes first in the archiveRalph Amissah2026-09-141-3/+18
| | | | | | | | | | | | | | | OCF requires the mimetype entry to be the first in the zip and stored uncompressed, so that a reader can tell what the file is by looking at a known offset without parsing the archive. The index is now set to: 0 for the mimetype, 1 upward for everything else as it is added. mimetype Stored first META-INF/container.xml Stored OEBPS/toc_nav.xhtml Stored ... (assisted by Claude-Code)
* epub: manifest lists files, spine orders filesRalph Amissah2026-09-141-57/+46
| | | | | | | | | | several fixes - for epub manifest is the list of files in the container, the spine is the reading order of those files, fixed. - The guide is dropped. It is EPUB2, optional in EPUB3 - numerous small fixes (assisted by Claude-Code)
* epub, html: metadata text clear of markupRalph Amissah2026-09-132-30/+53
| | | | | | | | special_characters_text escapes the markup characters and then puts <br /> back in for a line break. That is right for element content and wrong in metadata. fix (assisted by Claude-Code)
* epub, html, odt: namespace names as identifiersRalph Amissah2026-09-133-17/+17
| | | | | | | | | | | | | | namespace names are identifiers, not addresses, fixes A namespace URI is an opaque identifier. www.w3.org xhtml, xlink, MathML, rdf-syntax-ns, xml-events, XMLSchema, XMLSchema-instance, xforms, css3-text, data-view, and the xhtml1 strict DTD system identifier purl.org dc/elements/1.1, dc/terms openoffice.org office, writer, calc, table, report docs.oasis-open the ODF meta pkg and odf class identifiers (assisted by Claude-Code)
* some file renamesRalph Amissah2026-09-1210-19/+19
|
* spine: a .ocda.db can be fetched, as a pod zip canRalph Amissah2026-09-123-27/+24
| | | | | | | | | | | | | | | | | A URL ending in .ocda.db is now downloaded and processed in place, through the path that already did it for .zip: the same curl call, the same size and timeout limits, the same refusal of local and private addresses, the same --allow-downloads guard, the same temp file cleanup. Only the pattern had to widen. rgx_url_zip ^https?://...[.]zip$ rgx_url_source ^https?://...([.]zip|[.]ocda[.]db)$ downloadZipUrl is no downloadSourceUrl and the download temp directory is spine-download rather than spine-zip-pod; (the extraction directory keeps its name, being still only for zips). (assisted by Claude-Code)
* ocda: the images an artefact carries, and the ones it only describesRalph Amissah2026-09-123-9/+157
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The document markup sample collection builds byte identically from either artefact: 1457 files, no differences, from markup, from the 35 .ssp files, and from the 35 .ocda.db files. A .ocda.db carries its images as blobs. dbReadFiles() reads them and they are written where the output writers look for images: a directory of this run's making, removed when the run ends, not the pod beside the artefact, (which could be a working published tree). The document's source path moves with it, image_dir_path being reached from the document's own file rather than from the pod. Each blob is checked against the sha256 the database recorded beside it. The two were written together, so a mismatch means the file is damaged. (the point of having recorded the digest). A .ssp only describes its images, so those are the ones in the pod it sits in, and they are checked against the digests it recorded. That asymmetry arises from of what the two artefacts are: the ocda.db is self-sufficient and can prove it is looking at the right image, the .ssp describes a pod it sits alongside. Found while wiring it: setting the pod directory alone was not enough, since image_dir_path is derived from the source file's path (../../image from media/text/<lang>/), so the source path has to be moved with it. (assisted by Claude-Code)
* spine: process a document from .ocda.db or .sspRalph Amissah2026-09-128-47/+480
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Process a document from its .ocda.db or .ssp abstraction. A .ssp or a .ocda.db given as an argument is now a document like any other: read rather than parsed, and handed to the output writers as the same doc they already take. The 35 document sample collection built from its .ssp files is byte identical to the same collection built from markup: 1457 files, no differences. Three things had to be rebuilt rather than read, all of them a value parsed rather than a structure derived: - classify_topic_register_arr and its expanded twin. The split is not a plain one, so the rule moved to sisudoc.ocda.meta.topic_register and both the yaml reader and this one call it. - creator_author_arr, which is creator.author split on the ", " it was joined with. - title_sub, a copy of title_subtitle made where the header is read, and what epub3 puts in dc:title id="subtitle". Fixed ordering bug found by the acceptance test. A heading's own anchor can be a bare number taken from its text and this can collide with the ocn of an unrelated object. Whole output comparison found that before the fix there was one wrong link in one epub's table of contents. From a .ocda.db, 1446 of 1457 files are identical. The 11 that are not are the images: a database carries its own image blobs and nothing yet extracts them, so the five sisu_markup images are not copied and the epub that embeds them differs. That is the next step and is not a defect in this one. --source and --pod2 are refused with a warning rather than half done, no artefact carrying the markup. (assisted by Claude-Code)
* ocda: one constructor for doc_mattersRalph Amissah2026-09-122-143/+244
| | | | | | | | | | | | | | | | | | | | ST_DocumentMatters was declared inside spineAbstraction(), closing over the parser's locals, so only the parser could build it. A document loaded from a .ssp or a .ocda.db has to arrive at the same value from what the artefact carries, and had nothing to build. It is moved to sisudoc.ocda.meta.doc_matters as docMattersMake(), taking the seven things a document is described by: the run (program_info, opt_action), where it is and what it is called (manifest), its header and the site config (conf_make_meta), its counts and indexes (ST_DocHas), its source digests, and the files its markup inserted. The parser fills them from what it has just parsed; the loader will fill them from the artefact. Pure refactor: the struct's members are unchanged, only where it is declared and where its inputs come from. (assisted by Claude-Code)
* ocda: heading cross reference linkingRalph Amissah2026-09-127-25/+76
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The segment a cross reference to a heading lands on, gixed upstream in ocda, where the field is created, rather than inferred by the loader. A heading above level 4 opens no html segment of its own, so a link to it has to land on the level 4 heading that follows. The build loop cannot know that when it reads the heading, and says so in a comment of its own: "for html segname need following lv4 not yet known". It back-fills tag_assoc when the level 4 heading arrives (lv0to3_tags), and the answer then lives only in that map. tags.segment_lv4_is now holds it, resolved in a pass over the finished head and body sections: walking backwards, the last level 4 heading seen is the next one for everything above it, which is the same answer the back-fill gives. Emitted in the .ssp only where it differs from .segment_html_is, which is every heading at level 4 or below, so it is sparse: 317 lines over the 35 document reference set, none removed. Carried in the database as a column of its own, and read back by both readers. docHasFromAbstraction reads it instead of working it out. With that, and on top of the two defect fixes, a document loaded from an artefact and one parsed from markup agree: key sets identical, all 35 documents values 3 entries differ of some 30,000, all of them _the_title, where the abstraction supplies an epub segment the parser leaves unset link targets all 1,651 agree, against 89 differing before any of this work and 1 after the defect fixes alone Format stays v1.1. That version is new in this same run of work and nothing outside spine has read it, so this belongs in it rather than in a bump of its own. (assisted by Claude-Code)
* ocda: ST_DocHas from a loaded abstractionRalph Amissah2026-09-122-0/+270
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | docHasFromAbstraction() builds ST_DocHas from the objects and the @doc_has block, so a document read from a .ssp or a .ocda.db can reach the value the parser reaches. Everything it builds is an index over properties the artefact already carries, not a recomputation of something it does not: the counts come from the header, imagelist from the .image records, the segment name lists and the tag associations from .anchor, .segment, .segment_epub, .heading_lev_anchor and .segment_*_is, and section_keys_sequenced from which sections are non-empty plus the run's own flags. *The two defect fixes this now sits on did most of the closing.* Measured before them and after, over some 30,000 tag_associations entries and the 1,651 keys a document actually links to: imagelist 8 documents differed, now none. The parser was the one that was wrong, and rgx.image being anchored to image markup brought it into line with what the .image records always said. key sets four keys differed (_part_eof, "0", the empty key, and "toc" the other way about), now none. _part_eof came back the moment @tail was carried, and the rest with it. values 89 link targets resolved differently, now 1. The one left is free_culture's ocn 5, and it is the case this cannot reach: a heading above level 4 takes its segment by back-filling when the next level 4 heading arrives, so the object never holds the answer and reading the objects in order only approximates it. Fixed properly in the commit that follows, in ocda where the field is made, rather than guessed at here. (assisted by Claude-Code)
* ocda: what a document has, as plain dataRalph Amissah2026-09-122-58/+63
| | | | | | | | | | | | | | | | | | | | ST_DocHas in metadoc_object_setter, filled by the parser, in place of DocHas_, a struct of accessors nested inside docAbstraction() that closed over the parser's locals. The member names are the ones the output writers already use, so nothing downstream changes. The point is that a struct closing over parser locals can only ever be built by the parser. A document loaded from a .ssp or a .ocda.db has to arrive at the same value with only what the artefact carries, and now there is something for it to fill. Two small things fall out. imagelist is a string[] rather than the lazy uniq range it was, and images() is therefore imagelist.length rather than counting the commas in the range's string representation, which carried a "TODO not ideal rethink". Same answer unless a filename contains a comma; the reference test agrees on all 35. (assisted by Claude-Code)
* ocda: reading .ocda.db now faster than parsing markupRalph Amissah2026-09-122-74/+111
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | reading a .ocda.db was slower than parsing markup source. Taking the largest markup document sample War and peace, 12,135 objects, optimised build, before this commit and after: parse markup 0.87 s 0.87 s load .ssp 0.047 s 0.047 s load .ocda.db 1.09 s 0.128 s The database goes from being 1.25x slower than parsing the document to 6.8x faster. Two things fixed in the reader were: First, four queries per object. object_images, object_links, object_anchors and object_subtoc were queried per object as the objects were built, each statement compiled fresh from a concatenated string. For war and peace that is 48,540 statement preparations to collect 138 rows, which is all those four tables hold between them. They are now four ordered sweeps, kept by object id, so the cost is what the tables hold rather than what the document holds. Second, and even more consequentially: d2sqlite3's row["name"] is indexForName, a linear scan over the statement's columns that calls sqlite3_column_name and allocates a D string for every column it passes. At some fifty named reads per object over forty columns that is around a thousand of those per object, twelve million for the document. The column name to index map is now resolved once per statement and the reads are an integer index. Also here, since it was measured while doing this: sspReadFile no longer hashes the file it read unless asked (with_digest). Only the database writer wants that digest and it has the lines already, so every other read was paying for it. Existing outputs unaffected. (assisted by Claude-Code)
* ocda: the abstraction carries the document metadata output readsRalph Amissah2026-09-125-75/+207
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | .ssp format v0.1 -> v1.1, and the .ocda.db with it: they are two serialisations of one format and now move together. The gap this closes, measured against the writers' read set: of the 59 conf_make_meta fields outputs/io_out/ reads, 29 were in neither artefact. Five are conf.* and stay out, being site and run scoped (urls, papersize, the search db filename): the same abstraction published to two sites must take each site's. Of the rest, three are never assigned anywhere (title_short, publisher, original_publisher; read only by sqlite.d, always empty) and one is a copy of a field already carried (title_sub = title_subtitle), so 17 properties actually had to travel and now do: make breaks, footer, home_button_text meta title.edition, date.added_to_site, language.document_char, original.{title,source,language,language_char}, rights.copyright_{text,translation,illustrations, photographs,cover,audio,video} make splits by when it acts, which is worth keeping in mind: these three are read in outputs/io_out/ and must travel, while italics, bold, emphasis, substitute and headings are read at parse time and their effect is already in the objects. New @source block, and source.* rows in the database, so a reader can say which markup an abstraction came from rather than working from stale content in silence: language the document's own languages the pod's list, which is what the inter-language links in html need and neither artefact carried digest sha256 of the .sst; equals its digests.txt entry The database adds source.ssp_digest, the sha256 of the .ssp it was built from, since a file cannot hold its own hash. So the chain .sst -> .ssp -> .ocda.db is checkable end to end. The database's metadata table is now filled from the header blocks the .ssp gives back, not from a second list read off doc_matters. One list, in ssp.d: a property added there arrives in the database with nothing else changed, and one that is not in the .ssp cannot be in the database at all. That was the last place the two could drift; the objects stopped being able to on 2026-09-07. Version is checked on load. The major part must match, a newer minor is accepted (a minor bump only adds properties, and an unknown property line is ignored). A v0.1 artefact is now refused with a message saying to regenerate it, rather than loading half populated. test-abstraction-db.sh now compares the two artefacts' header blocks property by property, 36 per document on the wealth of networks, where it previously only checked that a schema.version row existed. Verified non-vacuous: dropping one property is reported. Reference regenerated, and the whole diff is this change and nothing else: 320 lines added, 35 removed over 35 files, being 35 format lines changed, 35 @source blocks (4 lines each), 35 language.document_char, 35 home_button_text (it has a default), 18 breaks, 16 footer, 4 date.added_to_site, 1 title.edition, 1 original.source. Existing outputs unaffected. (assisted by Claude-Code)
* ocda: .ssp should have all sections of abstractionRalph Amissah2026-09-122-3/+5
| | | | | | | The abstraction has nine sections and includes "tail". Both artefact writers hardcoded a list of eight, so "tail" was silently dropped. (assisted by Claude-Code)
* ocda: get image from is internal image markupRalph Amissah2026-09-123-6/+18
| | | | | | | | | | correct circumstance where regex was incorrectly able to match image shaped words as well as identified and marked up image (internal markup). take all matches. (assisted by Claude-Code)
* --serial default behaviour (--parallel an option)Ralph Amissah2026-09-101-31/+25
| | | | | | | | | | serial processing, it turns out, is significantly faster and more efficient for tested use-cases, which came as a surprise. As the parallelization option buys nothing, serial processing is set as default. Parallel processing remains as an option (where available, as before). (assisted by Claude-Code)
* parallelise: show_abstraction & ocda_db as the restRalph Amissah2026-09-091-0/+9
| | | | | | | | | | | | | | | | | | | | | | | | Fix issue with consistency (flags run serial & parallel inconsistenly). Both write one file per document per language and share no handle, so they belong on the list with html, epub, text and sqlite_discrete. The guard above the list still takes out --pod, --pod2, --source and the shared sqlite db actions before it is reached, so those stay serial as they were; checked. However measurement tests show parallel turn out to result in a processing slowdown, on the (35) sample markup documents, run on multiple passes: run on 16 cores a slow down of about 20% for eleven times the cpu! --text 4.83-5.10 s wall 48-52 s user --text --serial 3.94-4.05 s wall 4.2-4.4 s user will make serial run the default. (assisted by Claude-Code)
* ssp: abstraction directory cleared onceRalph Amissah2026-09-091-13/+33
| | | | | | | | | | | | | | | | | abstraction directory cleared once, by whichever language is first The .ssp writer clears stale files out of pod/<doc>/media/abstraction/, and that one directory is shared by every language of a document. The clearing is now done once per directory per run, by whichever language reaches it first, with the lock held across it. A language that finds the directory already prepared has passed through that same lock before writing, so the clearing it skipped had completed before its own write began: no .ssp produced on a given run can be removed during it. (removes possibility of a race condition on parallelisation) (assisted by Claude-Code)
* sqlite: schema version, & fail run on writes failRalph Amissah2026-09-092-4/+67
| | | | | | | | | | | | | | | | | | | | | | | | | Spine now declares sqlite_db_schema_version and stamps it into the database as PRAGMA user_version when the tables are created, in both the shared and the discrete DDL blocks. On opening an existing database it compares, and says once per run which version it found and which it writes. Failures are now tallied (shared, the output can run in parallel), reported one line each on stderr naming the operation, and main exits 1 without printing "run complete, ok". Two tests under test/, both taking the spine binary as their first argument and building their own database from data/pod unless $SpinePOD says otherwise: test-search-db-schema.sh names: every column the search form uses exists, and spine's declaration, the database's stamp and the search form's expectation all agree test-search-cgi.sh behaviour: the real search binary answers real requests against a fresh database, no web server involved, the probe values read out of whichever database it is given (assisted by Claude-Code)
* html metadata: a link to the ocda.dbRalph Amissah2026-09-092-4/+27
| | | | | | | The metadata page gains a line between the markup source and the source digests. (assisted by Claude-Code)
* --ocda-db replaces --show-abstraction-dbRalph Amissah2026-09-095-23/+43
| | | | | | | | | ocda (object centric document abstraction) flag --ocda-db (or --abstraction-db) replaces --show-abstraction-db rename results in consequently large diff
* output: the abstraction artefacts live with the podRalph Amissah2026-09-094-28/+106
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | pod/ holds all document source representations: pod/<doc>/ source tree pod/<doc>/media/abstraction/<uid>.ssp abstraction, as text pod/<doc>.zip tree, zipped, .ssp included pod/<doc>.digests.txt sha256s of what is in them pod/<uid>.ocda.db abstraction, as sqlite db <lang>/abstraction/ is gone. pod/<doc>/media/abstraction/<uid>.ssp preferred as having the images (found within the pod tree) which .ssp needs to reproduce a document but does not carry on its own. The .ocda.db sits carries the images as well and (like the pod.zip) can be used to reproduce a document directly. digests.txt now covers the database as well as the zip, the source and the .ssp; (as does the metadata html page). Two ordering issues addressed: - the pod builder clean-slates pod/<doc>/ before regenerating it, and the .ssp is now written before that runs. The clean slate now leaves media/abstraction/ alone, and the .ssp writer clears that directory itself on the first language of a run, so a .ssp for a language the document no longer is removed and cannot be bundled. - for a multi-language document the .ssp files accumulate one language at a time and are bundled on the last, which is why the directory cannot simply be emptied by whichever (language) gets there first. (assisted by Claude-Code)
* ocda loader: one way in whatever the sourceRalph Amissah2026-09-093-0/+224
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | sisudoc.ocda.abstraction.load names the five things a document can be read from, tells them apart, and loads the two that are self-describing artefacts: .sst / .ssm + images the markup source pod (dir) + images the same, bundled pod .zip the same, zipped .ssp + images the abstraction, as text .ocda.db the abstraction, sqlite, images inside abstractionSourceOf(path) is the detection, by name and for a directory by whether it holds pod.manifest. abstractionLoad(path) returns a LoadedAbstraction: the source kind, whether it was loaded, why not when it was not, and the document itself. The three source forms are deliberately not loaded here. Reading them is the parser's job (sisudoc.ocda.meta.metadoc spineAbstraction) and it needs the manifest, environment and configuration that spine.d assembles, none of which belongs in a loader. What this gives that case is the dispatch and a plain statement of where it is handled, rather than a silent empty result. spine --abstraction-source=<path> says what a path is and, for an artefact, loads it and reports what came back: the document, title and author, header block sizes, object counts and the objects in each section. Exit 0 when an abstraction was loaded, 1 when not. (assisted by Claude-Code)
* ocda db: <doc>.ocda.db, and a script to run 4 testsRalph Amissah2026-09-093-4/+4
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The per document database is now written as <doc_uid>.ocda.db rather than <doc_uid>.abstraction.db. Shorter, and it says what is in the file: the object centric document abstraction, not "some abstraction". .ocda.db pairs with .ssp and cannot be mistaken for the collection search database (spine.search.db). test/run-tests.sh runs the four in sequence, one line of result each, and a summary. It re-runs itself inside nix shell "nixpkgs#sqlite" if sqlite3 is not on PATH, so this is all that is needed: SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ ./test/run-tests.sh ./bin/spine-ldc The tests are independent (with test-abstraction-ssp.sh run first): 1 test-abstraction-ssp.sh is first because it is the one that says whether the abstraction itself moved; if it fails the others are answering a different question than you think 2 test-abstraction-ssp-roundtrip.sh reads the committed reference set, so it is a statement about the current binary only if 1 passes 3 test-abstraction-db.sh and 4 test-abstraction-db-roundtrip.sh generate both artefacts themselves and depend on nothing committed (assisted by Claude-Code)
* ocda db: built from the .ssp, (tethered)Ralph Amissah2026-09-094-10/+49
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The ocda database is now built from the .ssp itself: the writer's lines are emitted, read straight back by ssp_in, and the objects that come out populate the database. Anything the .ssp does not carry, the database will not have either, by construction (rather than by test). [instead of as previously through a second walk over the in-memory abstraction] - spineAbstractionTxt is split: sspDocumentLines(doc) returns the whole .ssp as lines, and the file writer emits them. Output neutral, the reference test confirms. - spineAbstractionDb takes the abstraction as an argument rather than taking doc.abstraction. - sspRoundTripAbstraction(doc) in ssp_in is the join: lines out, lines in, abstraction returned. Both call sites in spine.d use it. - the header blocks and the image blobs still come from doc_matters (as: the .ssp does not carry image bytes). All (35) markup sample sourced databases built through the .ssp have byte identical SQL dumps to the one built directly before the change. That comparison also found one reader inaccuracy, which the .ssp round trip could not see because the writer omits the field either way: an absent identifier was restored as the ocn in every case, but for an object with no ocn it was empty ("a"~N identifiers are always written). Fixed; the two artefacts checking each other is what caught it. (assisted by Claude-Code)
* a reader for the ocda db, and its round tripRalph Amissah2026-09-093-0/+307
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | sisudoc.ocda.abstraction.db_in reads a <doc>.abstraction.db back into ObjGenericComposite[][string], the same value ssp_in returns from a .ssp, so a consumer need not know which artefact it was handed. It mixes in the .ssp reader for that shared document struct rather than declaring a second one. --db-round-trip=<file.abstraction.db> reads a database and emits it as .ssp on stdout, through sspObjectRecord as the other round trip does. Held against the .ssp written from the same document, this says whether the two artefacts really carry the same thing: not a count of fields, as test-abstraction-db.sh does, but the whole document reconstructed from the database and compared to the text. SpinePOD=... ./test/test-abstraction-db-roundtrip.sh ./bin/spine-ldc PASS: all 35 databases re-emit their document's .ssp exactly It passed on the first run over the whole sample set, which is evidence that the database is now field-complete against the .ssp rather than merely counting the same. Four tests with different checks: test-abstraction-ssp.sh the abstraction has not changed test-abstraction-db.sh the two serialisations agree, field by field test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole test-abstraction-db-roundtrip.sh the .db can be read back whole (assisted by Claude-Code)