diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-21 16:12:56 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-23 08:22:15 -0400 |
| commit | 8b81e9a11c7af642c4d0d8584a9cb348cc887d6e (patch) | |
| tree | 8ba9c356ff71bf18f5623c22d30a8b2513321cfe | |
| parent | uid and paths separator "~" in place of ":" (diff) | |
ocda: warn if heading claims reserved segment name
spine automatically builds some segments, including: (toc, endnotes,
glossary, bibliography, bookindex, blurb, _the_title) these are now
identified as reserved names and a user is now warned if any of these
names have been manually assigned to a heading by markup. A document
still builds but to disambiguate the ocn of the heading is attached to
the markup (reserved) name and this is seeded before a document not
after. It is read off the finished abstraction rather than reported by
the parser, and beside the ocn alignment check for the same reason: it
is a statement about a document rather than a step in building one.
WARNING reserved segment name: the_autonomous_contract... [en]
heading 1 at ocn 135 asks for "endnotes", which spine gives its
own generated section
it is named "endnotes-135" instead; ...
A warning: the document is correct and complete and the name it ends
up with works.
--strict makes it a failure for the run, as it does for ocn alignment,
and by the same reasoning: the outputs are written and can be looked at,
and the exit status is taken at the end.
Two of the thirty-six sample documents have reserved segment names,
"1~endnotes" heading. Output is unchanged: nothing here touches the
abstraction.
(assisted by Claude-Code)
| -rw-r--r-- | org/ocda.org | 12 | ||||
| -rw-r--r-- | org/ocda_functions.org | 15 | ||||
| -rw-r--r-- | org/ocda_reserved.org | 212 | ||||
| -rw-r--r-- | org/spine.org | 58 | ||||
| -rw-r--r-- | org/tests_for_document_abstraction_shell_scripts.org | 194 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/metadoc_from_src.d | 12 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/metadoc_from_src_functions.d | 15 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/reserved_names.d | 206 | ||||
| -rw-r--r-- | src/sisudoc/spine.d | 58 | ||||
| -rwxr-xr-x | test/run-tests.sh | 29 | ||||
| -rwxr-xr-x | test/test-run-order.sh | 142 |
11 files changed, 903 insertions, 50 deletions
diff --git a/org/ocda.org b/org/ocda.org index a9f0368..b84b3ef 100644 --- a/org/ocda.org +++ b/org/ocda.org @@ -187,6 +187,18 @@ scope(exit) { anchor_tag = ""; _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); } +/+ ↓ seeded here, on the way in, which is the seeding that matters. + . + The two assignments in scope(exit) and at the tail of this function run + after a document has been abstracted, so they seed the *next* one. The + first document of a run had nothing before it and began with the set as + declared, empty: it alone was not held to the reserved names, and a heading + of its own claiming "endnotes" kept the name of the generated section. + Which document that was depended on what else was in the run, so a + document's abstraction depended on its company, and a single document + re-parsed to check an artefact made in a collection run disagreed with it. ++/ +_heading_anchor_tags_seen = _reserved_heading_anchor_tags(); mixin spineNode; int[string] node_para_int_ = node_metadata_para_int; string[string] node_para_str_ = node_metadata_para_str; diff --git a/org/ocda_functions.org b/org/ocda_functions.org index db6d63b..a5b60fa 100644 --- a/org/ocda_functions.org +++ b/org/ocda_functions.org @@ -70,16 +70,15 @@ bool[string] _heading_anchor_tags_seen; these names means the document's heading is disambiguated the same way a repeated anchor tag is, and the generated section keeps the name that the table of contents links to. + . + The list itself is in sisudoc.ocda.meta.reserved_names, with the + check that reports such a heading to its author. Two lists would be + two answers to what spine has taken. +/ bool[string] _reserved_heading_anchor_tags() { - bool[string] _reserved; - foreach (_name; [ - "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb", - "_the_title", - ]) { - _reserved[_name] = true; - } - return _reserved; + import sisudoc.ocda.meta.reserved_names; + mixin spineReservedNames _rn; + return _rn.reservedHeadingAnchorTags(); } string lev_anchor_tag; string[string][string] tag_assoc; diff --git a/org/ocda_reserved.org b/org/ocda_reserved.org new file mode 100644 index 0000000..eddf455 --- /dev/null +++ b/org/ocda_reserved.org @@ -0,0 +1,212 @@ +-*- mode: org -*- +#+TITLE: sisudoc spine (doc_reform) object-centric document abstraction +#+DESCRIPTION: documents - structuring, publishing in multiple formats & search +#+SUMMARY: process markup document, create document abstraction +#+FILETAGS: :spine:abstraction: +#+AUTHOR: Ralph Amissah +#+EMAIL: [[mailto:ralph.amissah@gmail.com][ralph.amissah@gmail.com]] +#+COPYRIGHT: Copyright (C) 2015 (continuously updated, current 2026) Ralph Amissah +#+LANGUAGE: en +#+STARTUP: content hideblocks hidestars noindent entitiespretty +#+PROPERTY: header-args+ :eval never-export :exports code +#+PROPERTY: header-args+ :noweb yes :padline no +#+PROPERTY: header-args+ :results silent :cache no +#+PROPERTY: header-args+ :mkdirp yes +#+OPTIONS: H:3 num:nil toc:t \n:t ::t |:t ^:nil -:t f:t *:t +- magic single double-quote → " ← FIX changes hilighting behavior (occuring + after it) in org document. INVESTIGATE (org-mode CONFIG?) FIND & FIX + +- [[./doc-reform.org][doc-reform.org]] [[./][org/]] + +* reserved file names + +#+HEADER: :tangle "../src/sisudoc/ocda/meta/reserved_names.d" +#+HEADER: :noweb yes +#+BEGIN_SRC d +<<doc_header_including_copyright_and_license>> +/++ + the segment names spine keeps for itself<br><br> + + the one list of them, and the check that says when a document's own + heading has claimed one<br><br> + + [sisudoc.ocda.meta.reserved_names] ++/ +module sisudoc.ocda.meta.reserved_names; +@safe: +/+ ↓ spine names the sections it generates, these names are reserved. + . + The table of contents, the gathered endnotes, the glossary, the + bibliography, the book index and the blurb are not in the markup: spine + makes them, and gives each a segment name of its own. A document whose + own heading asks for one of those names ("1~endnotes Endnote") is asking + for a name that is already taken, and before this was noticed both wrote + into the same file: the generated section and the author's heading, with + the navigation listing that file twice, the second time behind its own + earlier entry in reading order. + . + The parser therefore seeds the set of heading anchor names it has seen + with these before it reads a document, so that a heading claiming one is + disambiguated by ocn suffix the same way a repeated anchor tag is, and + the generated section keeps the name the table of contents links to. + . + Two things live here rather than in the parser: + . + the list so that the parser's seeding and the check below cannot + come to hold different ideas of what is reserved; + . + the check which runs over a finished abstraction, names the + headings that were renamed, and is what tells an author + that a heading of theirs is not called what they wrote. + A warning: the document is correct and complete, and the + name it ends up with works. --strict makes it a failure, + for the run that is meant to be publishable. + . + The renaming is not something the author has to accept. A heading given + any other anchor name ("1~my-notes Endnote") keeps it, and the warning + goes away. ++/ +/+ ↓ mixed in as a named mixin (mixin spineReservedNames _name;) and its + imports are inside its functions rather than at template scope: what a + mixin template declares at its own scope is visible in the scope it is + mixed into, and an import there can hijack a name the host was already + resolving by UFCS. ++/ +template spineReservedNames() { + /+ ↓ the names spine makes for itself, in one place. + _the_title is the head section's own; the rest are the generated + sections, and are the section keys of the abstraction. + +/ + string[] reservedHeadingAnchorNames() { + return [ + "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb", + "_the_title", + ]; + } + /+ ↓ the same, as the set the parser seeds itself with +/ + bool[string] reservedHeadingAnchorTags() { + bool[string] _reserved; + foreach (_name; reservedHeadingAnchorNames()) { _reserved[_name] = true; } + return _reserved; + } + /+ ↓ one heading that asked for a name spine had already taken +/ + struct ST_ReservedNameUse { + string section; // where in the abstraction it sits + string reserved; // the name it asked for + string anchor; // the name it was given instead + string level; // as marked up, :A :B 1 2 ... + int ocn; + } + /+ ↓ was this heading renamed out of the way of a reserved name? + . + Read off the anchor rather than reported by the parser: the parser + renames by appending the object's number, so "endnotes" becomes + "endnotes-135" on the object whose ocn is 135, and that pairing is what + identifies it. Requiring the number to be the object's own is what keeps + a heading an author really did call "endnotes-135" from being reported as + something it is not. + +/ + private string _reservedNameClaimedBy(O)(O obj) { + import std.conv : to; + string _anchor = obj.tags.anchor_tag_html; + if (_anchor.length == 0) { return ""; } + string _suffix = "-" ~ obj.metainfo.ocn.to!string; + if (_anchor.length <= _suffix.length) { return ""; } + if (_anchor[($ - _suffix.length) .. $] != _suffix) { return ""; } + string _stem = _anchor[0 .. ($ - _suffix.length)]; + foreach (_name; reservedHeadingAnchorNames()) { + if (_stem == _name) { return _name; } + } + return ""; + } + /+ ↓ every heading of a document that claimed a reserved name. + document order, the section order the .ssp is written in, so that two + runs over the same document report it the same way round. + +/ + ST_ReservedNameUse[] reservedNameHeadings(D)(D doc) { + import std.algorithm : canFind, sort; + import std.conv : to; + ST_ReservedNameUse[] _out; + string[] _sections = ["head", "toc", "body", "endnotes", + "glossary", "bibliography", "bookindex", "blurb", "tail"]; + string[] _extra; + foreach (_k; doc.abstraction.byKey) { + if (!_sections.canFind(_k)) { _extra ~= _k; } + } + foreach (_k; _extra.sort) { _sections ~= _k; } + foreach (section; _sections) { + if (section !in doc.abstraction) { continue; } + foreach (obj; doc.abstraction[section]) { + if (obj.metainfo.is_a != "heading") { continue; } + string _claimed = _reservedNameClaimedBy(obj); + if (_claimed.length == 0) { continue; } + /+ ↓ the generated sections carry the reserved names themselves and are + not what this is about: they are headings spine made, and they keep + the plain name. Only an object that was renamed reaches here, and a + generated one never is. + +/ + ST_ReservedNameUse _u; + _u.section = section; + _u.reserved = _claimed; + _u.anchor = obj.tags.anchor_tag_html; + _u.level = obj.metainfo.marked_up_level; + _u.ocn = obj.metainfo.ocn.to!int; + _out ~= _u; + } + } + return _out; + } + /+ ↓ the report, as the lines to print. + the document is named once and its headings indented under it, as the ocn + alignment check does, so that a run over a collection reads as a list of + documents. + +/ + string[] reservedNameReportLines( + string doc_key, + string lang, + ST_ReservedNameUse[] _uses, + ) { + import std.conv : to; + string[] _out; + if (_uses.length == 0) { return _out; } + _out ~= "WARNING reserved segment name: " ~ doc_key + ~ ((lang.length > 0) ? " [" ~ lang ~ "]" : ""); + foreach (_u; _uses) { + _out ~= " heading " ~ _u.level ~ " at ocn " ~ _u.ocn.to!string + ~ " asks for \"" ~ _u.reserved + ~ "\", which spine gives its own generated section"; + _out ~= " it is named \"" ~ _u.anchor + ~ "\" instead; give the heading another name to keep one of your own"; + } + return _out; + } +} +#+END_SRC + +* org includes +** project version + +#+NAME: spine_version +#+HEADER: :noweb yes +#+BEGIN_SRC emacs-lisp +<<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:spine_project_version()>> +#+END_SRC + +** year + +#+NAME: year +#+HEADER: :noweb yes +#+BEGIN_SRC emacs-lisp +<<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:year()>> +#+END_SRC + +** document header including copyright & license + +#+NAME: doc_header_including_copyright_and_license +#+HEADER: :noweb yes +#+BEGIN_SRC emacs-lisp +<<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:spine_doc_header_including_copyright_and_license()>> +#+END_SRC + +* __END__ + diff --git a/org/spine.org b/org/spine.org index 49ef331..a0f0570 100644 --- a/org/spine.org +++ b/org/spine.org @@ -75,6 +75,12 @@ string program_name = "spine"; +/ mixin spineOcnAlign _ocna; _ocna.ST_OcnProfile[] _ocn_profiles; + /+ ↓ headings that claimed a name spine keeps for a generated section. + Reported as each document goes by, where the document can be named, and + remembered here only so that --strict has something to fail on. + +/ + mixin spineReservedNames _rsvd; + bool _reserved_name_used = false; /+ ↓ what verify found, gathered as the documents go by +/ size_t _verify_checked = 0; size_t _verify_failed = 0; @@ -294,6 +300,19 @@ string program_name = "spine"; import core.stdc.stdlib : exit; exit(1); } + /+ ↓ --strict and a heading that claimed a name spine keeps for itself. + The same shape as the check above: the warning has already named the + document and the heading, the output of the run is complete and can be + looked at, and this is the exit status. A publishable run should not + contain a heading called something other than what its author wrote. + +/ + if (_reserved_name_used && _opt_action.strict) { + stderr.writeln( + "~ run FAILED ~ --strict: a heading claims a reserved segment name" + ); + import core.stdc.stdlib : exit; + exit(1); + } /+ ↓ a verification that failed is a failed run, whatever else went well. --no-verify has already said its piece per document and does not reach here: it turns the failure into a warning and the output is produced @@ -348,6 +367,7 @@ import sisudoc.ocda.meta.conf_make_meta_json; import sisudoc.ocda.meta.defaults; import sisudoc.ocda.meta.doc_debugs; import sisudoc.ocda.meta.ocn_align; +import sisudoc.ocda.meta.reserved_names; import sisudoc.ocda.meta.rgx; import sisudoc.ocda.meta.rgx_yaml; import sisudoc.ocda.meta.rgx_files; @@ -830,8 +850,11 @@ Verifying: check is otherwise automatic and runs before anything is written. - --strict document checks that warn become failures, ocn - alignment between languages among them. + --strict document checks that warn become failures: ocn + alignment between languages, and a heading that + claims a segment name spine keeps for a section of + its own (toc, endnotes, glossary, bibliography, + bookindex, blurb). ┃"); /+ ↓ --help is a question answered on stdout, not a run: the scope(success) banner would be the last line of the answer @@ -2384,6 +2407,23 @@ if (doc.matters.opt.action.ocda_db) { } synchronized { _ocn_profiles ~= _ocn_p; } } +/+ ↓ a heading of this document that asked for a name spine uses for a section + of its own. Said here, where the document can be named, and per language: + the markup is per language and so is the answer. ++/ +{ + auto _rsvd_uses = _rsvd.reservedNameHeadings(doc); + if (_rsvd_uses.length > 0) { + foreach (_line; _rsvd.reservedNameReportLines( + doc.matters.src.doc_uid_out_no_lang, + doc.matters.src.language, + _rsvd_uses, + )) { + stderr.writeln(_line); + } + synchronized { _reserved_name_used = true; } + } +} #+END_SRC ***** align ocn non-synchronized (serial processing) @@ -2401,6 +2441,20 @@ if (doc.matters.opt.action.ocda_db) { } _ocn_profiles ~= _ocn_p; } +/+ ↓ reserved segment names, as above +/ +{ + auto _rsvd_uses = _rsvd.reservedNameHeadings(doc); + if (_rsvd_uses.length > 0) { + foreach (_line; _rsvd.reservedNameReportLines( + doc.matters.src.doc_uid_out_no_lang, + doc.matters.src.language, + _rsvd_uses, + )) { + stderr.writeln(_line); + } + _reserved_name_used = true; + } +} #+END_SRC ***** abstraction curate :abstraction:curate: diff --git a/org/tests_for_document_abstraction_shell_scripts.org b/org/tests_for_document_abstraction_shell_scripts.org index 41ce11e..d9cccf6 100644 --- a/org/tests_for_document_abstraction_shell_scripts.org +++ b/org/tests_for_document_abstraction_shell_scripts.org @@ -46,12 +46,13 @@ # Order, and what depends on what: # # 1 test-abstraction-ssp.sh the abstraction has not changed -# 2 test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole -# 3 test-abstraction-db.sh the two serialisations agree -# 4 test-abstraction-db-roundtrip.sh the .db can be read back whole -# 5 test-ocn-alignment.sh the languages agree on their ocns -# 6 test-acceptance-db-to-pod.sh a database gives its document back -# 7 test-epub-validity.sh the epubs a reader can actually open +# 2 test-run-order.sh nor does it depend on the run +# 3 test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole +# 4 test-abstraction-db.sh the two serialisations agree +# 5 test-abstraction-db-roundtrip.sh the .db can be read back whole +# 6 test-ocn-alignment.sh the languages agree on their ocns +# 7 test-acceptance-db-to-pod.sh a database gives its document back +# 8 test-epub-validity.sh the epubs a reader can actually open # # None of them calls another, and each can be run on its own. The order # matters only for reading the result: @@ -59,21 +60,26 @@ # - 1 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, and its failure is the one to look at. -# - 2 reads the committed reference set, so it is only a statement about +# - 2 says that a document abstracted alone is the document abstracted in +# company. 1 only ever runs the whole sample set, so an abstraction +# that depended on what else was in the run passed it; this is the +# check that would have caught that, and the one that says an artefact +# can be verified on its own. +# - 3 reads the committed reference set, so it is only a statement about # the current binary if 1 passes. Run together, they say: the abstraction # is unchanged, and this binary can read that abstraction back. -# - 3 and 4 generate both artefacts themselves and depend on nothing +# - 4 and 5 generate both artefacts themselves and depend on nothing # committed. Since the database is built from the .ssp, they are checks # on the reader rather than on two independent writers. -# - 5 is about the sample documents rather than about a serialisation: it +# - 6 is about the sample documents rather than about a serialisation: it # asserts that spine reports the two known divergences in live-manual's # translations and reports nothing about the other documents. It will # fail if those translations are mended, which is the right moment to # hear about it. -# - 6 is the acceptance test for the document source claim, and the widest +# - 7 is the acceptance test for the document source claim, and the widest # of them: it builds a document from its pod and again from the database # that build produced, and requires the two output trees to be identical. -# Where 3 and 4 check one serialisation against another, this checks a +# Where 4 and 5 check one serialisation against another, this checks a # whole document through the whole pipeline twice. If it fails and the # others pass, what broke is the carrying or the materialising rather # than the abstraction. @@ -114,6 +120,7 @@ if [ -n "$_missing" ]; then fi TESTS="test-abstraction-ssp.sh +test-run-order.sh test-abstraction-ssp-roundtrip.sh test-abstraction-db.sh test-abstraction-db-roundtrip.sh @@ -170,7 +177,7 @@ else fi #+END_SRC -** 1&2. ssp ocda abstraction +** 1-3. ssp ocda abstraction *** 1. document.ssp abstraction test/test-abstraction-ssp.sh #+HEADER: :tangle ../test/test-abstraction-ssp.sh @@ -362,7 +369,156 @@ else fi #+END_SRC -*** 2. document.ssp abstraction test/test-abstraction-ssp-roundtrip.sh +*** 2. document.ssp abstraction test/test-run-order.sh + +#+HEADER: :tangle ../test/test-run-order.sh +#+HEADER: :tangle-mode (identity #o755) +#+HEADER: :shebang "#!/usr/bin/env sh" +#+BEGIN_SRC shell +# test-run-order.sh +# +# A document's abstraction must not depend on what else is in the run. +# +# It did. The set of heading anchor names spine has seen is seeded with the +# names it keeps for its own generated sections (toc, endnotes, glossary, +# bibliography, bookindex, blurb, _the_title), and the seeding was written +# only in scope(exit) and at the tail of docAbstraction(): both run *after* a +# document, so they seeded the next one. The first document of a run began +# with the set empty and was the one document not held to the reserved names, +# and a heading of its own claiming "endnotes" kept the name of the generated +# section rather than being renamed out of its way. +# +# So the same document abstracted alone and abstracted after another differed, +# and an artefact written in a collection run could not be verified on its +# own: --ocda-verify re-parses one document, where it is necessarily first, +# and reported a correct artefact as one whose markup no longer makes it. +# +# The check: abstract two documents in one run, abstract each of them alone, +# and require the .ssp to be identical either way. The second document is the +# one that matters (it is seeded in the pair and not when alone), so the pair +# is ordered with the affected document last. +# +# $SpineRunOrderDocs overrides the pair, space separated, unqualified +# directory names within $SpinePOD. The default pair is chosen so that the +# test would fail before the fix: the_autonomous_contract has a "1~endnotes" +# heading, which is what the seeding renames. +# +# Usage: +# SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ +# ./test/test-run-order.sh ./bin/spine-ldc +# +# Exit codes: +# 0 a document abstracts the same alone as in company +# 1 it does not (the differing files are printed) +# 2 set-up problem (no $SpinePOD, no binary, no samples) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [ -z "$SpinePOD" ]; then + echo "ERROR: \$SpinePOD is not set." >&2 + echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 + exit 2 +fi +SAMPLES_RAW="$SPINE_DIR/$SpinePOD" +if [ -d "$SAMPLES_RAW" ]; then + SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" +else + SAMPLES_DIR="$SAMPLES_RAW" +fi +if [ ! -d "$SAMPLES_DIR" ]; then + echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 + exit 2 +fi + +SPINE_BIN="${1:-}" +if [ -z "$SPINE_BIN" ]; then + if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" + elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" + elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" + else + echo "ERROR: spine binary not found. Specify path as argument." >&2 + exit 2 + fi +fi + +DOCS="${SpineRunOrderDocs:-gpl2.fsf the_autonomous_contract.ralph_amissah}" + +for d in $DOCS; do + if [ ! -d "$SAMPLES_DIR/$d" ]; then + echo "ERROR: sample document not found: $SAMPLES_DIR/$d" >&2 + echo " set \$SpineRunOrderDocs to a pair that is present" >&2 + exit 2 + fi +done + +OUT_DIR="$SCRIPT_DIR/current-run-order" +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +echo "spine binary: $SPINE_BIN" +echo "samples: $SAMPLES_DIR" +echo "documents: $DOCS" + +FAILURES=0 +fail() { echo " FAIL: $*"; FAILURES=$((FAILURES + 1)); } + +# the pair, in one run +_pair_args="" +for d in $DOCS; do + _pair_args="$_pair_args $SAMPLES_DIR/$d" +done +# shellcheck disable=SC2086 +$SPINE_BIN --show-abstraction --skip-output \ + --output="$OUT_DIR/together" $_pair_args > "$OUT_DIR/together.out" 2>&1 + +# and each of them on its own +for d in $DOCS; do + $SPINE_BIN --show-abstraction --skip-output \ + --output="$OUT_DIR/alone-$d" "$SAMPLES_DIR/$d" \ + > "$OUT_DIR/alone-$d.out" 2>&1 +done + +# every .ssp the pair run produced has to have a twin, identical, in the run +# that produced it alone +_compared=0 +for f in $(find "$OUT_DIR/together" -name "*.ssp" | sort); do + _rel="${f#"$OUT_DIR/together/"}" + _alone="" + for d in $DOCS; do + if [ -f "$OUT_DIR/alone-$d/$_rel" ]; then _alone="$OUT_DIR/alone-$d/$_rel"; fi + done + if [ -z "$_alone" ]; then + fail "$_rel was produced in the pair run and by neither document alone" + continue + fi + _compared=$((_compared + 1)) + if ! cmp -s "$f" "$_alone"; then + fail "$_rel differs between the pair run and the run of its own document" + diff --unified=2 "$_alone" "$f" | head -20 | sed 's/^/ /' + fi +done + +if [ "$_compared" -eq 0 ]; then + echo "ERROR: no .ssp files were produced to compare" >&2 + rm -rf "$OUT_DIR" + exit 2 +fi + +rm -rf "$OUT_DIR" + +if [ "$FAILURES" -eq 0 ]; then + echo "PASS: $_compared .ssp identical whether the document ran alone or in company" + exit 0 +else + echo "FAIL: $FAILURES check(s)" + exit 1 +fi +#+END_SRC + +*** 3. document.ssp abstraction test/test-abstraction-ssp-roundtrip.sh #+HEADER: :tangle ../test/test-abstraction-ssp-roundtrip.sh #+HEADER: :tangle-mode (identity #o755) @@ -452,8 +608,8 @@ else fi #+END_SRC -** 3&4. db ocda abstraction -*** 3. document db ocda test/test-abstraction-db.sh +** 4&5. db ocda abstraction +*** 4. document db ocda test/test-abstraction-db.sh #+HEADER: :tangle ../test/test-abstraction-db.sh #+HEADER: :tangle-mode (identity #o755) @@ -811,7 +967,7 @@ else fi #+END_SRC * -*** 4. document db ocda test-abstraction-db-roundtrip.sh +*** 5. document db ocda test-abstraction-db-roundtrip.sh #+HEADER: :tangle ../test/test-abstraction-db-roundtrip.sh #+HEADER: :tangle-mode (identity #o755) @@ -953,7 +1109,7 @@ else fi #+END_SRC -** 5. test ocn alignment +** 6. test ocn alignment #+HEADER: :tangle ../test/test-ocn-alignment.sh #+HEADER: :tangle-mode (identity #o755) @@ -1152,7 +1308,7 @@ else fi #+END_SRC -** 6. test acceptance db to pod +** 7. test acceptance db to pod #+HEADER: :tangle ../test/test-acceptance-db-to-pod.sh #+HEADER: :tangle-mode (identity #o755) @@ -1331,7 +1487,7 @@ else fi #+END_SRC -** 7. test epub validity +** 8. test epub validity #+HEADER: :tangle ../test/test-epub-validity.sh #+HEADER: :tangle-mode (identity #o755) diff --git a/src/sisudoc/ocda/meta/metadoc_from_src.d b/src/sisudoc/ocda/meta/metadoc_from_src.d index 2731573..3670a39 100644 --- a/src/sisudoc/ocda/meta/metadoc_from_src.d +++ b/src/sisudoc/ocda/meta/metadoc_from_src.d @@ -97,6 +97,18 @@ template docAbstraction() { anchor_tag = ""; _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); } + /+ ↓ seeded here, on the way in, which is the seeding that matters. + . + The two assignments in scope(exit) and at the tail of this function run + after a document has been abstracted, so they seed the *next* one. The + first document of a run had nothing before it and began with the set as + declared, empty: it alone was not held to the reserved names, and a heading + of its own claiming "endnotes" kept the name of the generated section. + Which document that was depended on what else was in the run, so a + document's abstraction depended on its company, and a single document + re-parsed to check an artefact made in a collection run disagreed with it. + +/ + _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); mixin spineNode; int[string] node_para_int_ = node_metadata_para_int; string[string] node_para_str_ = node_metadata_para_str; diff --git a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d index 0deeb72..c5a63cd 100644 --- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d +++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d @@ -77,16 +77,15 @@ template docAbstractionFunctions() { these names means the document's heading is disambiguated the same way a repeated anchor tag is, and the generated section keeps the name that the table of contents links to. + . + The list itself is in sisudoc.ocda.meta.reserved_names, with the + check that reports such a heading to its author. Two lists would be + two answers to what spine has taken. +/ bool[string] _reserved_heading_anchor_tags() { - bool[string] _reserved; - foreach (_name; [ - "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb", - "_the_title", - ]) { - _reserved[_name] = true; - } - return _reserved; + import sisudoc.ocda.meta.reserved_names; + mixin spineReservedNames _rn; + return _rn.reservedHeadingAnchorTags(); } string lev_anchor_tag; string[string][string] tag_assoc; diff --git a/src/sisudoc/ocda/meta/reserved_names.d b/src/sisudoc/ocda/meta/reserved_names.d new file mode 100644 index 0000000..8e49ae5 --- /dev/null +++ b/src/sisudoc/ocda/meta/reserved_names.d @@ -0,0 +1,206 @@ +/+ +- Name: SisuDoc Spine, Doc Reform [a part of] + - Description: documents, structuring, processing, publishing, search + - static content generator + + - Author: Ralph Amissah + [ralph.amissah@gmail.com] + + - Copyright: (C) 2015 (continuously updated, current 2026) Ralph Amissah, All Rights Reserved. + + - License: AGPL 3 or later: + + Spine (SiSU), a framework for document structuring, publishing and + search + + Copyright (C) Ralph Amissah + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU AFERO General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see [https://www.gnu.org/licenses/]. + + If you have Internet connection, the latest version of the AGPL should be + available at these locations: + [https://www.fsf.org/licensing/licenses/agpl.html] + [https://www.gnu.org/licenses/agpl.html] + + - Spine (by Doc Reform, related to SiSU) uses standard: + - docReform markup syntax + - standard SiSU markup syntax with modified headers and minor modifications + - docReform object numbering + - standard SiSU object citation numbering & system + + - Homepages: + [https://www.sisudoc.org] + [https://www.doc-reform.org] + + - Git + [https://git.sisudoc.org/] + ++/ +/++ + the segment names spine keeps for itself<br><br> + + the one list of them, and the check that says when a document's own + heading has claimed one<br><br> + + [sisudoc.ocda.meta.reserved_names] ++/ +module sisudoc.ocda.meta.reserved_names; +@safe: +/+ ↓ spine names the sections it generates, these names are reserved. + . + The table of contents, the gathered endnotes, the glossary, the + bibliography, the book index and the blurb are not in the markup: spine + makes them, and gives each a segment name of its own. A document whose + own heading asks for one of those names ("1~endnotes Endnote") is asking + for a name that is already taken, and before this was noticed both wrote + into the same file: the generated section and the author's heading, with + the navigation listing that file twice, the second time behind its own + earlier entry in reading order. + . + The parser therefore seeds the set of heading anchor names it has seen + with these before it reads a document, so that a heading claiming one is + disambiguated by ocn suffix the same way a repeated anchor tag is, and + the generated section keeps the name the table of contents links to. + . + Two things live here rather than in the parser: + . + the list so that the parser's seeding and the check below cannot + come to hold different ideas of what is reserved; + . + the check which runs over a finished abstraction, names the + headings that were renamed, and is what tells an author + that a heading of theirs is not called what they wrote. + A warning: the document is correct and complete, and the + name it ends up with works. --strict makes it a failure, + for the run that is meant to be publishable. + . + The renaming is not something the author has to accept. A heading given + any other anchor name ("1~my-notes Endnote") keeps it, and the warning + goes away. ++/ +/+ ↓ mixed in as a named mixin (mixin spineReservedNames _name;) and its + imports are inside its functions rather than at template scope: what a + mixin template declares at its own scope is visible in the scope it is + mixed into, and an import there can hijack a name the host was already + resolving by UFCS. ++/ +template spineReservedNames() { + /+ ↓ the names spine makes for itself, in one place. + _the_title is the head section's own; the rest are the generated + sections, and are the section keys of the abstraction. + +/ + string[] reservedHeadingAnchorNames() { + return [ + "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb", + "_the_title", + ]; + } + /+ ↓ the same, as the set the parser seeds itself with +/ + bool[string] reservedHeadingAnchorTags() { + bool[string] _reserved; + foreach (_name; reservedHeadingAnchorNames()) { _reserved[_name] = true; } + return _reserved; + } + /+ ↓ one heading that asked for a name spine had already taken +/ + struct ST_ReservedNameUse { + string section; // where in the abstraction it sits + string reserved; // the name it asked for + string anchor; // the name it was given instead + string level; // as marked up, :A :B 1 2 ... + int ocn; + } + /+ ↓ was this heading renamed out of the way of a reserved name? + . + Read off the anchor rather than reported by the parser: the parser + renames by appending the object's number, so "endnotes" becomes + "endnotes-135" on the object whose ocn is 135, and that pairing is what + identifies it. Requiring the number to be the object's own is what keeps + a heading an author really did call "endnotes-135" from being reported as + something it is not. + +/ + private string _reservedNameClaimedBy(O)(O obj) { + import std.conv : to; + string _anchor = obj.tags.anchor_tag_html; + if (_anchor.length == 0) { return ""; } + string _suffix = "-" ~ obj.metainfo.ocn.to!string; + if (_anchor.length <= _suffix.length) { return ""; } + if (_anchor[($ - _suffix.length) .. $] != _suffix) { return ""; } + string _stem = _anchor[0 .. ($ - _suffix.length)]; + foreach (_name; reservedHeadingAnchorNames()) { + if (_stem == _name) { return _name; } + } + return ""; + } + /+ ↓ every heading of a document that claimed a reserved name. + document order, the section order the .ssp is written in, so that two + runs over the same document report it the same way round. + +/ + ST_ReservedNameUse[] reservedNameHeadings(D)(D doc) { + import std.algorithm : canFind, sort; + import std.conv : to; + ST_ReservedNameUse[] _out; + string[] _sections = ["head", "toc", "body", "endnotes", + "glossary", "bibliography", "bookindex", "blurb", "tail"]; + string[] _extra; + foreach (_k; doc.abstraction.byKey) { + if (!_sections.canFind(_k)) { _extra ~= _k; } + } + foreach (_k; _extra.sort) { _sections ~= _k; } + foreach (section; _sections) { + if (section !in doc.abstraction) { continue; } + foreach (obj; doc.abstraction[section]) { + if (obj.metainfo.is_a != "heading") { continue; } + string _claimed = _reservedNameClaimedBy(obj); + if (_claimed.length == 0) { continue; } + /+ ↓ the generated sections carry the reserved names themselves and are + not what this is about: they are headings spine made, and they keep + the plain name. Only an object that was renamed reaches here, and a + generated one never is. + +/ + ST_ReservedNameUse _u; + _u.section = section; + _u.reserved = _claimed; + _u.anchor = obj.tags.anchor_tag_html; + _u.level = obj.metainfo.marked_up_level; + _u.ocn = obj.metainfo.ocn.to!int; + _out ~= _u; + } + } + return _out; + } + /+ ↓ the report, as the lines to print. + the document is named once and its headings indented under it, as the ocn + alignment check does, so that a run over a collection reads as a list of + documents. + +/ + string[] reservedNameReportLines( + string doc_key, + string lang, + ST_ReservedNameUse[] _uses, + ) { + import std.conv : to; + string[] _out; + if (_uses.length == 0) { return _out; } + _out ~= "WARNING reserved segment name: " ~ doc_key + ~ ((lang.length > 0) ? " [" ~ lang ~ "]" : ""); + foreach (_u; _uses) { + _out ~= " heading " ~ _u.level ~ " at ocn " ~ _u.ocn.to!string + ~ " asks for \"" ~ _u.reserved + ~ "\", which spine gives its own generated section"; + _out ~= " it is named \"" ~ _u.anchor + ~ "\" instead; give the heading another name to keep one of your own"; + } + return _out; + } +} diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d index 18beda2..c1a3f63 100644 --- a/src/sisudoc/spine.d +++ b/src/sisudoc/spine.d @@ -72,6 +72,7 @@ import sisudoc.ocda.meta.conf_make_meta_json; import sisudoc.ocda.meta.defaults; import sisudoc.ocda.meta.doc_debugs; import sisudoc.ocda.meta.ocn_align; +import sisudoc.ocda.meta.reserved_names; import sisudoc.ocda.meta.rgx; import sisudoc.ocda.meta.rgx_yaml; import sisudoc.ocda.meta.rgx_files; @@ -438,8 +439,11 @@ Verifying: check is otherwise automatic and runs before anything is written. - --strict document checks that warn become failures, ocn - alignment between languages among them. + --strict document checks that warn become failures: ocn + alignment between languages, and a heading that + claims a segment name spine keeps for a section of + its own (toc, endnotes, glossary, bibliography, + bookindex, blurb). ┃"); /+ ↓ --help is a question answered on stdout, not a run: the scope(success) banner would be the last line of the answer @@ -1681,6 +1685,12 @@ Verifying: +/ mixin spineOcnAlign _ocna; _ocna.ST_OcnProfile[] _ocn_profiles; + /+ ↓ headings that claimed a name spine keeps for a generated section. + Reported as each document goes by, where the document can be named, and + remembered here only so that --strict has something to fail on. + +/ + mixin spineReservedNames _rsvd; + bool _reserved_name_used = false; /+ ↓ what verify found, gathered as the documents go by +/ size_t _verify_checked = 0; size_t _verify_failed = 0; @@ -1854,6 +1864,23 @@ Verifying: } synchronized { _ocn_profiles ~= _ocn_p; } } + /+ ↓ a heading of this document that asked for a name spine uses for a section + of its own. Said here, where the document can be named, and per language: + the markup is per language and so is the answer. + +/ + { + auto _rsvd_uses = _rsvd.reservedNameHeadings(doc); + if (_rsvd_uses.length > 0) { + foreach (_line; _rsvd.reservedNameReportLines( + doc.matters.src.doc_uid_out_no_lang, + doc.matters.src.language, + _rsvd_uses, + )) { + stderr.writeln(_line); + } + synchronized { _reserved_name_used = true; } + } + } if (doc.matters.opt.action.curate) { auto _hvst = spineMetaDocCurate!()(doc.matters, hvst); if ( @@ -2058,6 +2085,20 @@ Verifying: } _ocn_profiles ~= _ocn_p; } + /+ ↓ reserved segment names, as above +/ + { + auto _rsvd_uses = _rsvd.reservedNameHeadings(doc); + if (_rsvd_uses.length > 0) { + foreach (_line; _rsvd.reservedNameReportLines( + doc.matters.src.doc_uid_out_no_lang, + doc.matters.src.language, + _rsvd_uses, + )) { + stderr.writeln(_line); + } + _reserved_name_used = true; + } + } if (doc.matters.opt.action.curate) { auto _hvst = spineMetaDocCurate!()(doc.matters, hvst); if ( @@ -2288,6 +2329,19 @@ Verifying: import core.stdc.stdlib : exit; exit(1); } + /+ ↓ --strict and a heading that claimed a name spine keeps for itself. + The same shape as the check above: the warning has already named the + document and the heading, the output of the run is complete and can be + looked at, and this is the exit status. A publishable run should not + contain a heading called something other than what its author wrote. + +/ + if (_reserved_name_used && _opt_action.strict) { + stderr.writeln( + "~ run FAILED ~ --strict: a heading claims a reserved segment name" + ); + import core.stdc.stdlib : exit; + exit(1); + } /+ ↓ a verification that failed is a failed run, whatever else went well. --no-verify has already said its piece per document and does not reach here: it turns the failure into a warning and the output is produced diff --git a/test/run-tests.sh b/test/run-tests.sh index d8822d4..f970194 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -23,12 +23,13 @@ # Order, and what depends on what: # # 1 test-abstraction-ssp.sh the abstraction has not changed -# 2 test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole -# 3 test-abstraction-db.sh the two serialisations agree -# 4 test-abstraction-db-roundtrip.sh the .db can be read back whole -# 5 test-ocn-alignment.sh the languages agree on their ocns -# 6 test-acceptance-db-to-pod.sh a database gives its document back -# 7 test-epub-validity.sh the epubs a reader can actually open +# 2 test-run-order.sh nor does it depend on the run +# 3 test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole +# 4 test-abstraction-db.sh the two serialisations agree +# 5 test-abstraction-db-roundtrip.sh the .db can be read back whole +# 6 test-ocn-alignment.sh the languages agree on their ocns +# 7 test-acceptance-db-to-pod.sh a database gives its document back +# 8 test-epub-validity.sh the epubs a reader can actually open # # None of them calls another, and each can be run on its own. The order # matters only for reading the result: @@ -36,21 +37,26 @@ # - 1 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, and its failure is the one to look at. -# - 2 reads the committed reference set, so it is only a statement about +# - 2 says that a document abstracted alone is the document abstracted in +# company. 1 only ever runs the whole sample set, so an abstraction +# that depended on what else was in the run passed it; this is the +# check that would have caught that, and the one that says an artefact +# can be verified on its own. +# - 3 reads the committed reference set, so it is only a statement about # the current binary if 1 passes. Run together, they say: the abstraction # is unchanged, and this binary can read that abstraction back. -# - 3 and 4 generate both artefacts themselves and depend on nothing +# - 4 and 5 generate both artefacts themselves and depend on nothing # committed. Since the database is built from the .ssp, they are checks # on the reader rather than on two independent writers. -# - 5 is about the sample documents rather than about a serialisation: it +# - 6 is about the sample documents rather than about a serialisation: it # asserts that spine reports the two known divergences in live-manual's # translations and reports nothing about the other documents. It will # fail if those translations are mended, which is the right moment to # hear about it. -# - 6 is the acceptance test for the document source claim, and the widest +# - 7 is the acceptance test for the document source claim, and the widest # of them: it builds a document from its pod and again from the database # that build produced, and requires the two output trees to be identical. -# Where 3 and 4 check one serialisation against another, this checks a +# Where 4 and 5 check one serialisation against another, this checks a # whole document through the whole pipeline twice. If it fails and the # others pass, what broke is the carrying or the materialising rather # than the abstraction. @@ -91,6 +97,7 @@ if [ -n "$_missing" ]; then fi TESTS="test-abstraction-ssp.sh +test-run-order.sh test-abstraction-ssp-roundtrip.sh test-abstraction-db.sh test-abstraction-db-roundtrip.sh diff --git a/test/test-run-order.sh b/test/test-run-order.sh new file mode 100755 index 0000000..29c1b7b --- /dev/null +++ b/test/test-run-order.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env sh +# test-run-order.sh +# +# A document's abstraction must not depend on what else is in the run. +# +# It did. The set of heading anchor names spine has seen is seeded with the +# names it keeps for its own generated sections (toc, endnotes, glossary, +# bibliography, bookindex, blurb, _the_title), and the seeding was written +# only in scope(exit) and at the tail of docAbstraction(): both run *after* a +# document, so they seeded the next one. The first document of a run began +# with the set empty and was the one document not held to the reserved names, +# and a heading of its own claiming "endnotes" kept the name of the generated +# section rather than being renamed out of its way. +# +# So the same document abstracted alone and abstracted after another differed, +# and an artefact written in a collection run could not be verified on its +# own: --ocda-verify re-parses one document, where it is necessarily first, +# and reported a correct artefact as one whose markup no longer makes it. +# +# The check: abstract two documents in one run, abstract each of them alone, +# and require the .ssp to be identical either way. The second document is the +# one that matters (it is seeded in the pair and not when alone), so the pair +# is ordered with the affected document last. +# +# $SpineRunOrderDocs overrides the pair, space separated, unqualified +# directory names within $SpinePOD. The default pair is chosen so that the +# test would fail before the fix: the_autonomous_contract has a "1~endnotes" +# heading, which is what the seeding renames. +# +# Usage: +# SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ +# ./test/test-run-order.sh ./bin/spine-ldc +# +# Exit codes: +# 0 a document abstracts the same alone as in company +# 1 it does not (the differing files are printed) +# 2 set-up problem (no $SpinePOD, no binary, no samples) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [ -z "$SpinePOD" ]; then + echo "ERROR: \$SpinePOD is not set." >&2 + echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 + exit 2 +fi +SAMPLES_RAW="$SPINE_DIR/$SpinePOD" +if [ -d "$SAMPLES_RAW" ]; then + SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" +else + SAMPLES_DIR="$SAMPLES_RAW" +fi +if [ ! -d "$SAMPLES_DIR" ]; then + echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 + exit 2 +fi + +SPINE_BIN="${1:-}" +if [ -z "$SPINE_BIN" ]; then + if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" + elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" + elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" + else + echo "ERROR: spine binary not found. Specify path as argument." >&2 + exit 2 + fi +fi + +DOCS="${SpineRunOrderDocs:-gpl2.fsf the_autonomous_contract.ralph_amissah}" + +for d in $DOCS; do + if [ ! -d "$SAMPLES_DIR/$d" ]; then + echo "ERROR: sample document not found: $SAMPLES_DIR/$d" >&2 + echo " set \$SpineRunOrderDocs to a pair that is present" >&2 + exit 2 + fi +done + +OUT_DIR="$SCRIPT_DIR/current-run-order" +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +echo "spine binary: $SPINE_BIN" +echo "samples: $SAMPLES_DIR" +echo "documents: $DOCS" + +FAILURES=0 +fail() { echo " FAIL: $*"; FAILURES=$((FAILURES + 1)); } + +# the pair, in one run +_pair_args="" +for d in $DOCS; do + _pair_args="$_pair_args $SAMPLES_DIR/$d" +done +# shellcheck disable=SC2086 +$SPINE_BIN --show-abstraction --skip-output \ + --output="$OUT_DIR/together" $_pair_args > "$OUT_DIR/together.out" 2>&1 + +# and each of them on its own +for d in $DOCS; do + $SPINE_BIN --show-abstraction --skip-output \ + --output="$OUT_DIR/alone-$d" "$SAMPLES_DIR/$d" \ + > "$OUT_DIR/alone-$d.out" 2>&1 +done + +# every .ssp the pair run produced has to have a twin, identical, in the run +# that produced it alone +_compared=0 +for f in $(find "$OUT_DIR/together" -name "*.ssp" | sort); do + _rel="${f#"$OUT_DIR/together/"}" + _alone="" + for d in $DOCS; do + if [ -f "$OUT_DIR/alone-$d/$_rel" ]; then _alone="$OUT_DIR/alone-$d/$_rel"; fi + done + if [ -z "$_alone" ]; then + fail "$_rel was produced in the pair run and by neither document alone" + continue + fi + _compared=$((_compared + 1)) + if ! cmp -s "$f" "$_alone"; then + fail "$_rel differs between the pair run and the run of its own document" + diff --unified=2 "$_alone" "$f" | head -20 | sed 's/^/ /' + fi +done + +if [ "$_compared" -eq 0 ]; then + echo "ERROR: no .ssp files were produced to compare" >&2 + rm -rf "$OUT_DIR" + exit 2 +fi + +rm -rf "$OUT_DIR" + +if [ "$FAILURES" -eq 0 ]; then + echo "PASS: $_compared .ssp identical whether the document ran alone or in company" + exit 0 +else + echo "FAIL: $FAILURES check(s)" + exit 1 +fi |
