Skip to content

mdcode: add OWL export (OSI -> Turtle), the round-trip inverse of owl import - #340

Draft
libei wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
libei:worktree-owl-export-roundtrip
Draft

mdcode: add OWL export (OSI -> Turtle), the round-trip inverse of owl import#340
libei wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
libei:worktree-owl-export-roundtrip

Conversation

@libei

@libei libei commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

What

Adds kcmd owl export <model.yaml> — the reverse of kcmd owl import. It writes a semantic model back out as a Turtle (.ttl) OWL ontology, the exact mirror of the import pipeline:

  • from_ir.ts — IR → staging OwlModel (the inverse of to_ir.ts)
  • serialize.tsOwlModel → Turtle (the inverse of parse.ts)
  • convert.ts — new convertOsiToOwl() orchestrator; commands.ts/main.ts gain the export action

Scope: round-trip fidelity

This was scoped deliberately (not a general "model → OWL" claim). A model that originated as OWL exports losslessly: OWL → OSI → OWL → OSI is stable at the IR level. It:

  • maps every native construct back (class → owl:Class, field → owl:DatatypeProperty, relationship → owl:ObjectProperty, primary_keyowl:hasKey, single-col unique_keyowl:InverseFunctionalProperty, extendsrdfs:subClassOf, datatype → its xsd: range);
  • re-emits the carried GOOGLE owl:/rdfs: extensions verbatim (owl:inverseOf, rdfs:subPropertyOf, property characteristics, per-term annotations, …), rebuilding a shortened in-namespace name to a full IRI from the carried owl:baseIri;
  • reproduces each entity's field order via a topological merge of per-entity orders (so multi-domain properties land correctly).

A model authored natively can hold things OWL has no shape for — a metric, a field whose expression is not a bare column, an importedExpression, a bound source, an M:N association, a composite unique_key, deployment targets. Those are dropped with a warning, never misrepresented; the class/property/edge they hang off is still exported.

Tests

  • owl_export.test.ts: all five OWL import fixtures round-trip OWL → OSI → OWL → OSI unchanged (zero warnings); sales is pinned to a golden sales.owl.golden.ttl; the golden re-imports to the same model; and each non-OWL construct is verified to warn + drop.
  • Full test:semantic suite (488 tests) green; verified end-to-end through the real CLI (kcmd owl exportkcmd owl import round-trips).

Docs

owl-import.md gains an Exporting to OWL section and the reverse-direction limitation is updated; README.md mentions the new command.

… import

`kcmd owl export <model.yaml>` writes a semantic model back out as a Turtle
ontology -- the exact mirror of `owl import`. It maps the IR to the staging
OwlModel (from_ir.ts) and serializes it to Turtle (serialize.ts), reusing the
loader for OSI YAML -> IR.

Scope is round-trip fidelity: a model that originated as OWL exports losslessly
(OWL -> OSI -> OWL -> OSI is stable at the IR level), re-emitting the carried
GOOGLE owl:/rdfs: extensions verbatim and rebuilding shortened in-namespace
names from owl:baseIri. Constructs OWL cannot express (metrics, non-column
expressions, bound sources, associations, composite unique keys, deployment
targets) are dropped with a warning, never misrepresented.

Every OWL import fixture round-trips unchanged; sales is pinned to a golden
.ttl. Docs updated (owl-import.md gains an Exporting to OWL section).
libei added 3 commits August 24, 2026 05:51
Address code-review findings on the OSI -> OWL exporter: several
hand-authored-model cases silently lost or misrepresented data instead
of following the "dropped with a warning, never misrepresented" contract.

- Multi-domain field merge: a same-named field defined differently on two
  entities is now compared (datatypeFactsKey) and warned, keeping the
  first domain's definition instead of silently taking it.
- inverseFunctional across domains: a single-column unique key on only
  some domains of a shared field now warns instead of being dropped from
  the non-first domains.
- Dimension flag: a field whose dimension role won't survive re-import
  (non-temporal marked time, temporal marked not, temporal with no flag)
  now warns, matching the checkFieldRepresentable comment.
- Model description: a model with no description now warns that re-import
  synthesizes a placeholder.
- Relationship comment: a relationship with both ai_context.instructions
  and a description now warns that the description is dropped.
- Bare carried cross-reference with no owl:baseIri now expands against the
  serializer's DEFAULT_BASE, emitting a valid absolute IRI instead of a
  broken relative <name>.
- Remove the unused entityNames parameter from isDestKeyColumn.

Add targeted tests for each warning path; extend the export doc's
"dropped with a warning" list. All fixtures still round-trip cleanly.
…anches

The export suite round-tripped only OWL-origin fixtures and covered the
headline loss cases, leaving several exporter branches with no direct test.
Add:

- An OSI-origin round-trip fixture (directory.osi.golden.yaml) and its two
  tests, the mirror of the OWL-origin suite. It is a hand-authored,
  fully-OWL-expressible fixed point whose distinctive job is the cross-entity
  field-order merge (orderFields): a multi-domain field sits at different
  absolute positions on its two domains, so the single exported property order
  must be a linear extension of both. A test pins that each entity's own field
  order survives re-import.

- Tests for the previously-untested export warning branches: an abstract
  entity, an imported vendor expression, a relationship endpoint outside the
  model, bound join columns, a non-GOOGLE vendor extension, an unparseable
  GOOGLE extension, and orderFields' cycle warning (entities that disagree on a
  shared field's relative order).

Pure test additions; no exporter changes. Full semantic suite: 506 pass.
- Rename directory.osi.golden.yaml -> directory.osi.yaml. In fixtures/owl a
  *.osi.golden.yaml is the derived golden OUTPUT of importing its paired
  *.owl.ttl; this fixture is a hand-authored SOURCE input with no such pair, so
  the .golden infix misled. The rename also correctly opts it into the
  osi-schema.json guardrail (which only enforces schema validity on source
  *.osi.yaml files), so add it to the same extends/abstract allow-list as the
  hand-authored hierarchy fixture.

- Bind the warning assertions to the specific construct (entity 'Customer' is
  abstract, relationship 'placedBy' has bound join columns, ACME (vendor
  extension), GOOGLE (unparseable data)) so a regression that fires the wrong
  warning can no longer pass on a loose substring match.

- Use a non-temporal field in the bound-join-columns test so the only warning
  it produces is the one under test (a Date field emitted its own incidental
  time-dimension warning).

- Dedupe the two OSI-origin tests' fixture load behind a local helper.
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