Context

ADR-014 and ADR-015 set out how the CellML 2.0 conformance corpus takes every expected verdict from the specification text: one rule per file, a valid control file for each place, and every other file for that place differing from its control by exactly one edit. A self-check proves that edit, and works out each verdict again from rules written straight from the specification.

By 30 October 2026 every one of the 58 rules of Release 2.0.1 had files: 385 files in ten slices, read by libCellML, Myokit and libOpenCOR. Each file is written one way. XML lets the same document be written in many ways, and rule 1.2.3.3 of the specification goes further: two CellML files mean the same thing when they differ only in comments, in namespace declarations and prefixes, or in whitespace between elements and at the start and end of text.

On 1 November 2026, as a trial outside the repository, fourteen such rewrites were each applied to a copy of the whole corpus, and every tool read every rewritten file.

This record captures the decision to make that check part of the corpus.

Problem

A tool can give the right verdict on a file only because of the way the file happens to be written. One edit per file cannot show this. Under rule 1.2.3.3 a comment or a namespace declaration is no edit at all, so no file in the corpus can be about one.

The trial showed what that leaves out. A comment placed before the name in a ci element was enough for libCellML to accept a name that matches no variable. A comment just before <diff/> led libCellML's Analyser, and libOpenCOR, which is built on it, to call a variable overconstrained in a valid model. An extra namespace declaration on a math element, or MathML written with a prefix, turned every valid model with maths into a "W3C MathML DTD error" in libCellML and libOpenCOR.

It also showed a gap in the self-check. The self-check reads a text value exactly as written, so a valid number in a cn element with a space on each side would be judged invalid, although rule 1.2.3.3.3 makes it the same number. No file pads a value today, so no published verdict is wrong.

We need a way to check that every file keeps its verdict however it is written, as long as its meaning stays the same.

Decision

Every file will also be read written in other ways that the specification says mean the same thing, and each tool and the self-check should give every rewritten file the same verdict as the file as written. The expected answer needs no new reading of the specification: it is the verdict the corpus already gives the file.

A probe rewrites every file in one such way. There are fourteen, in four groups, each named after the text that allows it:

  • How the XML is written (the XML 1.1 Recommendation): attribute order reversed, whitespace inside tags, single and double quotes swapped, empty elements written both ways, CR LF line ends, and characters written as character references. The parsed document stays exactly the same.

  • Comments (rule 1.2.3.3.1): a comment before every tag inside the root element, and a comment inside every run of text.

  • Namespaces (rule 1.2.3.3.2): every prefix renamed, each default namespace written as a prefix, an unused declaration on every element, and a declaration removed wherever the same one is already in scope.

  • Whitespace (rule 1.2.3.3.3): whitespace added between elements, and around every text value.

Before any tool reads a rewritten file, the probe proves it changed nothing it may not: both files are parsed, and the two documents must be the same apart from what that probe may change. The header comments before the root element stay as written, since they record what each file is for. The place for rule 1.2.1.1 is left as written, control included, because the exact text of its files is what they test.

Each tool's outcome on a rewritten file (accepted, reported an issue, or stopped unexpectedly) is compared with its outcome on the file as written. Messages are kept beside each outcome but not compared, because line numbers move with the layout and a message can quote a value as written.

file as written  →  probe  →  rewritten file
                       ↓
   proof: the same document, apart from what the probe may change
                       ↓
   self-check     libCellML     Myokit     libOpenCOR
                       ↓
         the same verdict as the file as written?

The self-check. The self-check runs on every rewritten copy too, and must give every file the verdict its folder names. A tool's changed outcome is a finding for the report. A changed verdict in the self-check is a mistake in the corpus's own code, and it is put right before anything is pushed.

Text values. Rule 1.2.3.3.3 lets the whitespace at the start and end of the text in a CellML or MathML element change. The self-check will remove those whitespace characters (space, tab, carriage return and line feed) from each text value before its grammar reads it: the name in a ci, the number in a cn, and both parts of e-notation. Attribute values are still read exactly as written, since the rule is about text. Text that is only whitespace stays as written too, because the files for rule 1.2.3.2 ask which characters count as whitespace inside a CellML element. A file that differs from its control only in such whitespace then has no edit, and the one-edit check fails it: that change is a probe's job, not a file's.

Copies, not files. Rewritten copies are made when the probes run and are never committed. The repository holds the probes as code, and the report holds what they found.

The report. A new part of the report, "Same meaning, same verdict", lists each probe with the text that allows it, how many files it rewrote, and how many outcomes changed for each tool and for the self-check. Below it, every changed outcome has a row: the file, the probe, the tool, the verdict the specification expects, the outcome as written and as rewritten, and the messages.

Keeping it current. Running all fourteen probes means fourteen full runs, about 20 minutes, so they run on demand with python -m runner --probes rather than on every push. The probe report records a fingerprint: one hash of every test file and every file they import, of the probes themselves, of the code that runs each tool, of the self-check, of each tool's version and of the environment line. python -m runner --check fails when any of them has changed since, so the published report never describes files that are no longer there.

The first run. The trial of 1 November 2026 used these fourteen probes, and the first full run with python -m runner --probes, the same day, gave the same counts. Nine probes changed no outcome in any tool. Five changed outcomes in libCellML and libOpenCOR, 262 in all: comments before a ci name, inside a number and before <diff/>, and namespace declarations and prefixes on MathML. Each was confirmed with a file one change away from its corpus original. A comment before a valid name also ended libCellML's Analyser, and libOpenCOR, with a segmentation fault. Myokit kept every outcome. In the trial, the self-check kept every verdict apart from the padded values above. Reading text values as rule 1.2.3.3.3 allows, it kept every one in the full run.

This keeps the boundary ADR-014 drew: what a file is expected to do is settled from the specification before any tool reads it. A probe adds no new expectation, only the same one for the same file written another way.

Alternatives Considered

Commit the Rewritten Copies as Corpus Files

Rejected.

The trial's fourteen copies held more than 4,000 rewritten files that differ only in how they are written. They would not fit the naming by rule, and a change to any original would need up to fourteen copies changed with it. The probes as code are small, and they rewrite whatever the corpus holds.

Give Semantic Equivalence Its Own Files

Rejected.

A file with one comment or one namespace declaration more than its control would be a test of rule 1.2.3.3. But 1.2.3.3 is a definition, which the corpus tests through the rules that use it (ADR-015), and under it such a file differs from its control by no edit at all. A probe asks the same question of every file at once.

Compare Messages as Well as Outcomes

Rejected.

Line numbers move with the layout, and a message can quote a value as written, so most rewrites would change some message without changing what the tool decided. The outcome is the verdict. Messages stay in the report, where a change in what a message says can still be read.

Run Every Probe on Every Push

Rejected.

The pipeline would take about 20 minutes instead of under three, for files that rarely change now that every rule has files. The fingerprint keeps the published report current without it.

Random Changes

Rejected.

No rule says a random change keeps a file's meaning, so it has no expected verdict, and every outcome would need a new reading of the specification. Every probe here has a rule that says its change means nothing.

Reorder Elements

Rejected.

The specification never says the order of elements may change, and in MathML the order is part of the meaning: the first child of an apply element is its operator, and the rest are its arguments, in order. A probe needs a rule that allows its change.

Score Each Changed Outcome by Severity or Confidence

Rejected.

An outcome either changed or it did not. A score would lay a judgment over a plain result, and the corpus keeps its results plain, as ADR-014 does for its verdicts.

Read Attribute Values Without Their Edge Whitespace

Rejected.

Rule 1.2.3.3.3 is about text inside elements, not attribute values. An initial_value of " 1" stays a different value from "1", and section 1.3 does not count it as a real number string.

Keep Reading Text Values Exactly as Written

Rejected.

The self-check would call a valid file invalid as soon as one pads a value, which rule 1.2.3.3.3 allows. No file does that yet, so the change costs no verdict now.

Consequences

Positive

  • A second kind of evidence, with no new reading of the specification: the expected answer for every rewritten file is the verdict the corpus already gives it.

  • Probes reach what one-edit files cannot. Comments, namespace declarations and prefixes, and whitespace are no edit at all, so no file in the corpus can be about them on its own.

  • The self-check is checked the same way as the tools, so a gap in it shows before any file depends on it, as padded values did.

  • Every changed outcome is tied to one probe and one file, and a file one change from its original can confirm it.

Trade-offs

  • Every change to a file, a probe or a tool's version means running the probes again, about 20 minutes, before pushing.

  • A probe is code that rewrites files, and a mistake in it could look like a finding. The proof on every file guards against that, and a changed outcome becomes an issue candidate only after a file one change from its original shows it too.

  • One cause can change many outcomes: a namespace declaration on math changes every model with maths. The report lists every changed outcome per probe, and finding the cause stays a step done by hand.

  • Fourteen probes are a chosen set, not every way to write a file. The same verdict on all of them is evidence, not proof.

  • The place for rule 1.2.1.1 is not probed.

This decision is implemented in the CellML 2.0 Conformance Corpus, starting with the fourteen probes of 1 November 2026.

Implementation Evidence

The Probes
Rewrite a copy of every file in each of fourteen ways, and prove each rewritten file is the same document apart from what that probe may change.

https://gitlab.com/dobybaxter127/cellml2-conformance/-/blob/main/runner/probes.py

The Self-Check
Reads each text value as rule 1.2.3.3.3 allows, and must give every rewritten file the verdict its folder names.

https://gitlab.com/dobybaxter127/cellml2-conformance/-/blob/main/tests/test_corpus.py

The Disagreement Matrix
The generated report, now with "Same meaning, same verdict": each probe, and every outcome that changed.

https://cellml2-conformance-7c34a8.gitlab.io

Upstream Evidence

CellML 2.0 Normative Specification, Release 2.0.1
Rule 1.2.3.3 sets out when two CellML infosets are semantically equivalent: they may differ in comments, in namespace declarations and prefixes, and in whitespace.

https://www.cellml.org/specifications/cellml_2.0/cellml_2_0_normative_specification.pdf

The Informative Specification on CellML Information Sets
Gives each of the three changes, and which text the whitespace change applies to.

https://cellml-specification.readthedocs.io/en/latest/reference/formal_and_informative/specA02.html

Extensible Markup Language (XML) 1.1
Attribute order, quotes, empty-element tags, line ends and character references, none of which change the parsed document.

https://www.w3.org/TR/xml11/

ADR-015: Compare Each Test File With Its Control by One Edit, Not Only One Value
The record this one builds on. A probe's change is exactly what that record counts as no edit.

https://dobybaxter127.gitlab.io/adr/adr-015-compare-each-test-file-with-its-control-by-one-edit-not-only-one-value/

Metamorphic Testing: A Review of Challenges and Opportunities
Chen and colleagues, ACM Computing Surveys, 2018. The method the probes follow: a relation between inputs, here the same meaning, that the outputs must keep.

https://doi.org/10.1145/3143561

Relationship to the Broader Engineering Approach

This ADR is part of a broader engineering principle:

Invalid states should be made difficult or impossible to reach the execution layer.

ADR-014 placed a boundary between a specification and the tools that implement it, and ADR-015 kept it in place for every kind of rule. This record carries it from what a file says to how the file is written. A tool that reads meaning into a comment or a prefix gives a verdict about the writing rather than the model, and nothing in its output says so. Checking that the verdict holds however the file is written keeps that kind of failure from passing in silence.

Decision Outcome

Same meaning, same verdict is considered part of the corpus's architectural boundary.

Future probes should preserve this principle where practical:

A rule that says a change keeps the meaning → that change on every file → proof the document is otherwise the same → the same verdict from every implementation and the self-check

rather than:

A change no rule allows → an outcome no one can judge

This ADR should be revisited if the specification's custodians change what rule 1.2.3.3 allows, if a probe is wanted for a change no rule names, or if the probes become fast enough to run on every push.