|
| 1 | +# ActionDef value-dispatch — body lowering + a value interpreter |
| 2 | + |
| 3 | +> **Status:** PROPOSAL (2026-07-07). Producer/executor capability gap; no |
| 4 | +> consumer-specific detail. Sibling of `OGAR-AS-IR.md` (the compiler framing) |
| 5 | +> and `OGAR-TRANSPILE-SUBSTRATE.md` (the bidirectional per-class transpiler). |
| 6 | +
|
| 7 | +## Problem |
| 8 | + |
| 9 | +`compile_graph_*` (the `ruby` / `python` / `sqlalchemy` / `csharp` frontends) |
| 10 | +emit `CompiledClass { class, facet, actions }`. Each `ActionDef` carries |
| 11 | +`identity` / `predicate` / `object_class` plus the effect sets `writes` / |
| 12 | +`reads` / `calls` / `raises` — but **not what the method computes**. |
| 13 | + |
| 14 | +Measured over a real compiled corpus (2748 `ActionDef`s), every emitted action has: |
| 15 | + |
| 16 | +- `kausal = None`, `body_source = None`, `on_enter = None`, `decorators = []` |
| 17 | +- `default_modal` / `default_temporal` / `default_subject` = the constants |
| 18 | + `Sync` / `Immediate` / `System` (no per-method information) |
| 19 | + |
| 20 | +Only `identity` / `predicate` / `object_class` + the name-level effect sets are |
| 21 | +populated. `crates/ogar-vocab/src/lib.rs` documents this directly: |
| 22 | + |
| 23 | +- `writes` — *"the value written is not captured by today's frontend, so this is |
| 24 | + name-level only and does NOT auto-build an `on_enter` `EnterEffect`"* |
| 25 | + (lib.rs:402-403). |
| 26 | +- `calls` — *"Effect annotation for call-graph analysis; no body is captured."* |
| 27 | + (lib.rs:406). |
| 28 | + |
| 29 | +Crucially, the **typed carriers already exist** — `on_enter: Option<EnterEffect>` |
| 30 | +(lib.rs:423), `EnterEffect` (lib.rs:447), and `ActionDef::kausal` / |
| 31 | +`ActionDef::body_source` (consumed by `ogar-emitter` at lib.rs:667-674). The gap |
| 32 | +is not the schema — it is that **the frontend never populates them**. |
| 33 | + |
| 34 | +## Consequence |
| 35 | + |
| 36 | +An `ActionDef` says *which fields a method touches*, not *what it computes*. A |
| 37 | +consumer can therefore verify **structural effect-parity** — does the running |
| 38 | +code's observed write-set match the declared `writes`? — but it **cannot execute |
| 39 | +an `ActionDef` to reproduce a recoverable method's return value**. There is no |
| 40 | +expression, formula, or lowered body in the artifact. |
| 41 | + |
| 42 | +The two executors that exist today are not value interpreters: |
| 43 | + |
| 44 | +- `ogar-action-handler::NativeCommandExecutor` / `CapabilityExecutor::execute` — |
| 45 | + runs a capability by shelling a bound `command` parameter to an external |
| 46 | + handler (SSH/REST/shell). The logic lives in the external handler plus the |
| 47 | + invocation params, not in the `ActionDef`. |
| 48 | +- `lance-graph-ogar::OgarActionProvider` + `contract::action::ActionInvocation::commit` |
| 49 | + — the authorization/gate path (RBAC → StateGuard → MUL `GateDecision`). A |
| 50 | + capability dispatcher / gate, not a value interpreter. |
| 51 | + |
| 52 | +So a consumer that wants a recoverable `Compute` / `Normalize` method to run |
| 53 | +*through* its `ActionDef` (instead of a hand-written reimplementation) is blocked: |
| 54 | +reproducing the value would force the consumer to re-derive the formula itself — |
| 55 | +exactly the "logic in the consumer" that the consumer-side boundary forbids |
| 56 | +(`OGAR-CONSUMER-BEST-PRACTICES.md`: the classid is address; the magic resolves at |
| 57 | +the target — behaviour is a property of the Core node, never re-implemented |
| 58 | +downstream). |
| 59 | + |
| 60 | +## Proposal |
| 61 | + |
| 62 | +Two coordinated pieces, so recoverable methods can dispatch value-for-value. |
| 63 | + |
| 64 | +### 1. Frontend lowering — populate the carriers that already exist |
| 65 | + |
| 66 | +During `compile_graph_*`, capture method bodies into an executable form and |
| 67 | +populate the **existing** typed fields rather than name-level effect sets only: |
| 68 | + |
| 69 | +- `ActionDef::on_enter: EnterEffect` — the value-producing transition for a |
| 70 | + `Compute` / `Normalize` recipe (the recipe-codebook's recoverable band). |
| 71 | +- `ActionDef::kausal: KausalSpec` — the guard/lifecycle/dependency structure for |
| 72 | + `Guard` / `Cascade`. |
| 73 | + |
| 74 | +This is the frontend half of the recipe-codebook's promise: a recoverable body is |
| 75 | +supposed to lower to a *declarative recipe*, but today the recipe classification |
| 76 | +lands while the recipe *content* does not. The `fuzzy-recipe-codebook` method |
| 77 | +(ruff `.claude/knowledge/`) already correlates a body to a `(verb, criteria)` |
| 78 | +recipe; the missing step is emitting that recipe's computable form into |
| 79 | +`EnterEffect` / `KausalSpec`. |
| 80 | + |
| 81 | +### 2. A value-reproducing interpreter |
| 82 | + |
| 83 | +An interpreter that takes an `ActionDef` (+ typed inputs) and yields its effect |
| 84 | +values. Per the "thinking lives in lance-graph" boundary, this belongs on the |
| 85 | +lance-graph side, consistent with `lance-graph-ogar` already owning the |
| 86 | +authorization/commit path. Consumers consume it through a thin trait seam: |
| 87 | + |
| 88 | +```rust |
| 89 | +trait ActionDefExecutor { |
| 90 | + fn apply(&self, def: &ActionDef, inputs: &EffectInputs) |
| 91 | + -> Result<EffectValues, Unsupported>; |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +A consumer wires this seam with a default `Unsupported` implementation today and |
| 96 | +flips it to the real impl the moment the interpreter lands — never implementing |
| 97 | +the formula itself. |
| 98 | + |
| 99 | +## Scope / non-goal |
| 100 | + |
| 101 | +This is the gap between *"the offline transpile emits effect descriptors"* |
| 102 | +(works today) and *"a runtime dispatches recoverable methods value-for-value"* |
| 103 | +(blocked). It is purely a producer/executor capability gap with no |
| 104 | +consumer-specific detail. |
| 105 | + |
| 106 | +**Staging.** Until both pieces land, a consumer's honest ceiling for any |
| 107 | +recoverable route is **structural-effect-parity green, value-dispatch pending**: |
| 108 | +it can diff a running handler's observed write-set against the declared `writes` |
| 109 | +(a mechanical, non-thinking check) and shadow it as observability, but it cannot |
| 110 | +promote a route to `ActionDef`-primary. The `ActionDefExecutor`-defaults-to- |
| 111 | +`Unsupported` seam is exactly the forward-compatible shape for that staging: no |
| 112 | +behaviour changes downstream until the interpreter exists, and no formula is ever |
| 113 | +re-implemented in a consumer. |
| 114 | + |
| 115 | +## Falsifier |
| 116 | + |
| 117 | +The proposal is validated when, for one recoverable `Compute` method, an |
| 118 | +`EnterEffect`-lowered `ActionDef` fed through the interpreter reproduces the |
| 119 | +hand-written oracle's return value **bit-for-bit** on a golden fixture set — the |
| 120 | +same shape as the existing byte-parity discipline elsewhere in the stack. |
0 commit comments