Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
96 changes: 96 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,102 @@

## Entries (newest first)

## 2026-06-22 — Schema-vs-source duality: schemas lift structure bijectively; source ASTs lift behaviour best-effort; they cross-validate at the structural boundary
**Status:** FINDING
**Scope:** producer architecture × MARS calibration × Foundry-Odoo lens × the bardioc migration

The work landing this session imported OGIT's MARS taxonomy (NTO/MARS,
SGO upper ontology, root `ogit.ttl`, MARS XSD oracle) and built the
`ogar-from-schema` producer to lift it. In the process the
structural-vs-behavioural arm split — already carved on the **codegen**
side by `SURREAL-AST-AS-ADAPTER.md` — turned out to apply with equal
sharpness on the **producer** side. Schema-driven producers (XSD, TTL,
JSON-Schema, OpenAPI, Prisma) lift the **structural arm** bijectively
because schemas are declarative-by-construction. Source-AST producers
(`ogar-from-rails`, `ogar-from-elixir`, future `ogar-from-python`) lift
the **behavioural arm** best-effort because source code is dynamic
(Ruby `method_missing`, Python decorators, Elixir macros all defeat
static extraction).

The two are not redundant. They cover **disjoint surfaces** that meet
only at the structural arm. At that meeting point they become each
other's **oracle**: emit a schema from a source-lifted `Class`, diff
against the committed schema, every PR catches structural drift on the
way in. **This is exactly what Palantir Foundry charges money for
("ontology change management"); the schema producer + 50 LOC of
reverse-emit gets it for free.**

For bardioc concretely: MARS-Schema XSD + OGIT NTO/MARS TTL are TWO
independent encodings of the same taxonomy. The schema lift's
agreement with the XSD oracle (`ttl::tests::application_class_values_appear_in_xsd_oracle`)
is the chess-grade calibration applied at the schema-vs-schema boundary
— stronger than chess's source-vs-runtime oracle because both witnesses
are frozen schemas.

This finding reshapes every future producer: structural arm gets a
schema front-end first (cheap, bijective); behavioural arm gets a
source-AST front-end second (expensive, best-effort); the cross-check
at the structural boundary is free and replaces a paid platform feature.

Evidence:
- `crates/ogar-from-schema/` (lift) + `ttl_emit::all_mars_ttl_files_roundtrip` (29 MARS TTLs)
- `sgo::all_sgo_verbs_roundtrip` (176 SGO verbs)
- `_oracle/extract_classes.py` (Python 2, runs unchanged on Py3 via mechanical 2to3)
- `vocab/imports/ogit/NTO/MARS/_oracle/classifications.adoc` (XSD-extracted reference)
- `docs/HIRO-IN-CLASSES.md §2` (the framing)
- `docs/MARS-TRANSCODING.md` (the calibration spec)
- `docs/FOUNDRY-ODOO-MARS-LENS.md` (the cross-domain learning)

The funny part: this was already implicit in the carved spine-adapter
split, just on the other side. The session ended with both ends of the
producer↔codegen pipeline using the same structural/behavioural carving.

## 2026-06-22 — Reverse-engineering bijection: OGAR Class structures emit back to OGIT-flavoured TTL with semantic equality
**Status:** FINDING
**Scope:** producer round-trip × bardioc migration safety × no two-way translation tables

The `ogar-from-schema::ttl` parser was made symmetric by adding
`ttl_emit::emit_entity` and `emit_attribute`. The contract is
**semantic bijection**: `parse(emit(parse(src))) == parse(src)` for
every predicate the OGIT TTL dialect uses; whitespace, comment
positions, and `@prefix` declaration order are not preserved (and
should not be — they are not load-bearing for the structural arm).

Pursuing byte-bijection would force the producer to carry raw text
alongside the parsed structure, defeating the "schema as IR" pattern.
The right contract is what survives a meaningful re-emit, not what
survives `diff -q`. Tested on every MARS TTL (29 files) and every SGO
verb TTL (176 files); zero failures.

**Migration consequence:** colleagues can author OGAR `Class`
structures in Rust, emit OGIT-flavoured TTL, and feed it back into
bardioc's existing ingest pipeline. No migration cliff, no two-way
translation table, no separate drift detector to wire up. **The
producer IS the translator.**

## 2026-06-22 — SGO is the AST predicate vocabulary
**Status:** FINDING
**Scope:** AST design × `ogit:allowed` resolution × Foundry-parity

Every NTO entity's `ogit:allowed ([verb target])` block references
verbs that live in OGIT's upper ontology (`SGO/sgo/verbs/`). 176 verb
TTLs — `dependsOn`, `contains`, `runsOn`, `generates`, `relates`,
`causes`, `affects`, `assignedTo`, `audits`, `bornIn`, `bills`, … —
each with a `dcterms:description`, `dcterms:creator`, validity range.

Before this session: those references were captured as raw strings;
no validation that a verb existed or matched its declared semantics.
After this session: `ogar-from-schema::sgo::parse_verb` lifts each
SGO verb TTL into a typed `VerbDecl`, and the NTO `ogit:allowed`
references resolve against a typed registry instead of string
compare. This is the **AST predicate vocabulary** OGAR's `Association`
and `ActionDef` surfaces have been needing — it was sitting in OGIT
the whole time.

The 176 verbs are the same verbs every Foundry "object graph link
type" represents. Foundry curates them as a platform feature; OGIT
ships them as MIT-licensed TTL. OGAR makes them typed Rust.

## 2026-06-04 — Sprint 7 muscle-memory is canonical; the OGAR#7 std::sync correction round-tripped
**Status:** FINDING
**Scope:** Sprint 7 wiring spec × three-way alignment (Kanban/ractor/SurrealQL) × cross-session correction round-trip
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"crates/ogar-from-elixir",
"crates/ogar-from-ruff",
"crates/ogar-from-rails",
"crates/ogar-from-schema",
"crates/ogar-class-view",
"crates/ogar-render-askama",
]
Expand Down
24 changes: 24 additions & 0 deletions crates/ogar-from-schema/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "ogar-from-schema"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
rust-version.workspace = true
description = "Schema-as-input producer family for OGAR IR. Lifts the STRUCTURAL ARM of a domain from declarative schemas (OGIT TTL today; XSD/JSON-Schema/OpenAPI/Prisma queued) into ogar-vocab Class/Attribute/Association/EnumSource without parsing source code. Pair with source-AST producers (ogar-from-rails, ogar-from-elixir) which carry the BEHAVIORAL arm — see docs/HIRO-IN-CLASSES.md §2 'Schema lifts structure; source lifts behavior'."

[features]
default = []
serde = ["dep:serde", "ogar-vocab/serde"]

[dependencies]
ogar-vocab = { path = "../ogar-vocab" }
serde = { workspace = true, optional = true }

# No external TTL/XSD parser pulled in yet: the v0 reader uses a
# narrow line-oriented walker that handles the shapes OGIT's TTL
# actually emits (a tiny, machine-stable subset of full Turtle).
# When the surface grows (Wikidata-shaped TTL, full RDF/XML, OWL
# imports), swap in `oxttl` / `oxrdf` here without touching the
# Class/Attribute target shape this crate produces.
200 changes: 200 additions & 0 deletions crates/ogar-from-schema/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//! `ogar-from-schema` — schema-as-input producer family.
//!
//! Sibling to source-AST producers (`ogar-from-rails`, `ogar-from-elixir`,
//! `ogar-from-ruff`). The pair carves the producer surface along the same
//! split [`SURREAL-AST-AS-ADAPTER.md`] already carved on the codegen side:
//!
//! ```text
//! STRUCTURAL ARM ──► ogar-from-schema (this crate)
//! (XSD, TTL, schemas are declarative & bijective
//! JSON-Schema, → round-trip provable
//! OpenAPI, Prisma) → 80 OGIT domains for free
//!
//! BEHAVIORAL ARM ──► ogar-from-{rails,elixir,ruff,…}
//! (callbacks, FSMs, source code only — schemas can't carry it
//! @api.depends, → best-effort, language-specific
//! gen_statem,…) → adds what schemas can't see
//! ```
//!
//! **Why both — the funny insight** (`docs/HIRO-IN-CLASSES.md` §2):
//!
//! A schema gets you LESS, more RELIABLY. A source AST gets you MORE,
//! less reliably. The two are not redundant — they cover disjoint surfaces
//! that meet only at the structural arm, and at that meeting point they
//! become each other's oracle:
//!
//! - Schema lift produces a `Class` set that is **byte-exact** (the schema
//! IS the contract).
//! - Source lift produces a `Class` set that is **best-effort** (Ruby is
//! dynamic; `method_missing` defeats static extraction).
//! - Where both cover the same domain, emitting a schema from the
//! source-lifted `Class` and diffing against the committed schema is a
//! **drift detector** for every PR. That is what Palantir Foundry charges
//! for as "ontology change management" — we get it from this crate
//! plus `extract_classes.py`.
//!
//! # v0 scope
//!
//! - `ttl` front-end: reads the line-oriented OGIT TTL dialect into
//! [`Class`]. Demo target: `vocab/imports/ogit/MARS/`.
//! - Cross-check: the **fixed-enum agreement test** asserts the TTL
//! `ogit:validation-parameter` set matches the XSD oracle's extracted
//! classifications (the chess-grade bijection, applied at the schema
//! level — see `docs/calibration/mars/README.md`).
//!
//! # Out of v0 (queued)
//!
//! - `xsd` front-end (lift `MARSSchema2015.xsd` directly; cross-check vs TTL).
//! - `json_schema`, `openapi`, `prisma` front-ends.
//! - Full Turtle / RDF-XML / OWL import via `oxttl`/`oxrdf`.
//! - Behavioral-arm fields on `Class` are intentionally left empty by this
//! crate — schemas can't carry them; source-AST producers fill them in.
//!
//! [`SURREAL-AST-AS-ADAPTER.md`]: ../docs/SURREAL-AST-AS-ADAPTER.md

#![warn(missing_docs)]
#![forbid(unsafe_code)]

use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language};

pub mod sgo;
pub mod ttl;
pub mod ttl_emit;

/// What a single TTL file describes — exactly one of: an entity (`Class`),
/// a datatype attribute (`Attribute`), or a verb (`Association` shape).
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TtlDeclaration {
/// An `rdfs:Class` declaration (`entities/<Name>.ttl`).
Entity(EntityDecl),
/// An `owl:DatatypeProperty` declaration (`<Entity>/attributes/<name>.ttl`).
DatatypeAttribute(AttributeDecl),
}

/// Lifted shape of a single OGIT entity TTL file (e.g.
/// `entities/Machine.ttl`). The shape captures every predicate the OGIT
/// TTL dialect uses on `rdfs:Class` subjects, so that round-tripping
/// (`parse → emit → parse`) preserves the semantic content. Mapping into
/// the full [`ogar_vocab::Class`] surface happens in [`into_class`];
/// emitting back to TTL happens in [`crate::ttl_emit::emit_entity`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EntityDecl {
/// The CURIE-form name (`ogit.MARS:Machine`).
pub curie: String,
/// The local name (`Machine`).
pub name: String,
/// `rdfs:label` text (often equal to `name`).
pub label: String,
/// `dcterms:description` text — multi-line preserved verbatim.
pub description: String,
/// `rdfs:subClassOf` target as written (`ogit:Entity`).
pub parent: Option<String>,
/// `dcterms:valid` validity-range string (`"start=2018-06-01;"`).
pub dcterms_valid: Option<String>,
/// `dcterms:creator` text (`"fotto@arago.de"` / `"FCO"`).
pub dcterms_creator: Option<String>,
/// `ogit:scope` text (`"NTO"` / `"SGO"`).
pub ogit_scope: Option<String>,
/// `ogit:parent` token (`ogit:Node`) — separate from
/// [`Self::parent`] (`rdfs:subClassOf`) — both can be present and
/// carry different information.
pub ogit_parent: Option<String>,
/// `ogit:mandatory-attributes` list, as written.
pub mandatory_attributes: Vec<String>,
/// `ogit:optional-attributes` list, as written.
pub optional_attributes: Vec<String>,
/// `ogit:indexed-attributes` list, as written.
pub indexed_attributes: Vec<String>,
/// `ogit:allowed (...)` block — each entry is `(verb, target)` as
/// written (`(ogit:dependsOn, ogit.MARS:Resource)`).
pub allowed: Vec<(String, String)>,
}

/// Lifted shape of a single OGIT attribute TTL file (e.g.
/// `Application/attributes/class.ttl`).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AttributeDecl {
/// The CURIE-form name (`ogit.MARS.Application:class`).
pub curie: String,
/// The local name (`class`).
pub name: String,
/// `rdfs:label` text.
pub label: String,
/// `dcterms:description` text.
pub description: String,
/// `dcterms:valid` validity-range string.
pub dcterms_valid: Option<String>,
/// `dcterms:creator` text.
pub dcterms_creator: Option<String>,
/// `ogit:validation-type` value (e.g. `"fixed"`) when present.
pub validation_type: Option<String>,
/// `ogit:validation-parameter` value — when `validation_type` is
/// `"fixed"`, this is the comma-separated set of allowed values.
pub validation_parameter: Option<String>,
}

impl AttributeDecl {
/// When this attribute is `validation-type="fixed"`, split the
/// comma-separated parameter into a sorted, de-duplicated value set —
/// the shape that **must agree with the XSD-extracted classification
/// set** for the same `(entity, attribute)` pair.
///
/// Returns `None` when the attribute is not fixed-enum.
#[must_use]
pub fn fixed_enum_values(&self) -> Option<Vec<String>> {
if self.validation_type.as_deref() != Some("fixed") {
return None;
}
let raw = self.validation_parameter.as_deref()?;
let mut values: Vec<String> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
values.sort();
values.dedup();
Some(values)
}
}

/// Lower a lifted [`EntityDecl`] into the canonical [`ogar_vocab::Class`].
/// Behavior-arm fields ([`Class::callbacks`], etc.) stay empty — this is
/// the structural arm only, per the §`# v0 scope` invariant.
#[must_use]
pub fn into_class(entity: &EntityDecl, attributes: &[(&str, &AttributeDecl)]) -> Class {
let enums: Vec<EnumDecl> = attributes
.iter()
.filter_map(|(col, attr)| {
let values = attr.fixed_enum_values()?;
// OGIT TTL carries the value list flat (`ogit:validation-parameter
// "Data,Development,..."`) with no separate label vocabulary, so the
// lowering renders each value as a self-labeled `(value, value)`
// pair. A future XSD lift will source labels from
// `<xs:documentation>` per `<xs:enumeration>`.
let pairs: Vec<(String, String)> = values.into_iter().map(|v| (v.clone(), v)).collect();
let mut decl = EnumDecl::default();
decl.column = (*col).to_owned();
decl.source = EnumSource::Static(pairs);
Some(decl)
})
.collect();

let attrs: Vec<Attribute> = attributes
.iter()
.map(|(col, _attr)| {
let mut a = Attribute::default();
a.name = (*col).to_owned();
a
})
.collect();

let mut cls = Class::default();
cls.name = entity.name.clone();
cls.parent = entity.parent.clone();
cls.language = Language::Unknown;
cls.attributes = attrs;
cls.enums = enums;
cls
}
Loading
Loading