Purpose. Hand the actor/runtime session the exact type surface to generate against, so
lance-graph-callcenter's ractor codegen lands on the OGAR Types instead of declaring a parallel set. Without this contract the codegen and the IR drift and it's a mess. With it, "the class IS the actor spec" (ARCHITECTURE.md) becomes literal: the actor is generated from theClass, never hand-written.Companion / grounded in:
ARCHITECTURE.md(Universal AST),SOA-IMPLEMENTATION.md(IR RecordBatch schemas + SurrealQL adapter + §5 runtime),ADAPTERS-AND-ACTORS.md(Action / SPO+TeKaMoLo),IDENTITY-MAPPING.md(Identity),LANCE-GRAPH-INTEGRATION.md(contract types). The companionRACTOR-STATEMdesign defines theStateMachineshim referenced in §3.Status: CONTRACT v0 (2026-06-04). Cross-session handover.
Per the runtime session, the design splits cleanly and the split matters:
- Generic
state_machinecrate (lives inractor_actors): OGAR-agnostic.Contextis an opaque associated type the crate never inspects;on_enterdelegates to aCommitHook. Zero OGAR types — so the OGAR IR can evolve without ever touchingractor_actors. (Owned by the runtime session.) - The OGAR/Rubicon binding (the only layer that references OGAR types):
fills
Context/Event/State, supplies theCommitHook+ aContextconstructor, and is where the callcenter codegen lands.
This document is the OGAR type surface + projection rules the binding maps
onto — Class, ActionDef / ActionInvocation, Identity (canonical), with
SurrealQL DDL as a bidirectional bridge (§2). It does not re-implement the
binding (that's the runtime session's, grounded in these types) and adds no
parallel actor/message/state types.
These mirror the ogar-vocab-soa RecordBatch schemas 1:1 (SOA-IMPLEMENTATION §3).
The codegen consumes the struct form; storage is the columnar form; same types,
SoA round-trip is identity.
/// Layer-1 routing key. Prefix-radix NiblePath; 27-bit segments;
/// ALWAYS dictionary-encoded on storage. The domain prefix (§4) lives here.
pub struct Identity { /* segments: SmallVec<NibleSeg>; canonical per IDENTITY-MAPPING */ }
/// Structural arm. One `Class` == one actor (§3). Field set == class_record_batch_schema().
pub struct Class {
pub identity: Identity,
pub name: String,
pub parent: Option<Identity>, // subClassOf == supervision edge (ARCHITECTURE)
pub language: Language, // Ruby | Python | SurrealQl | Sql | TypeScript | Elixir | Unknown
pub declared_in_module: Option<String>,
pub source_version: Option<String>,
pub description: Option<String>,
pub record_order: Option<String>,
pub rec_name: Option<String>,
pub abstract_model: bool,
pub transient: bool,
pub auto_create_table: Option<bool>,
pub log_access: Option<bool>,
pub inheritance_column_disabled: bool,
pub mixins: Vec<Identity>, // structural mixins (Elixir use_, Ruby include)
pub store_accessors: Vec<String>, // store-backed accessors (Rails, Ecto embeds)
// nested ListArrays of structs — SoA all the way down:
pub associations: Vec<Association>, // BelongsTo / HasMany / MemberOf ...
pub enums: Vec<EnumDecl>,
pub scopes: Vec<Scope>,
pub callbacks: Vec<Callback>,
pub computed_fields: Vec<ComputedField>,
pub methods: Vec<MethodDecl>,
pub validations: Vec<Validation>,
pub attributes: Vec<Attribute>,
}
/// Behavior arm — STATIC declaration (one per transition / handler). SPO + TeKaMoLo
/// (ADAPTERS-AND-ACTORS §3). Carries the four Sprint-7 statem terms landed in #10:
/// `ogar:onEnter`, `ogar:guardFailurePolicy`, `ogar:StateTimeout` (+ `stateTimeoutMillis`).
pub struct ActionDef {
pub identity: Identity,
pub predicate: String, // event name
pub object_class: Identity, // acted-upon Class
pub default_subject: ActionSubject, // User | System | Cron | Trigger | Cascade
pub default_temporal: TemporalSpec, // Immediate | Deferred | Scheduled | OnCommit | StateTimeout
pub default_modal: ModalSpec, // Sync | Async | Idempotent | Atomic | Requires
pub kausal: KausalSpec, // StateGuard{field,value} | LifecycleTrigger | DependsPath | None
pub method_body: Option<String>,
pub results_in: Option<StateTransition>, // ::S-to-T → applied at Pending→Committed
// statem terms (vocab/ogar.ttl, PR #10):
pub on_enter: Option<EnterEffect>, // ogar:onEnter (typed; range tightened from xsd:string)
pub guard_failure_policy: GuardFailurePolicy, // Reject (default) | Postponable
pub state_timeout_millis: Option<i64>, // ogar:stateTimeoutMillis
}
/// Behavior arm — DYNAMIC instance (one per fire). Carries lifecycle + provenance;
/// `state` is the StateMachine state per §3 (lifecycle = Pending → Committed/Failed/Cancelled).
pub struct ActionInvocation {
pub identity: Identity,
pub realizes: Identity, // → ActionDef.identity
pub state: ActionState, // StateMachine::State (lifecycle)
pub subject: ActionSubject, // provenance
pub object_instance: Identity, // the specific instance acted on
pub lokal: LokalSpec, // {actor, tenant, company}
pub idempotency_key: Option<String>, // OLD↔NEW correlation handle for §14 roundtrip
pub trace_id: Option<String>,
pub parent_invocation: Option<Identity>,
pub emitted_at_millis: Option<i64>, // decision #4 — keep Option<_> until HLC lands
pub failure_reason: Option<String>,
}
// (statem terms above are the three §6 vocab terms LANDED in PR #10's vocab/ogar.ttl.)KausalSpec::StateGuard { field, value } (on ActionDef) + ActionDef.results_in
are the only places a domain workflow survives the IR flattening; the
lifecycle state machine is carried by ActionInvocation.state: ActionState —
see §3.
We adapt the upstream surrealdb_core::sql::statements AST; we do not
reimplement it (pin exact version; migrate to surrealdb-parser/surrealdb-ast
when on crates.io). The bridge is bidirectional and the IR is the two-way meeting
point — SurrealDB becomes the dev-facing DSL over lance-graph with no storage
duplication.
| SurrealQL AST node | → OGAR IR type |
|---|---|
DefineTable { name } |
Class { identity: name } |
DefineField TYPE record<x> |
Association(BelongsTo, class = x) |
DefineField TYPE string + ASSERT $value IN [...] |
EnumDecl |
DefineField TYPE option<X> |
Attribute { required: false } |
DefineField TYPE <scalar> |
Attribute { type_name } |
pub fn parse_surrealql_ddl(input: &str) -> Result<Vec<Class>>; // producer
pub fn emit_surrealql_ddl(classes: &[Class]) -> String; // consumer
// invariant: parse(emit(parse(x))) == parse(x) (proptest)This supersedes the hand-written ClassActor::handle in SOA-IMPLEMENTATION
§5.1. The actor body is generated, not authored — and split so the generic
crate stays OGAR-free.
Generic layer (runtime session, zero OGAR types). Context opaque; the hook
is wired at spawn (no I/O surface threaded through on_event). Canonical
signatures from AdaWorldAPI/ractor_actors feat/state-machine-actor @ 38a71a4:
pub trait StateMachine: Send + Sync + 'static {
type State: Clone + PartialEq + std::fmt::Debug + Send + Sync + 'static;
type Event: Send + 'static;
type Context: Send + 'static; // OPAQUE — crate never inspects
fn initial(&self) -> Self::State;
/// Pure dispatch — guards live here; no hook param, no I/O surface.
fn on_event(&self,
state: &Self::State,
event: &Self::Event,
ctx: &mut Self::Context) -> Transition<Self::State>;
/// Per-state SLA deadline. Lowering target for `ogar:StateTimeout`.
fn timeout(&self, _state: &Self::State)
-> Option<ractor::concurrency::Duration> { None }
/// Timer fired. May NOT return `Postpone` (timeouts don't replay).
fn on_timeout(&self, _state: &Self::State, _ctx: &mut Self::Context)
-> Transition<Self::State> { Transition::Stay }
/// Commit predicate. Entering a state where this is `true` fires `CommitHook`.
fn is_commit(&self, _state: &Self::State) -> bool { false }
}
pub enum Transition<S> {
Goto(S), // if is_commit(new): CommitHook fires; then postponed replay (FIFO)
Stay, // no-op / guard rejects this event
Postpone, // defer until next state change; replay FIFO ahead of newer events
Stop, // stop the actor
}
/// Side-effect seam. SYNC + fallible (honours I-2 — no tokio at the membrane).
pub trait CommitHook<SM: StateMachine>: Send + Sync + 'static {
fn on_commit(&self,
from: &SM::State,
to: &SM::State,
ctx: &SM::Context) -> Result<(), ractor::ActorProcessingErr>;
}
// spawn / fire / current_state — see crate for the actor wiring.Binding layer (this contract's OGAR inputs). The codegen fills the three
associated types from OGAR and supplies a LanceCommitHook:
// GENERATED per Action-bearing Class:
impl StateMachine for <Class>Invocation {
type State = ActionState; // Pending → Committed / Failed / Cancelled — lifecycle / Rubicon
type Event = ActionDef; // realized as ActionInvocation via `realizes`
type Context = ActionInvocation; // state, object_instance, idempotency_key, trace_id,
// parent_invocation, emitted_at_millis: Option<_> (decision #4 HLC),
// failure_reason, lokal
fn is_commit(&self, s: &ActionState) -> bool { matches!(s, ActionState::Committed) }
fn timeout(&self, _s: &ActionState) -> Option<Duration> {
self.def.state_timeout_millis.map(Duration::from_millis) // ogar:StateTimeout carrier
}
// on_event: KausalSpec guard → Goto(Committed) | Stay | Postpone(if guardFailurePolicy=Postponable) | Goto(Failed)
}
struct LanceCommitHook { membrane: LanceMembrane } // LanceMembrane = callcenter's sole writer
impl CommitHook<<Class>Invocation> for LanceCommitHook {
fn on_commit(&self, _from: &ActionState, _to: &ActionState, ctx: &ActionInvocation)
-> Result<(), ActorProcessingErr>
{
// Atomic: apply ActionDef.on_enter to ctx.object_instance + append the Lance version.
let row = CognitiveEventRow::from(ctx);
self.membrane.commit_event(row); // sibling on LanceMembrane (runtime session, gate-1 follow-up)
Ok(())
}
}Two-level state (resolved binding). State = ActionState — the invocation
lifecycle the callcenter drives/audits; the Rubicon crossing is
Goto(Committed) where is_commit(Committed) = true → CommitHook::on_commit
= the Lance commit. The domain workflow (draft→sale) is not the machine
state — it's a guarded effect on object_instance, applied at the
Pending→Committed crossing, gated by KausalSpec::StateGuard, atomic under
ModalSpec::Atomic. Lifecycle formalized; workflow as data.
| Machine construct | Sourced from |
|---|---|
State = ActionState |
the lifecycle enum (Pending/Committed/Failed/Cancelled — vocab/ogar.ttl::ActionStateKind) |
Event |
ActionDef (predicate / object_class / kausal / defaults / on_enter / guard_failure_policy / state_timeout_millis) |
on_event (guard) |
KausalSpec::StateGuard satisfied on object_instance → Goto(Committed); transient fail + guardFailurePolicy=Postponable → Postpone; hard fail → Goto(Failed) |
is_commit(Committed) |
true → fires CommitHook::on_commit (the only commit gate) |
CommitHook::on_commit |
apply ActionDef.on_enter to object_instance + LanceMembrane::commit_event (Atomic) — "state history IS the version log" |
timeout(Pending) |
ActionDef.state_timeout_millis (ogar:StateTimeout) → per-state SLA deadline |
on_timeout(Pending) |
Goto(Failed) (or domain-specific Stay); never Postpone |
Full authoritative binding record → CROSS_SESSION_COORDINATION.md (runtime
session); this section is the OGAR-side type surface it binds to.
Hot-path constraint (I-2 invariant). CommitHook::on_commit is sync +
fallible (returns Result<(), ActorProcessingErr>) — no tokio at the
membrane. The actor's dispatch + postpone replay are std::sync; tokio is
reserved for Layer-3 cold sinks. The state_machine crate honors this; the
binding's CommitHook impl must not introduce an await.
The "flexible enough to be everything later" requirement is satisfied structurally, not by special-casing:
- Domain ==
Identityprefix. Now:ogit-op::(OpenProject/Rails),ogit-erp::(Odoo). Later:bardioc::,foundry::,wikidata-med::. - Each domain's source AST maps onto the same
Class/Actionvia anAdapter(Ruby AR → lib-ruby-parser; Python Odoo → libcst; SQL DDL → sqlparser-rs; SurrealQL → surrealdb-core; new domains → a producer/TTL hydrator). - The codegen is domain-agnostic — it lowers any
Class→ actor regardless of prefix. Adding a domain = adding a producer/adapter, never touching the codegen or the core types. That is the flexibility guarantee.
Odoo ogit-erp::sale.order action_confirm (draft→sale)
→ State {Draft, Sale, ...} Event=confirm Next(Sale)
→ on_enter(Sale): _send_order_confirmation_mail + Lance commit
OpenProject ogit-op::WorkPackage before_save :touch_parent
→ Action(subject=Cascade, temporal=OnCommit)
→ cascade-emit to ogit-op::Project actor (kanban-bounded)
Cross-system: SaleOrder.workPackage → one lance-graph traversal
(both extend OGAR under the same prefix-radix; both are
statem actors over the one append-only log)
| Target | object → Class |
link → Association |
action → Action |
how it attaches |
|---|---|---|---|---|
| Palantir Foundry | object-type | link-type | action-type | adapter (Foundry's ontology is this shape) |
| Wikidata-medical | item | property | — | TTL hydrator (joins SKOS/PROV-O/schema.org/FIBO/Odoo/ZUGFeRD/SKR03-04 already in lance-graph-ontology); planet-scale fits single-node (compression-to-the-floor — "Wikidata fits") |
| bardioc (NEW-stack) | capability shape | — | — | bardioc:: prefix; consumer-internal specs stay out of lance-graph (guardrail) |
AGI aspiration (the substrate wiring): "a thought is a Raft commit" — distributed cognition replicates the frozen generator (semantics + syntax) and re-runs the wave (pragmatics/actors) locally. The Rubicon statem over the append-only log is the wiring; this contract is the type surface it runs on. Same core, no new substrate.
- The generic
state_machinecrate references zero OGAR types —State/Event/Contextare associated types; all OGAR coupling lives in the binding (so the IR evolves without touchingractor_actors). The binding emitsimpl StateMachineover OGARClass/ActionDef/ActionInvocation/Identity— no parallel hand-rolled actor/message/state types. (Supersedes the hand-authoredClassActor::handle.) type Event = ActionDef— the event is the static transition declaration, not a bespoke enum. Actors never invent message enums.type State = ActionState— the lifecycle enum (Pending → Committed / Failed / Cancelled), drawn fromvocab/ogar.ttlActionStateKind. Never derived from per-classStateGuardvalues; never hand-declared. Domain workflow rides as guarded effect onActionInvocation.object_instance, not as machine state.type Context = ActionInvocation— the dynamic invocation row (state, object_instance, idempotency_key, trace_id, parent_invocation, emitted_at_millis, failure_reason, lokal). No row-form structs.ActionDefandActionInvocationstay split — never collapsed into a singleActiontype. Static decl ≠ dynamic instance;realizesis the link.- Routing is by
IdentityNiblePath only. - Hot path
std::sync(Condvar). The statem shim never pullstokiointo dispatch/postpone.tokio= Layer-3 cold only. - Inter-actor wire form is RecordBatch IPC — N invocations = 1 batch.
Three semantics didn't survive the Action-flattening; each is now a sub-property
of an existing TeKaMoLo slot in vocab/ogar.ttl (carve-out: "seven slots, no
eighth"), shipped in AdaWorldAPI/OGAR#10 and carried on ActionDef (§1):
| Term | Slot | Carrier field | Lowers to |
|---|---|---|---|
ogar:onEnter |
(entry effect) | ActionDef.on_enter: Option<EnterEffect> (typed since PR #13) |
StateMachine::on_enter(Committed) → CommitHook → Lance append (the Rubicon crossing) |
ogar:guardFailurePolicy ∈ {Postponable, Reject} |
Modal | ActionDef.guard_failure_policy: GuardFailurePolicy |
Postponable → Transition::Postpone (stay Pending); Reject (default) → Pending → Failed |
ogar:StateTimeout (+ stateTimeoutMillis) |
Temporal | ActionDef.state_timeout_millis: Option<i64> |
per-state SLA on Pending; gen-stamped timer auto-cancels at the crossing |
Without these the codegen could re-assemble states/transitions from existing triples but couldn't express enter-effects, postpone, or per-state SLA timeouts. Now expressible end-to-end, modulo the §7 runtime-side pieces.
Companion: docs/ELIXIR-HIRO-PREFETCH.md (also #10) — the OLD-Elixir-stack
→ OGAR debt ledger; gen_statem state_enter/postpone/state-timeout lower
onto exactly these three terms, so every migration debt has a type home before
Sprint 4.5 unmap lands. Language::Elixir is first-class in ogar-vocab.
Typed EnterEffect promotion (xsd:string → ogar:EnterEffect) — CLOSED in
PR #13. ActionDef.on_enter is now Option<EnterEffect> with field +
to_value fields; codegen applies the lifecycle-visible transition
structurally instead of string-parsing. Further tightening of to_value to
typed values beyond strings remains a tracked follow-up.
- Scope — RESOLVED. The lowering covers every Action-bearing
invocation, not Rubicon-only. The lifecycle (
Pending → Committed/Failed/ Cancelled) is universal to everyActionInvocation; "Rubicon" is just the name for thePending → Committedcrossing, not a special subset. Codegen is uniform — same lowering for Odooaction_confirm, OpenProjectbefore_save, and any future domain. LanceMembranesole-writer signature — ANSWERED, runtime-side to land.LanceMembraneadds a sibling to the zero-depExternalMembrane::project:returning the new Lance version. Action commits skip the cognitive-cyclefn commit_event(&self, row: Self::Commit /* = CognitiveEventRow */) -> u64
ShaderBusshape. This is the preciseCommitHook::on_commitcall target (see §3's binding example), to be added inlance-graph-callcenterby the runtime session. Until then the binding'sLanceCommitHookimpl compiles against a trait-object/stub forLanceMembrane.- Fork access — RESOLVED.
GH_TOKENPAT authenticates asAdaWorldAPIwith push onAdaWorldAPI/{OGAR, ractor, ractor_actors, lance-graph, bardioc}. The genericstate_machinecrate lives inractor_actors/(cargo-workspace member, pushed atfeat/state-machine-actor@38a71a4— 7/7 tests green, clippy clean, includes the load-bearing postpone-replay test); the binding lives inlance-graph-callcenter. - §3 signatures — RECONCILED (PR #11). §3 now carries the canonical
on_event(state, event, ctx) -> Transition,is_commit(state) -> bool,timeout(state)+on_timeout(state, ctx),Transition::{Goto, Stay, Postpone, Stop}, andCommitHook::on_commit(from, to, ctx) -> Result<(), ActorProcessingErr>(sync + fallible — honours I-2) fromractor_actorsfeat/state-machine-actor@38a71a4. The OGAR-side surface (§1, §5, §6) is unchanged by the reconciliation. - Sprint-7 timing — STILL APPLIES. Pin this contract into the
lance-graph-callcenterdesign before any hand-loop dispatch is written.