Skip to content

Commit 4a5c90b

Browse files
fabio-rovaiclaude
andauthored
OWL 2 functional syntax files load, including the ORE corpus published as .owl (#311)
Every file loader handed its bytes to an Oxigraph RDF parser and routed `.owl` to RDF/XML, so an OWL functional-syntax (OFN) document failed on its first line with `Unexpected text event: 'Prefix(owl:='`. That is every file of the ORE 2015 reasoner-competition pool, 1,920 OFN documents named `.owl`. `src/ofn.rs` is a hand-written tokeniser and recursive-descent parser that emits exactly the triples the W3C "OWL 2 Mapping to RDF Graphs (Second Edition)" assigns: Table 1 for declarations, class and property expressions, data ranges and axioms, Table 2 for annotations, and Section 2.3 for annotated axioms in all three of its cases. SWRL `DLSafeRule`s become the `swrl:` vocabulary `onto_rules_import` already reads. It adds no dependency; the established Rust OWL library is LGPL and this crate is MIT. A construct it does not translate fails the whole document with its name, count and first line, and parsing continues past it so one error names every such construct. Malformed input is a syntax error naming the line and what was expected. `GraphStore` now decides the format in one place for every path that reads a file, so `load`, `validate`, `convert`, `lint`, `batch`, `onto_load` with its compile cache, `onto_validate` and `onto_convert` all reach the reader. A document is OFN when its first token is `Prefix(` or `Ontology(`, whatever its extension, and `.ofn` is accepted by name. Inline validation sniffs the same way. Measured against OWL API 4.5.29 on 93 ORE files (20 each from the dl, el and pure_dl track lists spread across sizes from 10 KB to 22 MB, plus the smallest files carrying each rare construct, 2.8 million triples in all): all 93 have identical lean graphs once seven documented OWL API habits are undone, 38 with no adjustment at all. The seven are listed in `src/ofn.rs` and none is a defect in either reader: an owl:Thing-qualified cardinality written unqualified, owl:distinctMembers for owl:members, type triples for undeclared entities (115,749 triples over 53 files), rdf:List types on some list cells, a copied list as a reification's annotatedTarget, some annotated axioms written more than once, and one anonymous expression shared by two axioms that differ only in annotations. The OWL API also sorts set operands and reorders SWRL atoms; the reader keeps source order. A 94th sampled file could not be compared because its IRI contains U+FFFD, which RFC 3987 forbids and which Oxigraph rejects in the OWL API's own RDF/XML output too. Over the pool, the 1,828 smallest files (every file up to 49 MB) were validated: 1,821 read cleanly, 128 million triples, and the other 7 are refused for IRIs RFC 3987 does not allow (U+FFFD, and `[` or `]` in a fragment), each with the line and the IRI. The 92 files above 49 MB were not run here. `tests/ofn_reader_test.rs` holds each construct family against Turtle written from the specification and compared after RDFC 1.0 canonicalisation, the refusal and syntax-error contracts, every load path including the registry cache, and an OWL API fixture that must agree once the documented differences are undone and must disagree when any one rewrite is left out. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 818d2c6 commit 4a5c90b

17 files changed

Lines changed: 3176 additions & 46 deletions

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ All notable changes to Open Ontologies are documented here.
1717

1818
### Added
1919

20+
- **OWL 2 functional syntax loads, whatever the file is called.** `src/ofn.rs`
21+
reads OFN into exactly the triples the W3C "OWL 2 Mapping to RDF Graphs"
22+
assigns it, and every path that reads a file reaches it: `load`, `validate`,
23+
`convert`, `lint`, `batch`, `onto_load` with its compile cache,
24+
`onto_validate` and `onto_convert`. A document is OFN when its first token is
25+
`Prefix(` or `Ontology(`, so the ORE 2015 reasoner-competition corpus, 1,920
26+
OFN files published as `.owl` that all died in the RDF/XML parser, now loads,
27+
and `.ofn` is accepted by name. A construct the reader does not translate
28+
fails the whole document by name, count and first line, never a silent drop.
29+
SWRL `DLSafeRule`s become the `swrl:` vocabulary `onto_rules_import` already
30+
reads. Checked against OWL API 4.5.29 on 93 ORE files across the `dl`, `el`
31+
and `pure_dl` tracks: all 93 agree once seven documented OWL API habits are
32+
undone, and none of the seven is a defect in either reader.
33+
2034
- **A named profile hands a client one job's worth of the tool surface** (#263).
2135
A client aligning two vocabularies has no use for most of what this server
2236
registers, and every tool it has no use for is one more chance to call the

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Claude dynamically decides the next tool call based on what the previous tool re
5959
| `onto_query` | To verify structure, answer competency questions, explore the ontology |
6060
| `onto_diff` | To compare against a reference or previous version |
6161
| `onto_save` | To persist the ontology to a file |
62-
| `onto_convert` | To convert between formats (Turtle, N-Triples, RDF/XML, N-Quads, TriG) |
62+
| `onto_convert` | To convert between formats (reads Turtle, N-Triples, RDF/XML, N-Quads, TriG, JSON-LD and OWL 2 functional syntax; writes the first five) |
6363
| `onto_clear` | To reset the store before loading a different ontology |
6464
| `onto_marketplace` | To browse and install ontologies: 33 curated W3C/ISO/industry standards plus community packs from the open registry (`community/registry.json`, fetched at runtime; `community=false` for curated/offline only). Curated IDs always win over community IDs |
6565
| `onto_plugin_list` | To discover installed WASM plugins (community `.wasm` tools in `~/.open-ontologies/plugins` or `./plugins`) and the tools each declares. Requires a build with `--features plugins` |

‎SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ For aligning two ontologies:
152152
| `onto_query` | Verify structure, answer competency questions |
153153
| `onto_diff` | Compare against a reference or previous version |
154154
| `onto_save` | Persist ontology to a file |
155-
| `onto_convert` | Convert between formats (Turtle, N-Triples, RDF/XML, N-Quads, TriG) |
155+
| `onto_convert` | Convert between formats (reads Turtle, N-Triples, RDF/XML, N-Quads, TriG, JSON-LD and OWL 2 functional syntax; writes the first five) |
156156
| `onto_clear` | Reset the store before loading a different ontology |
157157
| `onto_pull` | Fetch ontology from a remote URL or SPARQL endpoint |
158158
| `onto_push` | Push ontology to a SPARQL endpoint |

‎docs/interop-with-graph-platforms.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ standards, so the handoff is a file or a SPARQL endpoint:
5353

5454
| Concern | Shared ground |
5555
|---|---|
56-
| Serialisation | Turtle, N-Triples, RDF/XML, JSON-LD, TriG |
56+
| Serialisation | Turtle, N-Triples, RDF/XML, JSON-LD, TriG; OWL 2 functional syntax read |
5757
| Schema | OWL, RDFS, SKOS |
5858
| Constraints | SHACL |
5959
| Provenance | PROV-O (`prov:wasDerivedFrom`) |

‎docs/tool-reference.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ it withheld.
104104
| `onto_coevolve_dependency_graph` | Build the shape→OWL-dependency map for a SHACL document. |
105105
| `onto_communities` | Detect communities in the loaded entity graph and return a SKELETON per community: size, top members by degree, internal relations, and the bridges connecting it to other communities. |
106106
| `onto_conservative_check` | Does adding these axioms change anything the ontology ALREADY said? Reasons base and base+extension to a fixpoint under one rule table and reports closure(base ∪ extension) minus closure(base), restricted to triples every name of which the base already used. |
107-
| `onto_convert` | Convert an RDF file between formats: turtle, ntriples, rdfxml, nquads, trig |
107+
| `onto_convert` | Convert an ontology file to turtle, ntriples, rdfxml, nquads or trig. |
108108
| `onto_cq_run` | Run a batch of competency questions (CQs) against the loaded ontology (#29). |
109109
| `onto_cq_verdicts_list` | List all stored verdicts for a CQ id, most-recent first. |
110110
| `onto_crosswalk` | Look up clinical crosswalk mappings for a code and system (ICD10, SNOMED, MeSH). |
@@ -141,7 +141,7 @@ it withheld.
141141
| `onto_lineage` | Get the compact lineage log for the current or specified session. |
142142
| `onto_lint` | Check an ontology for quality issues: missing labels, comments, domains, ranges |
143143
| `onto_lint_feedback` | Accept or dismiss a lint issue to improve future lint runs. |
144-
| `onto_load` | Load an RDF file or inline Turtle content into the in-memory ontology store. |
144+
| `onto_load` | Load an ontology file or inline Turtle content into the in-memory ontology store. |
145145
| `onto_lock` | Lock IRIs to prevent removal during plan/apply. |
146146
| `onto_map` | Generate a mapping config by inspecting a data file's schema against the currently loaded ontology. |
147147
| `onto_marketplace` | Browse and install ontologies from the curated catalogue of 33 W3C/ISO/industry standards plus the open community-pack registry (community/registry.json, fetched at runtime; override with OPEN_ONTOLOGIES_COMMUNITY_REGISTRY). |

‎skills/ontology-engineering/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ When embeddings exist, `onto_align` automatically uses them as a 7th alignment s
102102
| `onto_query` | Verify structure, answer competency questions |
103103
| `onto_diff` | Compare against a reference or previous version |
104104
| `onto_save` | Persist the active ontology to a file |
105-
| `onto_convert` | Convert between Turtle, N-Triples, RDF/XML, N-Quads, TriG |
105+
| `onto_convert` | Convert between formats (reads Turtle, N-Triples, RDF/XML, N-Quads, TriG, JSON-LD and OWL 2 functional syntax; writes the first five) |
106106
| `onto_clear` | Reset the in-memory store |
107107
| `onto_pull` | Fetch ontology from a remote URL or SPARQL endpoint |
108108
| `onto_push` | Push triples to a SPARQL endpoint |

‎src/graph.rs‎

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ use oxigraph::model::*;
77
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
88
use oxigraph::store::Store;
99

10+
/// How a document is serialised: one of the RDF formats Oxigraph parses, or
11+
/// OWL 2 functional syntax, which is not RDF and is read by [`crate::ofn`].
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13+
enum SourceFormat {
14+
Rdf(RdfFormat),
15+
OwlFunctional,
16+
}
17+
1018
/// What a parse produced. `statements` counts parser events; `triples` counts
1119
/// distinct triples, which is the size of the resulting graph. They differ
1220
/// whenever the source repeats a statement.
@@ -299,23 +307,42 @@ impl GraphStore {
299307

300308
fn load_file_mode(&self, path: &str, replace: bool) -> anyhow::Result<usize> {
301309
let content = std::fs::read_to_string(path)?;
302-
let format = Self::detect_format_sniffed(path, &content);
303-
let reader = Cursor::new(content.as_bytes());
304-
305-
// A document's own location is its default base, per RFC 3986. Without
306-
// it, any file using relative IRIs fails to parse at all, which is
307-
// most published RDF/XML: LUBM's generated data would not load a
308-
// single triple before this.
309-
let base = Self::file_base_iri(path);
310310
// All or nothing: see load_turtle (issue #93).
311-
let mut parser = RdfParser::from_format(format);
312-
if let Some(base) = base {
313-
parser = parser.with_base_iri(base)?;
314-
}
315-
let quads: Vec<_> = parser.for_reader(reader).collect::<Result<_, _>>()?;
311+
let quads = Self::parse_document(path, &content)?;
316312
self.store_parsed_quads(&quads, replace)
317313
}
318314

315+
/// Parse a whole document, whatever it is serialised as, into quads.
316+
///
317+
/// Every path that reads a FILE goes through here (`load_file`,
318+
/// `replace_file`, `validate_file`, `content_as_turtle`), so a format this
319+
/// engine learns to read is learned once and reaches the CLI, `batch`, the
320+
/// MCP tools and the compile cache together. Before the OWL functional
321+
/// syntax reader existed each of those called the RDF parser directly, and
322+
/// adding a format meant finding all of them.
323+
fn parse_document(path: &str, content: &str) -> anyhow::Result<Vec<Quad>> {
324+
match Self::detect_source_format(path, content) {
325+
SourceFormat::OwlFunctional => Ok(crate::ofn::parse(content)?
326+
.into_iter()
327+
.map(|t| t.in_graph(GraphName::DefaultGraph))
328+
.collect()),
329+
SourceFormat::Rdf(format) => {
330+
// A document's own location is its default base, per RFC 3986.
331+
// Without it, any file using relative IRIs fails to parse at
332+
// all, which is most published RDF/XML: LUBM's generated data
333+
// would not load a single triple before this. OFN needs none:
334+
// its grammar admits only absolute IRIs.
335+
let mut parser = RdfParser::from_format(format);
336+
if let Some(base) = Self::file_base_iri(path) {
337+
parser = parser.with_base_iri(base)?;
338+
}
339+
Ok(parser
340+
.for_reader(Cursor::new(content.as_bytes()))
341+
.collect::<Result<_, _>>()?)
342+
}
343+
}
344+
}
345+
319346
fn store_parsed_quads(&self, quads: &[Quad], replace: bool) -> anyhow::Result<usize> {
320347
let before = if replace {
321348
0
@@ -361,8 +388,8 @@ impl GraphStore {
361388

362389
/// As `read_as_turtle`, for content already in hand (stdin, for instance).
363390
pub fn content_as_turtle(path_hint: &str, content: String) -> anyhow::Result<String> {
364-
let format = Self::detect_format_sniffed(path_hint, &content);
365-
if format == RdfFormat::Turtle {
391+
let format = Self::detect_source_format(path_hint, &content);
392+
if format == SourceFormat::Rdf(RdfFormat::Turtle) {
366393
if let Some(base) = Self::file_base_iri(path_hint) {
367394
// Keep the default base on the first source line so downstream
368395
// parsers report the document's original line numbers. A later
@@ -374,14 +401,7 @@ impl GraphStore {
374401
let store = Self::new();
375402
{
376403
let inner = &store.store;
377-
let base = Self::file_base_iri(path_hint);
378-
let mut parser = RdfParser::from_format(format);
379-
if let Some(base) = base {
380-
parser = parser.with_base_iri(base)?;
381-
}
382-
let quads: Vec<_> = parser
383-
.for_reader(Cursor::new(content.as_bytes()))
384-
.collect::<Result<_, _>>()?;
404+
let quads = Self::parse_document(path_hint, &content)?;
385405
for quad in &quads {
386406
inner.insert(quad)?;
387407
}
@@ -391,13 +411,35 @@ impl GraphStore {
391411

392412
pub fn validate_file(path: &str) -> anyhow::Result<ValidationCounts> {
393413
let content = std::fs::read_to_string(path)?;
394-
let format = Self::detect_format_sniffed(path, &content);
395-
let reader = Cursor::new(content.as_bytes());
396-
let mut parser = RdfParser::from_format(format);
397-
if let Some(base) = Self::file_base_iri(path) {
398-
parser = parser.with_base_iri(base)?;
414+
if let SourceFormat::Rdf(format) = Self::detect_source_format(path, &content) {
415+
let reader = Cursor::new(content.as_bytes());
416+
let mut parser = RdfParser::from_format(format);
417+
if let Some(base) = Self::file_base_iri(path) {
418+
parser = parser.with_base_iri(base)?;
419+
}
420+
return Self::count_parsed(parser.for_reader(reader));
421+
}
422+
Self::count_ofn(&content)
423+
}
424+
425+
/// Validate text handed over without a file name: the CLI's `validate -`
426+
/// and `onto_validate` with `inline: true`. Both used to go straight to
427+
/// the Turtle parser, which is right for everything they were written for
428+
/// and wrong for an OWL functional-syntax document, which no Turtle parser
429+
/// can read. With no extension to go on, the content decides.
430+
pub fn validate_text(content: &str) -> anyhow::Result<ValidationCounts> {
431+
if crate::ofn::looks_like_ofn(content) {
432+
return Self::count_ofn(content);
399433
}
400-
Self::count_parsed(parser.for_reader(reader))
434+
Self::validate_turtle(content)
435+
}
436+
437+
fn count_ofn(content: &str) -> anyhow::Result<ValidationCounts> {
438+
Self::count_parsed(
439+
crate::ofn::parse(content)?
440+
.into_iter()
441+
.map(|t| Ok::<_, oxigraph::io::RdfParseError>(t.in_graph(GraphName::DefaultGraph))),
442+
)
401443
}
402444

403445
/// A local document's default base is its encoded file URL. Building this
@@ -1275,6 +1317,24 @@ impl GraphStore {
12751317
Ok(out)
12761318
}
12771319

1320+
/// The serialisation of a document, deciding between the RDF formats
1321+
/// Oxigraph parses and the one this crate parses itself.
1322+
///
1323+
/// OWL functional syntax wins on content before anything else is
1324+
/// consulted, because the extension it most often travels under is `.owl`,
1325+
/// and `.owl` means RDF/XML to [`Self::detect_format`]. The ORE 2015
1326+
/// reasoner-competition corpus is 1,920 such files and every one of them
1327+
/// reached the RDF/XML parser and died on its first line. `.ofn` is the
1328+
/// one extension that names the syntax, so it is honoured even when the
1329+
/// content does not sniff, which sends a malformed `.ofn` file to the
1330+
/// reader that can say what is wrong with it.
1331+
fn detect_source_format(path: &str, content: &str) -> SourceFormat {
1332+
if crate::ofn::looks_like_ofn(content) || path.to_ascii_lowercase().ends_with(".ofn") {
1333+
return SourceFormat::OwlFunctional;
1334+
}
1335+
SourceFormat::Rdf(Self::detect_format_sniffed(path, content))
1336+
}
1337+
12781338
fn detect_format(path: &str) -> RdfFormat {
12791339
if path.ends_with(".ttl") || path.ends_with(".turtle") {
12801340
RdfFormat::Turtle

‎src/inputs.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ use serde::Deserialize;
55

66
#[derive(Deserialize, JsonSchema)]
77
pub struct OntoValidateInput {
8-
/// Path to an RDF file OR inline Turtle content
8+
/// Path to an ontology file (any format onto_load reads, OWL functional
9+
/// syntax included) OR inline Turtle or OWL functional-syntax content
910
pub input: String,
1011
/// If true, treat input as inline content rather than a file path
1112
pub inline: Option<bool>,
1213
}
1314

1415
#[derive(Deserialize, JsonSchema)]
1516
pub struct OntoConvertInput {
16-
/// Path to source RDF file
17+
/// Path to the source file: any format onto_load reads, OWL functional
18+
/// syntax included
1719
pub path: String,
1820
/// Target format: turtle, ntriples, rdfxml, nquads, trig
1921
pub to: String,
@@ -23,7 +25,8 @@ pub struct OntoConvertInput {
2325

2426
#[derive(Deserialize, JsonSchema)]
2527
pub struct OntoLoadInput {
26-
/// Path to RDF file, OR inline Turtle/RDF content
28+
/// Path to an ontology file: Turtle, N-Triples, RDF/XML, N-Quads, TriG,
29+
/// JSON-LD or OWL 2 functional syntax
2730
pub path: Option<String>,
2831
/// Inline Turtle content to load (alternative to path)
2932
pub turtle: Option<String>,

‎src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ pub mod marketplace;
7979
/// answered, on rmcp 1.4.0 and on 1.8.0 alike.
8080
pub mod mcp_handshake;
8181
pub mod monitor;
82+
/// OWL 2 Functional-Style Syntax read into the RDF graph the W3C mapping
83+
/// assigns it, so `.ofn` files and the OFN-under-`.owl` corpora load.
84+
pub mod ofn;
8285
pub mod ontology;
8386
pub mod ossie;
8487
pub mod plan;

‎src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2553,7 +2553,7 @@ async fn async_main() -> anyhow::Result<()> {
25532553
let result = if input == "-" {
25542554
let mut buf = String::new();
25552555
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
2556-
GraphStore::validate_turtle(&buf)
2556+
GraphStore::validate_text(&buf)
25572557
} else {
25582558
GraphStore::validate_file(&input)
25592559
};

0 commit comments

Comments
 (0)