Skip to content

feat(openlabel-v2): add the structural LinkML schema for the ASAM JSON format - #81

Open
jdsika wants to merge 1 commit into
mainfrom
feat/openlabel-v2-schema
Open

feat(openlabel-v2): add the structural LinkML schema for the ASAM JSON format#81
jdsika wants to merge 1 commit into
mainfrom
feat/openlabel-v2-schema

Conversation

@jdsika

@jdsika jdsika commented Jun 15, 2026

Copy link
Copy Markdown

Description

Adds the structural LinkML schema for the ASAM OpenLABEL JSON file format, alongside the existing semantic model, and generates a JSON Schema from it.

The semantic model (openlabel-v2.yaml) describes the RDF tag vocabulary. It cannot say what a conforming OpenLABEL file looks like before that file is read as RDF — a malformed document fails as unparseable RDF rather than as a schema violation, with no useful message. The structural model (openlabel-v2-schema.yaml) describes the JSON document format and closes that gap.

Both stay generated. Neither artifact is hand-edited.

Diff to main

Rebased onto e74f6eaone commit, nothing behind main, no merge commit.

34 files: 9 added, 25 modified (+14,338 / −3,905). Of the 25 modified, 17 are
tests/data/openlabel-v2/** fixtures; the other 8 are:

.pre-commit-config.yaml · justfile · linkml/openlabel-v2/openlabel-v2.yaml ·
tests/catalog-v001.xml · and the four regenerated artifacts
(PROPERTIES.md, .context.jsonld, .owl.ttl, .shacl.ttl).

The bulk of the insertions are generated or fixture content: openlabel-v2.schema.json,
the regenerated SHACL, and tests/unit/test_openlabel_schema.py. Reviewable surface is
the two LinkML sources, the two scripts, the justfile recipe and the pre-commit ordering.

pyproject.toml is not touched — main's version is used as-is.

What lands

Path Role
linkml/openlabel-v2/openlabel-v2-schema.yaml structural model of the ASAM JSON format
linkml/openlabel-v2/jsonschema.genopts per-domain generator flags
artifacts/openlabel-v2/openlabel-v2.schema.json generated JSON Schema
scripts/sync_tag_type_enum.py keeps the structural TagTypeEnum in step with the semantic model
scripts/convert_openlabel_v1_to_v2.py converts ASAM v1 files into the v2 instance shape
linkml/openlabel-v2/SCHEMA_MODELING.md how the two schemas divide responsibility

just generate now builds a JSON Schema for any domain carrying a <domain>-schema.yaml, with flags read from jsonschema.genopts so the recipe stays domain-neutral. gen-owl gains --no-use-native-uris so class URIs follow the declared namespace.

sync-tag-type-enum is wired as a pre-commit hook that runs before generate-linkml — the reverse order would generate artifacts from a stale enum.

Consolidated into v2 rather than a parallel v3

A feat/openlabel-v3-schema branch proposed a separate openlabel-v3 namespace for validating ASAM's native JSON shape. The need it identified is real, but the mechanism duplicated too much:

  • its openlabel-v3.owl.ttl was the v2 ontology copied verbatim — identical named class sets (205 classes); every difference was blank-node relabelling — plus three boolean flags. Two sources of truth for one vocabulary, with nothing keeping them in step.
  • the file declared two owl:Ontology headers, including v2's at version 2.0.0, while the catalog mapped the v3 IRI to it. Loading v2 and v3 together yielded the same classes from two files.
  • it re-invented OpenLabelFile, OpenLabel, OntologyEntry and ResourceUid, all of which v2's structural schema already defines.

So the structural work belongs in v2, which is where it is here. No second namespace, no duplicated ontology.

Rebased onto current main

The branch predated two refactors and would not have merged:

Artifacts were regenerated under the locked toolchain (uv.lock pins linkml 6aa7702c) rather than carried over from the old branch, which had been generated with a different generator commit. That is why the OWL and SHACL differ from the previous branch state: same triples, different blank-node ordering from a different generator version. They now reproduce from source.

Removed from the previous branch state

These were on the previous branch state and are not carried over. They never existed
on main, so they do not appear as deletions in the diff above — the point is simply that
this PR does not reintroduce them:

  • REFINEMENT_PROOF.md — an auto-generated verdict report with gap tables
  • scripts/schema_refinement_prover.py (1,040 lines) and tests/unit/test_schema_refinement.py
  • linkml/openlabel-v2/proof_seeds/, proof_spec.yaml
  • the hypothesis / hypothesis-jsonschema dev dependencies and the slow pytest marker that existed only for the prover

SCHEMA_MODELING.md is kept — it documents how the two schemas divide responsibility, which is design documentation rather than analysis.

🧪 How Has This Been Tested?

  • just generate — artifacts reproduce byte-identically from the LinkML source under the locked toolchain
  • pytest tests/unit/test_openlabel_schema.py48 passed, including the functional-equivalence checks against ASAM's normative openlabel_json_schema-v1.0.0.json from the standards submodule
  • pre-commit: ruff, JSON-LD/Turtle parsers, context/registry/properties/README updaters all pass

Not in this PR

  • Container-level constraints. ASAM's objects / actions / events / contexts / relations uid-keyed containers, and the constraint that each entry's type string resolves to the right vocabulary category, are the remaining half of the v3 branch's intent. They belong in openlabel-v2-schema.yaml and its generated SHACL; tracked separately so this PR stays reviewable.
  • Container tooling. The Dockerfile / compose.yaml / .dockerignore from the v3 branch are unrelated to schema work and reference the pre-refactor src/ path. Separate PR.

@jdsika
jdsika force-pushed the feat/openlabel-v2-schema branch 3 times, most recently from bb363db to 7ddb7f7 Compare June 16, 2026 11:03
jdsika added a commit that referenced this pull request Jun 17, 2026
Addresses all findings from the PR #81 review.

Blocking (CI red):
- Reflow `extract_tag_value` signature in convert_openlabel_v1_to_v2.py so
  `ruff format` no longer rewrites the file (line exceeded the 88-char limit,
  which failed the Standards & Syntax "unexpected modified file" gate).

Medium:
- Add `encoding="utf-8"` to all file reads in sync_tag_type_enum.py and
  convert_openlabel_v1_to_v2.py (and the schema-reading test). The semantic
  model contains em-dashes; bare open() crashed on Windows (cp1252) — breaking
  the sync-tag-type-enum pre-commit hook and the converter for Windows users.
- Disclose the stricter `tag.ontology_uid` requirement in SCHEMA_MODELING.md
  (added equivalence-table row + note) and back it with a LINKML_STRICTER test.

Low / nits:
- Converter: accumulate repeated class+slot values into a list instead of
  silently overwriting (last-wins data loss); add coverage.
- Converter: collapse the identical if/else and four identical class-building
  branches.
- Converter: make `--pretty` a real toggle via argparse.BooleanOptionalAction
  (`--no-pretty`); previously it could never be False.
- Add a SHACL-conformance test that validates converter output against the
  openlabel-v2 shapes (the converter's stated purpose), not just @context/@type.
- sync_tag_type_enum.py: rewrite the TagTypeEnum end-detection to be
  position-independent (no longer relies on TagTypeEnum being the last enum).
- Docs: fix `gen_tag_type_enum.py` -> `sync_tag_type_enum.py` reference and
  document the structural-schema `$id` namespace choice in SCHEMA_MODELING.md.

No generated artifacts change; TagTypeEnum sync remains idempotent.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
…format

The semantic model describes the RDF tag vocabulary; it cannot say what a conforming
OpenLABEL *file* looks like before that file is read as RDF. A second LinkML schema,
openlabel-v2-schema.yaml, models the ASAM JSON document format and generates
artifacts/openlabel-v2/openlabel-v2.schema.json alongside the existing OWL, SHACL and
JSON-LD context. Both schemas stay generated: neither artifact is hand-edited.

`just generate` now builds a JSON Schema for any domain that carries a
<domain>-schema.yaml, with per-domain generator flags in jsonschema.genopts so the recipe
stays domain-neutral. gen-owl gains --no-use-native-uris so class URIs follow the declared
namespace rather than LinkML's native form.

sync_tag_type_enum.py keeps the structural schema's TagTypeEnum in step with the semantic
model, wired as a pre-commit hook that runs *before* generation - the reverse order would
generate artifacts from a stale enum. convert_openlabel_v1_to_v2.py converts ASAM v1 files
into the v2 instance shape.

Rebased onto main, which moved the package from src/tools to omb and replaced the Makefile
with just; the test that shells out to the validation suite now names omb.validators. The
submodules/linkml gitlink is not reinstated - main removed it, and the LinkML fork is
already carried as a dependency in pyproject.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika jdsika changed the title feat(openlabel): add structural LinkML schema for ASAM v1 JSON format feat(openlabel-v2): add the structural LinkML schema for the ASAM JSON format Aug 6, 2026
@jdsika
jdsika force-pushed the feat/openlabel-v2-schema branch from 2ae23c8 to 426e820 Compare August 6, 2026 17:16
@jdsika

jdsika commented Aug 6, 2026

Copy link
Copy Markdown
Author

What happened to feat/openlabel-v3-schema

@jobstbeckmann — a heads-up on what we did with your branch feat/openlabel-v3-schema (commit ac7c001, "feat(openlabel): add v3 structural validation + docker"), because this PR was rebased and force-pushed and the history no longer lines up with what you branched from.

Your branch is untouched and fully intact. The ref still points at ac7c001 and everything is reachable, so nothing is lost. What changed is that feat/openlabel-v2-schema was rebased onto current main and squashed to a single commit, so your branch no longer shares history with this PR. If you want to rework anything on top of the current state, branch from feat/openlabel-v2-schema again rather than rebasing your existing branch.

The idea we kept, because it was right

You identified a real gap that the v2 semantic model does not cover: validating an OpenLABEL file as ASAM actually emits it. Under v2 alone, a malformed file fails either as unparseable RDF or as an opaque SHACL violation — never as "your objects entry is missing ontology_uid". Putting a structural check before RDF interpretation puts the error at the layer that can describe it.

Your second insight is equally correct: ASAM's type is a plain string, not an @type IRI, so it has to be resolved against a vocabulary. Neither JSON Schema nor plain SHACL does that on its own.

Both of those are now in this PR. The structural schema lives in linkml/openlabel-v2-schema.yaml and generates artifacts/openlabel-v2/openlabel-v2.schema.json.

Why it became v2 rather than v3

We consolidated into v2 instead of adding a parallel namespace. The reasoning, from diffing the two artifact sets:

  1. openlabel-v3.owl.ttl was the v2 ontology copied verbatim. Identical named class sets — 205 classes, none present in only one file — and every apparent difference was blank-node relabelling. The v3 file was v2 plus 261 flag triples and 2 new classes. That leaves two sources of truth for one vocabulary with nothing keeping them in step.

  2. It declared two owl:Ontology headers in one file, including v2's own at version 2.0.0, while artifacts/catalog-v001.xml mapped the v3 IRI to it. Loading v2 and v3 together would deliver the same 205 classes from two files.

  3. v2 already had the wrapper classes. openlabel-v2-schema.yaml defines OpenLabelFile, OpenLabel, Metadata, OntologyEntry and ResourceUid. v2's OpenLabel already carried metadata and ontologies — it just used a single tags slot where ASAM has five uid-keyed containers. So the genuine delta was those containers, not a new namespace.

  4. A new namespace implies a new standard version. Nothing changed in ASAM; this is a second view of the same v2 terms. A v3 IRI would need w3id redirects and independent versioning for no semantic gain.

Smaller things we changed

  • Generated, not hand-authored. There was no linkml/openlabel-v3/ source, so just generate could not reproduce the artifacts and there was no PROPERTIES.md. The structural schema is now a LinkML source like every other domain, and the generate recipe builds a JSON Schema for any domain carrying a <domain>-schema.yaml.
  • Catalog resolution. The pre-check in ShaclValidator hardcoded artifacts/openlabel-v3/openlabel-v3.schema.json and matched an exact @context string. CLAUDE.md calls out bypassing catalogs in validators specifically, and the catalog already registers that schema — so this should resolve through the registry and key off the domain. It also returned on the first failing file rather than aggregating.
  • The src/toolsomb refactor. Your commit edited src/tools/validators/shacl/validator.py and the Dockerfile does COPY src ./src, but main deleted src/ in feat: make OMB an installable, publishable package + adopt just (W1 + W3b) #86. This is not your error — the branch you based on predated the refactor by 13 commits, which is also why this PR needed rebasing.
  • SHACL duplication. Six near-identical sh:sparql blocks differing only in container and flag, each re-hardcoding the v2 namespace inside CONCAT(...) so it appears as both a PREFIX and a string literal. Worth expressing once. Coverage was also uneven: objects, actions and contexts were constrained; events and relations were declared in the shape but never checked.
  • Docker split out. Dockerfile / compose.yaml / .dockerignore are unrelated to schema work and need the srcomb path fix; they belong in their own PR.

One thing where you were right and we were initially wrong

Our first reading was that allowedAction / allowedObject / allowedContext were redundant with rdfs:subClassOf and could simply be deleted. We tested that and it is false — v2 models those terms as boolean slots on Tag subclasses, a different shape, so your membership information is genuine and not derivable from the existing axioms. The measurement also showed every unflagged class is an *Enum wrapper, i.e. the flags correctly mark "usable vocabulary term".

The only criticism that survives is milder: three parallel booleans express three times what one annotation with a value (or a SKOS collection) would express once. That is a modelling preference, not a defect.

What is still open, and still yours

The other half of your intent is not in this PR, deliberately, so it stays reviewable:

  • the five uid-keyed containers (objects, actions, events, contexts, relations) on OpenLabel
  • the constraint that each entry's type resolves to the right vocabulary category

Both belong in openlabel-v2-schema.yaml and its generated SHACL, expressed once rather than six times, and designed against the ASAM spec rather than transliterated. Happy to pair on that, or to review it if you would rather carry it — it is your design and you have the ASAM context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant