OWL logoAutomation

Infer Once, Gate Always, Replay Forever: OWL in a Build Pipeline

Commit an exported ontology, re-run the export tomorrow, and look at the diff. If it shows reordered prefixes and reshuffled blocks, the export is not a build artifact — it is a rumor, and no reviewer will read it twice. If it shows exactly the statements your model changed, you have something CI can gate on.

Infer Once, Gate Always, Replay Forever: OWL in a Build Pipeline

Commit an exported ontology, re-run the export tomorrow, and look at the diff. If it shows reordered prefixes and reshuffled blocks, the export is not a build artifact — it is a rumor, and no reviewer will read it twice. If it shows exactly the statements your model changed, you have something CI can gate on.

Turtle is a format that usually lives in hand-tended files and tribal knowledge. This article is about moving it into pipelines: what CoreModels guarantees about output stability, why the mapping plan rather than the ontology is the thing you commit, how the three mapping kinds behave when nobody is watching, and where to put the policy check.

Two determinism properties

The encoder is stable. The @prefix header is emitted in sorted order, and subjects follow the model's own order: the ontology header, then classes, then properties, then concept schemes and their terms, then relations. Preserved prefixes, original datatypes, and rdfs:comment text are re-emitted verbatim. The practical consequence is stronger than "usually the same": run a Turtle document through decode-then-encode, then run the result through decode-then-encode again, and the second document is byte-identical to the first. The re-emitted form is a fixed point, so the only thing a diff can show you is a change in the model.

Execution is deterministic by contract. Between a validated plan and its output there is no I/O, no network, and no model call — the same plan applied to the same source produces the same result, every time. That is what allows the plan to become a build artifact rather than a debugging aid.

The plan is the pipeline's contract

Every call to the stateless mapping route returns the executed plan next to the output:

{
  "success": true,
  "lossiness": [ … ],
  "errors": [],
  "schema": "…",
  "plan": { "operations": [ … ] }    // commit this
}

Treat plan the way you treat a lockfile. Produce it once — interactively, in a pull request, wherever review actually happens — commit it, and let the pipeline replay it:

jq -n --rawfile src vocabularies/hr.ttl --rawfile plan plans/hr.plan.json '{
  sourceFormat: "owl",
  sourceSchema: $src,
  plan:         $plan,
  targetFormat: "jsonschema"
}' | curl -sS -X POST \
      "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      --data @-

Two details make this pleasant in automation. The plan travels as a JSON string, which is precisely what --rawfile produces, so no double-encoding gymnastics. And replay needs no target hint: the plan already records every target id, so targetHintFormat and targetHintSchema can be omitted entirely. Your CI job needs the source file, the plan file, a Viewer-role token, and nothing else.

The gate still runs. A stored plan earns no shortcut — it goes through the same validator as a freshly produced one. If somebody renames a class in the source ontology so the plan no longer fits, you get a gated failure with a path, not a silently different output. That is the property that makes committing plans safe rather than fragile.

Three mapping kinds, judged as automation

Inferred matches labels and types against a target hint, case-insensitively by default. It is the right default for the common case — a directory of departmental vocabularies that all roughly follow one corporate glossary. Ship the glossary as targetHintSchema and let inference do the alignment.

OWL sources have a quiet advantage here that other formats do not always get: a SKOS concept scheme decoded from Turtle carries the rdfs:label you wrote on the concept class, so controlled vocabularies participate in label matching like everything else. A matched taxonomy shows up in the plan as its own operation, which means the enumeration survives the mapping instead of dangling:

{ "kind": "TaxonomyMapping", "origin": "Inferred",
  "sourceTaxonomyId": "ex:AvailabilityStatus",
  "targetTaxonomyId": "ex:AvailabilityStatus",
  "targetTreatment": "InlineEnum" }

Explicit means you author a mapping-guide JSON and pass it as mapping.guide. This is where OWL is structurally stronger than any label-based format. When Turtle is decoded, every entity's own IRI — plus its owl:equivalentClass, owl:equivalentProperty, and skos:exactMatch assertions — is lifted into the model's cross-standard mapsTo annotations. So the guide's autoMatchByMapsTo switch aligns by IRI, not by name:

{
  "autoMatchByMapsTo": true,
  "taxonomyDirectives": [
    { "sourceTaxonomyId": "ex:AvailabilityStatus",
      "targetTaxonomyId": "ex:AvailabilityStatus",
      "treatment": "InlineEnum" }
  ],
  "drops": ["ex:tag"]
}

Two ontologies that both declare equivalence to the same external IRI will match even if one calls the class "Product" and the other "Item". Labels drift; IRIs are identity. Unknown keys in a guide are rejected with a path-carrying error, so a typo fails loudly instead of quietly executing a plan you did not write. Note the drops entry: a deliberate omission produces a StructuralDrop record in the ledger just like an accidental one — the difference is that you wrote it down.

AI proposes a plan server-side. For automation the boundaries are the point: it needs Editor or Admin membership and a server-configured key, it sends schema content to the Anthropic API server-side, and the proposal passes the identical gate with at most one repair attempt. Any self-reported confidence on plan operations is advisory and the gate never trusts it. The sane pattern is to use ai once, at design time, to draft an alignment nobody wants to hand-write; review the plan; commit it; and let plan/execute — deterministic, Viewer-role, no model in the loop — do the recurring work.

Batch: a directory of vocabularies

A complete converter. Every .ttl file is mapped onto one canonical hint, and the output, plan, and ledger are captured per file.

#!/usr/bin/env bash
set -euo pipefail

HOST="https://coremodels.example.com"
mkdir -p out plans

for ttl in vocabularies/*.ttl; do
  name="$(basename "$ttl" .ttl)"

  jq -n --rawfile src "$ttl" --rawfile hint canonical-hint.json '{
    sourceFormat:     "owl",
    sourceSchema:     $src,
    targetFormat:     "jsonschema",
    targetHintFormat: "jsonschema",
    targetHintSchema: $hint,
    mapping:          { kind: "inferred" }
  }' | curl -sS -X POST "$HOST/graph/transform/schema/map/$PROJECT_ID" \
        -H "Authorization: Bearer $TOKEN" \
        -H "Content-Type: application/json" \
        --data @- > "out/$name.response.json"

  jq -e '.success' "out/$name.response.json" > /dev/null \
    || { jq '.errors' "out/$name.response.json"; exit 1; }

  jq    '.schema'    "out/$name.response.json" > "out/$name.schema.json"
  jq    '.plan'      "out/$name.response.json" > "plans/$name.plan.json"
  jq -c '.lossiness' "out/$name.response.json" > "out/$name.lossiness.json"
done

The mapping route writes nothing to the project — every call is inherently a dry run — so this loop is safe to run with production credentials at Viewer role. On the first pass it produces plans; on every pass afterwards, switch the body to the plan/execute shape above and you have reproducibility by contract rather than by inference re-running the same guesses.

Gate on the ledger, not on success

success: true means the transform ran. The machine-readable ledger is where automation should make decisions.

for r in out/*.response.json; do
  jq -e '[.lossiness[] | select(.kind == "StructuralDrop")] | length == 0' "$r" > /dev/null \
    || { echo "FAIL: structural drop in $r"; jq '.lossiness' "$r"; exit 1; }
done

The four kinds partition cleanly for policy purposes:

  • StructuralDrop — something had no home in the target. Usually a build-breaker. For OWL sources the common cause is an element the target hint has no counterpart for, reported as Element is a member of the mapped type but no operation maps or drops it.
  • ConstraintRelaxation — a bound was weakened. Often acceptable, always worth logging.
  • TypeApproximation — a close-but-not-exact type. Usually fine.
  • SemanticNarrowing — meaning was narrowed. Threshold this rather than forbidding it when OWL is the source: every real ontology carries versioning and provenance metadata beyond the schema vocabulary, and those triples are counted and reported as one summarized record. Failing the build because someone added dcterms:created would train your team to ignore the ledger, which defeats the purpose.

The standing export

The reverse pipeline is just as useful: a scheduled job that publishes the governed project as an ontology, so the knowledge-graph side always loads current truth.

#!/usr/bin/env bash
set -euo pipefail

RESP="$(curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "format": "owl" }')"

jq -e '.success' <<<"$RESP" > /dev/null || { jq '.errors' <<<"$RESP"; exit 1; }

jq -r '.schema'    <<<"$RESP" > ontology/governed.ttl
jq -c '.lossiness' <<<"$RESP" > ontology/governed.lossiness.json

git add ontology/governed.ttl ontology/governed.lossiness.json
git diff --cached --quiet || git commit -m "chore: refresh governed ontology"

Because the encoder is stable, git diff --cached --quiet is a reliable change detector rather than a coin flip. Because required and cardinality facts are exported as genuine owl:Restriction axioms, downstream consumers enforce the same constraints your project governs without any CoreModels-specific tooling. And because the ledger is committed next to the document, the file's fidelity is reviewable in the same diff. Teams that pull the export over MCP get the same document from the export_owl tool, where the ledger arrives as # lossiness: comment lines at the top — valid Turtle, so the honesty report travels inside the artifact itself.

The shape of a trustworthy pipeline

Alignment intent is produced once, by inference, by an IRI-based guide, or by a reviewed AI draft. It is captured as a plan and committed. CI replays plans deterministically, gates on a lossiness policy you chose consciously, and publishes exports whose constraint semantics stand on their own in standard OWL. Nothing in the loop depends on anyone remembering how the conversion was done — the plan is how it was done.

For the full endpoint reference behind these calls, see the Transform section of the CoreModels documentation.