diff --git a/.github/workflows/robot-integrity.yml b/.github/workflows/robot-integrity.yml
new file mode 100644
index 0000000..f246cee
--- /dev/null
+++ b/.github/workflows/robot-integrity.yml
@@ -0,0 +1,56 @@
+name: ROBOT integrity checks
+
+on:
+ push:
+ branches:
+ - '**'
+
+env:
+ ROBOT_VERSION: 1.9.10
+ ROBOT_JAVA_ARGS: -Xmx4g
+
+jobs:
+ robot-integrity:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v7
+
+ - name: Set up Java
+ uses: actions/setup-java@v6
+ with:
+ distribution: temurin
+ java-version: '21'
+
+ - name: Cache ROBOT jar
+ uses: actions/cache@v6
+ with:
+ path: tests/scripts/.robot
+ key: robot-${{ env.ROBOT_VERSION }}
+
+ - name: Install ROBOT
+ run: tests/scripts/install-robot.sh
+
+ - name: Run ROBOT integrity checks
+ id: robot-check
+ run: tests/scripts/robot-check.sh
+
+ - name: Upload unsatisfiability explanation
+ if: failure() && steps.robot-check.outcome == 'failure'
+ uses: actions/upload-artifact@v7
+ with:
+ name: robot-unsatisfiable-explanation
+ path: unsatisfiable-explanation.owl
+ if-no-files-found: ignore
+
+ - name: Run ROBOT report tests
+ id: robot-report
+ run: tests/scripts/robot-report-tests.sh
+
+ - name: Upload ROBOT report
+ if: failure() && steps.robot-report.outcome == 'failure'
+ uses: actions/upload-artifact@v7
+ with:
+ name: robot-report
+ path: robot-report.tsv
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
index d7bde36..46d8767 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
# OS
.DS_Store
@@ -14,3 +15,8 @@ tmp/
# Sphinx documentation
docs/_build/
docs/source/
+
+# ROBOT testing temporary artifacts
+tests/scripts/.robot/
+unsatisfiable-explanation.owl
+robot-report.tsv
diff --git a/RadLex.owl b/RadLex.owl
index 5628b47..17fd4c2 100644
--- a/RadLex.owl
+++ b/RadLex.owl
@@ -926927,7 +926927,7 @@ of the nodule.
FMA:308886
RID47581
- superior subscapular nerve component of dorsal root of right fifth cervical
+ superior subscapular nerve component of dorsal root of right fifth cervical nerve
superior subscapular nerve component of posterior root of right fifth cervical nerve
zum Nervus subscapularis superior gehörender Teil der Radix posterior des rechten Nervus cervicalis V
@@ -926964,7 +926964,7 @@ of the nodule.
FMA:308888
RID47582
- superior subscapular nerve component of dorsal root of left fifth cervical nerve
+ superior subscapular nerve component of dorsal root of left fifth cervical nerve
superior subscapular nerve component of posterior root of left fifth cervical nerve
zum Nervus subscapularis superior gehörender Teil der Radix posterior des linken Nervus cervicalis V
@@ -979983,7 +979983,7 @@ clean (individual elements are visible )
4.2 pref, syn
PI-RADS 2
extraprostatic or periprostatic tumor extension
- periprostatic or extraprostatic tumor extension
+ periprostatic or extraprostatic tumor extension
Tumorinvasion des periprostatischen Bereichs
tumor invasion of periprostatic or extraprostatic area
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..2b94cf2
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,115 @@
+# Testing RadLex.owl
+
+RadLex is tested for logical and structural integrity using
+[ROBOT](https://robot.obolibrary.org/), a command-line tool for working with
+OWL ontologies. These checks catch problems like inconsistent logic,
+accidental duplicate/merged terms, and other curation mistakes before they
+land in `RadLex.owl`.
+
+## Continuous integration
+
+Every push to any branch triggers the **ROBOT integrity checks** GitHub
+Actions workflow, defined in
+[`.github/workflows/robot-integrity.yml`](../.github/workflows/robot-integrity.yml).
+The workflow:
+
+1. Installs ROBOT.
+2. Runs a reasoner-based consistency check (`tests/scripts/robot-check.sh`).
+3. Runs SPARQL-query-based checks (`tests/scripts/robot-report-tests.sh`).
+
+If either check fails, the workflow run is marked failed and the relevant
+output is uploaded as a build artifact:
+
+- `robot-unsatisfiable-explanation` — a minimal explanation of why the
+ ontology is logically inconsistent or incoherent, if the reasoner check
+ fails.
+- `robot-report`: The `robot-report.tsv` violations report, if the
+ SPARQL-query-based checks fail.
+
+To download an artifact from a failed run, open the run on GitHub and scroll
+to the **Artifacts** section at the bottom of the summary page (or use
+`gh run download `).
+
+## Running the tests locally
+
+All the scripts below live in `tests/scripts/` and are meant to be run from
+the repository root.
+
+### 1. Install ROBOT
+
+```sh
+tests/scripts/install-robot.sh
+```
+
+This downloads the pinned ROBOT version and verifies it against a pinned
+checksum, installing it to `tests/scripts/.robot/`. The script prints the
+`export PATH=...` line you need; run it (or add it to your shell profile) so
+that plain `robot` commands resolve to this install:
+
+```sh
+export PATH="$PWD/tests/scripts/.robot:$PATH"
+```
+
+You only need to re-run `install-robot.sh` when the pinned `ROBOT_VERSION`
+changes — it's a no-op if the correct version is already installed.
+
+### 2. Run the reasoner-based consistency check
+
+```sh
+tests/scripts/robot-check.sh
+```
+
+This runs the ELK reasoner over `RadLex.owl` and fails if the ontology is
+logically inconsistent, has unsatisfiable classes, or if the reasoner infers
+equivalences between classes that weren't explicitly asserted (a common
+symptom of accidentally duplicated or merged terms). On failure, see
+`unsatisfiable-explanation.owl` for details.
+
+### 3. Run the SPARQL-query-based checks
+
+```sh
+tests/scripts/robot-report-tests.sh
+```
+
+This runs `robot report` using the queries and severity levels listed in
+[`tests/robot-report-profile.txt`](../tests/robot-report-profile.txt), and
+writes the full results to `robot-report.tsv`. Open that file to see exactly
+which terms triggered which violation.
+
+### Adding a new SPARQL check
+
+1. **Add the query file.** Create a new `.rq` SPARQL query file under
+ `tests/sparql/robot/`, e.g. `tests/sparql/robot/my_new_check.rq`.
+
+2. **Bind exactly three variables.** `robot report`
+ ([documentation](https://robot.obolibrary.org/report.html)) requires every custom
+ query to be a `SELECT` that returns exactly `?entity`, `?property`, and
+ `?value` - no more, no fewer:
+ - `?entity`: The offending entity (e.g. a class or annotation subject).
+ If `?entity` is unbound for a row, that row can't be reported.
+ - `?property`: The property involved in the violation (e.g.
+ `rdfs:label`).
+ - `?value`: The value that's causing the violation (e.g. the malformed
+ string).
+
+ A row returned by the query is a violation; a query that returns no rows
+ means no violations. See
+ [`tests/sparql/robot/leading_trailing_multi_whitespace.rq`](../tests/sparql/robot/leading_trailing_multi_whitespace.rq)
+ for a working example.
+
+3. **Reference it from the profile.** Add a line to
+ [`tests/robot-report-profile.txt`](../tests/robot-report-profile.txt) with
+ the severity level, a tab, then `file:` followed by the query's path
+ relative to the repo root:
+
+ ```
+ ERROR file:./tests/sparql/robot/my_new_check.rq
+ ```
+
+ Severity is one of `ERROR`, `WARN`, or `INFO`. `ERROR`-level violations
+ fail the check (and therefore the CI job); `WARN`/`INFO` are reported but
+ don't fail it.
+
+4. **Run it locally** with `tests/scripts/robot-report-tests.sh` to confirm
+ the check behaves as expected — check `robot-report.tsv` for the results —
+ before pushing.
diff --git a/tests/robot-report-profile.txt b/tests/robot-report-profile.txt
new file mode 100644
index 0000000..00c1f4c
--- /dev/null
+++ b/tests/robot-report-profile.txt
@@ -0,0 +1 @@
+ERROR file:./tests/sparql/robot/leading_trailing_multi_whitespace.rq
\ No newline at end of file
diff --git a/tests/scripts/install-robot.sh b/tests/scripts/install-robot.sh
new file mode 100755
index 0000000..9e75250
--- /dev/null
+++ b/tests/scripts/install-robot.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+set -euo pipefail
+
+#
+# install ROBOT (https://robot.obolibrary.org/) for local development or CI
+#
+# Downloads the pinned ROBOT_VERSION jar and the official `robot` launcher
+# script from the ontodev/robot GitHub repo, verifying both against pinned
+# checksums, into ROBOT_INSTALL_DIR. Add that directory to your PATH to run
+# `robot` directly, e.g.:
+# export PATH="$PWD/tests/scripts/.robot:$PATH"
+#
+
+ROBOT_VERSION="${ROBOT_VERSION:-1.9.10}"
+ROBOT_JAR_SHA256="${ROBOT_JAR_SHA256:-16a73c074f3df359a7338a84b4e0788785fe06117f931bb9796e9619ea776105}"
+ROBOT_SCRIPT_SHA256="${ROBOT_SCRIPT_SHA256:-0f2c4bb7dc25be9fb1dad37f2b8c07336d0e2c0ef3d3983e952c23d8b180a5b0}"
+ROBOT_INSTALL_DIR="${ROBOT_INSTALL_DIR:-tests/scripts/.robot}"
+
+sha256_check() {
+ local file="$1" expected="$2" actual
+ if command -v sha256sum >/dev/null 2>&1; then
+ actual="$(sha256sum "$file" | awk '{print $1}')"
+ else
+ actual="$(shasum -a 256 "$file" | awk '{print $1}')"
+ fi
+ if [ "$actual" != "$expected" ]; then
+ echo "Checksum mismatch for $file: expected $expected, got $actual" >&2
+ return 1
+ fi
+}
+
+mkdir -p "$ROBOT_INSTALL_DIR"
+ROBOT_INSTALL_DIR="$(cd "$ROBOT_INSTALL_DIR" && pwd)"
+
+if [ "$(cat "$ROBOT_INSTALL_DIR/VERSION" 2>/dev/null)" != "$ROBOT_VERSION" ]; then
+ echo "Downloading ROBOT v${ROBOT_VERSION}..." >&2
+
+ curl -fsSL "https://github.com/ontodev/robot/releases/download/v${ROBOT_VERSION}/robot.jar" \
+ --output "$ROBOT_INSTALL_DIR/robot.jar"
+ sha256_check "$ROBOT_INSTALL_DIR/robot.jar" "$ROBOT_JAR_SHA256"
+
+ curl -fsSL "https://raw.githubusercontent.com/ontodev/robot/v${ROBOT_VERSION}/bin/robot" \
+ --output "$ROBOT_INSTALL_DIR/robot"
+ sha256_check "$ROBOT_INSTALL_DIR/robot" "$ROBOT_SCRIPT_SHA256"
+ chmod +x "$ROBOT_INSTALL_DIR/robot"
+
+ echo "$ROBOT_VERSION" > "$ROBOT_INSTALL_DIR/VERSION"
+fi
+
+if [ -n "${GITHUB_PATH:-}" ]; then
+ echo "$ROBOT_INSTALL_DIR" >> "$GITHUB_PATH"
+else
+ echo "ROBOT installed to $ROBOT_INSTALL_DIR/robot" >&2
+ echo "Add it to your PATH: export PATH=\"$ROBOT_INSTALL_DIR:\$PATH\"" >&2
+fi
diff --git a/tests/scripts/robot-check.sh b/tests/scripts/robot-check.sh
new file mode 100755
index 0000000..863a5bf
--- /dev/null
+++ b/tests/scripts/robot-check.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -ex
+
+#
+# run ROBOT reasoner-based ontology integrity tests
+#
+
+robot reason \
+ --input RadLex.owl \
+ --reasoner ELK \
+ --equivalent-classes-allowed asserted-only \
+ --dump-unsatisfiable unsatisfiable-explanation.owl \
+ --output /tmp/RadLex-reasoned.owl
diff --git a/tests/scripts/robot-report-tests.sh b/tests/scripts/robot-report-tests.sh
new file mode 100755
index 0000000..6540889
--- /dev/null
+++ b/tests/scripts/robot-report-tests.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -ex
+
+#
+# run SPARQL-query-based ontology integrity tests
+#
+
+robot report \
+ --input RadLex.owl \
+ --profile tests/robot-report-profile.txt \
+ --labels true \
+ --output robot-report.tsv \
+ --limit 50
\ No newline at end of file
diff --git a/tests/sparql/robot/leading_trailing_multi_whitespace.rq b/tests/sparql/robot/leading_trailing_multi_whitespace.rq
new file mode 100644
index 0000000..6b255df
--- /dev/null
+++ b/tests/sparql/robot/leading_trailing_multi_whitespace.rq
@@ -0,0 +1,23 @@
+PREFIX rdf:
+PREFIX rdfs:
+PREFIX oio:
+PREFIX rid:
+
+#
+# Part of the RadLex ontology SPARQL test suite. Bindings to this query represent violations of the test.
+#
+# Test for stray whitespace on values (strings) of the annotation properties used for names. Specifically, this test
+# prohibits: leading or trailing whitespace, or consecutive whitespace characters within a string.
+#
+# VIOLATIONS will be bindings that have, on ?entity, ?property, and ?value respectively:
+# - The entity on which the violation occurs (e.g., the class).
+# - The property on which the violation occurs (i.e., the annotation property, e.g. rdfs:label).
+# - The value violating the constraint (e.g., "anatomy " or "anatomic part")
+#
+SELECT ?entity ?property ?value
+WHERE {
+ VALUES ?constrained_property {rdfs:label rid:Synonym rid:UMLS_Term} .
+ ?property rdfs:subPropertyOf ?constrained_property .
+ ?entity ?property ?value .
+ FILTER(REGEX(STR(?value), "^\\s|\\s+$|\\s\\s"))
+}