From 59be3b27c98fa9c9282ebfb7d12000068c9a2b82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 05:17:24 +0000 Subject: [PATCH 1/2] =?UTF-8?q?ogar-from-schema:=20DO-arm=20producer=20?= =?UTF-8?q?=E2=80=94=20HIRO=20Automation=20=E2=86=92=20ActionDef=20(parity?= =?UTF-8?q?,=20schema=20half)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the schema-shape half of PROBE-OGAR-DO-ARM-LIFT: the MARS import lifted the STRUCTURAL arm (A→R→S→M Classes); this lifts the BEHAVIORAL arm the import left on the table — the OGIT Automation domain (HIRO's actuator vocabulary) into OGAR ActionDefs, per docs/HIRO-DO-ARM-LIFT.md §4-§5. New `do_arm` module in ogar-from-schema (reuses the TTL parser): - into_action_def(&EntityDecl) -> Option: KnowledgeItem → ActionDef (object_class ← `relates MARSNodeTemplate`, kausal ← `contains Trigger` → LifecycleTrigger, predicate = `execute` from the entity's own dcterms:description, decorators = the §4 actuator verbs present). - ActionHandler / ActionApplicability / Trigger recognized as contract PARTS (return None); ActionApplicability's environmentFilter → StateGuard via kausal_from_entity. - payload_attribute() records the knowledgeItemFormalRepresentation slot BY NAME; body_source stays None — the lossless-DO rule (the ActionDef points to the body, never inlines/flattens it into DDL), test-enforced. - canonical_object_class() + acronym-correct snake() (MARSNodeTemplate → mars_node_template). Calibration parity tests run against the real vendored OGIT Automation TTL bytes (no fixtures — same frozen-source discipline as the MARS structural arm). 24 tests green (5 new), clippy-clean. The behavioral half (ActionDef → adapter → execute → reproduce KI behavior bit-for-bit) stays CONJECTURE — it needs KI instances with bodies, which the vendored vocab does not carry. Docs: D-HIRO-DO ledger row (DISCOVERY-MAP §2.8); HIRO-DO-ARM-LIFT §6 promoted CONJECTURE → schema half SHIPPED [G]; MARS-TRANSCODING cross-ref. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP --- crates/ogar-from-schema/src/do_arm.rs | 425 ++++++++++++++++++++++++++ crates/ogar-from-schema/src/lib.rs | 1 + docs/DISCOVERY-MAP.md | 1 + docs/HIRO-DO-ARM-LIFT.md | 44 ++- docs/MARS-TRANSCODING.md | 4 + 5 files changed, 462 insertions(+), 13 deletions(-) create mode 100644 crates/ogar-from-schema/src/do_arm.rs diff --git a/crates/ogar-from-schema/src/do_arm.rs b/crates/ogar-from-schema/src/do_arm.rs new file mode 100644 index 0000000..ac9f09d --- /dev/null +++ b/crates/ogar-from-schema/src/do_arm.rs @@ -0,0 +1,425 @@ +//! OGIT **Automation** domain → OGAR **DO arm** (behavioral arm, schema level). +//! +//! The structural sibling [`crate::into_class`] lifts an OGIT entity into a +//! [`Class`] (the THINK arm). This module lifts the *actionable* Automation +//! entities — HIRO's actuator vocabulary — into [`ActionDef`]s (the DO arm), +//! per `docs/HIRO-DO-ARM-LIFT.md` §4–§5. +//! +//! # The lossless-DO rule (the governing invariant) +//! +//! `docs/HIRO-DO-ARM-LIFT.md` §1: **DO is lossless iff the `ActionDef` +//! *points to* the body (content-addressed) instead of *compressing it into* +//! a flat target (a DDL `DEFINE EVENT … WHEN … THEN …`).** This is +//! `I-VSA-IDENTITIES` applied to the DO arm — an identity that points to +//! content, never a bundle of the content's register. +//! +//! Concretely here: the KnowledgeItem's opaque body lives in the +//! `knowledgeItemFormalRepresentation` mandatory attribute. This lift **records +//! the attribute name** ([`payload_attribute`]) and **never inlines or reparses +//! the bytes** — [`ActionDef::body_source`] stays `None` at the schema level +//! (the schema carries no instance body; the instance lift fills it behind a +//! content hash, never by flattening into DDL). +//! +//! # Scope — schema level only (the producer half of `PROBE-OGAR-DO-ARM-LIFT`) +//! +//! This is the *structural* half of the DO-arm probe: it proves the **shape** +//! lifts (entity → `ActionDef` contract template with the §4 field mapping). +//! The *behavioral* half — an `ActionDef` → adapter → execute → result +//! reproducing a KnowledgeItem's recorded behavior bit-for-bit — needs KI +//! *instances* (with bodies) and stays CONJECTURE until that corpus is run, +//! exactly as `PROBE-OGAR-RBAC-AUTHORIZE` gates the RBAC arm. + +use ogar_vocab::{ActionDef, ActionSubject, KausalSpec, ModalSpec, TemporalSpec}; + +use super::EntityDecl; + +/// OGAR application prefix for the OGIT **Automation** (HIRO) domain. +/// +/// Mirrors the `ogit-erp` / `ogit-op` convention used by the other producers +/// (`ogar-adapter-ttl`, `ogar-from-elixir`). The OGIT namespace +/// `ogit.Automation:` lowers to this dashed app prefix. +pub const AUTOMATION_PREFIX: &str = "ogit-automation"; + +/// camelCase / PascalCase → `snake_case`, correct over acronym runs. +/// +/// `MARSNodeTemplate` → `mars_node_template`, `environmentFilter` → +/// `environment_filter`, `knowledgeItemFormalRepresentation` → +/// `knowledge_item_formal_representation`. An uppercase letter starts a new +/// word when it follows a lowercase letter, or when it is the tail of an +/// acronym run immediately before a lowercase letter (the `S` → `N` boundary +/// in `MARS|Node`). +#[must_use] +pub fn snake(name: &str) -> String { + let chars: Vec = name.chars().collect(); + let mut out = String::with_capacity(name.len() + 4); + for (i, &c) in chars.iter().enumerate() { + if c.is_ascii_uppercase() { + let prev_lower = i > 0 && chars[i - 1].is_ascii_lowercase(); + let prev_upper = i > 0 && chars[i - 1].is_ascii_uppercase(); + let next_lower = i + 1 < chars.len() && chars[i + 1].is_ascii_lowercase(); + if i > 0 && (prev_lower || (prev_upper && next_lower)) { + out.push('_'); + } + out.push(c.to_ascii_lowercase()); + } else { + out.push(c); + } + } + out +} + +/// Local name of a CURIE — the part after the final `:`. +/// `ogit.Automation:MARSNodeTemplate` → `MARSNodeTemplate`; `ogit:contains` +/// → `contains`. +fn local(curie: &str) -> &str { + curie.rsplit(':').next().unwrap_or(curie).trim() +} + +/// Map an OGIT CURIE to the OGAR-canonical class identity. +/// +/// - `ogit.Automation:MARSNodeTemplate` → `ogit-automation/mars_node_template` +/// - `ogit:Timeseries` → `ogit/timeseries` +/// - `ogit:Task` → `ogit/task` +/// +/// The namespace token left of `:` selects the app prefix (`ogit.` → +/// `ogit-`; bare `ogit` → `ogit`); the local name lowers to +/// `snake_case`. +#[must_use] +pub fn canonical_object_class(curie: &str) -> String { + let (ns, name) = match curie.split_once(':') { + Some((ns, name)) => (ns.trim(), name.trim()), + None => ("ogit", curie.trim()), + }; + let prefix = if let Some(domain) = ns.strip_prefix("ogit.") { + format!("ogit-{}", domain.to_ascii_lowercase()) + } else { + // bare `ogit` (core namespace) or any unprefixed token + ns.to_ascii_lowercase() + }; + format!("{prefix}/{}", snake(name)) +} + +/// The opaque-body attribute (the lossless-DO **pointer**), if the entity +/// declares one. +/// +/// Recognized by the `…FormalRepresentation` suffix on a **mandatory** +/// attribute — for `KnowledgeItem` that is +/// `knowledgeItemFormalRepresentation` (the XML body the schema references but +/// never parses; `docs/HIRO-DO-ARM-LIFT.md` §3). Returned in `snake_case`. +/// The bytes are **never** read here — only the slot name. +#[must_use] +pub fn payload_attribute(entity: &EntityDecl) -> Option { + entity + .mandatory_attributes + .iter() + .map(|curie| local(curie)) + .find(|name| name.ends_with("FormalRepresentation")) + .map(snake) +} + +/// Extract the Kausal precondition from an Automation entity. +/// +/// Per `docs/HIRO-DO-ARM-LIFT.md` §4: +/// - a `contains Trigger` allowed-edge → [`KausalSpec::LifecycleTrigger`] +/// (the `event` is the contained entity's local name); +/// - else a mandatory `environmentFilter` attribute (the `ActionApplicability` +/// shape) → [`KausalSpec::StateGuard`] on that field; +/// - else `None` (fires unconditionally at the right temporal point). +#[must_use] +pub fn kausal_from_entity(entity: &EntityDecl) -> Option { + // `contains Trigger` → LifecycleTrigger + if let Some((_, target)) = entity + .allowed + .iter() + .find(|(verb, target)| local(verb) == "contains" && local(target) == "Trigger") + { + return Some(KausalSpec::LifecycleTrigger { + event: local(target).to_owned(), + }); + } + // mandatory `environmentFilter` (ActionApplicability) → StateGuard + if entity + .mandatory_attributes + .iter() + .any(|curie| local(curie) == "environmentFilter") + { + return Some(KausalSpec::StateGuard { + guard_field: snake("environmentFilter"), + guard_values: Vec::new(), + }); + } + None +} + +/// The OGIT relation verbs that participate in the DO-arm mapping +/// (`docs/HIRO-DO-ARM-LIFT.md` §4) — recorded as the extraction provenance on +/// [`ActionDef::decorators`]. +const ACTUATOR_VERBS: &[&str] = &[ + "relates", // → object_class (the MARSNodeTemplate the action targets) + "contains", // → kausal (the Trigger) + "uses", // → params / defaults (the Variables) + "utilizes", // → params / defaults (Intent's Variables) + "worksOn", // → ActionInvocation binding (the AutomationIssue) + "solves", // → intent (the Task) + "generates", // → intent (the Timeseries) +]; + +/// Is this Automation entity an **action carrier** (→ a standalone +/// [`ActionDef`])? +/// +/// True iff it declares an opaque body ([`payload_attribute`]) **or** a +/// `contains Trigger` lifecycle edge. `KnowledgeItem` qualifies (it has both); +/// `ActionHandler` / `ActionApplicability` / `ActionCapability` / `Trigger` / +/// `Intent` do **not** — they are *parts* of the contract that map to +/// sub-fields (the adapter/membrane, a `StateGuard`, the params), per §4. +#[must_use] +pub fn is_actionable(entity: &EntityDecl) -> bool { + payload_attribute(entity).is_some() + || entity + .allowed + .iter() + .any(|(verb, target)| local(verb) == "contains" && local(target) == "Trigger") +} + +/// The canonical action verb (the `predicate`) for a known action carrier. +/// +/// `KnowledgeItem` → `execute` — sourced from the entity's own +/// `dcterms:description` (*"the KnowledgeItem executes operations"*), not +/// invented. Any other carrier defaults to its own `snake_case` name (the +/// instance lift overrides with the concrete KI name, per §5). +fn predicate_for(entity: &EntityDecl) -> String { + match entity.name.as_str() { + "KnowledgeItem" => "execute".to_owned(), + other => snake(other), + } +} + +/// Lift one actionable Automation entity into its [`ActionDef`] contract +/// **template** (`docs/HIRO-DO-ARM-LIFT.md` §5). +/// +/// Returns `None` for non-actionable entities (the contract *parts* — handler, +/// capability, applicability, trigger, intent — which map to sub-fields via +/// [`kausal_from_entity`] / [`payload_attribute`], not to a standalone def). +/// +/// Field mapping (§4): +/// - `predicate` ← [`predicate_for`] (the entity's canonical verb) +/// - `object_class` ← the `relates` allowed-edge target ([`canonical_object_class`]) +/// - `kausal` ← [`kausal_from_entity`] (`contains Trigger` → `LifecycleTrigger`) +/// - `decorators` ← the [`ACTUATOR_VERBS`] present (extraction provenance) +/// - `default_subject` = `System`, `default_temporal` = `Deferred`, +/// `default_modal` = `Async` — the automation-engine domain defaults +/// (every KI runs in the background on an issue; overridable per invocation) +/// - `body_source` = `None` — the body is **pointed to** +/// ([`payload_attribute`]), never inlined (the lossless-DO rule). +#[must_use] +pub fn into_action_def(entity: &EntityDecl) -> Option { + if !is_actionable(entity) { + return None; + } + + let entity_id = format!("{AUTOMATION_PREFIX}/{}", snake(&entity.name)); + let predicate = predicate_for(entity); + let identity = format!("{entity_id}::action_def::{predicate}"); + + // object_class ← the `relates` allowed-edge (the targeted MARSNodeTemplate). + // Falls back to the entity itself when no `relates` edge is present. + let object_class = entity + .allowed + .iter() + .find(|(verb, _)| local(verb) == "relates") + .map(|(_, target)| canonical_object_class(target)) + .unwrap_or_else(|| entity_id.clone()); + + let mut decorators: Vec = entity + .allowed + .iter() + .map(|(verb, _)| local(verb).to_owned()) + .filter(|v| ACTUATOR_VERBS.contains(&v.as_str())) + .collect(); + decorators.dedup(); + + let mut def = ActionDef::new(identity, predicate, object_class); + def.default_subject = ActionSubject::System; + def.default_temporal = TemporalSpec::Deferred; + def.default_modal = ModalSpec::Async; + def.kausal = kausal_from_entity(entity); + def.decorators = decorators; + // body_source intentionally left None — the lossless-DO rule: the body is + // POINTED TO via `payload_attribute(entity)`, never inlined here. + Some(def) +} + +/// Lift every actionable entity in a set into its DO-arm [`ActionDef`]. +/// The non-actionable parts are silently skipped (they contribute sub-fields, +/// not standalone defs). +#[must_use] +pub fn lift_action_defs(entities: &[EntityDecl]) -> Vec { + entities.iter().filter_map(into_action_def).collect() +} + +// ───────────────────────────────────────────────────────────── tests ── +// +// The schema-level half of PROBE-OGAR-DO-ARM-LIFT: prove the §4 field mapping +// lifts from the real vendored Automation TTL (no fixtures — the bytes are the +// frozen OGIT source, same calibration discipline as the MARS structural arm). + +#[cfg(test)] +mod tests { + use super::*; + use crate::{TtlDeclaration, ttl::parse_file}; + + const KNOWLEDGE_ITEM_TTL: &str = + include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/KnowledgeItem.ttl"); + const ACTION_HANDLER_TTL: &str = + include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/ActionHandler.ttl"); + const ACTION_APPLICABILITY_TTL: &str = + include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/ActionApplicability.ttl"); + const TRIGGER_TTL: &str = + include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/Trigger.ttl"); + + fn entity(src: &str) -> EntityDecl { + match parse_file(src).expect("parses") { + TtlDeclaration::Entity(e) => e, + other => panic!("expected entity, got {other:?}"), + } + } + + #[test] + fn snake_handles_acronym_runs() { + assert_eq!(snake("MARSNodeTemplate"), "mars_node_template"); + assert_eq!(snake("KnowledgeItem"), "knowledge_item"); + assert_eq!(snake("environmentFilter"), "environment_filter"); + assert_eq!( + snake("knowledgeItemFormalRepresentation"), + "knowledge_item_formal_representation" + ); + assert_eq!(snake("Trigger"), "trigger"); + } + + #[test] + fn canonical_object_class_maps_curies() { + assert_eq!( + canonical_object_class("ogit.Automation:MARSNodeTemplate"), + "ogit-automation/mars_node_template" + ); + assert_eq!(canonical_object_class("ogit:Timeseries"), "ogit/timeseries"); + assert_eq!(canonical_object_class("ogit:Task"), "ogit/task"); + } + + /// The canonical KnowledgeItem → ActionDef lift (§5 worked example), + /// asserted against the real OGIT TTL bytes. + #[test] + fn knowledge_item_lifts_to_action_def() { + let ki = entity(KNOWLEDGE_ITEM_TTL); + assert!( + is_actionable(&ki), + "KnowledgeItem must be an action carrier" + ); + let def = into_action_def(&ki).expect("KnowledgeItem → ActionDef"); + + assert_eq!(def.predicate, "execute"); + assert_eq!( + def.identity, + "ogit-automation/knowledge_item::action_def::execute" + ); + // object_class ← `relates ogit.Automation:MARSNodeTemplate` + assert_eq!(def.object_class, "ogit-automation/mars_node_template"); + // kausal ← `contains ogit.Automation:Trigger` + assert_eq!( + def.kausal, + Some(KausalSpec::LifecycleTrigger { + event: "Trigger".to_owned() + }) + ); + // automation-engine domain defaults + assert_eq!(def.default_subject, ActionSubject::System); + assert_eq!(def.default_temporal, TemporalSpec::Deferred); + assert_eq!(def.default_modal, ModalSpec::Async); + // extraction provenance: the §4 actuator verbs present on the KI + for verb in [ + "relates", + "contains", + "uses", + "worksOn", + "solves", + "generates", + ] { + assert!( + def.decorators.iter().any(|d| d == verb), + "decorator `{verb}` missing from {:?}", + def.decorators + ); + } + } + + /// The lossless-DO invariant (§1): the body is **pointed to**, never + /// inlined. The KI's opaque `knowledgeItemFormalRepresentation` slot is + /// recorded by name; `body_source` stays `None` at the schema level. + #[test] + fn knowledge_item_payload_is_pointed_to_never_inlined() { + let ki = entity(KNOWLEDGE_ITEM_TTL); + assert_eq!( + payload_attribute(&ki).as_deref(), + Some("knowledge_item_formal_representation"), + "the opaque body slot must be recorded by name" + ); + let def = into_action_def(&ki).expect("ActionDef"); + assert!( + def.body_source.is_none(), + "lossless-DO violated: body was inlined into body_source ({:?})", + def.body_source + ); + } + + /// `ActionApplicability` is a contract *part* — it maps to a `StateGuard` + /// (its `environmentFilter`), not to a standalone `ActionDef`. + #[test] + fn action_applicability_maps_to_state_guard_not_a_def() { + let app = entity(ACTION_APPLICABILITY_TTL); + assert!( + into_action_def(&app).is_none(), + "applicability is not a def" + ); + assert_eq!( + kausal_from_entity(&app), + Some(KausalSpec::StateGuard { + guard_field: "environment_filter".to_owned(), + guard_values: Vec::new(), + }) + ); + } + + /// `ActionHandler` is the adapter/membrane (§4) — never a standalone def. + #[test] + fn action_handler_is_not_a_standalone_def() { + let handler = entity(ACTION_HANDLER_TTL); + assert!(!is_actionable(&handler)); + assert!(into_action_def(&handler).is_none()); + } + + /// `Trigger` is the kausal source, not an action carrier itself. + #[test] + fn trigger_is_not_a_standalone_def() { + let trigger = entity(TRIGGER_TTL); + assert!(!is_actionable(&trigger)); + assert!(into_action_def(&trigger).is_none()); + } + + /// The set lift skips the parts and keeps only the carriers. + #[test] + fn lift_action_defs_keeps_only_carriers() { + let entities = [ + entity(KNOWLEDGE_ITEM_TTL), + entity(ACTION_HANDLER_TTL), + entity(ACTION_APPLICABILITY_TTL), + entity(TRIGGER_TTL), + ]; + let defs = lift_action_defs(&entities); + assert_eq!( + defs.len(), + 1, + "only KnowledgeItem is a standalone ActionDef" + ); + assert_eq!(defs[0].predicate, "execute"); + } +} diff --git a/crates/ogar-from-schema/src/lib.rs b/crates/ogar-from-schema/src/lib.rs index c5a1a8c..192c1db 100644 --- a/crates/ogar-from-schema/src/lib.rs +++ b/crates/ogar-from-schema/src/lib.rs @@ -57,6 +57,7 @@ use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language}; +pub mod do_arm; pub mod sgo; pub mod ttl; pub mod ttl_emit; diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 09fb8bb..209fdc9 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -208,6 +208,7 @@ two halves of a cell. ADR‑026 names the cascade that ties them. | D‑KNOWABLE | `KnowableFromStore` + `register_class_knowable_from`; `surrealql-hint`; **`vart-backend`** | G | CODED | `ogar-knowable-from/` (#25,#33,#43) | D‑IDENT | | D‑HINT | `schema_ddl_hint` loop closed — self‑describing registry via emit | G | CODED | (#33) | D‑SURREALQL, D‑KNOWABLE | | D‑ELIXIR | Elixir/HIRO SchemaSource scaffold (`gen_statem`→Rubicon) | G | CODED (scaffold) | `ogar-from-elixir/` | D‑VOCAB | +| D‑HIRO‑DO | OGIT Automation → DO arm: `into_action_def` lifts `KnowledgeItem`→`ActionDef` (object_class←`relates`, kausal←`contains Trigger`, body **pointed‑to** not inlined — lossless‑DO §1); schema half of `PROBE‑OGAR‑DO‑ARM‑LIFT` green | G | CODED (schema half) | `ogar-from-schema/src/do_arm.rs` | D‑VOCAB, D‑TTL, D‑ELIXIR | | D‑OSM | `ogar-from-osm-pbf` — Node/Way/Relation; quadkey NiblePath from resolved geometry | H | IDEA | (queued) | D‑VOCAB, `[per rt]` D‑OSM‑3 | | D‑PATTERN | `ogar-pattern` — recognition library + confidence (FMA‑D/FIBO/SKR/PROV‑O) | H | IDEA | (queued) | D‑TTL | | D‑ACTION | `ogar-actionable` — lifecycle → `ActionDef`/`KausalSpec` | H | IDEA | (queued) | D‑PATTERN | diff --git a/docs/HIRO-DO-ARM-LIFT.md b/docs/HIRO-DO-ARM-LIFT.md index 05b6b33..8408b64 100644 --- a/docs/HIRO-DO-ARM-LIFT.md +++ b/docs/HIRO-DO-ARM-LIFT.md @@ -128,24 +128,42 @@ this would keep "on ServiceDown, run X" and **lose** `guard_failure_policy`, --- -## 6. The producer (proposed) — `[H]`, gated on a probe +## 6. The producer — schema half `[G]` SHIPPED; behavioral half `[H]` gated -Parallel to `ogar-from-schema` (structural arm), an Automation-domain DO lift -(extend `ogar-from-schema` with a `do_arm` module, reusing its TTL parser): +Parallel to `ogar-from-schema` (structural arm), the Automation-domain DO lift +is the `do_arm` module in `ogar-from-schema`, reusing its TTL parser: ```text -Automation/entities/*.ttl → ActionDef{ predicate, object_class, kausal, - defaults, results_in, payload_ref } - + payload table (formalRepresentation blobs) +Automation/entities/*.ttl → into_action_def(&EntityDecl) -> Option + { predicate, object_class, kausal, decorators } + + payload_attribute(&EntityDecl) (the body pointer) ``` -- **`payload_ref` only** — the lift NEVER inlines or reparses - `knowledgeItemFormalRepresentation`; it hashes it into the payload table. -- **CONJECTURE until `PROBE-OGAR-DO-ARM-LIFT` is green:** an `ActionDef` → - adapter → execute → result must reproduce the KI's recorded behavior - bit-for-bit on a fixed corpus (the same falsification discipline as - `PROBE-OGAR-RBAC-AUTHORIZE`). Until then the mapping in §4 is a FINDING about - *shape*, not a certified executable equivalence. +**The schema half is now CODED and green** (`crates/ogar-from-schema/src/do_arm.rs`, +D‑HIRO‑DO). Against the real vendored OGIT TTL bytes (no fixtures — same +calibration discipline as the MARS structural arm): + +- `KnowledgeItem` → `ActionDef` with `object_class` ← `relates MARSNodeTemplate` + (`ogit-automation/mars_node_template`), `kausal` ← `contains Trigger` + (`LifecycleTrigger`), `predicate` = `execute` (from the entity's own + `dcterms:description`), `decorators` = the §4 actuator verbs present. +- `ActionHandler` / `ActionApplicability` / `Trigger` are recognized as + contract **parts**, not standalone defs — `into_action_def` returns `None`; + `ActionApplicability`'s `environmentFilter` lifts to a `StateGuard` via + `kausal_from_entity`. +- **Lossless‑DO is test‑enforced:** `payload_attribute` records the + `knowledgeItemFormalRepresentation` slot **by name**; `ActionDef.body_source` + stays `None` — the test `knowledge_item_payload_is_pointed_to_never_inlined` + fails if any bytes are inlined. (The live `ActionDef` carries `body_source` + rather than the §5 sketch's `payload_ref`; at the schema level neither is + populated — the *instance* lift fills the pointer behind a content hash.) + +- **Behavioral half — CONJECTURE until `PROBE-OGAR-DO-ARM-LIFT` (full) is + green:** an `ActionDef` → adapter → execute → result must reproduce the KI's + recorded behavior bit-for-bit on a fixed *instance* corpus (the same + falsification discipline as `PROBE-OGAR-RBAC-AUTHORIZE`). The shipped schema + half certifies the **shape**; executable equivalence needs KI instances + (with bodies), which the vendored vocab does not carry. --- diff --git a/docs/MARS-TRANSCODING.md b/docs/MARS-TRANSCODING.md index f8dff45..f1798f7 100644 --- a/docs/MARS-TRANSCODING.md +++ b/docs/MARS-TRANSCODING.md @@ -162,6 +162,10 @@ domains are paperwork. - `vocab/imports/ogit/NTO/MARS/PROVENANCE.md` — SHA + license + re-vendor recipe - `vocab/imports/ogit/NTO/MARS/_oracle/` — the XSD + `extract_classes.py` oracle - `crates/ogar-from-schema/` — the producer (TTL + reverse-emit + SGO verbs) +- `crates/ogar-from-schema/src/do_arm.rs` — the **DO-arm** sibling: lifts the + OGIT Automation entities (`KnowledgeItem`→`ActionDef`) the structural MARS + import left on the table (the behavioral arm, schema-shape half; D‑HIRO‑DO) +- `docs/HIRO-DO-ARM-LIFT.md` — the DO-arm mapping + the lossless-DO rule - `docs/HIRO-IN-CLASSES.md` — the bardioc-efficiency story - `docs/FOUNDRY-ODOO-MARS-LENS.md` — the cross-domain lens - `docs/ELIXIR-HIRO-PREFETCH.md` — the behavioural-arm prefetch (sibling) From 3c1555b9378e57c69873c1ea1e1f80e857653f46 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 05:29:50 +0000 Subject: [PATCH 2/2] =?UTF-8?q?ogar-from-schema:=20DO-arm=20lift=20?= =?UTF-8?q?=E2=86=92=20SPO=20emitter,=20proven=20end-to-end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the lift→triples seam for the behavioral arm: the DO-arm ActionDef (D-HIRO-DO) flows through the canonical ogar-emitter to SPO triples — the surface the RBAC / query layers consume. tests/do_arm_emit.rs (new integration test, ogar-emitter as dev-dep): - KnowledgeItem TTL → into_action_def → emit_action_def: asserts the §4 field mapping survives as ogar:ActionDef / ogar:actionPredicate (execute) / ogar:actionObjectClass (ogit-automation/mars_node_template) / LifecycleTrigger kausal / actuator-verb decorators. - lossless-DO holds END-TO-END: body_source is None, so the emitter produces NO ogar:actionBody triple — a body triple would mean bytes leaked into the IR. Test fails if any do. ogar-emitter is an acyclic dev-dep (it depends on ogar-vocab/-ontology, never on ogar-from-schema). 24 lib + 2 integration tests green, clippy-clean. Docs: D-HIRO-DO ledger row notes the proven emit path (+ D-EMIT dep). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP --- crates/ogar-from-schema/Cargo.toml | 7 ++ crates/ogar-from-schema/tests/do_arm_emit.rs | 95 ++++++++++++++++++++ docs/DISCOVERY-MAP.md | 2 +- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 crates/ogar-from-schema/tests/do_arm_emit.rs diff --git a/crates/ogar-from-schema/Cargo.toml b/crates/ogar-from-schema/Cargo.toml index efafbce..6fcefc4 100644 --- a/crates/ogar-from-schema/Cargo.toml +++ b/crates/ogar-from-schema/Cargo.toml @@ -29,3 +29,10 @@ roxmltree = { version = "0.20", optional = true } # 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. + +[dev-dependencies] +# Used only by tests/do_arm_emit.rs to prove the DO-arm lift's output +# (`ogar_vocab::ActionDef`) flows through the canonical SPO emitter +# end-to-end. Acyclic: ogar-emitter depends on ogar-vocab/-ontology, +# never on this crate. +ogar-emitter = { path = "../ogar-emitter" } diff --git a/crates/ogar-from-schema/tests/do_arm_emit.rs b/crates/ogar-from-schema/tests/do_arm_emit.rs new file mode 100644 index 0000000..39f9921 --- /dev/null +++ b/crates/ogar-from-schema/tests/do_arm_emit.rs @@ -0,0 +1,95 @@ +//! End-to-end integration: the DO-arm lift reaches the SPO emitter. +//! +//! Proves the full behavioral-arm path closes — +//! +//! ```text +//! OGIT Automation TTL → ttl::parse_file → EntityDecl +//! → do_arm::into_action_def → ogar_vocab::ActionDef +//! → ogar_emitter::TripleEmitter::emit_action_def → Vec +//! ``` +//! +//! i.e. the `KnowledgeItem` → `ActionDef` contract template (D‑HIRO‑DO) +//! emits as canonical `ogar:ActionDef` SPO triples carrying the §4 field +//! mapping, with the lossless‑DO invariant preserved across the whole path +//! (no body inlined, so no `ogar:actionBody` triple is produced at the schema +//! level). This is the lift→triples seam the RBAC / query layers consume. + +use ogar_emitter::TripleEmitter; +use ogar_from_schema::do_arm::into_action_def; +use ogar_from_schema::ttl::parse_file; +use ogar_from_schema::TtlDeclaration; + +const KNOWLEDGE_ITEM_TTL: &str = + include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/KnowledgeItem.ttl"); + +fn obj(triples: &[ogar_emitter::Triple], subject: &str, predicate: &str) -> Option { + triples + .iter() + .find(|t| t.subject == subject && t.predicate == predicate) + .map(|t| t.object.clone()) +} + +#[test] +fn knowledge_item_lifts_and_emits_as_action_def_triples() { + // Lift. + let TtlDeclaration::Entity(ki) = parse_file(KNOWLEDGE_ITEM_TTL).expect("parses") else { + panic!("expected entity"); + }; + let def = into_action_def(&ki).expect("KnowledgeItem → ActionDef"); + let id = def.identity.clone(); + assert_eq!(id, "ogit-automation/knowledge_item::action_def::execute"); + + // Emit. + let triples = TripleEmitter::emit_action_def(&def); + + // Type triple — the def IS an ogar:ActionDef on the SPO surface. + assert_eq!(obj(&triples, &id, "rdf:type").as_deref(), Some("ogar:ActionDef")); + + // The §4 field mapping survives the lift→emit path verbatim. + assert_eq!( + obj(&triples, &id, "ogar:actionPredicate").as_deref(), + Some("execute") + ); + assert_eq!( + obj(&triples, &id, "ogar:actionObjectClass").as_deref(), + Some("ogit-automation/mars_node_template") + ); + + // The `contains Trigger` → LifecycleTrigger kausal emits its triple(s). + assert!( + triples + .iter() + .any(|t| t.subject == id && t.predicate.starts_with("ogar:") && t.object == "Trigger"), + "LifecycleTrigger event `Trigger` missing from emitted triples: {triples:?}" + ); + + // The §4 actuator verbs ride as ogar:actionDecorator provenance. + for verb in ["relates", "contains", "uses"] { + assert!( + triples + .iter() + .any(|t| t.subject == id + && t.predicate == "ogar:actionDecorator" + && t.object == verb), + "decorator `{verb}` missing from emitted triples" + ); + } +} + +#[test] +fn lossless_do_survives_the_emit_path() { + // The lossless-DO invariant must hold END-TO-END, not just at the lift: + // since `body_source` is None (the body is pointed-to, never inlined), + // the emitter must NOT produce an `ogar:actionBody` triple. A body triple + // here would mean some bytes leaked into the IR. + let TtlDeclaration::Entity(ki) = parse_file(KNOWLEDGE_ITEM_TTL).expect("parses") else { + panic!("expected entity"); + }; + let def = into_action_def(&ki).expect("ActionDef"); + let triples = TripleEmitter::emit_action_def(&def); + + assert!( + !triples.iter().any(|t| t.predicate == "ogar:actionBody"), + "lossless-DO violated across emit: a body triple was produced ({triples:?})" + ); +} diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 209fdc9..188456f 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -208,7 +208,7 @@ two halves of a cell. ADR‑026 names the cascade that ties them. | D‑KNOWABLE | `KnowableFromStore` + `register_class_knowable_from`; `surrealql-hint`; **`vart-backend`** | G | CODED | `ogar-knowable-from/` (#25,#33,#43) | D‑IDENT | | D‑HINT | `schema_ddl_hint` loop closed — self‑describing registry via emit | G | CODED | (#33) | D‑SURREALQL, D‑KNOWABLE | | D‑ELIXIR | Elixir/HIRO SchemaSource scaffold (`gen_statem`→Rubicon) | G | CODED (scaffold) | `ogar-from-elixir/` | D‑VOCAB | -| D‑HIRO‑DO | OGIT Automation → DO arm: `into_action_def` lifts `KnowledgeItem`→`ActionDef` (object_class←`relates`, kausal←`contains Trigger`, body **pointed‑to** not inlined — lossless‑DO §1); schema half of `PROBE‑OGAR‑DO‑ARM‑LIFT` green | G | CODED (schema half) | `ogar-from-schema/src/do_arm.rs` | D‑VOCAB, D‑TTL, D‑ELIXIR | +| D‑HIRO‑DO | OGIT Automation → DO arm: `into_action_def` lifts `KnowledgeItem`→`ActionDef` (object_class←`relates`, kausal←`contains Trigger`, body **pointed‑to** not inlined — lossless‑DO §1); schema half of `PROBE‑OGAR‑DO‑ARM‑LIFT` green; lift→`emit_action_def`→SPO triples proven end‑to‑end (`tests/do_arm_emit.rs`, lossless‑DO holds across emit) | G | CODED (schema half) | `ogar-from-schema/src/do_arm.rs` | D‑VOCAB, D‑TTL, D‑ELIXIR, D‑EMIT | | D‑OSM | `ogar-from-osm-pbf` — Node/Way/Relation; quadkey NiblePath from resolved geometry | H | IDEA | (queued) | D‑VOCAB, `[per rt]` D‑OSM‑3 | | D‑PATTERN | `ogar-pattern` — recognition library + confidence (FMA‑D/FIBO/SKR/PROV‑O) | H | IDEA | (queued) | D‑TTL | | D‑ACTION | `ogar-actionable` — lifecycle → `ActionDef`/`KausalSpec` | H | IDEA | (queued) | D‑PATTERN |