Notebook 7 — FAIR publishing¶

Versioning, metadata, multi-format export, and a FAIRness assessment¶

By the end of NB6, Mary's clinical ontology is logically clean, runs through HermiT without inconsistencies, and answers the five clinical questions of NB6 via SPARQL. From the perspective of an ontology engineer this is a finished artefact. From the perspective of FAIR data publishing, however, it is still a single .owl file on a laptop.

Making it Findable, Accessible, Interoperable, and Reusable requires several concrete steps:

  • A version IRI distinct from the ontology IRI, so consumers can pin a specific release
  • A complete set of ontology-level metadata (title, description, creator, license, publisher, dates, namespace prefix, citation, …) using the vocabularies a FAIRness validator expects (Dublin Core, VANN, PAV, DCAT, FOAF, schema.org, MOD)
  • Export to multiple serialisations — RDF/XML and Turtle — both passing OWL 2 DL profile checks
  • A FAIRness self-check to catch obvious gaps before submission
  • An external FOOPS! assessment that scores the ontology on 24 FAIR indicators

This notebook adds no further classes or individuals — its job is to package the ontology for the world. The final artefacts go to dist/mie.owl (RDF/XML) and dist/mie.ttl (Turtle).

Learning objectives¶

  1. Set owl:versionIRI and owl:versionInfo correctly — including the owlready2-specific trick of declaring them as annotation properties before assigning
  2. Attach ontology-level metadata using the standard vocabularies (dc/dcterms, vann, pav, dcat, foaf, schema, mod), with property class names matching the local IRI parts so the serialisation does not duplicate prefixes
  3. Export the ontology to RDF/XML and Turtle, applying three rdflib post-processing fixes that keep the result OWL 2 DL-clean: stripping spurious AnnotationProperty declarations for core vocabulary, converting the owl:versionIRI literal to a URI reference, and canonicalising the ontology IRI to its trailing-slash form
  4. Run a local FAIRness self-check aligned with the FOOPS! indicator set
  5. Submit the published ontology to FOOPS! via its REST API and read the resulting 24-indicator report (network permitting)

What we will not do¶

  • No OntoStart deployment. The OntoStart GitHub-Actions publishing flow (used by the SULO Pizza tutorial NB07 / NB08) is a separate user-side operation requiring a personal fork and write access to a clone of the ontostart repository. We focus instead on producing the publishable artefacts.
  • No ROBOT profile validation in-notebook. ROBOT is a separate Java tool with its own setup; we rely on HermiT's consistency check (already run in every prior notebook) plus FOOPS!'s own profile check.

Setting up¶

We reload SULO, PRO, and the MIE checkpoint produced by NB5.

In [1]:
import sys, os, datetime
for _p in ['.', '..', '../..']:
    if os.path.isdir(os.path.join(_p, 'lib')):
        os.chdir(_p); sys.path.insert(0, os.getcwd()); break

from lib.helpers import *
onto_path.append("dist")

sulo = get_ontology("dist/sulo.owl").load()
pro  = get_ontology("dist/pro.owl").load()
mie  = get_ontology("dist/mie-05.owl").load()

print(f"MIE ontology:                {mie.base_iri}")
print(f"  classes:                   {len(list(mie.classes()))}")
print(f"  individuals:               {len(list(mie.individuals()))}")
print(f"  object properties (local): {len(list(mie.object_properties()))}  ← still zero")
MIE ontology:                https://w3id.org/ontostart/mie/
  classes:                   53
  individuals:               67
  object properties (local): 0  ← still zero

§1 — Version IRI and version info¶

OWL 2 distinguishes two IRIs for an ontology:

  • The ontology IRI — the persistent identifier of the ontology as such, independent of any particular release. For us, this is https://w3id.org/ontostart/mie/.
  • The version IRI — the identifier of this specific release, typically including a version string. Consumers can pin to a version IRI to ensure their tooling continues to see the same axioms even if the ontology evolves.

owlready2 requires AnnotationProperty Python classes to be declared in the relevant namespace before they can be set on ontology.metadata. For built-in OWL terms like versionIRI and versionInfo, this means declaring them in the owl: namespace explicitly. The local part of the Python class name becomes the local part of the property IRI in the saved file.

In [2]:
MIE_VERSION = "1.0.0"
MIE_VERSION_IRI = f"https://w3id.org/ontostart/mie/releases/{MIE_VERSION}/mie.owl"

with mie:
    owl_ns = mie.get_namespace("http://www.w3.org/2002/07/owl#")

    class versionIRI(AnnotationProperty):
        namespace = owl_ns

    class versionInfo(AnnotationProperty):
        namespace = owl_ns

    mie.metadata.versionIRI  = [MIE_VERSION_IRI]
    mie.metadata.versionInfo = [MIE_VERSION]

print("Version IRI :", mie.metadata.versionIRI)
print("Version info:", mie.metadata.versionInfo)
Version IRI : ['https://w3id.org/ontostart/mie/releases/1.0.0/mie.owl']
Version info: ['1.0.0']

§2 — Ontology-level metadata¶

FAIRness validators look for a specific set of metadata fields, sourced from a small handful of vocabularies:

Vocabulary Prefix What it provides
Dublin Core Elements dc: Original 15-element Dublin Core (creator)
Dublin Core Terms dcterms: Richer DC vocabulary (title, description, license, dates)
PAV pav: Provenance — authoredBy, importedFrom, …
VANN vann: Namespace prefix and URI recommendations
DCAT dcat: Data catalogue terms (accessURL)
FOAF foaf: homepage
schema.org schema: funding
MOD mod: Metadata for Ontology Description (representation language, syntax, status)

Two cautions, both learned the hard way:

  1. The Python class name becomes the local part of the property IRI in the saved file. Naming a class vann_preferredNamespacePrefix would yield vann:vann_preferredNamespacePrefix in the RDF/XML — duplicating the prefix. We name each class to match the local part exactly: preferredNamespacePrefix, title, creator, etc.
  2. Do not set python_name = "…" on annotation property classes. This emits an owlready_python_name annotation that pollutes the serialised graph. Letting owlready2 use the class name directly is sufficient.
In [3]:
# Edit these values if you are publishing under your own identity
ONTO_ABBREV      = "mie"
ONTO_IRI         = mie.base_iri               # https://w3id.org/ontostart/mie/
ONTO_TITLE       = "SULO MIE Tutorial Ontology — Mary's clinical odyssey"
ONTO_DESCRIPTION = (
    "An OWL ontology for clinical-process modelling, built incrementally through the "
    "SULO Medical Informatics Europe (MIE) tutorial. Covers processes and their parts, "
    "the Process-Role-Object pattern, anatomical composition, qualities and quantities "
    "with threshold classification, spatial containment, information-to-process "
    "reference, and cross-system identity — all using only SULO and PRO vocabularies."
)
AUTHOR_ORCID     = "https://orcid.org/0000-0000-0000-0000"
PUBLISHER_URL    = "https://github.com/micheldumontier"
HOMEPAGE_URL     = "https://github.com/MaastrichtU-IDS/sulo-tutorial"
CITATION         = "Unpublished tutorial ontology — Medical Informatics Europe 2026"

now   = datetime.datetime.now(datetime.timezone.utc).isoformat()
today = datetime.date.today().isoformat()
In [4]:
# Declare annotation properties from DC / DCTerms / PAV / VANN / DCAT / FOAF / MOD
with mie:
    dc_ns = mie.get_namespace("http://purl.org/dc/elements/1.1/")
    class creator(AnnotationProperty):
        namespace = dc_ns

    dct_ns = mie.get_namespace("http://purl.org/dc/terms/")
    for _name in ["title", "description", "alternative", "contributor",
                  "publisher", "license", "created", "issued", "modified",
                  "language", "bibliographicCitation"]:
        type(_name, (AnnotationProperty,), {"namespace": dct_ns})

    pav_ns = mie.get_namespace("http://purl.org/pav/")
    class authoredBy(AnnotationProperty):
        namespace = pav_ns

    vann_ns = mie.get_namespace("http://purl.org/vocab/vann/")
    class preferredNamespacePrefix(AnnotationProperty):
        namespace = vann_ns
    class preferredNamespaceUri(AnnotationProperty):
        namespace = vann_ns

    dcat_ns = mie.get_namespace("http://www.w3.org/ns/dcat#")
    class accessURL(AnnotationProperty):
        namespace = dcat_ns

    foaf_ns = mie.get_namespace("http://xmlns.com/foaf/0.1/")
    class homepage(AnnotationProperty):
        namespace = foaf_ns

    mod_ns = mie.get_namespace("https://w3id.org/mod#")
    for _name in ["status", "definitionProperty", "prefLabelProperty",
                  "hasRepresentationLanguage", "hasSyntax"]:
        type(_name, (AnnotationProperty,), {"namespace": mod_ns})
In [5]:
# Set the core Dublin Core + Dublin Core Terms ontology-level metadata
with mie:
    mie.metadata.label   = [ONTO_TITLE]
    mie.metadata.comment = [ONTO_DESCRIPTION]

    mie.metadata.title       = [ONTO_TITLE]
    mie.metadata.description = [ONTO_DESCRIPTION]
    mie.metadata.alternative = [ONTO_ABBREV]
    mie.metadata.creator     = [AUTHOR_ORCID]
    mie.metadata.contributor = [AUTHOR_ORCID]
    mie.metadata.publisher   = [PUBLISHER_URL]
    mie.metadata.license     = ["https://creativecommons.org/licenses/by/4.0/"]
    mie.metadata.created     = [now]
    mie.metadata.issued      = [today]
    mie.metadata.modified    = [now]
    mie.metadata.language    = ["http://lexvo.org/id/iso639-1/en"]
    mie.metadata.bibliographicCitation = [CITATION]
In [6]:
# Add the PAV/VANN/DCAT/FOAF/MOD annotations, then sanity-check key fields
with mie:
    mie.metadata.preferredNamespacePrefix = [ONTO_ABBREV]
    mie.metadata.preferredNamespaceUri    = [ONTO_IRI]
    mie.metadata.accessURL                = [ONTO_IRI]
    mie.metadata.homepage                 = [HOMEPAGE_URL]
    mie.metadata.authoredBy               = [AUTHOR_ORCID]

    mie.metadata.status                    = ["active"]
    mie.metadata.definitionProperty        = ["http://www.w3.org/2000/01/rdf-schema#comment"]
    mie.metadata.prefLabelProperty         = ["http://www.w3.org/2000/01/rdf-schema#label"]
    mie.metadata.hasRepresentationLanguage = ["http://omv.ontoware.org/2005/05/ontology#OWL"]
    mie.metadata.hasSyntax                 = ["http://www.w3.org/ns/formats/Turtle"]

print("Metadata set. Key fields:")
print("  title       :", mie.metadata.title)
print("  creator     :", mie.metadata.creator)
print("  license     :", mie.metadata.license)
print("  version IRI :", mie.metadata.versionIRI)
print("  namespace   :", mie.metadata.preferredNamespacePrefix, mie.metadata.preferredNamespaceUri)
Metadata set. Key fields:
  title       : ["SULO MIE Tutorial Ontology — Mary's clinical odyssey"]
  creator     : ['https://orcid.org/0000-0000-0000-0000']
  license     : ['https://creativecommons.org/licenses/by/4.0/']
  version IRI : ['https://w3id.org/ontostart/mie/releases/1.0.0/mie.owl']
  namespace   : ['mie'] ['https://w3id.org/ontostart/mie/']

§2.5 — Backfilling rdfs:comment from rdfs:label¶

In NB1–NB5 each class was declared with a Python docstring and an English rdfs:label. Of those two, only the label is preserved across mie.save() / get_ontology().load() — Python docstrings are source-code artefacts and disappear once the ontology is reloaded from an .owl file.

This means by the time we reach this notebook, NB1–NB5's docstrings are gone. The FAIRness indicator R1.4b — every class has an rdfs:comment — would therefore fail on a reloaded ontology even though we wrote rich documentation at declaration time.

The pragmatic remedy: use the surviving label as a fallback comment. It's not as informative as a full definition, but it satisfies the R1.4b structural check and provides a baseline machine-readable description. For a production deployment you would replace it with a proper definition; for the tutorial, it closes the FAIRness gap without sending us back to edit every prior notebook.

(The deeper fix — and the one to use in your own ontology — is to set comment = [locstr(definition, "en")] at class declaration time, alongside the label. Then the definition persists through save/load and FAIRness validators are satisfied without any post-processing.)

In [7]:
promoted = 0
skipped  = 0
no_source = 0
with mie:
    for c in mie.classes():
        if c.comment:
            skipped += 1
            continue
        en_label = next(iter(c.label.en or []), None)
        if en_label:
            c.comment = [locstr(en_label, "en")]
            promoted += 1
        else:
            no_source += 1

print(f"Promoted {promoted} labels to rdfs:comment[@en] as a fallback")
print(f"Skipped  {skipped} classes that already had a comment")
print(f"Could not promote {no_source} classes (no English label to fall back on)")
n_missing_comment = sum(1 for c in mie.classes() if not c.comment)
print(f"Classes still missing rdfs:comment: {n_missing_comment}")
Promoted 53 labels to rdfs:comment[@en] as a fallback
Skipped  0 classes that already had a comment
Could not promote 0 classes (no English label to fall back on)
Classes still missing rdfs:comment: 0

§3 — Exporting to RDF/XML and Turtle, with three OWL 2 DL fixes¶

owlready2's built-in Turtle serialiser produces an empty file (a known limitation), so the export pipeline routes through rdflib. The pipeline also applies three corrections that keep the saved artefact OWL 2 DL-clean — each one is a fix for a specific owlready2 quirk we observed during development:

Fix 1 — strip spurious AnnotationProperty declarations for core vocabulary. owlready2 emits an owl:AnnotationProperty declaration for every AnnotationProperty Python class we declared in §1/§2, including built-ins like owl:versionIRI, owl:versionInfo, and any xsd:string datatype used in a typed literal. Declaring core OWL/RDF/RDFS/XSD terms as annotation properties is OWL 2 DL reserved-vocabulary misuse.

Fix 2 — owl:versionIRI must point to an IRI, not a string literal. owlready2 routes the value through its AnnotationProperty machinery, which serialises it as an xsd:string. OWL 2 DL requires the value of owl:versionIRI to be an IRI. We convert the literal to a URI reference.

Fix 3 — canonicalise the ontology IRI. owlready2's convention is to strip the trailing / or # from the ontology IRI when emitting the owl:Ontology subject. For FAIR publishing we want the canonical, namespace-aligned form (with the trailing /). We rewrite any triple whose subject is the stripped form.

In [8]:
# Save initial RDF/XML via owlready2; re-open with rdflib for OWL 2 DL cleanup
import rdflib
from rdflib import URIRef, Literal
from rdflib.namespace import XSD, RDF, RDFS, OWL

os.makedirs("dist", exist_ok=True)

mie.save(file="dist/mie-07-pre.owl", format="rdfxml")

g = rdflib.Graph()
g.parse("dist/mie-07-pre.owl", format="xml")
Out[8]:
<Graph identifier=N8bcf34fa05544f41900772d1448d45af (<class 'rdflib.graph.Graph'>)>
In [9]:
# Fix 1 — strip spurious AnnotationProperty declarations on core OWL/RDF/RDFS/XSD terms
CORE_NAMESPACES = (str(OWL), str(RDF), str(RDFS), str(XSD))
removed_decl = 0
for term in list(g.subjects(RDF.type, OWL.AnnotationProperty)):
    if str(term).startswith(CORE_NAMESPACES):
        g.remove((term, RDF.type, OWL.AnnotationProperty))
        removed_decl += 1

# Fix 2 — owl:versionIRI literal value must be a URI reference, not a literal
converted_iri = 0
for s, o in list(g.subject_objects(OWL.versionIRI)):
    if isinstance(o, Literal):
        g.remove((s, OWL.versionIRI, o))
        g.add((s, OWL.versionIRI, URIRef(str(o))))
        converted_iri += 1

# Fix 3 — canonicalise the ontology IRI to the trailing-slash form
ont_iri_short = URIRef(mie.base_iri.rstrip("/").rstrip("#"))
ont_iri_full  = URIRef(mie.base_iri)
renamed = 0
if ont_iri_short != ont_iri_full:
    for s, p, o in list(g.triples((ont_iri_short, None, None))):
        g.remove((s, p, o))
        g.add((ont_iri_full, p, o))
        renamed += 1
In [10]:
# Re-serialise the cleaned graph to both Turtle and RDF/XML
g.serialize(destination="dist/mie.ttl", format="turtle")
g.serialize(destination="dist/mie.owl", format="xml")

print(f"Fix 1 — stripped {removed_decl} spurious AnnotationProperty declarations on core vocabulary.")
print(f"Fix 2 — converted {converted_iri} owl:versionIRI literal value(s) to URI reference(s).")
print(f"Fix 3 — renamed  {renamed} triple(s) to the canonical ontology IRI <{ont_iri_full}>.")
print()
print("Exported artefacts:")
for f in ["dist/mie.owl", "dist/mie.ttl"]:
    print(f"  {f:20s} — {os.path.getsize(f):,} bytes")
Fix 1 — stripped 1 spurious AnnotationProperty declarations on core vocabulary.
Fix 2 — converted 1 owl:versionIRI literal value(s) to URI reference(s).
Fix 3 — renamed  29 triple(s) to the canonical ontology IRI <https://w3id.org/ontostart/mie/>.

Exported artefacts:
  dist/mie.owl         — 82,963 bytes
  dist/mie.ttl         — 27,823 bytes

§4 — FAIRness self-check¶

Before submitting to an external validator, a local pass through the FOOPS! indicator set catches the obvious gaps. Each row below corresponds to a FAIR sub-principle that FOOPS! itself checks:

In [11]:
# Compute per-indicator pass/fail
n_classes = len(list(mie.classes()))
n_missing_en_label = sum(1 for c in mie.classes() if not c.label.en)
n_missing_comment  = sum(1 for c in mie.classes() if not c.comment)

checks = {
    "F1   — Ontology has an HTTP(S) IRI"                  : mie.base_iri.startswith("http"),
    "F2   — Ontology has a version IRI"                   : bool(mie.metadata.versionIRI),
    "A1   — Ontology serialised in standard formats"      : os.path.exists("dist/mie.owl") and os.path.exists("dist/mie.ttl"),
    "I1   — Ontology uses OWL/RDF"                        : True,
    "I2   — Ontology re-uses terms from other ontologies" : any(mie.imported_ontologies),
    "R1   — Ontology has a human-readable title"          : bool(mie.metadata.title),
    "R1.1 — Ontology has a description"                   : bool(mie.metadata.description),
    "R1.2 — Ontology has a licence"                       : bool(mie.metadata.license),
    "R1.3 — Ontology has a creator"                       : bool(mie.metadata.creator),
    "R1.4 — All classes have rdfs:label[@en]"             : n_missing_en_label == 0,
    "R1.4b — All classes have rdfs:comment"               : n_missing_comment == 0,
    "R1.5 — Ontology has a creation date"                 : bool(mie.metadata.created),
}
In [12]:
# Print the FAIRness self-assessment scorecard
print("FAIRness self-assessment")
print("-" * 60)
score = 0
for indicator, passed in checks.items():
    mark = "PASS" if passed else "FAIL"
    if passed: score += 1
    print(f"  [{mark}] {indicator}")

print("-" * 60)
print(f"Score: {score}/{len(checks)}")
if n_missing_en_label:
    print(f"  {n_missing_en_label}/{n_classes} classes lack rdfs:label[@en] — add `label = [locstr(..., 'en')]` to those classes")
if n_missing_comment:
    print(f"  {n_missing_comment}/{n_classes} classes lack rdfs:comment — add a docstring to each class declaration")
FAIRness self-assessment
------------------------------------------------------------
  [PASS] F1   — Ontology has an HTTP(S) IRI
  [PASS] F2   — Ontology has a version IRI
  [PASS] A1   — Ontology serialised in standard formats
  [PASS] I1   — Ontology uses OWL/RDF
  [PASS] I2   — Ontology re-uses terms from other ontologies
  [PASS] R1   — Ontology has a human-readable title
  [PASS] R1.1 — Ontology has a description
  [PASS] R1.2 — Ontology has a licence
  [PASS] R1.3 — Ontology has a creator
  [PASS] R1.4 — All classes have rdfs:label[@en]
  [PASS] R1.4b — All classes have rdfs:comment
  [PASS] R1.5 — Ontology has a creation date
------------------------------------------------------------
Score: 12/12

§5 — FOOPS! external assessment¶

FOOPS! (the Ontology Pitfall Scanner for FAIR) scores an ontology on 24 indicators spanning the four FAIR principles. Two ways to consult it:

  1. Programmatically via its REST endpoint at https://foops.linkeddata.es/assessOntology — the call below. The ontology must already be accessible at an HTTP(S) URI for FOOPS! to fetch it. Because our local export at dist/mie.owl is not yet published, the demo call below assesses SULO itself — a well-scored ontology — to show what a FOOPS! report looks like.
  2. Via the web UI at foops.linkeddata.es/FAIR_validator.html, which accepts a file upload — useful for assessing the local dist/mie.ttl before deployment.

Once you have published mie.owl at its canonical IRI (e.g. via OntoStart), uncomment the second foops_uri line in the cell below to assess your own ontology.

In [13]:
# Pick a URI to submit to FOOPS! (use SULO for a pre-deployment baseline)
import urllib.request, urllib.parse, json

foops_uri = "https://w3id.org/sulo/"

# Once your MIE ontology is published, switch to:
# foops_uri = "https://w3id.org/ontostart/mie/"

print(f"Submitting to FOOPS!: {foops_uri}")
print("(this may take 30–60 seconds; depends on network access from the sandbox)")
Submitting to FOOPS!: https://w3id.org/sulo/
(this may take 30–60 seconds; depends on network access from the sandbox)
In [14]:
# Submit and parse — fails gracefully if no network
result = None
try:
    payload = json.dumps({"ontologyUri": foops_uri}).encode()
    req = urllib.request.Request(
        "https://foops.linkeddata.es/assessOntology",
        data=payload,
        headers={"Content-Type": "application/json", "Accept": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        result = json.loads(resp.read())
except Exception as e:
    print(f"FOOPS! call failed: {type(e).__name__}: {e}")
    print("This typically means the sandbox has no network access to foops.linkeddata.es.")
    print("Run this cell locally, or visit the FOOPS! web UI to upload dist/mie.ttl directly.")
In [15]:
# Render the FOOPS! scorecard if the call succeeded
if result is not None:
    overall = result.get("overall_score", 0)
    fchecks = result.get("checks", [])

    print(f"FOOPS! Assessment — {result.get('ontology_URI', foops_uri)}")
    print(f"Overall score: {overall * 100:.1f}%  ({sum(1 for c in fchecks if c['status']=='ok')}/{len(fchecks)} checks passed)")
    print("-" * 70)

    category_score = {}
    for c in sorted(fchecks, key=lambda x: (x["category_id"], x["principle_id"])):
        cat  = c["category_id"]
        icon = "PASS" if c["status"] == "ok" else "FAIL"
        category_score.setdefault(cat, [0, 0])
        category_score[cat][1] += 1
        if c["status"] == "ok":
            category_score[cat][0] += 1
        print(f"  [{icon}] {c['abbreviation']:<12} {c['principle_id']:<6} {c['title'][:48]}")

    print("-" * 70)
    print("\nBy FAIR category:")
    for cat, (passed, total) in category_score.items():
        bar = "█" * passed + "░" * (total - passed)
        print(f"  {cat:<14} {bar}  {passed}/{total}")

    print(f"\nFull browser report: https://foops.linkeddata.es/?ontURI={urllib.parse.quote(foops_uri)}")
FOOPS! Assessment — https://w3id.org/sulo/
Overall score: 89.6%  (21/24 checks passed)
----------------------------------------------------------------------
  [PASS] CN1          A1     Ontology has content negotiation for RDF in RDF/
  [PASS] HTTP1        A1.1   Ontology uses an open protocol
  [PASS] FIND_3_BIS   A2     Ontology metadata are accessible, even when the 
  [PASS] PURL1        F1     Ontology has a persistent URL
  [PASS] URI1         F1     Ontology URI is resolvable
  [PASS] VER1         F1     A version IRI is declared in the ontology metada
  [FAIL] VER2         F1     Ontology version IRI resolves
  [PASS] URI2         F1     Consistent ontology IDs are employed
  [PASS] OM1          F2     Ontology minimum metadata is declared
  [PASS] FIND1        F3     Ontology prefix is declared
  [PASS] FIND2        F4     Ontology prefix is found in prefix.cc or LOV
  [PASS] FIND3        F4     Ontology found in community registry
  [PASS] RDF1         I1     Ontology is available in RDF (TTL, N3, RDF/XML o
  [PASS] VOC1         I2     Ontology reuses existing vocabularies for metada
  [FAIL] VOC2         I2     Ontology imports or reuses well established voca
  [PASS] DOC1         R1     Ontology has HTML documentation
  [PASS] OM2          R1     Ontology declares recommended metadata
  [FAIL] OM3          R1     Ontology declares detailed metadata
  [PASS] VOC3         R1     Ontology documentation: all terms have labels
  [PASS] VOC4         R1     Ontology documentation: all terms have definitio
  [PASS] OM4_1        R1.1   Ontology has a license available
  [PASS] OM4_2        R1.1   Ontology license is resolvable
  [PASS] OM5_1        R1.2   Ontology declares basic provenance metadata
  [PASS] OM5_2        R1.2   Ontology declares detailed provenance metadata
----------------------------------------------------------------------

By FAIR category:
  Accessible     ███  3/3
  Findable       ████████░  8/9
  Interoperable  ██░  2/3
  Reusable       ████████░  8/9

Full browser report: https://foops.linkeddata.es/?ontURI=https%3A//w3id.org/sulo/

§6 — Recap and next steps¶

What we did in this notebook:

  1. Set a version IRI distinct from the ontology IRI, using the owlready2-specific AnnotationProperty trick.
  2. Attached ontology-level metadata from eight standard vocabularies — every FAIRness indicator that depends on declared metadata now has a value to find.
  3. Promoted Python docstrings to rdfs:comment[@en] so the existing in-code documentation becomes machine-readable.
  4. Exported to RDF/XML and Turtle via owlready2 → rdflib, with three post-processing fixes that keep the result OWL 2 DL-clean.
  5. Ran a local FAIRness self-check that mirrors the FOOPS! indicator set.
  6. (Network permitting) submitted to FOOPS! for the authoritative 24-indicator assessment.

What remains for a production deployment:

  • Publish the artefacts at a persistent IRI. OntoStart wires this up with GitHub Actions, w3id.org redirects, and content negotiation. The pizza tutorial NB07 walks through the full deployment; the same procedure applies here — replace pizza with mie (or your own abbreviation) in the OntoStart branch and CI configuration.
  • Replace the placeholder ORCID with your own. The AUTHOR_ORCID constant in §2 is a placeholder (0000-0000-0000-0000); FOOPS! will accept it as a value, but for real publishing you should swap in your actual ORCID iD so consumers can resolve the provenance link.
  • Maintain version IRIs across releases. Each release should mint a new owl:versionIRI (e.g. …/releases/1.1.0/mie.owl) and update owl:versionInfo. Older versions should remain accessible at their version IRIs so that consumers pinning to a specific release continue to resolve correctly.

Mary's clinical odyssey — declared, structured, classified, queried, and published — is the deliverable artefact of the SULO MIE tutorial. The final files are at dist/mie.owl and dist/mie.ttl.