ADR-031: The executor grammar as a facts-driven Plan walker — per-protocol hop facts + mechanics, derived enclosure

Status: accepted; implemented (A1–A3 of epic 62V6Q5 complete) and D6 realized (epic 6SU5LM).

D6 realization (post re-drive + enclosure-derivation migration), as corrected 2026-08-16 (arch review epic PZBGP7, task SCMSTK): all 35 build_*_walk family producers route to a single derive_plan post-refactor, and the hand-written per-family derive_2hop_* / derive_3hop_* / derive_all_v2 bodies are eliminated. But the earlier “enclosure is derived from Repay/OutDest tags, NOT prot-tuple match arms” claim overstated what shipped. The dispatch is facts-keyed — a (len, repay-sequence) partition — but the enclosure bodies are hand-authored per shape: ≈24 facts[i].prot == Prot::… if-branches dispatch between shapes’ internal variants (including the 3-hop block’s ~17-arm per-family enumeration). Only the single-V4-middle residual is a genuine Repay/OutDest-tag partition. V4 hops do carry Repay::NetZero (via v4_hop_facts_netzero) and that fact is load-bearing for the residual partition. The misreading came from the citation of a source scan that counted literal match arms while the same dispatch existed as if/else chains — a spelling check, not a structural one.

Structural correction (same epic): the fused ≈3,849-line derive_plan is now six shape modules under grammar_walker/shapes/ (all_v2_chain, two_hop_seed_v4, two_hop_v4_led, three_hop, two_hop_uniswap_only, tag_residual) behind a ≈29-line dispatcher on (len, repay-sequence) — one module per enclosure block, dispatcher keyed on exactly what the blocks already key on.

Consequence of the correction: enclosure ordering defects are caught, not unrepresentable — correctness rests on the revm contract matrix + golden suites (per ADR-029 D5, the designated source of truth) plus LedgerValidator’s always-fatal Reject (ADR-030). Whether the Repay/OutDest tag vocabulary can genuinely absorb the enumerated shapes (i.e. make D6’s “derived” claim literally true) is spike RQQIUK; until it reports, treat “tag-derived enclosure” as aspiration for the enumerated shapes, not fact.

Resolution (same epic, tasks T5+T6 — the aspiration is now realized): tags alone could not absorb the shapes (spike RQQIUK, negative on the merged-pair holdout), but the vocabulary could: the terminal_form axis merged the “blocked” v3v4 pair, and the topology-rule analysis found 21/23 arms derivable from a 3-rule debt-flow set, with the last 2 unlocked by the repay_mechanism + seed_delivery facts. (The design docs behind both — docs/plans/pzbgp7-terminal-form-axis-draft.md and docs/spikes/t6-topology-rules-analysis.md — were removed in the stale-docs cleanup 71ec78b2; their findings survive in this paragraph, the CONTEXT.md walker glossary, and the rule walkers in grammar_walker/shapes/three_hop.rs.) All 23 3-hop bodies are deleted; three rule walkers (rule_walk_v2v3, rule_walk_v4_led, rule_walk_v2v3_v4_mixed in grammar_walker/shapes/three_hop.rs) derive the enclosures from the facts, byte-identical (golden suites + revm matrix green; the shadow-walk pin tests caught three rule corrections pre-cutover). D6’s “enclosure is derived from facts, NOT chosen” claim is now literal.

Context

grammar_shape.rs is a 7,013-line monolith of 30 hand-written per-family Plan producers (build_*_plan), dispatched by the 30-row build_for/AxisSupport table. Each re-derives the ordering invariants by hand, so correctness is gated only post-hoc by the LedgerValidator; the D0 defect class (V4-take-before- credit, terminal-V2 1-wei overdraw) escaped the hand-authored producers and was caught only by the revm matrix. ADR-029 D4 chose per-family Plan authoring as the interim mechanism and deferred a generic walker (6ZIE5X a-branch); CM5V3X costed it. The corpus — 30 builders + AxisSupport + validator + the 25-family revm matrix — now exists to generalize over and regress against.

Decision

Adopt the hybrid deepening: the grammar becomes per-protocol hop facts (data) + per-protocol mechanics (code) + one generic walker that derives enclosure and emits a single Plan. The encoder (plan_to_bytes) and the validator gate (plan_to_ledger_ops + LedgerValidator) are reused unchanged — both are pure functions of the Plan, so the walker’s only output contract is “a Plan”. build_for/AxisSupport dissolve into hop facts (family axis-support becomes a fact, not 30 rows). Most per-protocol mechanics already exist as shared, byte-identical helpers (v4_scaffold_table, v4_bridge_steps, v4_terminal_capture_steps, funding_branch, enc_v*).

Landing was feature-flag parallel (A1/A2, --features walk) gated by byte- identity to the hand-written producers on every family, then a hard cutover (A3). The cutover is complete: the walker is now the sole producer — the 30 build_*_plan bodies, build_all_v2_chain, and the build_for/AxisSupport rows are deleted, and family_axis_support is facts-derived from the hop-protocol patterns rather than a 30-row table. Correctness is gated by the revm contract-matrix (execution against the on-chain cmd_executor, per ADR-029 D5 — not byte-parity against the suspect producers, which no longer exist), plus the golden-byte corpora and honesty invariant. A validator Reject remains always-fatal (ADR-030).

Considered options

  • Fully per-family Plan authoring (status quo, D4 interim): keeps ordering hand-reasoned per family — the adversarial surface this epic deletes.

  • Per-family declarative trace tables: data, but still ~30 rows, one per family — doesn’t kill the combinatorial fan-out D6 targets.

  • Walker without a mechanics seam (all data): blurs D4’s data-vs-code split and can’t express imperative Solidity callback wiring. Rejected.

Consequences

  • A new protocol is one hop-facts descriptor + one mechanics module (D6 additive proof), never a per-family body.

  • Enclosure ordering defects are caught, not unrepresentable: the validator gate + revm matrix are the enforcement (see the header’s record correction — the per-shape bodies are hand-authored code; the “derived” claim applies fully only to the residual tag partition).

  • grammar_shape.rs shrank from 7,013 lines to ~1,600 (the shared mechanics helpers + dispatch + derive seam + tests); the per-protocol facts table and walkers live in grammar_walker.rs.

  • Reject stays reachable (amounts from solver inputs + hand-authored facts can still err), so the validator and ADR-030’s fatal-Reject remain load-bearing.

Addendum

The mechanics unification + 2-hop walks (epic 6SWFBS, 2026-08-18)

At the D6 cutover the six per-shape modules existed, but two of them still hand-built per-family PlanStep bodies: the 2-hop seed→V4 shape (v2v4/v3v4 — 60 literal sites) and the 2-hop V4-led shape (v4v2/v4v3/v4v4 — 49 literal sites). Epic 6SWFBS removed the last hand-built Plan body:

  • T1 (gates before code): per-shape golden stream pins (families × base/batch/erc6909 amount sets) + a RED probe counting PlanStep:: literals in each walk region. Probes flip GREEN per file and are retained as honesty invariants (D6 precedent).

  • T2 — folded the v3_flash/v3_flash_to pair into the single mechanics::v3_flash: the recipient triple may be omitted and is then derived from facts.out_dest (Executor→SELF, PoolManager→PM); Some((idx, pool, repays)) makes it explicit. All flash-bearing sites in the three-hop, tag-residual, and uniswap-only modules route through the one primitive.

  • T3/T4 — the two 2-hop shapes are now one arm function each, composed purely of mechanics primitives (v4_swap, v4_unlock, v4_take_compact/v4_take_compact_at, v4_take_delta, v4_settle/v4_settle_delta/v4_settle_all, v4_sync, erc20_transfer, native_transfer, self_fund, weth_deposit/ weth_withdraw, v2_flash, v3_flash, v2_swap, v4_batch/v4_batch_entry) + the shared v4_terminal_capture_steps / v4_bridge_steps helpers. The v2↔v3 delta within each 2-hop shape collapsed to the lead protocol — plus, once, weth_deposit standing in for self_fund.

  • T5 — deduped the five facts_of_* producers onto the shared per-protocol builders. Exposed that the shared defaults are not the per-family defaults: the v3v4v3 terminal needs repay: Offstream (builder default is SelfRefund), and fee overflow must decline the path (the builder default zeros the fee, which would mis-encode; a latent TakeBeforeCredit validator reject, caught by the byte pin before commit).

Byte-transparency proof: zero golden-hash edits across all per-shape pin tables and glopcn_bytepin (every family × amount set × both entry points); the executor suite ends all-green (118 lib tests) with no RED probes remaining. Net across T2–T5: ~1,100 lines removed.

Corrections to the main text: (a) “the per-shape bodies are hand-authored code” — as of 6SWFBS, no shape module contains a single PlanStep literal; all step construction lives in mod mechanics plus the two shared grammar_shape helpers, and the D4 data/code split is complete for every family. (b) The D4 additive claim (“a new protocol is one hop-facts descriptor + one mechanics module”) now holds for the walked 2-hop arms too: walking six families cost ten small primitives and one arm function per shape.