Context

ADR-014 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 in exactly one value. A self-check proves that one-value difference, and works out each verdict again from grammars written straight from the specification.

By 29 October 2026 the corpus had seven slices: 276 files about 33 of the 58 rules of Release 2.0.1, read by libCellML, Myokit and libOpenCOR. The method held for every rule about a value: numbers, identifiers, references, and values judged against the rest of the model, such as a cycle of mappings or two connections for one pair of components.

The 25 rules left are of three kinds. Sixteen are about structure: which information items may appear, which namespaces elements and attributes may use, what the top-level element is, and which element children each element may hold, and how many. Seven are about imports, where a model takes units or components from another file. Two are special cases: rule 1.2.1.1, a file that is not well-formed XML, and rule 3.10.4, a duplicate mapping.

This record captures the decision to carry the control principle into those rules, so the corpus can cover the whole specification.

Problem

A structural rule cannot be broken by changing one value. "A model element must not contain more than one encapsulation element" (rule 2.1.3) is broken by adding an element, and "a reset element must contain exactly two element children" (rule 2.9.2) by removing one. A file judged on its own, without a control, no longer tells us which part of it a tool responded to.

Imports bring in a second file. libCellML's Parser and Validator read one file and do not open the files it imports, so a column built on them alone would accept every import file, whatever it imported.

One rule cannot be broken on its own at all. Rule 3.10.4 forbids a pair of map_variables elements that duplicates an arc in the variable equivalence network, and every such pair also breaks rule 2.15.4 (two connections for one pair of components) or rule 2.16.3 (one connection mapping the same pair of variables twice).

We need a way to keep one change between each test and its control for every kind of rule.

Decision

Every variant will differ from its control by exactly one edit, and a changed value is one kind of edit. The others are an element or attribute added, removed or renamed, and an item added before the root element, such as a document type declaration.

The self-check finds that edit itself, by comparing the two files' XML trees, and fails any file that differs from its control by more or less than one edit. A place's variants are either all value edits or all structural edits. For a structural place, the self-check judges the whole file against structural rules written from sections 1.2 and 2 of the specification: the information items that may appear, the namespaces elements and attributes may use, the top-level element, and the element children each element may hold, and how many. Every value inside an element the edit adds must still meet the grammar for its kind.

Every file in the corpus is now checked against those structural rules and against the rules about the model as a whole, whatever its place, so a valid file cannot hold an element or a mapping the specification does not allow. The one-value method of ADR-014 is the case where the edit is a value, and every earlier file keeps its verdict.

control file  →  one edit  →  variant
                    ↓
   self-check: find the edit, judge the whole file
                    ↓
   libCellML     Myokit     libOpenCOR
                    ↓
          disagreement matrix

Imports. The files that tests import live in models_2_0/imported/, and are never run as tests themselves. Each test stays one file with a control, and its one edit can be an href, a units_ref, a component_ref or a name.

libCellML. The libCellML column will read each file with libCellML's Parser, then resolve its imports beside the file with libCellML's own Importer, then run its Validator. Files without imports give the same outcomes as before, and the report says what the column means.

Rule 3.10.4. It is covered by the files for rules 2.15.4 and 2.16.3, each of which also duplicates an arc. The rule list says so, and the coverage map counts it as covered.

Where the corpus ends. The finish line is all 58 rules of Release 2.0.1. The 36 definitions are tested through the rules that use them. The 13 interpretation items say what a valid model means, such as when a reset happens; they never change whether a file is valid, so a validation corpus does not test them.

The first slice under this decision is structure: 54 files in sixteen places, one place per structural rule. With it, the corpus holds 330 files about 49 of the 58 rules.

This keeps the boundary ADR-014 drew: what a file is expected to do is settled from the specification before any tool reads it, and one change separates it from a file every tool should accept.

Alternatives Considered

Standalone Structural Files Without a Control

Rejected.

A structural file judged on its own is simpler to write, and cellml-validation's CellML 1.x files work that way. Once a tool reports an issue on such a file, the report cannot say which part of the file the tool responded to. A control with one edit keeps every outcome tied to one change, as ADR-014 asks.

Compare a Variant With Its Control as Text

Rejected.

A line-by-line text comparison is easy to read, but XML text can change without changing the document: namespace declarations, quoting and layout. And one renamed element changes two lines, its start tag and its end tag. Comparing the parsed trees counts the edits the specification sees.

One Folder per Import Test

Rejected.

A folder holding each test's main file and the files it imports would be self-contained, but it would change the layout, the runner and the naming that every file in the corpus shares. A shared folder of imported files keeps one file per test.

Leave the libCellML Adapter as Parser and Validator

Rejected.

The column would accept every import file, showing only that the Validator does not open imported files. libCellML's Importer is its own way to open them, and libOpenCOR already uses it, so the column can show what libCellML itself is able to check.

Count Rule 3.10.4 as Not Covered

Rejected.

The coverage map would show 57 of 58 for good, for a rule the corpus does test. A note in the rule list says how it is tested instead.

Name Files After Rule 3.10.4 That Also Break Another Rule

Rejected.

Each file names one rule (ADR-014). A file that broke 3.10.4 and 2.16.3 together would be named after one of them and still be about the other.

Test the Interpretation Items by Simulation

Deferred.

Running models and comparing their numbers would test what a model means, not whether it is valid. That is a different kind of corpus, with different tools in the comparison.

Consequences

Positive

  • Every rule of Release 2.0.1 can have files with a control, so the corpus can cover the whole specification.

  • An outcome on a variant still points at one change, whether a value or a piece of structure.

  • Every earlier file keeps its verdict, and every file is now also checked against the structural and model-wide rules.

  • Cases the specification does not settle still go to unclear. The first under this decision is <approx/> inside an apply element: it is Content MathML but not in Table 2.1, and rule 2.12.2 names only the element children of math.

  • The libCellML column will show what libCellML itself can check in a file that imports another.

Trade-offs

  • The self-check is now a small validator of its own. It is kept honest the same way as the grammars: each rule has examples written from the specification, and every slice is tested with mistakes made on purpose in a copy before it ships.

  • Some edits break a second rule as well. An element in another namespace is also not a child its parent may hold, so the files about element namespaces break rule 2.7.2 too. Each file is named after the rule its edit is about.

  • Some structural edits leave a variable with no value, such as removing a connection's only mapping. A tool that also checks whether a model can be simulated reports that, and its message shows which it found.

  • The libCellML column for import files describes its Parser, Importer and Validator together, which is a reading of libCellML too. The report says so.

  • Rule 3.10.4 is counted through files named after other rules.

This decision is implemented in the CellML 2.0 Conformance Corpus, starting with the structural slice of 29 October 2026.

Implementation Evidence

The Structural Slice
Writes the 54 structural files, each as its control with one edit, with the reason for its verdict in a header comment.

https://gitlab.com/dobybaxter127/cellml2-conformance/-/blob/main/tools/make_structure_cases.py

The Self-Check
Finds each file's one edit from its control, and judges every file against the structural rules of sections 1.2 and 2 and the rules about the model as a whole.

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

The Disagreement Matrix
The generated report, with the coverage map: what the specification expects for each file, what each implementation did, and which rules have files.

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

Upstream Evidence

CellML 2.0 Normative Specification, Release 2.0.1
Section 1.2 defines the information items and namespaces a file may hold, section 2 the element children of each element, and rule 3.10.4 forbids a duplicate mapping.

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

The Informative Specification on MathML
Lists Table 2.1, the supported MathML elements, and says no presentation markup is permitted.

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

ADR-014: Take Validity From the Spec Text, One Rule Per File, Not From Any One Implementation
The record this one extends: one rule per file, verdicts from the specification text, controls, and the unclear folder.

https://dobybaxter127.gitlab.io/adr/adr-014-take-validity-from-the-spec-text-one-rule-per-file-not-from-any-one-implementation/

cellml-validation by Michael Clerx
The CellML 1.0 and 1.1 corpus this one continues, whose layout and naming by rule number the structural files share.

https://github.com/MichaelClerx/cellml-validation

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. This record keeps that boundary in place as the corpus reaches the rest of the specification. The move is the same: a test's expected verdict is settled from the specification before any tool reads it, and exactly one change separates each test from a file every tool should accept, so whatever a tool does with the test is about that change.

Update, 30 October 2026: Imports, Files That Are Not Well-Formed XML, and Rule 3.10.4

This record named three reasons to revisit it: the import files, the files for rule 1.2.1.1, and a rule that needs more than one edit to break. The first two have landed, and no rule has needed more than one edit.

Imports. The ninth slice holds 43 files for the seven import rules, built as decided. Each test is one file with a control, the files they import live in models_2_0/imported/, and the libCellML column reads each file with its Parser, then its Importer, then its Validator. The 330 earlier files kept every outcome and message. The self-check follows each href beside the test file, and reads the imported file to judge references and cycles.

Files that are not well-formed XML. A file that breaks rule 1.2.1.1 has no infoset, so it cannot be compared with its control as an XML tree. These files are compared as text instead. Each differs from its control by one change to the text after the header comments, and the self-check confirms three things: the control is well-formed XML, the file is not, and exactly one run of text changed. Each file's header names the production or well-formedness constraint of XML 1.1 that it breaks, such as Element Type Match or Unique Att Spec. Python's XML parser decides well-formedness in the self-check, and every such file breaks a rule that XML 1.0 and XML 1.1 share.

Broken files with no control would have been simpler to write, but an outcome on them could not be tied to one change. Leaving the rule out would have left the coverage map at 57 of 58 for good.

One file in the tenth slice is valid: it declares XML 1.1, the version rule 1.2.1.1 names. Myokit accepts it. libCellML reports the version as unsupported, and libOpenCOR does not read the file as CellML.

Rule 3.10.4. Checking the coverage claim above against the files showed that only the 2.16.3 file duplicated an arc: the 2.15.4 files join one pair of components twice, but map different variables. A new 2.15.4 place now duplicates an arc across two connections, with one edit. The rule list names both files in a short table of rules tested through other rules' files, the coverage map counts them for 3.10.4, and the self-check confirms that they are exactly the files in the corpus that break it.

With these slices, every one of the 58 rules of Release 2.0.1 has files: 385 files in ten slices.

Decision Outcome

The one-edit comparison is considered part of the corpus's architectural boundary.

Future slices should preserve this principle where practical:

Control → one edit → variant → whole-file verdict from the spec → every implementation

rather than:

A file judged on its own → an outcome that could be about anything in it

This ADR should be revisited if a rule needs more than one edit to break, or if the specification's custodians publish a release that adds rules, such as the limit on import units names in later drafts.