aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-21 12:38:11 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-22 15:11:02 -0400
commitba461c98594f41d53a29bdb6bd54640cd14cc810 (patch)
treeda538f13090f757d5771fdf0c78b35dcb263425f
parentcheck: languages of a document, against each other (diff)
check: warn when document's languages ocn diverge
The check runs once every language of a document has been abstracted. Providing a warning by default. --strict makes divergence a failure, taken at the end of the run so that outputs are complete and can be examined. (the name --strict is general, so that later checks can be added without a second flag). Under --parallel the profiles are appended under synchronized, and the comparison sorts a document's languages by name rather than taking them in the order the threads finished, so two runs print the same lines in the same order. The outcome is noted per language in the database as translation.ocn_aligned, for documents that have more than one language. (assisted by Claude-Code)
-rw-r--r--org/in_abstraction_artefacts.org10
-rw-r--r--org/out_ocda_sqlite_db.org52
-rw-r--r--org/spine.org124
-rw-r--r--src/sisudoc/ocda/abstraction/db_in.d10
-rw-r--r--src/sisudoc/outputs/io_out/sqlite_ocda_db.d52
-rw-r--r--src/sisudoc/spine.d90
6 files changed, 333 insertions, 5 deletions
diff --git a/org/in_abstraction_artefacts.org b/org/in_abstraction_artefacts.org
index 6eeaf8a..6c6fca5 100644
--- a/org/in_abstraction_artefacts.org
+++ b/org/in_abstraction_artefacts.org
@@ -748,6 +748,16 @@ template spineAbstractionDbRead() {
/+ ↓ this database's own note of the .ssp it was built from. it is
not part of the .ssp, so it does not go back into the block +/
doc.ssp_digest = _v;
+ } else if (_k.startsWith("translation.")) {
+ /+ ↓ what the database knows about this language's place among the
+ others, translation.ocn_aligned being the one such key today.
+ A language cannot state it about itself: it is the outcome of a
+ comparison made when every language had been read, which is
+ after the .ssp was written. So it is in the database and not in
+ the .ssp, and putting it back into the header here would make a
+ round trip differ from the file it is held against. Read it from
+ the metadata table, which is where it is.
+ +/
} else if (_k.startsWith("source.")) {
string _kk = _k["source.".length .. $];
doc.source_info[_kk] = _v;
diff --git a/org/out_ocda_sqlite_db.org b/org/out_ocda_sqlite_db.org
index 8a42965..b75ea83 100644
--- a/org/out_ocda_sqlite_db.org
+++ b/org/out_ocda_sqlite_db.org
@@ -60,7 +60,7 @@ template spineAbstractionDb() {
pod/<doc>/ the source tree
pod/<doc>.zip the same, zipped
pod/<doc>.digests.txt what is in them
- pod/<uid>.ocda.db the abstraction, one per language
+ pod/<doc>.ocda.db the abstraction, every language of it
+/
auto pths_pod = spinePathsPods!()(doc_matters);
string base_pth = pths_pod.pod_dir_();
@@ -734,6 +734,56 @@ template spineAbstractionDb() {
db.run("COMMIT TRANSACTION");
}
}
+/+ ↓ a metadata row for one language of a database, written after the fact.
+ .
+ For the one thing a language cannot know while it is being written: how
+ it compares with its siblings. The ocn alignment check can only run when
+ every language of a document has been abstracted, which is after this
+ database has been written, so the outcome is noted here in a pass of its
+ own rather than held for by the writer.
+ .
+ A row that is about the artefact and not in the .ssp, as source.ssp_digest
+ already is. It is INSERT OR REPLACE against UNIQUE(doc_id, key), so a
+ second run over the same file restates rather than accumulates.
+ .
+ Its own template: writing one row should not instantiate the writer.
++/
+template spineOcdaDbDocNote() {
+ @trusted void spineOcdaDbDocNote(
+ string db_file,
+ string lang,
+ string key,
+ string value,
+ ) {
+ import std.file : exists;
+ import d2sqlite3;
+ if (db_file.length == 0 || !db_file.exists) { return; }
+ try {
+ auto db = Database(db_file);
+ long _doc_id = -1;
+ {
+ auto _q = db.prepare("SELECT id FROM documents WHERE lang = :lang");
+ _q.bind(":lang", lang);
+ foreach (row; _q.execute()) { _doc_id = row["id"].as!long; }
+ _q.finalize();
+ }
+ if (_doc_id < 0) { return; }
+ auto _s = db.prepare(
+ "INSERT OR REPLACE INTO metadata (doc_id, key, value)"
+ ~ " VALUES (:doc_id, :key, :value)"
+ );
+ _s.bind(":doc_id", _doc_id);
+ _s.bind(":key", key);
+ _s.bind(":value", value);
+ _s.execute();
+ _s.finalize();
+ } catch (Exception ex) {
+ import std.stdio : stderr;
+ stderr.writeln("WARNING: could not note ", key, " in ", db_file,
+ ": ", ex.msg);
+ }
+ }
+}
#+END_SRC
* org includes
diff --git a/org/spine.org b/org/spine.org
index 3158191..fce7ca6 100644
--- a/org/spine.org
+++ b/org/spine.org
@@ -67,6 +67,12 @@ string program_name = "spine";
<<spine_args_get_configuration_details>>
<<spine_args_call_output_hub>>
<<spine_args_process_args>>
+ /+ ↓ the ocn alignment profiles, one per language, compared after the loop.
+ A named mixin, so that what the template declares stays in a scope of
+ its own rather than joining main's.
+ +/
+ mixin spineOcnAlign _ocna;
+ _ocna.ST_OcnProfile[] _ocn_profiles;
if (_manifests.length > 1 // _manifests[0] initialized dummy element
&& _opt_action.abstraction) {
<<spine_initialize_output>>
@@ -76,7 +82,7 @@ string program_name = "spine";
if (!empty(manifest.src.filename)) {
<<spine_each_file_do_scope>>
<<spine_each_file_do_abstraction>>
- <<spine_each_file_do_show>>
+ <<spine_each_file_do_show_sync>>
<<spine_each_file_do_selected_output>>
<<spine_each_file_do_scope_exit>>
} else {
@@ -89,7 +95,7 @@ string program_name = "spine";
if (!empty(manifest.src.filename)) {
<<spine_each_file_do_scope>>
<<spine_each_file_do_abstraction>>
- <<spine_each_file_do_show>>
+ <<spine_each_file_do_show_no_sync>>
<<spine_each_file_do_selected_output>>
<<spine_each_file_do_scope_exit>>
} else {
@@ -98,6 +104,40 @@ string program_name = "spine";
}
}
}
+ /+ ↓ the languages of a document, held against each other.
+ .
+ Here because it is the first point at which it can be done: a language
+ cannot know whether it numbers its objects the same way as its
+ siblings until they have all been abstracted. A warning, so that a
+ document with a diverging translation still builds; --strict turns it
+ into a failure for the run that is meant to be publishable.
+ .
+ The outcome is noted per language in the database, where there is one,
+ under translation.ocn_aligned. Only for documents that have more than
+ one language: a single language document has nothing to align with,
+ and "true" there would be an answer to a question nobody asked.
+ +/
+ bool _ocn_align_failed = false;
+ if (_ocn_profiles.length > 1) {
+ auto _ocn_results = _ocna.ocnAlignCompare(_ocn_profiles);
+ foreach (_line; _ocna.ocnAlignReportLines(_ocn_results)) {
+ stderr.writeln(_line);
+ }
+ bool[string][string] _aligned; // doc_key -> lang -> aligned
+ foreach (_r; _ocn_results) {
+ _aligned[_r.doc_key][_r.lang] = _r.aligned;
+ _aligned[_r.doc_key][_r.reference_lang] = true;
+ if (!_r.aligned) { _ocn_align_failed = true; }
+ }
+ foreach (_p; _ocn_profiles) {
+ if (_p.db_file.length == 0) { continue; }
+ if (_p.doc_key !in _aligned) { continue; }
+ if (_p.lang !in _aligned[_p.doc_key]) { continue; }
+ import sisudoc.outputs.io_out.sqlite_ocda_db : spineOcdaDbDocNote;
+ spineOcdaDbDocNote!()(_p.db_file, _p.lang, "translation.ocn_aligned",
+ _aligned[_p.doc_key][_p.lang] ? "true" : "false");
+ }
+ }
/+ ↓ documents given as an abstraction artefact rather than as markup.
.ssp and .ocda.db are read, not parsed, and what comes back is the
doc the output writers already take, so this loop is the markup one
@@ -196,6 +236,18 @@ string program_name = "spine";
foreach (ref _dlr; _url_downloads) {
cleanupDownload(_dlr);
}
+ /+ ↓ --strict and a document whose languages diverged: fatal to the run.
+ At the end rather than where the check runs, so that the outputs of
+ the run are complete and can be looked at. The warning has already
+ been printed with the detail; this is the exit status.
+ +/
+ if (_ocn_align_failed && _opt_action.strict) {
+ stderr.writeln(
+ "~ run FAILED ~ --strict: ocn alignment differs between languages"
+ );
+ import core.stdc.stdlib : exit;
+ exit(1);
+ }
/+ ↓ sqlite statements that failed are fatal to the run, report and exit non-zero +/
{
import sisudoc.outputs.io_out.sqlite_collection_db : sqliteFailureTally;
@@ -235,6 +287,7 @@ import sisudoc.ocda.meta.conf_make_meta_structs;
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.rgx;
import sisudoc.ocda.meta.rgx_yaml;
import sisudoc.ocda.meta.rgx_files;
@@ -479,6 +532,7 @@ bool[string] opts = [
"section_blurb" : true,
"backmatter" : true,
"skip-output" : false,
+ "strict" : false,
"theme-dark" : false,
"theme-light" : false,
"workon" : false,
@@ -587,6 +641,7 @@ auto helpInfo = getopt(args,
"section-toc", "table of contents (default)", &opts["section_toc"],
"serial", "serial document processing (default)", &opts["serial"],
"skip-output", "skip output", &opts["skip-output"],
+ "strict", "document checks that warn become failures", &opts["strict"],
"show-abstraction", "show document abstraction (write .ssp file)", &opts["show-abstraction"],
"show-config", "show config", &opts["show-config"],
"show-curate", "show curate", &opts["show-curate"],
@@ -1086,6 +1141,9 @@ struct OptActions {
@trusted bool skip_output() {
return opts["skip-output"];
}
+ @trusted bool strict() {
+ return opts["strict"];
+ }
@trusted bool workon() {
return opts["workon"];
}
@@ -1833,7 +1891,7 @@ if ((doc.matters.opt.action.debug_do)
**** 0. abstraction
***** show document summary - abstraction _print summary_ (to screen) :abstraction:summary:
-#+NAME: spine_each_file_do_show
+#+NAME: spine_each_file_do_show_sync
#+HEADER: :noweb yes
#+BEGIN_SRC d
<<spine_each_file_do_debugs_checkdoc_0>>
@@ -1842,6 +1900,21 @@ if ((doc.matters.opt.action.debug_do)
<<spine_each_file_do_debugs_checkdoc_3>>
<<spine_each_file_show_abstraction_peg>>
<<spine_each_file_ocda_db>>
+<<spine_ocn_alignment_synchronized>>
+<<spine_each_file_do_debugs_checkdoc_4>>
+<<spine_each_file_do_debugs_checkdoc_5>>
+#+END_SRC
+
+#+NAME: spine_each_file_do_show_no_sync
+#+HEADER: :noweb yes
+#+BEGIN_SRC d
+<<spine_each_file_do_debugs_checkdoc_0>>
+<<spine_each_file_do_debugs_checkdoc_1>>
+<<spine_each_file_do_debugs_checkdoc_2>>
+<<spine_each_file_do_debugs_checkdoc_3>>
+<<spine_each_file_show_abstraction_peg>>
+<<spine_each_file_ocda_db>>
+<<spine_ocn_alignment_non_synchronized>>
<<spine_each_file_do_debugs_checkdoc_4>>
<<spine_each_file_do_debugs_checkdoc_5>>
#+END_SRC
@@ -1923,6 +1996,51 @@ if (doc.matters.opt.action.ocda_db) {
}
#+END_SRC
+***** align ocn synchronized (parallel processing)
+
+#+NAME: spine_ocn_alignment_synchronized
+#+BEGIN_SRC d
+/+ ↓ the ocn alignment profile of this language, kept for the
+ check that runs once every language of the document has been
+ abstracted. Taken whatever the outputs are: the languages of
+ a document share their object numbering whether or not
+ anything is being written.
++/
+{
+ auto _ocn_p = _ocna.ocnProfile(doc);
+ if (doc.matters.opt.action.ocda_db) {
+ import sisudoc.outputs.io_out.paths_output;
+ auto _pths_pod_ocn = spinePathsPods!()(doc.matters);
+ _ocn_p.db_file = _pths_pod_ocn.pod_dir_() ~ "/"
+ ~ doc.matters.src.doc_uid_out_no_lang ~ ".ocda.db";
+ }
+ synchronized { _ocn_profiles ~= _ocn_p; }
+}
+#+END_SRC
+
+***** align ocn non-synchronized (serial processing)
+
+#+NAME: spine_ocn_alignment_non_synchronized
+#+BEGIN_SRC d
+/+ ↓ the ocn alignment profile of this language, kept for the
+ check that runs once every language of the document has been
+ abstracted. Taken whatever the outputs are: the languages of
+ a document share their object numbering whether or not
+ anything is being written.
++/
+{
+ auto _ocn_p = _ocna.ocnProfile(doc);
+ if (doc.matters.opt.action.ocda_db) {
+ import sisudoc.outputs.io_out.paths_output;
+ auto _pths_pod_ocn = spinePathsPods!()(doc.matters);
+ _ocn_p.db_file = _pths_pod_ocn.pod_dir_() ~ "/"
+ ~ doc.matters.src.doc_uid_out_no_lang ~ ".ocda.db";
+ }
+ _ocn_profiles ~= _ocn_p;
+}
+#+END_SRC
+
+
***** abstraction curate :abstraction:curate:
- abstraction curate
diff --git a/src/sisudoc/ocda/abstraction/db_in.d b/src/sisudoc/ocda/abstraction/db_in.d
index 5535870..7f1b2c3 100644
--- a/src/sisudoc/ocda/abstraction/db_in.d
+++ b/src/sisudoc/ocda/abstraction/db_in.d
@@ -214,6 +214,16 @@ template spineAbstractionDbRead() {
/+ ↓ this database's own note of the .ssp it was built from. it is
not part of the .ssp, so it does not go back into the block +/
doc.ssp_digest = _v;
+ } else if (_k.startsWith("translation.")) {
+ /+ ↓ what the database knows about this language's place among the
+ others, translation.ocn_aligned being the one such key today.
+ A language cannot state it about itself: it is the outcome of a
+ comparison made when every language had been read, which is
+ after the .ssp was written. So it is in the database and not in
+ the .ssp, and putting it back into the header here would make a
+ round trip differ from the file it is held against. Read it from
+ the metadata table, which is where it is.
+ +/
} else if (_k.startsWith("source.")) {
string _kk = _k["source.".length .. $];
doc.source_info[_kk] = _v;
diff --git a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
index 998c317..aa91e89 100644
--- a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
+++ b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
@@ -78,7 +78,7 @@ template spineAbstractionDb() {
pod/<doc>/ the source tree
pod/<doc>.zip the same, zipped
pod/<doc>.digests.txt what is in them
- pod/<uid>.ocda.db the abstraction, one per language
+ pod/<doc>.ocda.db the abstraction, every language of it
+/
auto pths_pod = spinePathsPods!()(doc_matters);
string base_pth = pths_pod.pod_dir_();
@@ -752,3 +752,53 @@ template spineAbstractionDb() {
db.run("COMMIT TRANSACTION");
}
}
+/+ ↓ a metadata row for one language of a database, written after the fact.
+ .
+ For the one thing a language cannot know while it is being written: how
+ it compares with its siblings. The ocn alignment check can only run when
+ every language of a document has been abstracted, which is after this
+ database has been written, so the outcome is noted here in a pass of its
+ own rather than held for by the writer.
+ .
+ A row that is about the artefact and not in the .ssp, as source.ssp_digest
+ already is. It is INSERT OR REPLACE against UNIQUE(doc_id, key), so a
+ second run over the same file restates rather than accumulates.
+ .
+ Its own template: writing one row should not instantiate the writer.
++/
+template spineOcdaDbDocNote() {
+ @trusted void spineOcdaDbDocNote(
+ string db_file,
+ string lang,
+ string key,
+ string value,
+ ) {
+ import std.file : exists;
+ import d2sqlite3;
+ if (db_file.length == 0 || !db_file.exists) { return; }
+ try {
+ auto db = Database(db_file);
+ long _doc_id = -1;
+ {
+ auto _q = db.prepare("SELECT id FROM documents WHERE lang = :lang");
+ _q.bind(":lang", lang);
+ foreach (row; _q.execute()) { _doc_id = row["id"].as!long; }
+ _q.finalize();
+ }
+ if (_doc_id < 0) { return; }
+ auto _s = db.prepare(
+ "INSERT OR REPLACE INTO metadata (doc_id, key, value)"
+ ~ " VALUES (:doc_id, :key, :value)"
+ );
+ _s.bind(":doc_id", _doc_id);
+ _s.bind(":key", key);
+ _s.bind(":value", value);
+ _s.execute();
+ _s.finalize();
+ } catch (Exception ex) {
+ import std.stdio : stderr;
+ stderr.writeln("WARNING: could not note ", key, " in ", db_file,
+ ": ", ex.msg);
+ }
+ }
+}
diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d
index ab4eaa3..1286c24 100644
--- a/src/sisudoc/spine.d
+++ b/src/sisudoc/spine.d
@@ -71,6 +71,7 @@ import sisudoc.ocda.meta.conf_make_meta_structs;
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.rgx;
import sisudoc.ocda.meta.rgx_yaml;
import sisudoc.ocda.meta.rgx_files;
@@ -219,6 +220,7 @@ string program_name = "spine";
"section_blurb" : true,
"backmatter" : true,
"skip-output" : false,
+ "strict" : false,
"theme-dark" : false,
"theme-light" : false,
"workon" : false,
@@ -313,6 +315,7 @@ string program_name = "spine";
"section-toc", "table of contents (default)", &opts["section_toc"],
"serial", "serial document processing (default)", &opts["serial"],
"skip-output", "skip output", &opts["skip-output"],
+ "strict", "document checks that warn become failures", &opts["strict"],
"show-abstraction", "show document abstraction (write .ssp file)", &opts["show-abstraction"],
"show-config", "show config", &opts["show-config"],
"show-curate", "show curate", &opts["show-curate"],
@@ -799,6 +802,9 @@ string program_name = "spine";
@trusted bool skip_output() {
return opts["skip-output"];
}
+ @trusted bool strict() {
+ return opts["strict"];
+ }
@trusted bool workon() {
return opts["workon"];
}
@@ -1458,6 +1464,12 @@ string program_name = "spine";
}
}
}
+ /+ ↓ the ocn alignment profiles, one per language, compared after the loop.
+ A named mixin, so that what the template declares stays in a scope of
+ its own rather than joining main's.
+ +/
+ mixin spineOcnAlign _ocna;
+ _ocna.ST_OcnProfile[] _ocn_profiles;
if (_manifests.length > 1 // _manifests[0] initialized dummy element
&& _opt_action.abstraction) {
/+ ↓ output hub +/
@@ -1533,6 +1545,22 @@ string program_name = "spine";
mixin spineAbstractionRead;
spineAbstractionDb!()(doc, sspRoundTripDocument(doc));
}
+ /+ ↓ the ocn alignment profile of this language, kept for the
+ check that runs once every language of the document has been
+ abstracted. Taken whatever the outputs are: the languages of
+ a document share their object numbering whether or not
+ anything is being written.
+ +/
+ {
+ auto _ocn_p = _ocna.ocnProfile(doc);
+ if (doc.matters.opt.action.ocda_db) {
+ import sisudoc.outputs.io_out.paths_output;
+ auto _pths_pod_ocn = spinePathsPods!()(doc.matters);
+ _ocn_p.db_file = _pths_pod_ocn.pod_dir_() ~ "/"
+ ~ doc.matters.src.doc_uid_out_no_lang ~ ".ocda.db";
+ }
+ synchronized { _ocn_profiles ~= _ocn_p; }
+ }
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
if (
@@ -1648,6 +1676,22 @@ string program_name = "spine";
mixin spineAbstractionRead;
spineAbstractionDb!()(doc, sspRoundTripDocument(doc));
}
+ /+ ↓ the ocn alignment profile of this language, kept for the
+ check that runs once every language of the document has been
+ abstracted. Taken whatever the outputs are: the languages of
+ a document share their object numbering whether or not
+ anything is being written.
+ +/
+ {
+ auto _ocn_p = _ocna.ocnProfile(doc);
+ if (doc.matters.opt.action.ocda_db) {
+ import sisudoc.outputs.io_out.paths_output;
+ auto _pths_pod_ocn = spinePathsPods!()(doc.matters);
+ _ocn_p.db_file = _pths_pod_ocn.pod_dir_() ~ "/"
+ ~ doc.matters.src.doc_uid_out_no_lang ~ ".ocda.db";
+ }
+ _ocn_profiles ~= _ocn_p;
+ }
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
if (
@@ -1696,6 +1740,40 @@ string program_name = "spine";
}
}
}
+ /+ ↓ the languages of a document, held against each other.
+ .
+ Here because it is the first point at which it can be done: a language
+ cannot know whether it numbers its objects the same way as its
+ siblings until they have all been abstracted. A warning, so that a
+ document with a diverging translation still builds; --strict turns it
+ into a failure for the run that is meant to be publishable.
+ .
+ The outcome is noted per language in the database, where there is one,
+ under translation.ocn_aligned. Only for documents that have more than
+ one language: a single language document has nothing to align with,
+ and "true" there would be an answer to a question nobody asked.
+ +/
+ bool _ocn_align_failed = false;
+ if (_ocn_profiles.length > 1) {
+ auto _ocn_results = _ocna.ocnAlignCompare(_ocn_profiles);
+ foreach (_line; _ocna.ocnAlignReportLines(_ocn_results)) {
+ stderr.writeln(_line);
+ }
+ bool[string][string] _aligned; // doc_key -> lang -> aligned
+ foreach (_r; _ocn_results) {
+ _aligned[_r.doc_key][_r.lang] = _r.aligned;
+ _aligned[_r.doc_key][_r.reference_lang] = true;
+ if (!_r.aligned) { _ocn_align_failed = true; }
+ }
+ foreach (_p; _ocn_profiles) {
+ if (_p.db_file.length == 0) { continue; }
+ if (_p.doc_key !in _aligned) { continue; }
+ if (_p.lang !in _aligned[_p.doc_key]) { continue; }
+ import sisudoc.outputs.io_out.sqlite_ocda_db : spineOcdaDbDocNote;
+ spineOcdaDbDocNote!()(_p.db_file, _p.lang, "translation.ocn_aligned",
+ _aligned[_p.doc_key][_p.lang] ? "true" : "false");
+ }
+ }
/+ ↓ documents given as an abstraction artefact rather than as markup.
.ssp and .ocda.db are read, not parsed, and what comes back is the
doc the output writers already take, so this loop is the markup one
@@ -1794,6 +1872,18 @@ string program_name = "spine";
foreach (ref _dlr; _url_downloads) {
cleanupDownload(_dlr);
}
+ /+ ↓ --strict and a document whose languages diverged: fatal to the run.
+ At the end rather than where the check runs, so that the outputs of
+ the run are complete and can be looked at. The warning has already
+ been printed with the detail; this is the exit status.
+ +/
+ if (_ocn_align_failed && _opt_action.strict) {
+ stderr.writeln(
+ "~ run FAILED ~ --strict: ocn alignment differs between languages"
+ );
+ import core.stdc.stdlib : exit;
+ exit(1);
+ }
/+ ↓ sqlite statements that failed are fatal to the run, report and exit non-zero +/
{
import sisudoc.outputs.io_out.sqlite_collection_db : sqliteFailureTally;