Skip to content

Commit e578fbe

Browse files
authored
Merge pull request #106 from AdaWorldAPI/claude/medcare-bridge-lance-graph-wmx76z
ogar-from-schema follow-up: 9-domain test + XSD transcode + framings
2 parents 09ac8eb + 8945b7f commit e578fbe

9 files changed

Lines changed: 1205 additions & 30 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,211 @@
1515
1616
## Entries (newest first)
1717

18+
## 2026-06-22 — extract_classes.py transcoded to Rust byte-faithfully; XSD↔TTL bijection closed; Python dependency removed from the oracle
19+
**Status:** FINDING
20+
**Scope:** XSD front-end × calibration self-containment × the queued bijection
21+
22+
The MARS XSD classification extractor (`arago/MARS-Schema/tools/extract_classes.py`,
23+
~360 lines, ~140 logic + ~150 table formatting) is now a faithful Rust
24+
transcode at `crates/ogar-from-schema/src/xsd.rs`, behind the optional
25+
`xsd` feature (pulls `roxmltree`, a pure-Rust read-only XML DOM; the
26+
default TTL path stays zero-parser-deps).
27+
28+
Three things this lands:
29+
30+
1. **Byte-for-byte transcode proof.** `xsd::to_asciidoc()` reproduces
31+
the Python `-F asciidoc` output exactly — 628 lines, including the
32+
verbatim XSD-documentation whitespace and the `printAsciiDocFooter`
33+
trailing newline. Test: `xsd::tests::asciidoc_matches_python_oracle`
34+
diffs against the cached `_oracle/classifications.adoc`.
35+
36+
2. **The XSD↔TTL bijection is closed (was "queued" in
37+
`MARS-TRANSCODING.md §2`).** `xsd::tests::xsd_classes_match_ttl_enum`
38+
asserts FULL bidirectional set-equality between the XSD-extracted
39+
Application value set and the TTL `validation-parameter` enum — not
40+
just one-directional membership. The XSD and the TTL are two
41+
independent encodings of one taxonomy and they now provably agree
42+
in both directions.
43+
44+
3. **The Python dependency is removed from the calibration path.**
45+
`cargo test --features xsd` is the whole oracle now; no `python3`
46+
interpreter needed. `extract_classes.py` stays vendored in
47+
`_oracle/` as the provenance witness (what the transcode was proven
48+
against), not a runtime dep.
49+
50+
Transcode discipline notes (for the next source→Rust port):
51+
- The Python `getAttribute("xml:lang")` returns `""` for absent (not
52+
`None`); the lang-filter is "absent OR en". roxmltree resolves `xml:`
53+
to the xml namespace — match on `attribute.name() == "lang"`.
54+
- `getXMLText` concatenates DIRECT text-node children only (not
55+
recursive); the documentation's internal whitespace is load-bearing
56+
for the byte-match.
57+
- The `:revdate:` is `datetime.now()` in Python (non-deterministic);
58+
the Rust `to_asciidoc(c, revdate)` takes it as a parameter so the
59+
output is reproducible and testable.
60+
61+
Answer to "is it huge": no — ~360 lines, half output formatting; the
62+
transcode is ~350 LOC Rust including tests. And it doubles as the seed
63+
of the broader XSD→`Class` front-end (the same walk that extracts
64+
classifications is the structural-arm lift for any XSD).
65+
66+
Evidence: `crates/ogar-from-schema/src/xsd.rs` (20/20 tests pass with
67+
`--features xsd`; 16/16 on default). `docs/MARS-TRANSCODING.md §2`
68+
updated to mark the bijection closed.
69+
70+
## 2026-06-22 — OGIT is the canonical template store; Odoo (and any source-AST producer) digests INTO it; consumers relive agnostically via askama
71+
**Status:** FRAMING
72+
**Scope:** Foundry-parity collapse × cross-consumer architecture × digest-once-relive-N
73+
74+
The operator's framing — *"basically digest Odoo and store it in TTL
75+
'Jinja' Templates in OGIT and relive it agnostically for any
76+
'verb/entity as a class'"* — crystallizes the four pieces shipped
77+
across this PR (TTL mirror, schema lift, verb-as-class template,
78+
author-provenance discriminator) into one coherent flow:
79+
80+
```
81+
Odoo source → ogar-from-python → Class IR → ttl_emit → OGIT TTL templates
82+
(stored at
83+
vocab/imports/ogit/NTO/<Domain>/,
84+
dcterms:creator = bus-compiler)
85+
86+
87+
ogar-render-askama
88+
89+
90+
any consumer (woa-rs, smb-office-rs, medcare-rs, q2, future renderers)
91+
re-instantiates any entity/verb-as-class with a fresh binding;
92+
never touches Odoo Python
93+
```
94+
95+
The Python runtime is **only** touched at digest time. After that,
96+
every consumer talks to TTL templates plus the askama engine.
97+
98+
**Why store in OGIT NTO (not a parallel `vocab/imports/odoo/`):** the
99+
`dcterms:creator` author-scan finding from this same session makes
100+
provenance unambiguous without a separate namespace. The precedent
101+
exists today — `Accounting/` already has 11 Claude-digested files
102+
sitting alongside 23 Viktor Voss originals.
103+
104+
**Foundry-parity collapse** (the punchline). Foundry's four-layer
105+
platform pitch (ingest / storage / render / IAM+audit) maps to four
106+
free open-source pieces already in this repo:
107+
108+
| Foundry layer | Our equivalent | New code needed |
109+
|---|---|---|
110+
| Ingest | `ogar-from-python` digest | ~1500 LOC (queued) |
111+
| Storage | `vocab/imports/ogit/NTO/<Domain>/` TTL with `dcterms:creator` | zero (exists) |
112+
| Render | `ogar-render-askama::{views, actions}` | ~200 LOC for actions submodule |
113+
| IAM + audit | verb-as-class `requires-perm` slot + Lance-version-as-audit | zero (exists) |
114+
| Ontology change mgmt | `diff -r` of digest re-runs | zero |
115+
116+
Total marginal code: <2000 LOC for what Foundry charges $$$ for. The
117+
architecture was latent the whole time; the digest→relive framing is
118+
what makes it visible as a single shape.
119+
120+
Concrete next steps (independent, can ship in parallel PRs):
121+
122+
1. `ogar-from-python` digester — structural-arm filter (`_name`,
123+
`_inherit`, `fields.*`, selections); behavioural-arm signatures
124+
(decorator names + action method signatures); drops method bodies
125+
2. `ogar-render-askama::actions` — verb-as-class render path, mirroring
126+
the existing `views/` submodule
127+
128+
Doc: `docs/ODOO-DIGEST-TO-OGIT.md` (FRAMING v0) carries the full
129+
pipeline, the v0 mapping table (6 minted concepts + ~9 queued for
130+
codebook mint), the drift detector recipe, and the Foundry-parity
131+
collapse table.
132+
133+
## 2026-06-22 — Verb-as-class is an ontological askama template — compile-time-validated action declaration, not a quirk
134+
**Status:** FINDING
135+
**Scope:** WorkOrder convention × `ogar-render-askama` integration × Foundry action-type parity
136+
137+
WorkOrder's 12 `verbs/*.ttl` are declared as `rdfs:Class`, not
138+
`owl:ObjectProperty`. The earlier framing (commit `cce8420`) called
139+
this "an unusual convention we're free to revise toward standard
140+
`owl:ObjectProperty`" — **that framing was wrong** and is hereby
141+
corrected.
142+
143+
The verb-as-class encoding is **load-bearing**: it makes each verb a
144+
typed template carrying its own slot list (`ogit:mandatory-attributes`),
145+
inheritance chain (`rdfs:subClassOf`), and policy metadata
146+
(`ogit:requires-perm`, `ogit:emits-audit`). That's not a flat predicate;
147+
that's a **compile-time-validated action declaration** — the ontological
148+
counterpart to askama (Rust) and jinja (Python) HTML templating.
149+
150+
The structural correspondence is exact:
151+
152+
- TTL file = template (`.html.j2` equivalent)
153+
- `ogit:mandatory-attributes` = struct field list (askama context shape)
154+
- Per-call binding = struct instance (askama render input)
155+
- Render = SPO triple emit + declared side effects (audit, ACL gate)
156+
- `rdfs:subClassOf` = template inheritance (`{% extends %}`)
157+
- Lift-time slot validation = askama's compile-time `{{ field }}` check
158+
159+
This is the integration point `ogar-render-askama` was always going
160+
to need for actions. The crate currently renders `Class` *views*
161+
(noun-shaped: HTML/JSON/OpenAPI); a parallel `actions/` submodule
162+
renders `Class` *actions* (verb-shaped: SPO triple + side-effect spec).
163+
Same engine, same compile-time-validated context model, different
164+
output medium.
165+
166+
**Foundry-parity sharpening:** Foundry's "action types" carry exactly
167+
the four properties this encoding gives — typed parameters, slot
168+
validation, declared side effects, inheritance. Foundry sells it as a
169+
paid platform feature; verb-as-class TTL + `ogar-render-askama` gives
170+
the same four from open-source schemas and Rust templates.
171+
172+
Implications:
173+
- **WorkOrder's convention stays.** Don't normalise to `owl:ObjectProperty`.
174+
- **WorkOrder is the natural prototyping ground** (we're upstream per
175+
`dcterms:creator` = `bus-compiler` + `family-codec-smith`) for new
176+
verb-as-class predicates before pitching the pattern to OGIT upstream.
177+
- **`ogar-render-askama::actions` is the next natural module**
178+
~200 LOC mirroring the existing `views/` render path.
179+
180+
Doc: `docs/VERB-AS-CLASS-TEMPLATE.md` (FRAMING v0) carries the full
181+
analogy table + worked example + render flow.
182+
183+
## 2026-06-22 — Author provenance via `dcterms:creator` discriminates "ours to revise" from "upstream-coordinated"
184+
**Status:** FINDING
185+
**Scope:** OGIT NTO governance × multi-domain lift × who-can-change-what
186+
187+
OGIT TTL files carry `dcterms:creator` on every subject. The field is
188+
free-form text but carries one of two semantic shapes in practice:
189+
190+
- **Human author + email** (`chris.boos@almato.com`, `Viktor Voss`,
191+
`fotto@arago.de`, `Marek Meyer`, `Peter Larem`, `Ola Irgens Kylling`,
192+
…) — original arago/almato authors. Structural changes need upstream
193+
coordination.
194+
- **Internal agent name** (`bus-compiler`, `family-codec-smith`,
195+
`Claude (AdaWorldAPI/lance-graph 3-hop optim)`, …) — files authored
196+
by our agent fleet against this org's forks. We are upstream for
197+
these; structural changes need no external coordination.
198+
199+
The 9-domain spot check (Transport, Accounting, SalesDistribution,
200+
Credit, Cost, ServiceManagement, WorkOrder, Compliance, Audit) revealed:
201+
202+
- **WorkOrder is fully ours** — 100% internal-agent authorship (`bus-compiler`,
203+
`family-codec-smith`). The unusual `rdfs:Class`-as-verb convention is
204+
ours to revise toward standard `owl:ObjectProperty`-as-verb whenever
205+
the AST predicate registry needs the WorkOrder verbs.
206+
- **Accounting is mixed-authorship** — Viktor Voss (23 files, original)
207+
+ a prior session's `Claude` extension (11 files). Structural changes
208+
to the original 23 require upstream coordination; the 11 are ours.
209+
- **All other 7 domains are pure-upstream** — single-or-few external
210+
human authors.
211+
212+
This makes WorkOrder the **natural prototyping ground** for new TTL
213+
predicates OGAR wants to add: ship in WorkOrder first (no external
214+
coordination cost), validate the bijection, then pitch the pattern
215+
to OGIT upstream once it's proven.
216+
217+
Evidence: the `dcterms:creator` provenance scan recipe lives in
218+
`docs/OGIT-DOMAIN-LIFT-CATALOGUE.md § Verifying domain authorship`;
219+
the round-trip stress test for the 9 domains is
220+
`ttl_emit::tests::nine_domains_lift_surface_round_trip` (zero failures
221+
on 210 TTLs across the nine).
222+
18223
## 2026-06-22 — Schema-vs-source duality: schemas lift structure bijectively; source ASTs lift behaviour best-effort; they cross-validate at the structural boundary
19224
**Status:** FINDING
20225
**Scope:** producer architecture × MARS calibration × Foundry-Odoo lens × the bardioc migration

crates/ogar-from-schema/Cargo.toml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@ description = "Schema-as-input producer family for OGAR IR. Lifts the STRUCTURAL
1111
[features]
1212
default = []
1313
serde = ["dep:serde", "ogar-vocab/serde"]
14+
# The `xsd` feature pulls in a read-only XML parser for the XSD
15+
# front-end (the faithful Rust transcode of arago/MARS-Schema's
16+
# extract_classes.py). Kept optional so the default TTL path stays
17+
# zero-parser-deps.
18+
xsd = ["dep:roxmltree"]
1419

1520
[dependencies]
1621
ogar-vocab = { path = "../ogar-vocab" }
1722
serde = { workspace = true, optional = true }
23+
roxmltree = { version = "0.20", optional = true }
1824

19-
# No external TTL/XSD parser pulled in yet: the v0 reader uses a
20-
# narrow line-oriented walker that handles the shapes OGIT's TTL
21-
# actually emits (a tiny, machine-stable subset of full Turtle).
22-
# When the surface grows (Wikidata-shaped TTL, full RDF/XML, OWL
23-
# imports), swap in `oxttl` / `oxrdf` here without touching the
25+
# The default TTL reader uses a narrow line-oriented walker that
26+
# handles the shapes OGIT's TTL actually emits (a tiny, machine-stable
27+
# subset of full Turtle) — no external parser. The `xsd` feature adds
28+
# roxmltree (pure-Rust, read-only DOM) ONLY for the XSD front-end.
29+
# When the TTL surface grows (Wikidata-shaped TTL, full RDF/XML, OWL
30+
# imports), swap in `oxttl` / `oxrdf` without touching the
2431
# Class/Attribute target shape this crate produces.

crates/ogar-from-schema/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language};
6060
pub mod sgo;
6161
pub mod ttl;
6262
pub mod ttl_emit;
63+
#[cfg(feature = "xsd")]
64+
pub mod xsd;
6365

6466
/// What a single TTL file describes — exactly one of: an entity (`Class`),
6567
/// a datatype attribute (`Attribute`), or a verb (`Association` shape).

crates/ogar-from-schema/src/ttl_emit.rs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,20 +276,102 @@ mod tests {
276276
/// predicate.
277277
#[test]
278278
fn all_mars_ttl_files_roundtrip() {
279+
let stats = assert_domain_roundtrip("MARS");
280+
// 29 .ttl files in NTO/MARS at the SHA pinned by PROVENANCE.md.
281+
assert!(
282+
stats.total >= 29,
283+
"expected ≥ 29 TTL files in MARS, got {}",
284+
stats.total
285+
);
286+
}
287+
288+
/// Generic helper that walks `vocab/imports/ogit/NTO/<domain>/`,
289+
/// dispatches each TTL to the right parser (`parse_file` for entities
290+
/// and datatype attributes, `crate::sgo::parse_verb` for in-domain
291+
/// `owl:ObjectProperty` verbs), and asserts semantic round-trip.
292+
/// Returns per-shape counts so callers can sanity-check the lift
293+
/// surface they're claiming.
294+
fn assert_domain_roundtrip(domain: &str) -> DomainStats {
279295
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
280-
.join("../../vocab/imports/ogit/NTO/MARS");
281-
let mut checked = 0usize;
296+
.join("../../vocab/imports/ogit/NTO")
297+
.join(domain);
298+
let mut stats = DomainStats::default();
282299
for entry in walk_ttl(&dir) {
283300
let src = std::fs::read_to_string(&entry).expect("read");
301+
stats.total += 1;
284302
match parse_file(&src) {
285-
Some(TtlDeclaration::Entity(_)) => assert_entity_roundtrip(&src),
286-
Some(TtlDeclaration::DatatypeAttribute(_)) => assert_attribute_roundtrip(&src),
287-
None => {}
303+
Some(TtlDeclaration::Entity(_)) => {
304+
assert_entity_roundtrip(&src);
305+
stats.entities += 1;
306+
}
307+
Some(TtlDeclaration::DatatypeAttribute(_)) => {
308+
assert_attribute_roundtrip(&src);
309+
stats.attributes += 1;
310+
}
311+
None => {
312+
// Try the verb path — some NTO domains carry their own
313+
// in-domain `owl:ObjectProperty` verbs (Transport,
314+
// Accounting, Credit, Compliance) alongside SGO's
315+
// upstream-shared vocabulary.
316+
let Some(once) = crate::sgo::parse_verb(&src) else {
317+
panic!(
318+
"TTL has no recognised subject type in {domain}: {}",
319+
entry.display()
320+
);
321+
};
322+
let emitted = crate::sgo::emit_verb(&once);
323+
let twice = crate::sgo::parse_verb(&emitted).expect("re-parse verb");
324+
assert_eq!(
325+
once,
326+
twice,
327+
"verb round-trip lost a predicate in {domain}: {}",
328+
entry.display()
329+
);
330+
stats.verbs += 1;
331+
}
288332
}
289-
checked += 1;
290333
}
291-
// 29 .ttl files in NTO/MARS at the SHA pinned by PROVENANCE.md.
292-
assert!(checked >= 29, "expected ≥ 29 TTL files, got {checked}");
334+
stats
335+
}
336+
337+
#[derive(Debug, Default)]
338+
struct DomainStats {
339+
total: usize,
340+
entities: usize,
341+
attributes: usize,
342+
verbs: usize,
343+
}
344+
345+
/// Cross-domain bijection coverage. Each row is one of the nine
346+
/// domains the operator asked OGAR to verify before promoting
347+
/// the lift surface from MARS-only to multi-domain. If any of
348+
/// these fails, the producer can't land on that domain without
349+
/// extending `EntityDecl` / `AttributeDecl` / `VerbDecl` first.
350+
///
351+
/// Counts are also a sanity check on the inventory — they prove
352+
/// the catalogue's per-domain numbers match what's actually in
353+
/// `vocab/imports/`.
354+
#[test]
355+
fn nine_domains_lift_surface_round_trip() {
356+
for (domain, expected_total) in [
357+
("Transport", 27),
358+
("Accounting", 36),
359+
("SalesDistribution", 23),
360+
("Credit", 21),
361+
("Cost", 5),
362+
("ServiceManagement", 59),
363+
("WorkOrder", 27),
364+
("Compliance", 9),
365+
("Audit", 3),
366+
] {
367+
let stats = assert_domain_roundtrip(domain);
368+
assert_eq!(
369+
stats.total, expected_total,
370+
"{domain}: TTL count drifted from inventory \
371+
(expected {expected_total}, got {})",
372+
stats.total
373+
);
374+
}
293375
}
294376

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

0 commit comments

Comments
 (0)