rdf/IPSDatedSites-bundle.ttl holds the data, the whole vocabulary and a
materialised CIDOC CRM crosswalk in a single file, so that it can be
loaded into a triplestore on its own — the NFDI4Objects knowledge graph in
particular.
Subclass axioms alone are not enough. Most triplestores perform no RDFS entailment by default, and a shared knowledge graph should not be assumed to. With the axioms present but nothing inferred, this is the result:
SELECT (COUNT(DISTINCT ?x) AS ?n) WHERE { ?x a crm:E53_Place }
0
Every findspot is a place by the axioms, yet a consumer querying in CIDOC
CRM — which is the entire purpose of the crosswalk — sees nothing. The
builder therefore computes the transitive closure over rdfs:subClassOf
and writes the inferred rdf:type triples out explicitly. The axioms are
kept alongside them, so a store that does reason derives nothing new and
nothing conflicts: the materialised triples are exactly what a reasoner
would produce.
Only rdf:type over rdfs:subClassOf is inferred. No domain or range
inference, no property closure, no owl:sameAs propagation — those would
add bulk and embed assumptions the source data do not support.
The closure is computed from the axioms found in the graph rather than from a hard-coded table, so adding a superclass in the export propagates into the bundle without a second edit.
flowchart LR
I["samian:fs_1003978_969c47<br/>one findspot"]
A["lado:Findspot"]
I -->|rdf:type, asserted| A
crm_E53_Place["crm:E53_Place"]
I -.->|rdf:type, materialised| crm_E53_Place
lado_Location["lado:Location"]
I -.->|rdf:type, materialised| lado_Location
class I io
class A local
class crm_E53_Place crm
class lado_Location local
classDef local fill:#e8eef7,stroke:#4a6b96,stroke-width:1px,color:#12181f
classDef crm fill:#efe7f5,stroke:#7a5a96,stroke-width:1px,color:#12181f
classDef time fill:#e3f2ec,stroke:#3f8a70,stroke-width:1px,color:#12181f
classDef ext fill:#f3f1ec,stroke:#8a857a,stroke-width:1px,color:#12181f
classDef io fill:#faf3e3,stroke:#a8872e,stroke-width:1px,color:#12181f
Solid edge asserted, dashed edges written out by the builder. Without them a CIDOC CRM query returns nothing.
JPG · SVG · Mermaid source — generated, do not edit.
The same node in the finished bundle, drawn from the triples:
SVG · GraphViz source — cut from the graph by CONSTRUCT, generated, do not edit.
flowchart TB
SRC["py/ips_rdf_export.py<br/><i>the model</i>"]
GEN["py/make_webjs.py<br/><i>injects prefixes, measure lists,<br/>figure constants, subclass closure,<br/>the vocabulary prelude</i>"]
BODY["py/templates/ips_rdf_body.js<br/><i>graph shape, hand-written</i>"]
JS["webjs/ips_rdf.js"]
CSV["one CSV"]
GPY["graph, built in Python"]
GJS["graph, built in the browser"]
GATE{"sorted N-Triples<br/>SHA-256 compared"}
OUT["identical, or the build fails"]
SRC --> GEN --> JS
BODY --> JS
CSV --> GPY
CSV --> GJS
SRC --> GPY
JS --> GJS
GPY --> GATE
GJS --> GATE
GATE --> OUT
class SRC,GEN,BODY local
class JS,CSV,GPY,GJS ext
class GATE,OUT time
classDef local fill:#e8eef7,stroke:#4a6b96,stroke-width:1px,color:#12181f
classDef crm fill:#efe7f5,stroke:#7a5a96,stroke-width:1px,color:#12181f
classDef time fill:#e3f2ec,stroke:#3f8a70,stroke-width:1px,color:#12181f
classDef ext fill:#f3f1ec,stroke:#8a857a,stroke-width:1px,color:#12181f
classDef io fill:#faf3e3,stroke:#a8872e,stroke-width:1px,color:#12181f
The browser emitter is generated where the model is tabular and hand-written where it is a graph shape. What holds the two together is the hash comparison, not the generation.
JPG · SVG · Mermaid source — generated, do not edit.
Each of these is reported as a void:classPartition in the file’s own
self-description:
| Class | Counted as |
|---|---|
lado:DiscoverySite |
sites |
lado:Findspot |
findspots |
lado:FindspotDating |
datings |
lado:PlotRow |
plot rows |
lado:DatingActivity |
activities |
crm:E7_Activity |
CIDOC CRM activities (after materialisation) |
crm:E53_Place |
CIDOC CRM places (after materialisation) |
crm:E52_Time-Span |
CIDOC CRM time-spans (after materialisation) |
time:ProperInterval |
OWL-Time intervals (after materialisation) |
The incremental export makes no rdf:type statement about the published
samian:loc_ds_* nodes; it only references them. That is correct for an
export intended to sit alongside loc_discoverysite_1.ttl.
A standalone bundle is a different case. Without the published file
present, crm:P89_falls_within would point at an untyped node and a query
for places would miss the sites entirely. The builder therefore asserts
lado:DiscoverySite on them, which materialises through lado:Location
to crm:E53_Place.
This is a restatement rather than a new claim: the published file already
types those nodes the same way. Merging the two files was checked
directly, and the bundle adds only what the axioms entail —
crm:E53_Place on each site — with no contradiction. Use
--no-type-sites to suppress it where the bundle is only ever loaded
together with the published data.
One exception surfaced during that check and is worth recording: of the 32
sites referenced here, samian:loc_ds_1004709 (Pecio del Guadiaro, a
shipwreck) has no triples at all in the 2019 release of
loc_discoverysite_1.ttl. The source database knows the identifier
but that file does not contain the node — the site postdates the release.
In a merged graph it would be a reference into nothing; in the bundle it
is at least typed and labelled. A regenerated discovery-site export would
close the gap, and nothing here would need to change, because the export
only ever references those nodes.
The file is self-contained Turtle. No imports, no external ontology needs to be resolved at load time — the CIDOC CRM and OWL-Time terms are used by URI and their own definitions can be loaded separately if wanted, but nothing here depends on their presence.
The file describes itself as a void:Dataset and a dcat:Dataset,
carrying title, issue date, licence and source, and stating in an
rdfs:comment how many of its rdf:type triples are inferred rather than
asserted.