← Semantechs home The SemOps Manual A4 PDFGitHub
Contents
  1. Introduction

Part I — Why this is hard

  1. 1The business case
  2. 2People and cognition
  3. 3Across the boundary: peers and supply chains
  4. 4From research to industry
  5. 5GenAI and agents

Part II — The SemOps frame

  1. 6Stages and stories
  2. 7The toolchain

Part III — The practice

  1. 8Model and validate
  2. 9Continuous integration
  3. 10Ingest and transform
  4. 11Rules and inference
  5. 12Release and change
  6. 13Operate and consume

Part IV — The ledger

  1. 14Coverage and gaps
  2. 15Adoption roadmap
  3. ADiagram and brand conventions

7. The toolchain

Part II — The SemOps frame

When I wrote the first edition of this manual there were two tools: one for the author's desk and one for the build. There are six now. The count is the least interesting thing about it, though. What changed is who wrote the engines.

Why would anyone write their own SPARQL engine in 2026? Not for speed. Fast ones exist, and racing them is a race already lost. The reason is the one this chapter keeps coming back to. A lenient engine let a check pass in the suite's own tests and fail in the field, and the only way to stop that happening again was to evaluate the rules on engines that do what the specification says, and that someone in the room can fix.

Role What it is Where
Ontology Development Suite The desk VS Code extension: live diagnostics, outline, graph view, query workbench, and a TypeScript DSL for writing ontologies as code .vsix from the release · DSL on npm as ontology-suite · GitHub
Ontology Quality Suite The gate Python CLI and library: the 61-check registry, OWL2 reasoning, version diffing, change impact and repair, live-store checks, reference docs PyPI ontology-quality-suite · GitHub
SHACL Engine The validator SHACL in Rust: Python bindings, WebAssembly and a CLI; SHACL-AF rules; 425 of the 426 W3C tests, the exception named PyPI shacl · npm shacl-wasm, shacl-wasm-node · CLI from the releases
HOLOS The store An RDF 1.2 store with SPARQL 1.2 and access policy enforced at the index scan PyPI holosdb · npm holos-wasm, holos-wasm-node · CLI from the releases
tarka The triplifier CSV, Parquet and Polars data frames to RDF, through TARQL queries or OTTR templates PyPI tarka-rdf · CLI from the releases
The testing repository The evidence Deliberately broken ontologies, and a 38-question competency board, asserting what the suite must and must not report GitHub

Two of these you install on every machine (the extension and the suite). Two you mostly meet inside the others, because the suite and the extension evaluate on them (the SHACL engine and HOLOS). One replaces a step you already have (tarka, for triplification). And one you never install at all, but should read, because it is how the rest are kept honest.

Versions this edition was measured against. Every number in Part III comes from running these, between 13 October and 3 November 2026:

Version From
Ontology Quality Suite 0.25.0 PyPI ontology-quality-suite. Three releases were cut while this edition was being checked, for ten defects the checking found: 0.23.0 for three (§7.4, and Chapter 14 §14.4), 0.24.0 for six more, every one of them a case where a run looked cleaner than it was (Chapter 14 §14.6), and 0.24.1 for one that 0.24.0 itself introduced (Chapter 13 §13.1). 0.24.2 and 0.24.3 change no code: they publish the suite's floor on holosdb, >=0.24 from 0.24.3. 0.25.0 adds --severity-policy and --stringency, how strict each check is at each stage of a project, applied to finished findings; it moves no figure here. Where a fix changed a figure, the manual gives both
Ontology Development Suite 0.17.1 ontology-dev-suite-0.17.1.vsix from the v0.17.1 release; DSL as npm ontology-suite 0.16.1. 0.16.1 fixes two DSL defects this edition found (Chapter 8 §8.2); 0.17.0 reads the same severity policies as the suite; 0.16.2 to 0.17.1 move the engines it bundles to their current releases
SHACL Engine 0.3.4 PyPI shacl; npm shacl-wasm, shacl-wasm-node; the shacl command from the v0.3.4 release. 0.3.3 handed the RDFS closure out, which the suite needed for a fix (§7.4); 0.3.4 makes the command line refuse to validate against no shapes (Chapter 11 §11.1)
HOLOS 0.26.0 PyPI holosdb; npm holos-wasm, holos-wasm-node; the holos command from the v0.26.0 release. Two releases were cut while this edition was being checked, for defects the checking found: 0.21.1 stopped validation writing into the store (Chapter 13 §13.2), and 0.22.0 made holos validate a gate (Chapter 11 §11.8). Three followed: 0.23.0 and 0.24.0 send every way of asking a query, from Python, the command line, the server and the browser, through one query pipeline, and 0.24.0 fixes a wrong answer it gave for a zero-length path; 0.25.0 brings five SHACL-SPARQL fixes into its validator; and 0.26.0 lets an in-memory store back up to a file, and needs Python 3.11 or later
tarka 0.2.1 PyPI tarka-rdf; the tarka command from the v0.2.1 release. 0.2.0 stopped run dropping instances it could not build without saying so (Chapter 10 §10.3)
Testing repository 31bda0d main, 3 November 2026

These move quickly. The suite went from 0.14.2 to 0.25.0 between this edition and the last, and the engine underneath every check changed. Yet every figure in Chapter 9 came out the same, with one exception, and it is the useful kind: 0.24.0 stopped reporting one contradiction as two findings, so a few counts came down by one without any check getting weaker. That is no accident, and §7.1 says why. Pin your own versions anyway, for the reasons in Chapter 9 §9.2.


7.1 The idea: one registry, two runtimes, one set of engines

The suite's quality rules are not code. They are data: a registry.json plus a directory of SPARQL .rq files and SHACL .ttl shape files. The Python CLI reads that data. The VS Code extension reads the same data, copied into its own resources/checks-registry/. And since the end of September both of them evaluate it on the same two engines.

Diagram

The consequence is still the thing to hold on to:

The rule that fails your build is the same rule that underlined the problem in your editor twenty minutes earlier.

In the first edition, that sentence meant "the same data". By the last edition the SHACL half also ran on one engine, the Rust validator, compiled natively for the CLI and to WebAssembly for the editor. Now the SPARQL half does too: the 44 portable checks run on holosdb in Python and on holos-wasm-node in the editor. The rule and the engine are the same on both sides, and so is the version: the 0.17.1 extension bundles holos-wasm-node 0.26.0, and the suite's repository locks holosdb 0.26.0. Since HOLOS 0.24.0 every way into it, Python's and the editor's included, runs one query pipeline, so a check is planned the same way on both sides as well as written the same way.

Why does that matter so much? Because a team whose local linting disagrees with its CI learns to distrust both. Green locally, red in CI, and the gate becomes something to work around rather than something that helps. Sharing the rules as data is what stops the disagreement starting. Sharing the engines is what stops it creeping back in.

Why own the engines

Here is the story that decided it, because it is the clearest case I know of a dependency being too kind.

STY-003 reports a label with no language tag. Its message was built with STR(?e). SPARQL 1.1 (§17.4.2.5) defines STR() for literals and IRIs only, so STR() of a blank node is a type error. And on a real ontology most labelled things are blank nodes: anonymous restrictions and axioms carrying an rdfs:label.

What happened next depended on the engine:

  • rdflib, which the suite used, does not raise. It returns the blank node's internal identifier, so the message was present and named something meaningless that changed on every run.
  • Oxigraph, which the extension used, raises as the specification says. The CONCAT errors, the BIND leaves the variable unbound, and the message vanishes. On the suite's stress fixture, 60 of 65 findings had no message at all.

Every test in the suite passed throughout. The check fired at the right count, the right severity and the right focus node. It shipped like that in the extension. What found it was a new test that ran every check under two SPARQL engines and failed on any disagreement.

The lesson the suite drew is worth borrowing whole. Its own engines now do the evaluating, and the third-party engines it used to depend on stay in the development group, as dissenting witnesses:

Engine that does the work Checked against Why keep the witness
holosdb (SPARQL) rdflib, pyoxigraph pyoxigraph is what the extension used to ship, so it speaks for those users
shacl (SHACL) pyshacl pyshacl caught a real regression in the native engine, isIRI($this) on blank nodes, in 0.1.5

A dependency's leniency is a defect waiting for a stricter reader. If your checks will ever run somewhere else (a partner's validator, a browser, a store), test them under a conformant engine too, and keep the lenient one as a witness rather than a judge.

The move had costs, and the suite is candid about them. Blank-node labels are document-scoped, and different engines rename them, so two arms of one check stopped agreeing about which anonymous node they meant. The dedup now pairs anonymous findings by position within (check, path, value) instead. A message must never be built out of a label, so both repositories now render an anonymous focus node as the literal text [a blank node], identically, so the two arms still merge. And --inference owlrl left the checks path, because pyshacl was what served it (§7.4).

Shared does not mean identical

The extension carries an exact copy of the 61-check registry, with the same ids and the same ten categories. Then it adds six of its own with no CLI equivalent: MDL-001/002/003, modelling-guidance checks informed by gist, and since 0.16.0 MAL-001/002/003, which find malformed OWL that earlier versions of the extension itself wrote. So the editor can flag something CI never will. That is the right way round. Advice belongs where the author is, and a build gate should not fail on a matter of modelling taste. Still, it's worth knowing before someone asks why a warning they saw while typing never appears in the pipeline.

Being in the registry is not the same as being runnable

I got this wrong in an earlier edition, so let me be exact about it. VOC-001, the closed-world vocabulary check in Chapter 8, moved out of the extension's private extras into the shared registry, and I reported that as "so CI runs it too". It does not. Nothing in the Python package implements it; only registry.json mentions the id.

What the registry is, really, is a shared vocabulary of findings: id, title, severity, category, remediation. Being declared makes a finding nameable by both sides. Being implemented makes it producible by one. The suite's own docs now put it in five words: the registry declares; the directories decide. A query with no registry entry still runs and reports as UNMAPPED. An entry with no file never fires.

Neither side runs all sixty-one. Three are the extension's alone (VOC-001, REA-005, REA-006), on top of the six MDL and MAL checks outside the registry. Eleven are the CLI's alone: REA-020/021/022 need a full DL reasoner; REA-010/011/012 are OWL2 profile membership, which the extension computes but does not report as findings; CNF-001/002/005 need a two-graph split the editor does not have; and TQL-004/005 read a graph only the CLI builds.

Both repositories pin their own exception list, and each names the other's. That is the part to copy. A shared registry with no such list is a promise the UI does not keep, because a check that cannot fire is indistinguishable from a check that found nothing. Publishing the rules is the easy half. Publishing what your implementation actually does with them is the half that makes the rules trustworthy across an organisational boundary (Chapter 3).

Shared data is not shared behaviour

The registry travels as data, so a test can compare the two copies character for character, and one does. But a handful of algorithms are written twice, once in Python and once in TypeScript, and a test that compares files cannot see them.

The one that proved it decides which text a tool may rewrite: strip_comments in the CLI, behind --apply-repairs, and stripComments in the extension, behind rename and find-references. Both shipped the same defect for the same reason. A scan skipped a line beginning with # but read a trailing comment in full, so this line

ex:Dog a owl:Class .  # was ex:Dgo, renamed 2026-09-04

recorded an occurrence of ex:Dgo inside the remark. Rename it and the note explaining the rename was rewritten too, turning a true comment into a false one. Both sides were fixed in the same week, independently, in two languages, and nothing noticed they had been wrong together.

The fix is a fixture of sixteen cases that both repositories carry, written by hand from the intent rather than generated from either implementation. That detail is what makes it work. A fixture derived from the code only restates what the code already does, and would have agreed with both ports on the day they were both wrong. You will meet the same principle again in Chapter 10, where a tool generates SHACL shapes from a mapping: useful, and for exactly the same reason unable to tell you the mapping is wrong.

The same discipline now runs in the other direction too. The blank-node message fix and the positional dedup landed in both repositories within hours of each other, each written against its own stack and each tested end to end through both real engines. Neither was copied across on trust.

Wherever one rule has two runtimes, sort what you share into data and behaviour. Publish the data and compare copies. Guard the behaviour with a fixture written from the intent. A conformance suite a vendor generates from their own product tells you nothing about whether they agree with anyone else.


7.2 Installing them

The Python suite

PyPI · source · requires Python 3.11+

pip install ontology-quality-suite

or, working from a checkout:

uv sync

That one line now brings both engines: holosdb and shacl are runtime dependencies, not options. (uv sync --extra native-shacl still parses but installs nothing; plain uv sync used to uninstall the native engine and quietly move the default back to pyshacl, which is why it was promoted.) It also brings owlready2, the bridge to HermiT. What is genuinely optional is a Java runtime for HermiT to run in. Without one you still get the always-on owlrl closure and pattern checks, which catch the fixture's deliberate contradiction on their own, and an explicit REA-022 finding telling you the DL reasoner did not run (Chapter 4).

Upgrading a checkout. uv pip install --upgrade does not move a locked dependency, because uv run re-syncs from uv.lock. uv lock --upgrade-package holosdb is the step. The suite learned this the hard way. Its own lock went stale at 0.14.3 for a while without anyone noticing, and the 0.22.0 release locked holosdb at 0.15.0 while a fresh install resolved 0.16.0, so its tests ran against a version no new user would get.

Two invocation styles appear in this manual:

ontology-quality-suite <command> ...        # installed entry point
python -m ontology_suite <command> ...      # module form, from a checkout

They are equivalent. The module form is used in Part III because that is how the commands were actually run while writing it.

The VS Code extension

Source · latest release

It is not on the Marketplace, so download the .vsix from the release and install it with Extensions: Install from VSIX…, or:

code --install-extension ontology-dev-suite-<version>.vsix

Core functionality has no external runtime dependency at all: holos-wasm-node, shacl-wasm-node and eyereasoner are WebAssembly. Oxigraph went in 0.13.11, and the .vsix dropped from 20.2 MB to 17.5 MB with it.

One small packaging lesson rode along. vsce package --no-dependencies produces an archive that packages and installs without error and contains none of the engines, because they are loaded from node_modules at run time. The extension then quietly offers fewer checks. The build now reads its own archive back and fails if an engine is missing. The size was the only clue the first time.

The Python CLI is detected via ontologySuite.pythonCliPath or on PATH, and is used only for the commands that genuinely need it: Run Deep Validation (full OWL2 DL reasoning), Run Full Triplify (the oxi-gen binary) and Generate Documentation. All three degrade gracefully when it is absent.

The DSL, outside the editor

npm install --save-dev ontology-suite

That makes import … from 'ontology-suite/dsl' resolve in CI, in another editor, or under Node, where build() returns the ontology as RDF/JS quads. Keep it at the same version as the extension. Chapter 8 §8.2 shows what it is for.

The engine, the store and the triplifier

Each comes two ways: as a library from PyPI (and, for two of them, npm), and as a command from its GitHub release.

pip install shacl          # the SHACL engine, as a Python library
pip install holosdb        # HOLOS, as a Python library
pip install tarka-rdf      # tarka, as a Python library (imported as tarka_rdf)

npm install shacl-wasm     # or shacl-wasm-node, for require()
npm install holos-wasm     # or holos-wasm-node, for require()

The wheels are libraries only. Of the four PyPI packages in this manual, only ontology-quality-suite installs a command. The shacl, holos and tarka commands used in Part III come from the release archives on GitHub (SHACL Engine, HOLOS, tarka), built for Linux, macOS and Windows, each release with a SHA256SUMS file. Check it. An earlier edition said pip install shacl brought the command line too; it does not.

Watch the names. shacl on crates.io is a different project, and so is tarka on PyPI and npm, which is why the Python package is tarka-rdf.

That zero-dependency property of the extension is still a governance fact, not just a convenience. As Chapter 3 argues, a validation step that requires a JVM excludes a class of supply-chain partners from self-service compliance. The same goes for the single-binary engine and triplifier: a partner can run them without procuring anything.


7.3 Decision table: question → command

You want to… Run Chapter
Know if the ontology itself is sound — no data ontology 8
Run the full registry against ontology and/or data checks 8, 9
See only findings in terms you own checks --own-namespace <IRI prefix> 9
Group findings under questions in your own words --themes <file> 9
Write only the two reports a person and a script read --reports minimal 9
Run your house rules alongside the built-in ones --sparql my-checks/ --sparql @builtin 8
Check CSV→RDF queries against the ontology, no CSV needed sketch 10
Actually produce RDF from CSV or Parquet tarka run (or triplify, which drives oxi-gen) 10
Load the output straight into a store tarka run … --post <GSP endpoint> 10
Assess real triplified data data 10
Derive triples with SHACL-AF rules shacl … --advanced 11
Compare two versions, get MAJOR/MINOR/PATCH version-diff 12
Find and auto-repair ontology↔︎query drift consistency 12
Check a controlled-vocabulary layer too pattern-consistency 12
Run the checks against a live store consistency-remote 13
Answer a query as a particular person holosdb: Store.query(…, principal=, policy=) 13
Produce something a stakeholder can read docgen 13
Run everything applicable, one report run 9

And the editor-side equivalents, by VS Code command:

You want to… Command
Start a new ontology with imports wired up New Ontology
Add a class in the right place in the hierarchy Add Class or Category / Add Subclass / Add Sibling Class
Write the ontology as a TypeScript script Run Ontology Script (.ontology.ts)
Run the registry locally, in-process Run Local Checks
See the model as a picture Visualize Subject Graph
Draft an ontology and query from a raw CSV Infer Ontology + Query from CSV
Preview a transformation against sample rows Open Query Workbench
See DL expressivity and OWL2 profile membership Show Metrics & DL Expressivity
Escalate to full OWL2 DL reasoning Run Deep Validation

7.4 Two flags worth knowing before you start

--engine

checks, data and run accept --engine. Since 0.22.0 the choice is simpler than it was, because there are no longer two SHACL engines to choose between: shacl and native are the same thing now, and so are both and native+sparql. Both spellings survive because scripts pass them.

All modes, against this manual's fixture with --own-namespace, same ontology, same registry:

--engine Findings Severity split What it runs
native / shacl 22 1 V / 21 W The SHACL shapes only. Misses QUA-004, which exists only as a SPARQL check
sparql 23 1 V / 22 W The .rq checks only, on holosdb. The numbers used throughout this manual
native+sparql / both 23 1 V / 22 W Both, deduplicated. The default

Neither formulation is complete alone. The registry is implemented as 44 SPARQL .rq files and 6 SHACL shape files, and they do not cover the same checks. If you want a single-formulation mode, sparql is the one with the broader registry behind it.

On the unscoped run the three take about 1.7 s (native), 2.0 s (sparql) and 2.2 s (native+sparql), whole process, on the laptop this edition was measured on. So the choice is not about speed at this size.

The severity lesson is about shape authoring, not engines. Until 0.6.0 --engine both reported 5 Violations where native+sparql reported 2 on identical findings. The shapes declared sh:severity inside each nested sh:sparql [ … ] block, where SHACL defines it as a property of the enclosing shape. The native engine read it there anyway; pyshacl fell back to the spec default of sh:Violation. With --fail-on Violation, a class named person_record failed CI exactly as hard as a logical contradiction. Moving the declaration onto the shape fixed it.

I tripped over the same thing myself while testing the engine for this edition, one level over. I put sh:severity sh:Warning on a node shape, and the result came from its nested property shape, which still defaulted to Violation. Severity belongs to the shape that produces the result. If your gate is failing on something you declared Warning, look at where the declaration sits before you blame the engine.

--inference owlrl and both are gone from the checks path. pyshacl served them, and the native engine has no OWL2-RL reasoner. Downgrading silently to RDFS would have dropped findings that depend on OWL2-RL rules, so the suite refuses and points you at the reasoning stage, which still builds an owlrl closure. At 0.22.0 the refusal came as a Python traceback while --help still offered both choices. From 0.23.0 they are not offered, and asking for one is a one-line usage error.

And --inference rdfs reached only half the suite until 0.23.0. It was a parameter of the SHACL arm alone. Under --engine sparql the flag was accepted and ignored, so a pipeline asking for it passed having inferred nothing. Under the default engine the two arms checked different graphs, and their findings were then merged as though they were one run. On the fixture:

--engine no inference rdfs at 0.22.0 rdfs from 0.23.0
sparql 479 479 489
both 479 497 489

I first read the 497 as eighteen extra findings. It was not: closing the graph properly gains 21 findings (STY-003, STR-007 and STR-009 on entailed types) and loses 11, because an entailed type also resolves a check about an untyped resource. 497 described a graph that never existed. From 0.23.0 the closure is made once, by the SHACL engine itself (shacl.rdfs_closure, new in engine 0.3.3, so it cannot disagree with the closure the shapes would have made), and every engine reads the result. Expect findings to move in both directions when you turn it on; that is the closure working.

--verbose

Every subcommand takes -v/--verbose, which prints what each input option actually resolved to before running: which files matched a glob, which owl:imports resolved and from where, which engine ran.

Reach for it whenever a result looks surprising, before assuming a bug. Most "the tool is wrong" reports turn out to be an owl:imports that quietly did not resolve, and --verbose shows that in one line. There is a worked instance of exactly this kind of mistake in Chapter 9, where an IRI prefix off by one character produced a confidently empty report.


7.5 The fixture used throughout Part III

Every command in Part III runs against examples/acme_robotics/ in the Python suite: a small org chart built on two real, standards-track vocabularies rather than an invented domain.

Vocabulary Used for
W3C Organization Ontology (org:) org:OrganizationalUnit as the base class for departments
FOAF (foaf:) foaf:Person as the base for employees; foaf:name, foaf:mbox

The fixture is deliberately imperfect, with each flaw commented with the check it demonstrates:

Term Flaw Check it triggers
acme:Contractor rdfs:subClassOf acme:Employee and owl:disjointWith acme:Employee LOG-001, logically unsatisfiable
acme:hasSkill no rdfs:label, and no skos:prefLabel either QUA-001, QUA-004
acme:reports_to local name is not lowerCamelCase; no domain or range STY-002, STR-003

Each deliberate flaw produces exactly one finding, which is what makes the fixture usable for teaching: every finding traces to a line someone wrote on purpose.

The scoped run in Chapter 9 reports 23, and the gap is instructive rather than a discrepancy. Eighteen of those 23 are QUA-009 and QUA-010, nine each: a documentation policy the registry asks for (a skos:prefLabel per language, a skos:definition) that this fixture does not follow. That is one decision you have not taken, reported nine times, not nine mistakes. Telling those two apart is the subject of Chapter 9 §9.4.

acme-org-v2.ttl is a plausible next release. It renames acme:Engineer to acme:SoftwareEngineer (a breaking change, with a migration annotation) and adds one genuinely additive class and property. It deliberately leaves the flaws above untouched, because real releases do not fix everything at once.

Using real external vocabularies rather than toy ones is what makes Chapters 9 and 10 possible. The most valuable lessons in this manual come from the collision between a general-purpose checker and the conventions real vocabularies actually use.


7.6 Where the deeper documentation lives

This manual is the SemOps layer. For tool-level depth:

Document Covers
Suite PRIMER.md Task-oriented guide, worked examples, CI wiring
Suite CHECKS.md All 61 checks, by category
Suite ARCHITECTURE.md Which engine does the work, and why; file loading; benchmarks
Suite REPORTS.md What each report is for, where finding positions come from, --themes
Suite EXTENDING.md Authoring your own checks; "the registry declares, the directories decide"
Suite REASONING.md Reasoner backends and their real limitations
Suite CONSISTENCY_AND_REPAIR.md Finding-kind → fix-kind → confidence table
Suite FUSEKI.md Live-store manifest spec and Python API (any SPARQL endpoint)
Suite VERSIONING.md · UPDATING.md Semver for ontologies; sequencing and rollback
Extension README.md · TUTORIAL.md The extension; Part 11 is the DSL
HOLOS ACCESS-CONTROL.md · holosdb README Policy at the scan: the guarantee, and the Python API
tarka README.md TARQL, OTTR, data frames, shapes from mappings
SHACL Engine README.md Conformance, the sh:conforms default, rules, performance

The testing repository deserves a sentence of its own, because you never install it and it is the most useful of the lot to read. consolidated-ontology-quality-suite-python-testing holds seventeen deliberately broken ontologies, a 38-question competency board with one folder per question, and a harness asserting which checks each one must and must not trigger. Its COMMANDS.md gives every fixture as both a harness invocation and the equivalent bare CLI command. Chapter 9 §9.8 is about the practice it demonstrates.