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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,211 @@

## Entries (newest first)

## 2026-06-22 — extract_classes.py transcoded to Rust byte-faithfully; XSD↔TTL bijection closed; Python dependency removed from the oracle
**Status:** FINDING
**Scope:** XSD front-end × calibration self-containment × the queued bijection

The MARS XSD classification extractor (`arago/MARS-Schema/tools/extract_classes.py`,
~360 lines, ~140 logic + ~150 table formatting) is now a faithful Rust
transcode at `crates/ogar-from-schema/src/xsd.rs`, behind the optional
`xsd` feature (pulls `roxmltree`, a pure-Rust read-only XML DOM; the
default TTL path stays zero-parser-deps).

Three things this lands:

1. **Byte-for-byte transcode proof.** `xsd::to_asciidoc()` reproduces
the Python `-F asciidoc` output exactly — 628 lines, including the
verbatim XSD-documentation whitespace and the `printAsciiDocFooter`
trailing newline. Test: `xsd::tests::asciidoc_matches_python_oracle`
diffs against the cached `_oracle/classifications.adoc`.

2. **The XSD↔TTL bijection is closed (was "queued" in
`MARS-TRANSCODING.md §2`).** `xsd::tests::xsd_classes_match_ttl_enum`
asserts FULL bidirectional set-equality between the XSD-extracted
Application value set and the TTL `validation-parameter` enum — not
just one-directional membership. The XSD and the TTL are two
independent encodings of one taxonomy and they now provably agree
in both directions.

3. **The Python dependency is removed from the calibration path.**
`cargo test --features xsd` is the whole oracle now; no `python3`
interpreter needed. `extract_classes.py` stays vendored in
`_oracle/` as the provenance witness (what the transcode was proven
against), not a runtime dep.

Transcode discipline notes (for the next source→Rust port):
- The Python `getAttribute("xml:lang")` returns `""` for absent (not
`None`); the lang-filter is "absent OR en". roxmltree resolves `xml:`
to the xml namespace — match on `attribute.name() == "lang"`.
- `getXMLText` concatenates DIRECT text-node children only (not
recursive); the documentation's internal whitespace is load-bearing
for the byte-match.
- The `:revdate:` is `datetime.now()` in Python (non-deterministic);
the Rust `to_asciidoc(c, revdate)` takes it as a parameter so the
output is reproducible and testable.

Answer to "is it huge": no — ~360 lines, half output formatting; the
transcode is ~350 LOC Rust including tests. And it doubles as the seed
of the broader XSD→`Class` front-end (the same walk that extracts
classifications is the structural-arm lift for any XSD).

Evidence: `crates/ogar-from-schema/src/xsd.rs` (20/20 tests pass with
`--features xsd`; 16/16 on default). `docs/MARS-TRANSCODING.md §2`
updated to mark the bijection closed.

## 2026-06-22 — OGIT is the canonical template store; Odoo (and any source-AST producer) digests INTO it; consumers relive agnostically via askama
**Status:** FRAMING
**Scope:** Foundry-parity collapse × cross-consumer architecture × digest-once-relive-N

The operator's framing — *"basically digest Odoo and store it in TTL
'Jinja' Templates in OGIT and relive it agnostically for any
'verb/entity as a class'"* — crystallizes the four pieces shipped
across this PR (TTL mirror, schema lift, verb-as-class template,
author-provenance discriminator) into one coherent flow:

```
Odoo source → ogar-from-python → Class IR → ttl_emit → OGIT TTL templates
(stored at
vocab/imports/ogit/NTO/<Domain>/,
dcterms:creator = bus-compiler)
ogar-render-askama
any consumer (woa-rs, smb-office-rs, medcare-rs, q2, future renderers)
re-instantiates any entity/verb-as-class with a fresh binding;
never touches Odoo Python
```

The Python runtime is **only** touched at digest time. After that,
every consumer talks to TTL templates plus the askama engine.

**Why store in OGIT NTO (not a parallel `vocab/imports/odoo/`):** the
`dcterms:creator` author-scan finding from this same session makes
provenance unambiguous without a separate namespace. The precedent
exists today — `Accounting/` already has 11 Claude-digested files
sitting alongside 23 Viktor Voss originals.

**Foundry-parity collapse** (the punchline). Foundry's four-layer
platform pitch (ingest / storage / render / IAM+audit) maps to four
free open-source pieces already in this repo:

| Foundry layer | Our equivalent | New code needed |
|---|---|---|
| Ingest | `ogar-from-python` digest | ~1500 LOC (queued) |
| Storage | `vocab/imports/ogit/NTO/<Domain>/` TTL with `dcterms:creator` | zero (exists) |
| Render | `ogar-render-askama::{views, actions}` | ~200 LOC for actions submodule |
| IAM + audit | verb-as-class `requires-perm` slot + Lance-version-as-audit | zero (exists) |
| Ontology change mgmt | `diff -r` of digest re-runs | zero |

Total marginal code: <2000 LOC for what Foundry charges $$$ for. The
architecture was latent the whole time; the digest→relive framing is
what makes it visible as a single shape.

Concrete next steps (independent, can ship in parallel PRs):

1. `ogar-from-python` digester — structural-arm filter (`_name`,
`_inherit`, `fields.*`, selections); behavioural-arm signatures
(decorator names + action method signatures); drops method bodies
2. `ogar-render-askama::actions` — verb-as-class render path, mirroring
the existing `views/` submodule

Doc: `docs/ODOO-DIGEST-TO-OGIT.md` (FRAMING v0) carries the full
pipeline, the v0 mapping table (6 minted concepts + ~9 queued for
codebook mint), the drift detector recipe, and the Foundry-parity
collapse table.

## 2026-06-22 — Verb-as-class is an ontological askama template — compile-time-validated action declaration, not a quirk
**Status:** FINDING
**Scope:** WorkOrder convention × `ogar-render-askama` integration × Foundry action-type parity

WorkOrder's 12 `verbs/*.ttl` are declared as `rdfs:Class`, not
`owl:ObjectProperty`. The earlier framing (commit `cce8420`) called
this "an unusual convention we're free to revise toward standard
`owl:ObjectProperty`" — **that framing was wrong** and is hereby
corrected.

The verb-as-class encoding is **load-bearing**: it makes each verb a
typed template carrying its own slot list (`ogit:mandatory-attributes`),
inheritance chain (`rdfs:subClassOf`), and policy metadata
(`ogit:requires-perm`, `ogit:emits-audit`). That's not a flat predicate;
that's a **compile-time-validated action declaration** — the ontological
counterpart to askama (Rust) and jinja (Python) HTML templating.

The structural correspondence is exact:

- TTL file = template (`.html.j2` equivalent)
- `ogit:mandatory-attributes` = struct field list (askama context shape)
- Per-call binding = struct instance (askama render input)
- Render = SPO triple emit + declared side effects (audit, ACL gate)
- `rdfs:subClassOf` = template inheritance (`{% extends %}`)
- Lift-time slot validation = askama's compile-time `{{ field }}` check

This is the integration point `ogar-render-askama` was always going
to need for actions. The crate currently renders `Class` *views*
(noun-shaped: HTML/JSON/OpenAPI); a parallel `actions/` submodule
renders `Class` *actions* (verb-shaped: SPO triple + side-effect spec).
Same engine, same compile-time-validated context model, different
output medium.

**Foundry-parity sharpening:** Foundry's "action types" carry exactly
the four properties this encoding gives — typed parameters, slot
validation, declared side effects, inheritance. Foundry sells it as a
paid platform feature; verb-as-class TTL + `ogar-render-askama` gives
the same four from open-source schemas and Rust templates.

Implications:
- **WorkOrder's convention stays.** Don't normalise to `owl:ObjectProperty`.
- **WorkOrder is the natural prototyping ground** (we're upstream per
`dcterms:creator` = `bus-compiler` + `family-codec-smith`) for new
verb-as-class predicates before pitching the pattern to OGIT upstream.
- **`ogar-render-askama::actions` is the next natural module** —
~200 LOC mirroring the existing `views/` render path.

Doc: `docs/VERB-AS-CLASS-TEMPLATE.md` (FRAMING v0) carries the full
analogy table + worked example + render flow.

## 2026-06-22 — Author provenance via `dcterms:creator` discriminates "ours to revise" from "upstream-coordinated"
**Status:** FINDING
**Scope:** OGIT NTO governance × multi-domain lift × who-can-change-what

OGIT TTL files carry `dcterms:creator` on every subject. The field is
free-form text but carries one of two semantic shapes in practice:

- **Human author + email** (`chris.boos@almato.com`, `Viktor Voss`,
`fotto@arago.de`, `Marek Meyer`, `Peter Larem`, `Ola Irgens Kylling`,
…) — original arago/almato authors. Structural changes need upstream
coordination.
- **Internal agent name** (`bus-compiler`, `family-codec-smith`,
`Claude (AdaWorldAPI/lance-graph 3-hop optim)`, …) — files authored
by our agent fleet against this org's forks. We are upstream for
these; structural changes need no external coordination.

The 9-domain spot check (Transport, Accounting, SalesDistribution,
Credit, Cost, ServiceManagement, WorkOrder, Compliance, Audit) revealed:

- **WorkOrder is fully ours** — 100% internal-agent authorship (`bus-compiler`,
`family-codec-smith`). The unusual `rdfs:Class`-as-verb convention is
ours to revise toward standard `owl:ObjectProperty`-as-verb whenever
the AST predicate registry needs the WorkOrder verbs.
- **Accounting is mixed-authorship** — Viktor Voss (23 files, original)
+ a prior session's `Claude` extension (11 files). Structural changes
to the original 23 require upstream coordination; the 11 are ours.
- **All other 7 domains are pure-upstream** — single-or-few external
human authors.

This makes WorkOrder the **natural prototyping ground** for new TTL
predicates OGAR wants to add: ship in WorkOrder first (no external
coordination cost), validate the bijection, then pitch the pattern
to OGIT upstream once it's proven.

Evidence: the `dcterms:creator` provenance scan recipe lives in
`docs/OGIT-DOMAIN-LIFT-CATALOGUE.md § Verifying domain authorship`;
the round-trip stress test for the 9 domains is
`ttl_emit::tests::nine_domains_lift_surface_round_trip` (zero failures
on 210 TTLs across the nine).

## 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
Expand Down
17 changes: 12 additions & 5 deletions crates/ogar-from-schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ description = "Schema-as-input producer family for OGAR IR. Lifts the STRUCTURAL
[features]
default = []
serde = ["dep:serde", "ogar-vocab/serde"]
# The `xsd` feature pulls in a read-only XML parser for the XSD
# front-end (the faithful Rust transcode of arago/MARS-Schema's
# extract_classes.py). Kept optional so the default TTL path stays
# zero-parser-deps.
xsd = ["dep:roxmltree"]

[dependencies]
ogar-vocab = { path = "../ogar-vocab" }
serde = { workspace = true, optional = true }
roxmltree = { version = "0.20", 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
# The default TTL reader uses a narrow line-oriented walker that
# handles the shapes OGIT's TTL actually emits (a tiny, machine-stable
# subset of full Turtle) — no external parser. The `xsd` feature adds
# roxmltree (pure-Rust, read-only DOM) ONLY for the XSD front-end.
# When the TTL surface grows (Wikidata-shaped TTL, full RDF/XML, OWL
# imports), swap in `oxttl` / `oxrdf` without touching the
# Class/Attribute target shape this crate produces.
2 changes: 2 additions & 0 deletions crates/ogar-from-schema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language};
pub mod sgo;
pub mod ttl;
pub mod ttl_emit;
#[cfg(feature = "xsd")]
pub mod xsd;

/// What a single TTL file describes — exactly one of: an entity (`Class`),
/// a datatype attribute (`Attribute`), or a verb (`Association` shape).
Expand Down
98 changes: 90 additions & 8 deletions crates/ogar-from-schema/src/ttl_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,102 @@ mod tests {
/// predicate.
#[test]
fn all_mars_ttl_files_roundtrip() {
let stats = assert_domain_roundtrip("MARS");
// 29 .ttl files in NTO/MARS at the SHA pinned by PROVENANCE.md.
assert!(
stats.total >= 29,
"expected ≥ 29 TTL files in MARS, got {}",
stats.total
);
}

/// Generic helper that walks `vocab/imports/ogit/NTO/<domain>/`,
/// dispatches each TTL to the right parser (`parse_file` for entities
/// and datatype attributes, `crate::sgo::parse_verb` for in-domain
/// `owl:ObjectProperty` verbs), and asserts semantic round-trip.
/// Returns per-shape counts so callers can sanity-check the lift
/// surface they're claiming.
fn assert_domain_roundtrip(domain: &str) -> DomainStats {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../vocab/imports/ogit/NTO/MARS");
let mut checked = 0usize;
.join("../../vocab/imports/ogit/NTO")
.join(domain);
let mut stats = DomainStats::default();
for entry in walk_ttl(&dir) {
let src = std::fs::read_to_string(&entry).expect("read");
stats.total += 1;
match parse_file(&src) {
Some(TtlDeclaration::Entity(_)) => assert_entity_roundtrip(&src),
Some(TtlDeclaration::DatatypeAttribute(_)) => assert_attribute_roundtrip(&src),
None => {}
Some(TtlDeclaration::Entity(_)) => {
assert_entity_roundtrip(&src);
stats.entities += 1;
}
Some(TtlDeclaration::DatatypeAttribute(_)) => {
assert_attribute_roundtrip(&src);
stats.attributes += 1;
}
None => {
// Try the verb path — some NTO domains carry their own
// in-domain `owl:ObjectProperty` verbs (Transport,
// Accounting, Credit, Compliance) alongside SGO's
// upstream-shared vocabulary.
let Some(once) = crate::sgo::parse_verb(&src) else {
panic!(
"TTL has no recognised subject type in {domain}: {}",
entry.display()
);
};
let emitted = crate::sgo::emit_verb(&once);
let twice = crate::sgo::parse_verb(&emitted).expect("re-parse verb");
assert_eq!(
once,
twice,
"verb round-trip lost a predicate in {domain}: {}",
entry.display()
);
stats.verbs += 1;
}
}
checked += 1;
}
// 29 .ttl files in NTO/MARS at the SHA pinned by PROVENANCE.md.
assert!(checked >= 29, "expected ≥ 29 TTL files, got {checked}");
stats
}

#[derive(Debug, Default)]
struct DomainStats {
total: usize,
entities: usize,
attributes: usize,
verbs: usize,
}

/// Cross-domain bijection coverage. Each row is one of the nine
/// domains the operator asked OGAR to verify before promoting
/// the lift surface from MARS-only to multi-domain. If any of
/// these fails, the producer can't land on that domain without
/// extending `EntityDecl` / `AttributeDecl` / `VerbDecl` first.
///
/// Counts are also a sanity check on the inventory — they prove
/// the catalogue's per-domain numbers match what's actually in
/// `vocab/imports/`.
#[test]
fn nine_domains_lift_surface_round_trip() {
for (domain, expected_total) in [
("Transport", 27),
("Accounting", 36),
("SalesDistribution", 23),
("Credit", 21),
("Cost", 5),
("ServiceManagement", 59),
("WorkOrder", 27),
("Compliance", 9),
("Audit", 3),
] {
let stats = assert_domain_roundtrip(domain);
assert_eq!(
stats.total, expected_total,
"{domain}: TTL count drifted from inventory \
(expected {expected_total}, got {})",
stats.total
);
}
}

fn walk_ttl(root: &std::path::Path) -> Vec<std::path::PathBuf> {
Expand Down
Loading
Loading