Skip to content

docs: record the intent architecture (venue adapters, egress guard) - #132

Draft
mfw78 wants to merge 29 commits into
developfrom
docs/intent-architecture
Draft

docs: record the intent architecture (venue adapters, egress guard)#132
mfw78 wants to merge 29 commits into
developfrom
docs/intent-architecture

Conversation

@mfw78

@mfw78 mfw78 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Adds docs/09-intent-architecture.md plus ADR-0010 (venues as dynamically installed adapter components; CoW becomes the first adapter) and ADR-0011 (one egress guard pipeline for intent submissions, EIP-712 signing, and transaction signing, with analysers as installable components and simulate as a pluggable host primitive).

A second commit folds in the red-team amendments: authorisation-source classification in the guard (pre-authorised EIP-1271 flows are audit-plus-advisory, never prompted or blocked, with the limits-are-observability caveat stated plainly), chain routing carried in venue bodies with settlement exposed in the derived header, appData travelling as the on-chain hash with no venue read path (see #133 for the corresponding 0.2 code cleanup), a read-only query verb deferred to open questions, and a full SDK-surfaces section: the two authoring personas and crate map, the one-crate-per-venue rule, borsh as the body codec with venue schemas as language-neutral specifications (golden vectors, classification tables as data), the no-composition rule (module to host to adapter, for policy interposition, fuel attribution, and fault isolation), metering attribution including router-level per-caller quotas, and the non-Rust module path via componentize-py.

Why

Closes #131. Records the July 2026 architecture review outcome: the CoW integration generalizes into a venue-adapter layer over a shared nexum:value value-flow vocabulary, venues become user-installable without host releases, and the wallet embedding's signing analysis unifies with intent policy in a single guard pipeline. ADR-0010 supersedes the compiled-in backend framing of ADR-0005; the strategy-vs-protocol boundary of ADR-0006 is preserved.

Testing

Documentation only; no code paths changed. Checked for style conformance (no em-dashes, no hard-wrapped paragraphs, no issue references inside the docs).

AI Assistance: Claude Code used for drafting the design doc and ADRs from the architecture review discussion.

@mfw78

mfw78 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Required action to land as M1 train step 0 (design gate for #137). This PR is the intent-architecture design authority (docs/09-intent-architecture.md, docs/adr/0010-venues-as-adapter-components.md, egress-guard ADR).

  1. Rebase onto train HEAD 502175c (currently based on develop, below the M0/mock/fault stack).
  2. Renumber the egress-guard ADR 0011 -> 0012 — the train already added docs/adr/0011-per-interface-typed-errors.md, so the current number collides.
  3. Merge first, ahead of the WIT/router work — intent: WIT packages, venue-adapter world, pool router #137's venue-adapter world (runtime: venue-adapter world, manifest module-kind, and [[adapters]] engine config with scoped transport grants #228) and pool-router (host: implement the nexum:intent pool-router (derive-header -> guard-seam -> submit, serialization, quotas) #229) are gated on this design being ratified.

Closes #131 on merge.

Comment thread docs/09-intent-architecture.md Outdated
}

variant intent-status { pending, open, settled(option<list<u8>>), failed(fail-reason), expired, cancelled }
type receipt = list<u8>; // venue-scoped stable id (CoW: the 56-byte order UID)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A list<u8> receipt is hashable, but the WIT sketch does not specify an encoding, and different venues may produce receipts that differ in length, encoding, and uniqueness scope. For the CoW case (56-byte order UID), this is fine, but the "venue-scoped stable id" needs to be more precisely specified -- stable within a session? Across restarts? Globally? The SDK conformance kit (golden vectors, classification tables) is where this belongs, but the doc should name the stability requirements the host relies on.

offchain(offchain-desc), // RWA: deed, chattel, ...
}

record asset-amount { asset: asset, amount: list<u8> } // big-endian unsigned

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no length constraint and no maximum on "amount: list". For the purposes of WIT, this is correct (WIT has no fixed-size integer arrays), but downstream code parsing this will need to bound-check and handle values that overflow any native integer type.

Suggestion: a one-sentence note in the vocabulary section: "policy implementations must treat any amount exceeding the precision of the native type as exceeding any configured limit."


### No direct module-to-adapter linking

Component-model composition (linking the module's `pool` import straight to the adapter's exports) looks like an optimisation and is a correctness bug three ways: the host must interpose policy between `derive-header` and `submit`; wasmtime fuel is per-store, so host-in-the-middle is what keeps module work on the module's meter and adapter work on the adapter's; and an adapter trap must not poison the calling module (separate stores, separate restart policies). Every hop is module to host to adapter, and the SDK's job is to make that feel like a typed function call.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule does not address the case where a strategy module needs to use multiple venues in a single logical operation (e.g., a cross-venue arbitrage that must submit to venue A and venue B atomically, or fail gracefully). With the current pool interface, the module submits twice and handles partial failure. That is probably the right answer, but the atomicity story is silently delegated to the strategy author.

interface types {
variant settlement {
evm-chain(u64),
offchain(string), // jurisdiction / venue-defined domain

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This offchain(string) is likely to become a registry of venue-specific domain identifiers, and the guard will likely accumulate pattern-matching on it. A structured type (even just an enum with an other(string) arm) would be easier to extend safely.

- Interactive signing acquires a latency budget shared by simulation and analysers. Deadlines are enforced with the existing metering machinery (fuel, epoch interruption); partial verdicts render as "analysis incomplete" per profile.
- The engine grows three surfaces: the `simulate` primitive with a backend seam, the fact-bundle assembly, and the analyser host-call path with deadline scheduling. The analyser world finally gives the experimental `query-module` lineage a shipping consumer.
- Verdict aggregation across multiple analysers starts as max-severity-wins; contradictions and reputation are open questions recorded in the design doc.
- Every embedder profile (server, wallet, super-app) must declare its fail-open/fail-closed matrix; the embedding API exposes verdicts and consent hooks so wallets render them natively.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the guard ships, that API needs a concrete WIT sketch -- even a rough one -- because it is the surface wallets depend on, and "explicit configuration" without an API shape is an extensibility promise that cannot be verified.

native-token(settlement),
erc20(tuple<u64, list<u8>>), // (chain, address)
erc721(tuple<u64, list<u8>, list<u8>>), // (chain, address, id)
erc1155(tuple<u64, list<u8>, list<u8>>),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
erc1155(tuple<u64, list<u8>, list<u8>>),
erc1155(tuple<u64, list<u8>, list<u8>>), // (chain, address, id)

Comment thread docs/09-intent-architecture.md Outdated
// narrow, manifest-declared, per-adapter-scoped imports:
import http; // e.g. the CoW adapter: api.cow.fi only
import messaging; // e.g. a Waku venue: its content topics only
import chain; // read-only lookups where needed

@lgahdl lgahdl Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: for more clarity

Suggested change
import chain; // read-only lookups where needed
import chain; // on-chain state reads; no signing, no send

interface types {
variant settlement {
evm-chain(u64),
offchain(string), // jurisdiction / venue-defined domain

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separator makes confusing when each one applies and when, removing jurisdiction.
Note: The prose section already says venues define this string, so the "why a string" question is answered elsewhere.

Suggested change
offchain(string), // jurisdiction / venue-defined domain
offchain(string), // venue-defined domain

Comment thread docs/09-intent-architecture.md Outdated
wants: list<asset-amount>,
valid-until: option<u64>,
settlement: settlement,
authorisation: auth-scheme, // eip712 | eip1271 | presign | offchain-sig | unsigned

@lgahdl lgahdl Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accuracy risk: auth-scheme is not defined anywhere in the snippet or in the PR. If auth-scheme gains or loses arms, the comment will silently drift.

Suggested change
authorisation: auth-scheme, // eip712 | eip1271 | presign | offchain-sig | unsigned
authorisation: auth-scheme, // see auth-scheme variant;

interface pool {
submit: func(venue: string, body: list<u8>) -> result<receipt, venue-error>;
status: func(venue: string, receipt: receipt) -> result<intent-status, venue-error>;
cancel: func(venue: string, receipt: receipt) -> result<_, venue-error>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

venue-error is not defined

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a few observations

* feat(sdk): add the keeper conditional-source seam, retry ledger, and run

Introduce the world-neutral half of the poll loop in
nexum_sdk::keeper: a ConditionalSource trait (one watch in, one
outcome out, generic over the host, no venue-transport
pre-abstraction), a Tick carrying the dispatch instant, and the
RetryAction (try-next-block / backoff / drop) whose effects the
Retrier runs through the gate and watch-set stores. RetryAction
moves out of shepherd-sdk; the CoW crate re-exports it so no module
rewires.

shepherd-sdk keeps classify_api_error as the CoW errorType table but
returns the keeper action, and encodes two fixes in the one
classification path: classify_poll_error maps an unrecognised revert
selector with a structured payload to DontTryAgain instead of
re-polling every block forever, and DuplicatedOrder (both spellings)
classifies as already-submitted - the run records the
submitted: receipt and keeps the watch rather than dropping every
future tranche. classify_submit_error widens the table to the whole
CowApiError surface and gives Backoff its producer: a rate-limit
fault with server guidance gates the watch on the epoch clock.

cow::run composes the loop: watch-set scan -> gate check ->
source poll -> PollOutcome dispatch, driving submission through the
existing CowApiHost seam with the journal as the idempotency guard
and the retry ledger as the failure dispatch. Acceptance tests run
against the composed shepherd-sdk-test MockHost as integration
tests; no module is rewired yet.

* docs(sdk): disambiguate the run intra-doc link

* fix(cow): build the classify_poll_error test RpcError data as Bytes

RpcError.data is now Bytes; the test helper takes raw Vec<u8> and wraps
it (Vec -> Bytes is O(1)).

* fix(cow): keep the sweep alive when a post-submit journal write faults

journal.record after a successful submit_order was propagating a store
fault out of submit_ready, aborting the whole watch sweep with the
receipt unwritten - the next tick then re-polled and re-posted the same
order. Log the failure and carry on instead; the already-submitted arm
keeps the re-post idempotent.

Both post-submit writes are covered: the Ok arm and the
already-submitted (DuplicatedOrder) arm.
mfw78 added 18 commits July 15, 2026 00:07
Doc 05 was stamped future-direction and described the superseded
macro/TypedState/Signer/HostTransport vision. Replace it with the
shipped module-author persona (nexum-sdk + shepherd-sdk + the landed
#[nexum::module] macro) and the planned venue-adapter persona
(nexum-venue-sdk / per-venue-crate), clearly labelled as design intent
tracked by a separate epic. Drop the deferred-features table and
cross-link ADR-0009 and sdk.md.
* fix(macros): reject typo'd handlers and malformed impls at expansion

A method named on_blocks (or any other on_-prefixed typo) previously
compiled as an ordinary helper while its event silently no-opped; the
only signal was a dead_code warning that vanishes on pub items.
Reserve the on_ prefix for the recognised handler set and error on
anything else.

Also reject trait impls, generic impls, and impls with no recognised
handlers - all previously slipped past the self-type guard and failed
later with incidental, unhygienic errors (or compiled into a do-nothing
module). Document the wit-bindgen direct-dependency and prelude-
shadowing invariants inherited from the generated glue.

* docs(migration): rewrite SDK section 7 around the shipped surface

Section 7 still described the superseded Signer/Messaging/RemoteStore/
provider() vision - APIs that never shipped - and a #[shepherd::module]
macro that does not exist, while doc 05 endorsed it as the authoritative
0.1 -> 0.2 SDK table. Replace it with the real story: 0.1 had no SDK
crate, 0.2 ships nexum-sdk/shepherd-sdk host traits, typed errors, and
the single #[nexum_sdk::module] attribute. Point doc 05's cross-link
text at what the section now says, and fix doc 05 details flagged by
the sweep: the CowApiHost parenthetical (submit_order, cow_api_request),
the crate trees (proptests.rs in both SDK crates), the testing-story
claim (nexum-runtime's feature-gated test_utils::TestRuntime is an
in-tree component-level harness), and the first #[nexum::module]
mention now names the real nexum_sdk path.

* docs(sdk): document the #[nexum_sdk::module] attribute in sdk.md

sdk.md is doc 05's designated day-to-day reference but still presented
the pre-macro bind_host_via_wit_bindgen! flow as the authoring pattern.
Add an authoring section covering the attribute and its handler set,
mention it in the intro, and include nexum-macros in the rustdoc
command.
feat(wit): author the nexum:value-flow types package

Add the egress-neutral value-flow vocabulary at 0.1.0: the settlement
domain (evm-chain or offchain), the asset variant (native-token, erc20,
erc721, erc1155, service, offchain), the service and offchain
descriptors, and the big-endian asset-amount. The package is
dependency-free so it can outlive any interface built on it.

Every identifier is vetted against WIT keywords, in-flight component-model
proposals, and the nine binding-target reserved-word lists, preferring a
two-word kebab id wherever a single word is a keyword anywhere:
native-token over native (a Java modifier), offchain over external (a
Dart keyword), value-flow over value (value-imports is circling that
word). A wasmtime bindgen smoke, compiled under test in the host crate
through a throwaway world, names every generated type and field by its
plain Rust spelling so a keyword collision surfaces as an escaped
identifier and fails the build rather than a downstream binding.
feat(wit): author the nexum:intent package with the pool interface

Add the venue-neutral intent ontology at 0.1.0 over the value-flow
vocabulary: the intent-header (gives/wants/valid-until/settlement/
authorisation), the auth-scheme variant (eip712, eip1271, presign,
offchain-sig, unsigned), the intent-status lifecycle with a structured
fail-reason, the opaque receipt alias, and a self-contained venue-error
(deliberately not embedding the host fault type, so the package's freeze
cadence is not pinned to nexum:host versioning).

The pool interface routes submit/status/cancel by a venue string with
the body as opaque bytes. submit returns a submit-outcome variant from
day one: accepted(receipt), or requires-signing(unsigned-tx) for venues
whose settlement is a host-signed on-chain call. The unsigned-tx record
only describes the call (chain, to, value, input); the host routes it
through the guard's host-signed class and fills gas and fees, so
adapters still structurally cannot move value.

Identifier hygiene follows the value-flow rule (internal-error rather
than internal, a Swift declaration keyword). A wasmtime bindgen smoke,
compiled under test in the host crate through a throwaway world that
imports the pool interface, names every generated type, case, and field
by its plain Rust spelling and pins the three pool function signatures
with a dummy host impl.
* feat(runtime): introduce the venue-adapter component kind

Add the second component kind alongside the event-module. A venue
adapter speaks one venue's protocol over scoped transport only and
exports the intent adapter face.

- WIT: a nexum:intent/adapter interface (derive-header, submit, status,
  cancel) and a nexum:adapter/venue-adapter world that imports scoped
  chain and messaging, exports init plus the adapter face, and withholds
  the core-only primitives.
- Bindings: a second bindgen path binding VenueAdapter beside
  EventModule, reusing the shared nexum:host interfaces via with.
- Manifest: a module-kind discriminator defaulting to event-module, plus
  an adapter capability registry that recognises only the scoped
  transport.
- Engine config: an [[adapters]] table carrying path, manifest, a
  per-adapter HTTP allowlist, and messaging-topic scopes.
- Supervisor: adapters instantiate into supervised stores against a
  dedicated scoped linker, reusing the store, fuel, and restart
  machinery; the kind discriminator gates the load path. No routing yet.
- Messaging: per-store content-topic scope enforced ahead of the
  deferred backend.

* docs(runtime): keep the venue-adapter world token on one line

The bindgen module rustdoc split the `nexum:adapter/venue-adapter` code
span across a line break after the slash, so rustdoc rendered it with a
stray space. Rewrap so the token stays intact.
)

Implement the strategy-facing pool import as a host binding linked into
every module linker, dispatching to a shared router that owns the
installed adapters.

- Bindings: a pool-host bindgen world importing nexum:intent/pool,
  reusing the intent and value-flow types from the venue-adapter
  bindings via `with`, so the outcome and error the router hands back to
  a module are the very types an adapter's submit produced.
- Router: resolve a venue id to its adapter, serialise invocation per
  adapter behind an async mutex (the wasmtime Store is not Sync),
  sequence derive-header, a no-op guard interposition seam, then submit.
  Status and cancel pass through without the header, guard, or quota.
- Quota: a per-caller sliding-window submission budget, with adapter
  decode failures charged to the caller so a module feeding garbage
  bodies exhausts its own budget rather than the adapter's fuel.
- Supervisor: adapters instantiate first and install into the router;
  modules then boot carrying the shared handle, rebuilt identically on
  restart. An init-failed adapter is loaded but not routable.
- Manifest: register nexum:intent/pool as a declarable module capability.
- Config: a [limits.quota] section resolving the per-caller budget.
* feat(sdk): ship nexum-venue-sdk over the venue-adapter world

Introduce the venue-author persona crate: the VenueAdapter trait and
export macro over an in-crate bindgen of the adapter world, the borsh
IntentBody derive whose outer version enum fails unknown tags typedly,
the typed intent client core over the byte-level pool seam, and typed
wrappers over the scoped chain, messaging, and wasi:http imports. The
derive expansion lands in nexum-macros and re-exports from the SDK;
borsh enters the workspace dependency table.

* fix(venue-sdk): wrap the wit chain-error data into Bytes

The SDK RpcError.data is Bytes; the wit-bindgen chain-error carries
Vec<u8>, so wrap it (Vec -> Bytes is O(1)) when reprojecting.
* feat(sdk): capability-select the wit-bindgen host adapter

Split bind_host_via_wit_bindgen! into a types-only base plus one block
per capability, selected through a caps list; the zero-argument form
keeps emitting the full chain, local_store, logging set for modules
compiled against a blanket world. Narrow read_latest_answer to
ChainHost + LoggingHost, the two capabilities it exercises, so modules
whose worlds omit local-store can still call it.

* feat(sdk): emit a per-module world from declared capabilities

Teach #[nexum_sdk::module] to read the crate's module.toml and
synthesize an inline WIT world whose imports are exactly the
[capabilities].required and optional declarations, replacing the
blanket shepherd:cow/shepherd world every module compiled against. The
built component's imports now equal its declarations by construction,
so the runtime's capability check no longer leans on the toolchain
eliding unused imports; an undeclared capability has no bindings at
all, and an unknown capability name is a compile-time error. The
emitted host adapter is capability-selected to match, and a rebuild
anchor on module.toml recompiles the module when the manifest changes.

Narrow price-alert's strategy bound to ChainHost + LoggingHost: its
world imports only its declared chain and logging capabilities, so the
full Host supertrait (which adds local-store) is unimplementable there
by design.

* test(runtime): pin the example component's imports to its declarations

The per-module world acceptance: compile the built example component
and assert its capability-bearing imports resolve to exactly the
manifest's declared set, with no extension interface leaking in. Skips
gracefully when the wasm fixture is not built, like the other e2e
tests.

* docs: record the retirement of import-elision for macro-built modules

Macro-built components import what they declare by construction, so
capability enforcement is a backstop for them; the elision dependency
ADR-0009 flagged now applies only to hand-rolled modules still
compiled against the supertype world.
* feat(runtime): add the intent-status case to the host event variant

The host event variant gains intent-status(intent-status-update): a
venue id, the receipt, and the nexum:intent status vocabulary, so a
subscriber sees where an intent is in its life without knowing how the
host learnt it. nexum:host now depends on nexum:intent, so every
bindgen over the host package carries the intent and value-flow
packages on its resolve path, in dependency order; the runtime
generates the intent types once and the adapter and pool bindgens
remap onto them with 'with'. The module macro recognizes an
on_intent_status handler and the example module ships one.

* feat(runtime): poll venue statuses and fan intent-status events to subscribers

The pool router puts every accepted receipt under watch and, on a
configurable cadence ([limits.status_poll] interval_ms), polls each
adapter's status export, reporting only transitions: the first poll
reports the current status, repeats are deduplicated, a terminal
status prunes the watch, and a receipt the venue disowns is dropped.
A dedicated event-loop stream fans each transition to modules
declaring [[subscription]] kind = "intent-status", optionally
filtered by venue; polling only runs when there is at least one
subscriber and one installed adapter.
Extend nexum-macros with the venue-adapter counterpart to #[module]:
read the crate's module.toml, synthesize a per-component venue-adapter
world exporting init plus nexum:intent/adapter and importing exactly the
declared scoped transport, then emit the per-cdylib wit-bindgen call, the
Guest export glue wiring the adapter's associated functions, and export!.

An adapter built this way imports what its manifest declares and nothing
else, retiring the toolchain-elision dependency on the venue side rather
than as follow-on work. A capability outside the venue-permitted set
(chain, messaging, http) is rejected at expansion, so an adapter
structurally cannot reach host key material or persistent state.

Ship echo-venue as the reference adapter and pin its built component's
capability-bearing imports to its single declared capability.
Venue adapters need a way to prove their wire behaviour against the
venue's published contract, in a form a non-Rust author can consume:
codec vectors and header goldens ship as JSON files (bytes as
lowercase hex) rather than Rust-only test code.

CodecVectors publishes IntentBody wire bytes and holds a codec to
them: round-trip vectors must decode and re-encode byte-exactly, and
the failure vectors pin the typed empty, unknown-version, and
malformed error contract. HeaderGoldens pairs published bodies with
the header a conforming derive-header projects, spelled in serde
mirror types whose case names match the WIT. MockTransport composes
chain, messaging, and outbound HTTP mocks behind the SDK seams,
including the messaging_topics scoping refusal.

A reference venue (schema, derivation, and checked-in fixture files
pinned by regeneration tests) documents both formats and proves the
borsh primitive encodings for authors porting the codec.
feat(examples): pair echo-venue with an echo-client and prove the round-trip

Add echo-client, the strategy half of the echo pair: it submits an
opaque body through nexum:intent/pool on every block and logs the
intent-status transitions the router fans back. Wire both real
components through an integration test that proves the intent core end
to end, module -> host router -> echo adapter submit and status back,
which is the first-train acceptance path.

Settle the echo venue instantly (status now reports settled) so the
round-trip reaches a terminal transition, and hold its header
derivation to the nexum-venue-test kit so echo-venue doubles as the
conformance target alongside its tutorial role.

Register echo-client in the workspace, the build recipes, and CI.
* docs(wit): pin canonical amount encoding and reserve native-token(offchain)

Mandate minimal-length big-endian for asset-amount and require decoders
to compare by value, so the same amount has one canonical wire form
across the binding targets. Document native-token(offchain) as
representable-but-invalid: an off-chain settlement domain has no gas
token, so consumers must reject the pairing rather than interpret it.

* fix(macros): correct venue manifest error attribution and accumulate venue packages

The shared missing-[capabilities] error named #[nexum_sdk::module] even
on the venue path; make it macro-neutral so a venue crate is not told to
use the wrong attribute. Mirror synthesize's package-accumulation loop in
synthesize_venue so a future venue capability carrying an extra WIT
package reaches the resolve path (all venue capabilities are packageless
today, so the base set is unchanged).

* docs: normalise doc comments to British -ise spelling

Align the authorise/authorisation prose in the intent WIT and the
conformance kit with the British spelling used elsewhere and with the
authorisation identifier itself. Doc comments only; no identifier, wire
format, or API name changes.

* docs(runtime): document the pool submit charge asymmetry

State the deliberate rule that a forwarded submission is charged once the
guard admits it regardless of the venue outcome, while a derive-stage
non-decode venue error is left uncharged so the caller may retry.
* feat(cow-venue): scaffold default body slice with borsh IntentBody codec

Stage the CoW venue as a crate of feature slices. The default `body`
slice carries the venue-neutral order and composable intent body types
and the borsh IntentBody codec over the outer per-venue version enum, so
a future adapter component or a strategy module can link the bodies and
codec without the host-side CoW machinery.

The slice links only the venue SDK (for the IntentBody derive) and borsh;
`--no-default-features` drops it to an empty crate. shepherd-sdk gains a
re-export shim under `cow` so the module ports can move off the legacy
path without a breaking rename.

* style(cow-venue): use -ize spelling and drop test reach into borsh __private
* feat(cow-venue): drive cow retry classification from a shipped data table

Ship the CoW orderbook errorType retry policy as data in
crates/cow-venue/data/classification.toml, readable and editable by
non-Rust authors, and add a `client` feature slice that embeds it,
exposes a typed CowClient bound to the CoW venue, and a table-driven
classification API returning the keeper RetryAction.

Replace the hand-coded classify_api_error/is_retriable in the cow error
surface with the table lookup, preserving the already-submitted and
drop-on-unrecognized behaviour. A throttle errorType now maps to
Backoff, so every RetryAction arm is reachable from the table alone,
guarded by a data-vs-code parity test and an untyped-parse test that
proves any TOML reader consumes the same file.

* perf(cow-venue): generate the classification table at build time

shepherd-sdk unconditionally enables the cow-venue client slice, and the
wasm modules (twap-monitor, ethflow-watcher) reach classify_api_error
through shepherd_sdk::cow::run, so the runtime LazyLock TOML
parse linked the full toml/serde/winnow stack into every guest: the
twap-monitor wasm grew ~237 KiB (350 KiB -> 586 KiB).

Move the parse to build.rs. It reads data/classification.toml, validates
the table invariants, and emits a static lookup table; the runtime slice
carries only generated rows and no TOML parser. serde/toml/thiserror
become build- and dev-only. The parse and invariants live in a shared
classification_data module that build.rs and the parity test both link,
so the data-vs-code parity test now re-parses the shipped file and
checks the generated table against it. The guest returns to 351 KiB.
* fix(cow): use canonical InvalidEip1271Signature errorType spelling

The permanent_kinds_yield_drop test asserted on "InvalidErc1271Signature",
which is not a real CoW orderbook errorType (the OpenAPI OrderPostError enum
and cowprotocol's OrderbookApiErrorType both spell it InvalidEip1271Signature,
matching data/classification.toml). The phantom spelling passed only via the
unlisted-default Drop, so it never exercised the real table row. Use the
canonical spelling so the test covers the actual entry.

* docs(cow-venue): ratify deliberate divergence from cowprotocol retry_hint

shepherd-sdk already depends on cowprotocol, whose ApiError::retry_hint()
classifies the same orderbook errorTypes and disagrees with this table on
several (e.g. it retries/backs off InvalidEip1271Signature, InsufficientBalance,
InsufficientAllowance and InvalidAppData where this table drops, and backs off
TooManyLimitOrders for an hour rather than 30s). The table is deliberately not
delegated to it: it is shepherd's own, more conservative policy, kept as data
of record so a non-Rust author owns it and the guest client slice stays free
of the upstream error module. Record that this divergence is intentional so a
future maintainer revisits the policy here rather than swapping the source of
truth. (Also fixes the -ise spelling in the same comment.)

* style(cow-venue): adopt British -ise spelling to match house convention

The crate used American -ize (normalizes/normalization, unrecognized) while
the adjacent domain code (shepherd-sdk cow/order.rs normalised, composable.rs
unrecognised, run.rs) and the repo at large use British -ise. Align
the prose so the sibling modules read consistently.

* fix(cow-venue): allow GenAction variants unused by shipped data

GenAction's variants are constructed only by the build-generated table, so a
classification.toml that carries no row of a given action leaves that variant
unconstructed and trips the -D warnings build. Since which actions appear is a
property of the data, not the code, mark the enum allow(dead_code) so a
data-only edit cannot break the workspace lint gate.
Tip-level residual after the chassis to keeper rename was folded down through the train. Carries the doc-comment prose the fold could not move mechanically (the private-keeper caveat, keeper-run wording) and the current-state design docs brought in line with the Verdict poll seam (PollOutcome to Verdict, decode_revert to LegacyRevertAdapter).
@mfw78 mfw78 added the base: develop Open PR targets develop — merge FIRST, before the dev/m1 integration PRs. label Jul 15, 2026
mfw78 and others added 6 commits July 15, 2026 07:18
No transaction semantics and no concurrent access (deferred to the
MockRuntime refactor, #94). Salvaged from #168 before closing it as
superseded by the shipped namespaced MockLocalStore.
The M0 train adds ADR-0011 (per-interface typed errors), which collides
with the egress-guard record this branch introduced at the same number.
Renumber it to 0012 and update the two cross-references so both records
land with distinct numbers.
…sions

Updates docs/09 + ADR-0010 + ADR-0012 against the shipped M1 tree and the
seven platform decisions (2026-07-14): marks the shipped surfaces as
shipped (venue-adapter kind, nexum:intent/pool, nexum-venue-sdk,
#[nexum::venue], echo-venue, cow-venue, ADR-0013 Verdict seam), reframes
the egress-guard pipeline as target design deferred to the egress-guard
epic (M1 ships an advisory AllowAllGuard no-op, R3), corrects the
world-composition category error (an adapter targets one world + wasi:http;
there is no "cow-protocol WASI"), and folds in transport-only adapters (Q1),
opaque host status (Q2/R6), install-time schema handshake (R7), EVM-only 0.1
(Q5), and #[venue] as the blessed path (Q6).
@mfw78
mfw78 force-pushed the docs/intent-architecture branch from 2a49526 to 23aa95d Compare July 15, 2026 07:35
@mfw78
mfw78 changed the base branch from develop to dev/m1 July 15, 2026 07:35
Base automatically changed from dev/m1 to develop July 27, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

base: develop Open PR targets develop — merge FIRST, before the dev/m1 integration PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: record the intent architecture (venue adapters, egress guard)

2 participants