Skip to content

feat(ogar-adapter-ttl): scaffold Turtle (RDF/OWL) adapter — Phase 2a#37

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/phase-2a-ogar-adapter-ttl-scaffold
Jun 5, 2026
Merged

feat(ogar-adapter-ttl): scaffold Turtle (RDF/OWL) adapter — Phase 2a#37
AdaWorldAPI merged 1 commit into
mainfrom
claude/phase-2a-ogar-adapter-ttl-scaffold

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Summary

New crate crates/ogar-adapter-ttl — companion to ogar-adapter-surrealql. Completes the Morris-syntax axis (per ADR-023 + docs/RDF-OWL-ALIGNMENT.md §2) for OGAR's two canonical wire formats:

source dialect adapter direction
SurrealQL DDL ogar-adapter-surrealql parse + emit (PR #32)
Turtle (RDF/OWL) ogar-adapter-ttl (this PR) parse + emit

This is Phase 2a of the brutal-upgrade sequencing in RDF-OWL-ALIGNMENT.md §10.

Public API

pub fn emit_ttl(classes: &[Class], prefix: &str) -> String;        // always available
pub fn parse_ttl(ttl: &str) -> Result<Vec<Class>, TtlParseError>;  // behind `ttl-parser`

Emit (feature-free)

Composes ogar-emitter::TripleEmitter::emit_class output → canonical Turtle: @prefix block + subject-grouped triples + full identity URIs.

Includes percent-encoding for the angle-bracket IRI form (RFC 3987) — necessary because OGAR's association_identity uses -> as the class-to-relation separator (ogit-erp/Invoice->customer), and > is illegal inside Turtle IRIs. Encoded as %3E on emit; oxttl recovers it symmetrically on parse.

Parse (behind ttl-parser feature)

Uses oxttl v0.2.3 + oxrdf v0.3 (oxigraph project, pure Rust, RFC-compliant). The walker:

  1. Drives oxttl::TurtleParser::for_slice → triple stream
  2. Groups by subject in a HashMap
  3. Finds every ?s rdf:type ogar:Class → candidate Class
  4. Strips OGAR namespace from predicate URIs to match local names (parentClass, hasField, hasAssociation, …)
  5. Recursively lifts fields (ogar:Field) → Attribute and associations (ogar:Association) → Association
  6. Preserves definition order

Supported lifts (v1)

OGAR IR Triples → IR mapping
Class { name, parent, description, table_name, record_order } full
Attribute (rdfs:range typed) via ogar:hasFieldogar:fieldName / ogar:fieldType / rdfs:range
Association (BelongsTo / HasOne / HasMany / HasAndBelongsToMany) via ogar:hasAssociationogar:kind / ogar:targetClass / ogar:relationName

Not yet supported (next sprint)

  • EnumDecl (lift from owl:oneOf / owl:unionOf)
  • ActionDef / KausalSpec (lifecycle — TTL doesn't carry state machines natively; OGAR-extension predicates only)
  • Alignment-axiom parsing (owl:equivalentClass / owl:equivalentProperty) — Phase 4 ogar-pattern shape
  • Full vocab/ogar.ttl round-trip

Tests (9 — 5 emit always, 4 parse + round-trip behind feature)

Test Feature What it asserts
emit_empty_classes_produces_just_prefix_block always prefix block always emitted
emit_minimal_class_includes_rdf_type_ogar_class always basic class subject + rdf:type ogar:Class
emit_class_with_parent_emits_parent_class_predicate always ogar:parentClass predicate fires
emit_class_with_attribute_emits_field_triples always ogar:hasField + ogar:fieldName fire
parse_minimal_class_lifts_one_class ttl-parser parse-side lift of ?s rdf:type ogar:Class
parse_returns_unimplemented_or_error_on_invalid_ttl ttl-parser error propagation
round_trip_minimal_class_preserves_name ttl-parser name byte-identical after round-trip
round_trip_class_with_parent_preserves_inheritance ttl-parser parent preserved
round_trip_class_with_belongs_to_preserves_association ttl-parser the load-bearing one — exercises class→field→association lift + percent-encoding fix for -> in URIs

Workspace + CI

Verification

$ cargo test --workspace                                            -> clean
$ cargo test -p ogar-adapter-ttl                                    -> 5/5
$ cargo test -p ogar-adapter-ttl --features ttl-parser              -> 9/9
$ cargo test -p ogar-adapter-surrealql --features surrealdb-parser  -> 33/33
$ cargo test -p ogar-knowable-from --features surrealql-hint        -> 10/10
$ cargo check --workspace --all-targets                             -> clean

PII abort-guard (word-boundary): CLEAN on all touched files.

Note on the queue

PR #34 (SPLAT-NATIVE-CUSTOMER.md) + PR #35 (its Codex follow-up — IVD-MDR → MDR Annex VIII) are the other session's work — left to them per your previous instruction. This PR is OGAR session's own.

https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY

New crate `crates/ogar-adapter-ttl` — companion to ogar-adapter-
surrealql, completes the Morris-syntax axis (per ADR-023 and
docs/RDF-OWL-ALIGNMENT.md §2) for OGAR's two canonical wire formats:

  | source dialect       | adapter                | direction       |
  |----------------------|------------------------|-----------------|
  | SurrealQL DDL        | ogar-adapter-surrealql | parse + emit    |
  | Turtle (RDF/OWL)     | ogar-adapter-ttl (NEW) | parse + emit    |

# Public API

  emit_ttl(classes, prefix) -> String              (always available)
  parse_ttl(ttl) -> Result<Vec<Class>, TtlParseError>
                                                   (behind `ttl-parser`)

# Emit (feature-free)

Composes `ogar-emitter::TripleEmitter::emit_class` output and renders
the triples as a canonical Turtle document — `@prefix` block at the
head, subject-grouped triples body, full identity URIs (with RFC 3987-
unsafe characters percent-encoded for the angle-bracket form).

The percent-encoding step is necessary because OGAR's
`association_identity` uses `->` as the class-to-relation separator
(`ogit-erp/Invoice->customer`), and `>` is illegal inside Turtle
angle-bracket IRIs. Encoded as `%3E` on emit; oxttl recovers it
symmetrically on parse.

# Parse (behind `ttl-parser` feature)

Uses `oxttl` (oxigraph project's pure-Rust streaming Turtle parser,
v0.2.3 + oxrdf 0.3 for the typed Term values). The walker:

  1. Drives `oxttl::TurtleParser::for_slice` to get the triple stream.
  2. Groups by subject in a HashMap.
  3. Finds every subject with `?s rdf:type ogar:Class` -> Candidate
     Class. Strips the OGAR namespace from predicate URIs to match
     against local names (`parentClass`, `hasField`, `hasAssociation`,
     `description`, `tableName`, `recordOrder`).
  4. For each `ogar:hasField`, recursively lifts the field subject
     (must have `rdf:type ogar:Field`) -> Attribute, populating
     `fieldName` / `fieldType` (or `rdfs:range` as the OWL-standard
     variant).
  5. For each `ogar:hasAssociation`, recursively lifts -> Association,
     reading `kind`, `relationName`, `targetClass`.
  6. Preserves definition order (sort by subject IRI).

# Supported lifts (v1)

  - Class { name, parent, description, table_name, record_order }.
  - Attribute (rdfs:range typed) via ogar:hasField -> ogar:fieldName /
    ogar:fieldType / rdfs:range.
  - Association (BelongsTo / HasOne / HasMany / HasAndBelongsToMany)
    via ogar:hasAssociation -> ogar:kind / ogar:targetClass /
    ogar:relationName.

# Not yet supported (next sprint — Phase 2b or follow-up)

  - EnumDecl (owl:oneOf / owl:unionOf lift).
  - ActionDef / KausalSpec (lifecycle — TTL doesn't carry state
    machines natively; OGAR-extension predicates only).
  - Alignment-axiom parsing (owl:equivalentClass /
    owl:equivalentProperty) — Phase 4 ogar-pattern shape.
  - Full vocab/ogar.ttl round-trip.

# Tests (9 — 5 emit always, 4 parse + round-trip behind feature)

  emit_empty_classes_produces_just_prefix_block
  emit_minimal_class_includes_rdf_type_ogar_class
  emit_class_with_parent_emits_parent_class_predicate
  emit_class_with_attribute_emits_field_triples

  parse_minimal_class_lifts_one_class                  [feature-on]
  parse_returns_unimplemented_or_error_on_invalid_ttl  [feature-on]

  round_trip_minimal_class_preserves_name              [feature-on]
  round_trip_class_with_parent_preserves_inheritance   [feature-on]
  round_trip_class_with_belongs_to_preserves_association
                                                       [feature-on]

The third round-trip is the load-bearing one: it exercises the
class -> field -> association path and the percent-encoding fix
for `->` in URIs.

# Workspace + CI

  - `Cargo.toml`: added `crates/ogar-adapter-ttl` to workspace members.
  - `.github/workflows/ci.yml`: added
    `cargo test -p ogar-adapter-ttl --features ttl-parser` —
    same crate-scoped pattern as #29's `surrealdb-parser` step and
    #33's `surrealql-hint` step. Feature is opt-in; default workspace
    build stays light.

# Verification

  cargo test --workspace                                    -> clean
  cargo test -p ogar-adapter-ttl                            -> 5/5
  cargo test -p ogar-adapter-ttl --features ttl-parser      -> 9/9
  cargo test -p ogar-adapter-surrealql --features surrealdb-parser
                                                            -> 33/33
  cargo test -p ogar-knowable-from --features surrealql-hint -> 10/10
  cargo check --workspace --all-targets                     -> clean

PII abort-guard (word-boundary): CLEAN on all touched files.

# Position in sequencing

Per `docs/RDF-OWL-ALIGNMENT.md §10`:

  Phase 1 (#30):  RDF-OWL-ALIGNMENT doc                          MERGED
  Phase 2a (this): ogar-adapter-ttl scaffold                     OPENS
  Phase 2b:       ogar-from-ecto + ogar-adapter-clickhouse-ddl   QUEUED
  Phase 3:        vart-backend                                   QUEUED
  Phase 4:        ogar-pattern                                   QUEUED
  ...

https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY
@AdaWorldAPI AdaWorldAPI merged commit 9c41346 into main Jun 5, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04493de47e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +403 to +407
match ogar_local(p) {
Some("parentClass") => class.parent = Some(local_name(o)),
Some("description") => class.description = Some(o.clone()),
Some("tableName") => class.table_name = Some(o.clone()),
Some("recordOrder") => class.record_order = Some(o.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve sourceLanguage when lifting classes

For any emitted non-default language such as Python or SurrealQl, TripleEmitter always writes ogar:sourceLanguage, but this match never handles it; Class::new leaves language at its default (Ruby), so parse_ttl(emit_ttl(...)) silently changes non-Ruby classes to Ruby despite the documented supported lift.

Useful? React with 👍 / 👎.


// Preserve definition order by subject string (subject IRIs are
// emitted in definition order from `ogar-emitter`).
classes.sort_by(|a, b| a.0.cmp(&b.0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve input class order instead of sorting subjects

When parsing TTL emitted for classes in a meaningful order such as [B, A], this sort alphabetizes by subject IRI after the HashMap has already discarded stream order, so parse_ttl returns [A, B]. The adapter promises to preserve definition order, and reordering top-level Vec<Class> entries can create unstable downstream projections and diffs for callers that keep source declaration order.

Useful? React with 👍 / 👎.

AdaWorldAPI pushed a commit that referenced this pull request Jun 29, 2026
… merged)

ruff #37 (field_type capture + soc operator-veto fix) merged to ruff main, so
the temporary rev-pin to 4860e79 introduced in #141 is no longer needed.
Restore branch = "main" for ruff_spo_triplet and ruff_spo_address.
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.

2 participants