//! `#[handler(...)]` and `#[plugin(...)]` attribute macros.
//!
//! Phase 5a (S25): handlers are `&self` methods on the plugin
//! struct (not free-standing functions). The handler macro emits a
//! thunk `fn(&dyn ModelPlugin, &GameEvent, &dyn GameContext)
//! -> HandlerResponse` that downcasts to the concrete plugin type
//! and invokes the method. `#[plugin]` additionally generates a
//! `ModelPlugin::dispatch_handler` impl whose `local_id` table is
//! `REGISTERED_HANDLERS` filtered by `(plugin_crate_name,
//! plugin_struct)` and sorted by `method_name`. The manifest's
//! `declared_handlers` is built with the same sort key, so the two
//! `local_id` spaces stay aligned.

use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, ToTokens};
use std::collections::BTreeSet;
use syn::{
    parse::Parse, parse2, punctuated::Punctuated, visit::Visit, Expr, ExprArray, ExprLit, ExprPath,
    ImplItem, ItemFn, ItemImpl, ItemStruct, Lit, Meta, Token, Type,
};

// ---------------------------------------------------------------------------
// WASM1-5-whitelist-minimize-deep — body-level call-graph mapping
// tables. The `#[handlers]` macro shallowly visits each method body
// in the impl block (via `MethodCallCollector` below) to collect
// the names of all method calls (e.g. `ctx.current_player()` →
// `"current_player"`). Each collected name is looked up in
// `CTX_METHOD_TO_IMPORTS` / `SUBTRAIT_METHOD_TO_IMPORTS` and the
// corresponding `__maat_*` host imports are added to the per-plugin
// allowlist emitted by `#[plugin]`'s `manifest_static()`.
//
// **Two-tier model.**
//
//   - **Tier 1 (vtable-included).** When any `#[handler]` or
//     `#[compose(kind = Wizard)]` exists, the plugin code uses
//     `&dyn GameContext` somewhere. The Rust wasm32 codegen emits
//     the full `WasmGameContext` vtable, and LLVM (even with
//     `lto = true`) does NOT DCE individual vtable entries — each
//     method's `__maat_ctx_*` import gets emitted into the wasm
//     module regardless of whether the plugin author calls it.
//
//     So when ANY vtable consumer exists, the deep allowlist
//     conservatively includes the full direct-GameContext surface
//     (`CTX_VTABLE_BASE` below). The body-level call-graph
//     analysis can't shrink below this without restructuring
//     `sdk-wasm-rt`'s `WasmGameContext` impl (filed as
//     `WASM1-5-whitelist-minimize-narrow-shim` follow-up).
//
//   - **Tier 2 (call-site-included).** Sub-trait methods reached
//     via `ctx.predicates()` / `ctx.vision()` / `ctx.spatial()`
//     are reference-counted differently: the wasm linker DOES
//     drop unused sub-trait imports because the ZST shim structs
//     (`WasmPredicateQuery` / `WasmVisionQuery` / `WasmSpatialQuery`)
//     don't use dyn-dispatch — the plugin calls a concrete method
//     on a stack-allocated shim. Body-level analysis correctly
//     narrows here: a handler that calls `ctx.spatial().distance(...)`
//     pulls in `__maat_ctx_spatial_distance` but NOT
//     `__maat_ctx_spatial_entities_at` / `__maat_ctx_spatial_entities_in_area`.
//
// **Conservative sub-trait getter policy.** When a plugin calls
// `ctx.spatial()` / `ctx.vision()` / `ctx.predicates()` but the
// visitor can't trace through the returned reference (e.g. stored
// in a local variable, passed to another method), it must
// conservatively include ALL imports of that sub-trait. The
// visitor ALSO catches the chained `.distance()` / `.can_see()`
// / etc. method names directly, so plain `ctx.spatial().distance(...)`
// expressions still yield a tight `__maat_ctx_spatial_distance`-only
// allowlist via Tier 2.
//
// **Receiver-agnostic visitor.** Records bare method names without
// trying to type-track the receiver. Acceptable over-permissiveness
// — if a plugin happens to name a struct method `current_player`,
// we'd false-positively include `__maat_ctx_current_player` (but
// the corresponding wasm import doesn't exist so no load failure;
// the manifest just over-declares an unused name).
// ---------------------------------------------------------------------------

/// Direct `GameContext` methods reachable via the `WasmGameContext`
/// vtable. Included in the allowlist iff any `#[handler]` or
/// `#[compose(kind = Wizard)]` is present (these are the only
/// surfaces that take `&dyn GameContext`).
///
/// Counting: 12 entries — every method on the `GameContext` trait
/// that has a host-import counterpart in `sdk-wasm-rt`. The
/// sub-trait getters (`predicates`, `vision`, `spatial`) are NOT
/// here — they don't have their own host import; they construct
/// ZST shims locally.
#[allow(dead_code)] // canonical reference list; the actual emission inlines these in `manifest_static` for cleaner quote!{}
const CTX_VTABLE_BASE: &[&str] = &[
    "__maat_ctx_current_player",
    "__maat_ctx_entity_view_for",
    "__maat_ctx_players",
    "__maat_ctx_entities_at",
    "__maat_ctx_is_owners_turn",
    "__maat_ctx_contained_by",
    "__maat_ctx_seat_role_for",
    "__maat_ctx_alloc_id",
    "__maat_ctx_settings",
    "__maat_ctx_this_turn_events",
    "__maat_ctx_this_phase_events",
    "__maat_ctx_since",
];

/// Map a sub-trait method name (or sub-trait getter) to the set of
/// `__maat_*` host imports that calling it requires. The visitor
/// catches both `ctx.spatial()` AND chained `.distance()` — we
/// include both forms to be safe.
const SUBTRAIT_METHOD_TO_IMPORTS: &[(&str, &[&str])] = &[
    // Sub-trait getters — conservatively include the entire surface.
    ("predicates", &["__maat_ctx_predicate_eval"]),
    ("vision", &[
        "__maat_ctx_vision_can_see",
        "__maat_ctx_vision_perceivability",
    ]),
    ("spatial", &[
        "__maat_ctx_spatial_entities_at",
        "__maat_ctx_spatial_distance",
        "__maat_ctx_spatial_entities_in_area",
    ]),
    // Sub-trait methods — caught directly via the call chain.
    ("evaluate", &["__maat_ctx_predicate_eval"]),
    ("can_see", &["__maat_ctx_vision_can_see"]),
    ("perceivability", &["__maat_ctx_vision_perceivability"]),
    ("distance", &["__maat_ctx_spatial_distance"]),
    // NOTE: `entities_at` resolves to `__maat_ctx_spatial_entities_at`
    // when called on the spatial sub-trait. It's also a direct
    // `GameContext` method (different host import:
    // `__maat_ctx_entities_at`). We include both in the map; the
    // vtable base already covers the direct form.
    ("entities_at", &["__maat_ctx_spatial_entities_at"]),
    ("entities_in_area", &["__maat_ctx_spatial_entities_in_area"]),
];

/// Walk a method body and collect every method-call ident (regardless
/// of receiver). The visitor's collected set is then resolved against
/// `CTX_METHOD_TO_IMPORTS` to produce the body-level allowlist.
///
/// We *only* look at method calls. Free function calls (`f()`) and
/// associated function calls (`Self::g()`) are out of scope —
/// `__maat_*` host imports are reached exclusively through
/// `&dyn GameContext` and `&mut ComposeCtx` method calls in plugin
/// code. Helper methods that *the plugin author wrote* and that
/// internally call `ctx.foo()` are caught by visiting all method
/// bodies in the impl block (not just `#[handler]` / `#[compose]`
/// ones) — see `collect_called_methods` below.
struct MethodCallCollector {
    names: BTreeSet<String>,
}

impl<'ast> Visit<'ast> for MethodCallCollector {
    fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
        self.names.insert(node.method.to_string());
        // Recurse — chained calls like `ctx.spatial().distance(...)`
        // surface both `.spatial()` and `.distance()`.
        syn::visit::visit_expr_method_call(self, node);
    }
}

/// Walk all method bodies in `impl_block` and resolve the union
/// against `SUBTRAIT_METHOD_TO_IMPORTS`. Returns a sorted,
/// deduplicated `Vec<&'static str>` of host-import names.
///
/// **Tier 2 only.** This function does NOT include the vtable
/// base (`CTX_VTABLE_BASE`) — that's added unconditionally by
/// `manifest_static()` when any handler / Wizard compose is
/// present. The two tiers are summed in `#[plugin]`'s emission.
fn collect_called_subtrait_imports(impl_block: &ItemImpl) -> Vec<&'static str> {
    let mut collector = MethodCallCollector {
        names: BTreeSet::new(),
    };
    for it in &impl_block.items {
        if let ImplItem::Fn(method) = it {
            collector.visit_block(&method.block);
        }
    }
    let mut imports: BTreeSet<&'static str> = BTreeSet::new();
    for name in &collector.names {
        if let Some(&(_, imps)) = SUBTRAIT_METHOD_TO_IMPORTS
            .iter()
            .find(|(k, _)| *k == name.as_str())
        {
            for imp in imps {
                imports.insert(*imp);
            }
        }
    }
    imports.into_iter().collect()
}

/// WASM1-narrow-shim — validate that a user method's ctx
/// parameter is generic (not `&dyn GameContext`). The thunk
/// emission requires a generic ctx so it can monomorphize
/// per-target (`C = dyn GameContext` on native; `C =
/// WasmGameContext` on wasm32). If the ctx parameter is a
/// dyn trait object the thunk's turbofish substitution
/// would fail with a confusing error; we pre-empt with a
/// clear migration message.
///
/// Heuristic: scan typed args (skip `&self`), find the first
/// arg whose name is `ctx`, `_ctx`, `game_ctx`, or `_game_ctx`.
/// If its type is `&dyn ...`, emit an error. The scan is
/// receiver-agnostic and accepts both inherent methods (with
/// `&self`) and free fns (no `&self`).
fn check_ctx_param_is_generic(
    sig: &syn::Signature,
    kind_label: &'static str,
) -> Option<TokenStream> {
    for input in &sig.inputs {
        let syn::FnArg::Typed(pat_ty) = input else { continue };
        // Extract the parameter name.
        let name = match &*pat_ty.pat {
            syn::Pat::Ident(p) => p.ident.to_string(),
            _ => continue,
        };
        let is_ctx_like = matches!(
            name.as_str(),
            "ctx" | "_ctx" | "game_ctx" | "_game_ctx" | "compose_ctx",
        );
        if !is_ctx_like {
            continue;
        }
        // Inspect the type — looking for `&dyn ...`.
        let syn::Type::Reference(tr) = &*pat_ty.ty else { continue };
        let elem = &*tr.elem;
        if matches!(elem, syn::Type::TraitObject(_)) {
            return Some(
                syn::Error::new_spanned(
                    &pat_ty.ty,
                    format!(
                        "WASM1-narrow-shim: {kind_label} method ctx \
                         parameter must be `&impl GameContext` (NOT \
                         `&dyn GameContext`). Per-target thunks \
                         monomorphize the user method at `C = &dyn \
                         GameContext` on native (via the blanket \
                         `impl<T: ?Sized + GameContext> GameContext \
                         for &T`) and `C = WasmGameContext` on \
                         wasm32 to enable per-method DCE of unused \
                         `__maat_ctx_*` host imports. See \
                         docs/superpowers/specs/completed/\
                         2026-05-16-wasm1-narrow-shim-restructure.md",
                    ),
                )
                .to_compile_error(),
            );
        }
        // The first ctx-like param is the one we check.
        break;
    }
    None
}

#[derive(Default)]
struct HandlerArgs {
    plugin: Option<syn::Path>,
    event: Option<syn::Path>,
    role: Option<syn::Path>,
    priority: Option<Expr>,
    name: Option<String>,
}

/// WASM1 Phase 3c-compose-macro — parsed shape of a `#[compose(...)]`
/// attribute. The four `kind`s map to four registration entry types in
/// `maat_sdk::ComposeEntry`. `kind` is required; the other
/// fields are required per-kind (validated in `validate_compose_args`).
#[derive(Default)]
struct ComposeArgs {
    /// `Creation` | `LevelUp` | `Wizard` | `DistanceMetric` — case-sensitive.
    kind: Option<String>,
    /// Required for `Creation` / `LevelUp` — the `EntityTypeId` expression
    /// (e.g. `Hero::TYPE_ID` or `Character::TYPE_ID`).
    type_id: Option<Expr>,
    /// Required for `Wizard` — the `WizardKind` expression.
    wizard_kind: Option<Expr>,
    /// Required for `DistanceMetric` — the `MetricKey` expression.
    key: Option<Expr>,
    /// Required for `Creation` / `LevelUp` / `Wizard` — the stateless
    /// sibling fn that constructs the spec. Path form
    /// (`<Plugin>::<fn>` or bare `fn_ident`).
    spec_fn: Option<Expr>,
}

fn parse_compose_args(args: TokenStream) -> syn::Result<ComposeArgs> {
    let mut out = ComposeArgs::default();
    for meta in parse_kvs(args)? {
        let Meta::NameValue(nv) = meta else {
            return Err(syn::Error::new_spanned(
                meta,
                "expected `key = value` form in #[compose(...)]",
            ));
        };
        let key_ident = nv
            .path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key_ident.as_str() {
            "kind" => match &nv.value {
                Expr::Path(ExprPath { path, .. }) => {
                    let s = path
                        .segments
                        .last()
                        .map(|s| s.ident.to_string())
                        .unwrap_or_default();
                    out.kind = Some(s);
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        &nv.value,
                        "compose `kind` must be one of Creation, LevelUp, Wizard, DistanceMetric",
                    ));
                }
            },
            "type_id" => out.type_id = Some(nv.value),
            "wizard_kind" => out.wizard_kind = Some(nv.value),
            "key" => out.key = Some(nv.value),
            "spec_fn" => out.spec_fn = Some(nv.value),
            other => {
                return Err(syn::Error::new_spanned(
                    nv.path,
                    format!("unknown compose key `{other}`"),
                ));
            }
        }
    }
    Ok(out)
}

fn parse_kvs(args: TokenStream) -> syn::Result<Vec<Meta>> {
    if args.is_empty() {
        return Ok(Vec::new());
    }
    // Parse a comma-separated list of `key = value` pairs.
    struct MetaList(Punctuated<Meta, Token![,]>);
    impl Parse for MetaList {
        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
            Ok(MetaList(Punctuated::parse_terminated(input)?))
        }
    }
    let MetaList(list) = parse2(args)?;
    Ok(list.into_iter().collect())
}

fn parse_handler_args(args: TokenStream) -> syn::Result<HandlerArgs> {
    let mut out = HandlerArgs::default();
    for meta in parse_kvs(args)? {
        let Meta::NameValue(nv) = meta else {
            return Err(syn::Error::new_spanned(
                meta,
                "expected `key = value` form",
            ));
        };
        let key = nv
            .path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key.as_str() {
            "plugin" => match &nv.value {
                Expr::Path(ExprPath { path, .. }) => out.plugin = Some(path.clone()),
                _ => return Err(syn::Error::new_spanned(nv.value, "plugin must be a path")),
            },
            "event" => match &nv.value {
                Expr::Path(ExprPath { path, .. }) => out.event = Some(path.clone()),
                _ => return Err(syn::Error::new_spanned(nv.value, "event must be a path")),
            },
            "role" => match &nv.value {
                Expr::Path(ExprPath { path, .. }) => out.role = Some(path.clone()),
                _ => return Err(syn::Error::new_spanned(nv.value, "role must be a path")),
            },
            "priority" => out.priority = Some(nv.value),
            "name" => match nv.value {
                Expr::Lit(ExprLit {
                    lit: Lit::Str(s), ..
                }) => out.name = Some(s.value()),
                v => return Err(syn::Error::new_spanned(v, "name must be a string literal")),
            },
            other => {
                return Err(syn::Error::new_spanned(
                    nv.path,
                    format!("unknown handler key `{other}`"),
                ));
            }
        }
    }
    Ok(out)
}

/// `#[handlers]` — attribute macro on an `impl PluginStruct { ... }`
/// block. Walks the inner methods; any method tagged with an inner
/// `#[handler(event = ..., role = ..., priority = ..., name = "...")]`
/// is registered as a pipeline handler.
///
/// Phase 5a (S25): handler methods take `&self` so plugin state in
/// the struct's fields is reachable. The macro emits, *outside* the
/// impl block (siblings at module scope):
///
/// 1. A free-standing thunk `fn(&dyn ModelPlugin, &GameEvent, &dyn
///    GameContext) -> HandlerResponse` that downcasts to the
///    plugin's concrete type and calls the method.
/// 2. A linkme registration into `REGISTERED_HANDLERS` carrying the
///    thunk + the `(method_name, wire_name, event, role, priority)`
///    tuple. `method_name` is the deterministic sort key for
///    `local_id` assignment — matched by `#[plugin]`'s manifest
///    builder.
///
/// The impl block itself is emitted with the inner `#[handler(...)]`
/// attributes stripped so the methods compile cleanly.
pub fn handlers_impl(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut item: ItemImpl = match parse2(input.clone()) {
        Ok(i) => i,
        Err(e) => return e.to_compile_error(),
    };

    // Determine the plugin struct ident from `impl X { ... }`.
    let plugin_path = match &*item.self_ty {
        Type::Path(p) => p.path.clone(),
        _ => {
            return syn::Error::new_spanned(
                &item.self_ty,
                "#[handlers] needs an `impl Type { ... }` form",
            )
            .to_compile_error();
        }
    };
    let plugin_struct_str = plugin_path
        .segments
        .last()
        .map(|s| s.ident.to_string())
        .unwrap_or_default();

    // Walk each method, look for `#[handler(...)]`, build emit-list.
    let mut emitted: Vec<TokenStream> = Vec::new();
    // WASM1 Phase 3c — per-handler info collected during the loop, then
    // emitted as a single sorted-by-method-name static slice on wasm32.
    // The native path uses linkme to assemble the same view at runtime;
    // wasm32 has no global ctors so the slice is the only mechanism.
    struct WasmHandlerInfo {
        method_name: String,
        name: String,
        event: syn::Path,
        role: syn::Path,
        priority: Expr,
        thunk_ident: Ident,
    }
    let mut wasm_handlers: Vec<WasmHandlerInfo> = Vec::new();
    for it in item.items.iter_mut() {
        let ImplItem::Fn(method) = it else { continue };
        let mut handler_attr: Option<(usize, TokenStream)> = None;
        for (i, attr) in method.attrs.iter().enumerate() {
            if attr.path().is_ident("handler") {
                let toks = match &attr.meta {
                    Meta::List(ml) => ml.tokens.clone(),
                    Meta::Path(_) => TokenStream::new(),
                    Meta::NameValue(nv) => {
                        return syn::Error::new_spanned(nv, "#[handler] expects (...)")
                            .to_compile_error();
                    }
                };
                handler_attr = Some((i, toks));
                break;
            }
        }
        let Some((idx, args_toks)) = handler_attr else { continue };
        method.attrs.remove(idx);

        let args = match parse_handler_args(args_toks) {
            Ok(a) => a,
            Err(e) => return e.to_compile_error(),
        };
        // `plugin = ...` is optional inside `#[handlers]`; the impl
        // block already names the plugin.
        let event = match args.event {
            Some(e) => e,
            None => {
                return syn::Error::new_spanned(&method.sig.ident, "missing `event = ...`")
                    .to_compile_error();
            }
        };
        let role = match args.role {
            Some(r) => r,
            None => {
                return syn::Error::new_spanned(&method.sig.ident, "missing `role = ...`")
                    .to_compile_error();
            }
        };
        let priority = args
            .priority
            .unwrap_or_else(|| syn::parse_quote!(::maat::__sdk::Priority::BASE));
        let fn_ident = method.sig.ident.clone();
        let method_name_str = fn_ident.to_string();
        let name_str = args.name.unwrap_or_else(|| fn_ident.to_string());

        // WASM1-narrow-shim — reject `ctx: &dyn GameContext`
        // bodies. The user method must be generic over a ctx
        // type with `?Sized` bound so the per-target thunk
        // monomorphizes for the wasm32 concrete ctx and DCEs
        // unreferenced host imports.
        if let Some(err) = check_ctx_param_is_generic(
            &method.sig,
            "handler",
        ) {
            return err;
        }

        let reg_ident = format_ident!(
            "__METAGAMER_HANDLER_REG_{}_{}",
            plugin_struct_str.to_uppercase(),
            fn_ident.to_string().to_uppercase()
        );
        let thunk_ident = format_ident!(
            "__maat_handler_thunk_{}_{}",
            plugin_struct_str,
            fn_ident
        );

        // Free-standing items emitted *after* the impl block.
        //
        // WASM1-narrow-shim — user method takes `&impl GameContext`
        // (Sized). On native, the thunk receives `&dyn GameContext`
        // from the pipeline and passes it as `&&dyn GameContext`
        // — the outer reference is Sized, so `C = &dyn
        // GameContext` satisfies the user method's bound. The
        // blanket `impl<T: ?Sized + GameContext> GameContext for
        // &T` in `sdk-types::plugin_slices` makes the
        // monomorphization typecheck.
        //
        // On wasm32, the thunk receives `&WasmGameContext` and
        // passes it directly — `C = WasmGameContext`, enabling
        // per-method DCE of unused `__maat_ctx_*` host imports.
        emitted.push(quote! {
            #[cfg(not(target_arch = "wasm32"))]
            #[doc(hidden)]
            #[allow(non_snake_case)]
            fn #thunk_ident(
                plugin: &dyn ::maat::__sdk::PluginInstance,
                event: &::maat::__sdk::GameEvent,
                ctx: &dyn ::maat::__sdk::GameContext,
            ) -> ::maat::__sdk::HandlerResponse {
                match plugin.downcast_ref::<#plugin_path>() {
                    Some(p) => <#plugin_path>::#fn_ident(p, event, &ctx),
                    None => ::maat::__sdk::HandlerResponse::from(
                        ::maat::__sdk::Decision::Allow,
                    ),
                }
            }

            #[cfg(not(target_arch = "wasm32"))]
            #[::maat::__sdk::linkme::distributed_slice(::maat::__sdk::REGISTERED_HANDLERS)]
            #[linkme(crate = ::maat::__sdk::linkme)]
            #[doc(hidden)]
            #[allow(non_upper_case_globals)]
            static #reg_ident: ::maat::__sdk::HandlerRegistration =
                ::maat::__sdk::HandlerRegistration {
                    plugin_crate_name: env!("CARGO_PKG_NAME"),
                    plugin_struct:     #plugin_struct_str,
                    method_name:       #method_name_str,
                    name:              #name_str,
                    event_kind:        #event,
                    role:              #role,
                    priority:          #priority,
                    thunk:             #thunk_ident,
                    signature:         [0u8; 32],
                };

            // WASM1-narrow-shim — wasm32 thunk takes the CONCRETE
            // `&WasmGameContext` (not `&dyn GameContext`) so the
            // compiler monomorphizes the plugin's generic
            // handler method at `C = WasmGameContext`. Each
            // `ctx.<method>()` call site within the handler body
            // becomes a static call to a `WasmGameContext`
            // method, letting LLVM's tree-shaker DCE the
            // `__maat_ctx_*` host imports that the handler
            // never reaches.
            //
            // The user method must be generic over a ctx-type
            // with a `?Sized` bound (so the same body works at
            // `C = dyn GameContext` on native AND `C =
            // WasmGameContext` on wasm32; `dyn GameContext` is
            // `!Sized`, `WasmGameContext` is `Sized`).
            #[cfg(target_arch = "wasm32")]
            #[doc(hidden)]
            #[allow(non_snake_case)]
            fn #thunk_ident(
                plugin: &dyn ::maat::__sdk::PluginInstance,
                event: &::maat::__sdk::GameEvent,
                ctx: &::maat_sdk_wasm_rt::WasmGameContext,
            ) -> ::maat::__sdk::HandlerResponse {
                match plugin.downcast_ref::<#plugin_path>() {
                    Some(p) => <#plugin_path>::#fn_ident(p, event, ctx),
                    None => ::maat::__sdk::HandlerResponse::from(
                        ::maat::__sdk::Decision::Allow,
                    ),
                }
            }
        });
        wasm_handlers.push(WasmHandlerInfo {
            method_name: method_name_str.clone(),
            name: name_str.clone(),
            event: event.clone(),
            role: role.clone(),
            priority: priority.clone(),
            thunk_ident: thunk_ident.clone(),
        });
    }

    // WASM1 Phase 3c — emit a single per-plugin handler-table slice
    // for wasm32. Sort by method_name so positional `LocalHandlerId`
    // assignment matches the native sort order in
    // `manifest().declared_handlers` and `__maat_dispatch_table()`.
    wasm_handlers.sort_by(|a, b| a.method_name.cmp(&b.method_name));
    let wasm_slice_ident = format_ident!(
        "__MAAT_HANDLERS_{}",
        plugin_struct_str.to_uppercase()
    );
    let wasm_entries: Vec<TokenStream> = wasm_handlers
        .iter()
        .map(|h| {
            let method_name = &h.method_name;
            let name = &h.name;
            let event = &h.event;
            let role = &h.role;
            let priority = &h.priority;
            let thunk = &h.thunk_ident;
            quote! {
                ::maat_sdk_wasm_rt::HandlerEntry {
                    method_name: #method_name,
                    name:        #name,
                    event_kind:  #event,
                    role:        #role,
                    priority:    #priority,
                    thunk:       #thunk,
                }
            }
        })
        .collect();
    let wasm_emit = quote! {
        #[cfg(target_arch = "wasm32")]
        #[doc(hidden)]
        #[allow(non_upper_case_globals)]
        pub const #wasm_slice_ident: &[::maat_sdk_wasm_rt::HandlerEntry] = &[
            #( #wasm_entries ),*
        ];
    };

    // -----------------------------------------------------------------
    // WASM1 Phase 3c-compose-macro — walk methods again looking for
    // `#[compose(...)]`. Each compose method gets a per-method thunk +
    // an entry in the per-plugin `__MAAT_COMPOSES_<STRUCT>` slice.
    // Sorted by `method_name` so the position-as-index (`compose_idx`)
    // matches across native + wasm32 builds.
    // -----------------------------------------------------------------
    struct ComposeInfo {
        method_name: String,
        kind: String,
        thunk_ident: Ident,
        /// WASM1-narrow-shim — separate wasm32-thunk ident. The
        /// native thunk (`thunk_ident`) takes `&dyn GameContext`;
        /// the wasm thunk takes `&WasmGameContext` so the user's
        /// generic compose method monomorphizes for per-method DCE.
        /// For `DistanceMetric` this is the same as `thunk_ident`
        /// (no GameContext involved).
        wasm_thunk_ident: Ident,
        identifier_expr: TokenStream,
        spec_fn_expr: Option<TokenStream>,
    }
    let mut composes: Vec<ComposeInfo> = Vec::new();
    let mut compose_emitted: Vec<TokenStream> = Vec::new();
    for it in item.items.iter_mut() {
        let ImplItem::Fn(method) = it else { continue };
        let mut compose_attr: Option<(usize, TokenStream)> = None;
        for (i, attr) in method.attrs.iter().enumerate() {
            if attr.path().is_ident("compose") {
                let toks = match &attr.meta {
                    Meta::List(ml) => ml.tokens.clone(),
                    Meta::Path(_) => TokenStream::new(),
                    Meta::NameValue(nv) => {
                        return syn::Error::new_spanned(nv, "#[compose] expects (...)")
                            .to_compile_error();
                    }
                };
                compose_attr = Some((i, toks));
                break;
            }
        }
        let Some((idx, args_toks)) = compose_attr else { continue };
        method.attrs.remove(idx);

        let args = match parse_compose_args(args_toks) {
            Ok(a) => a,
            Err(e) => return e.to_compile_error(),
        };
        let kind = match args.kind.as_deref() {
            Some(k) => k.to_string(),
            None => {
                return syn::Error::new_spanned(
                    &method.sig.ident,
                    "#[compose] requires `kind = Creation | LevelUp | Wizard | DistanceMetric`",
                )
                .to_compile_error();
            }
        };

        // WASM1-narrow-shim — for compose kinds that take a
        // GameContext parameter (everything except DistanceMetric),
        // require the ctx param to be generic.
        if kind != "DistanceMetric" {
            if let Some(err) = check_ctx_param_is_generic(
                &method.sig,
                "compose",
            ) {
                return err;
            }
        }

        let fn_ident = method.sig.ident.clone();
        let method_name_str = fn_ident.to_string();
        let thunk_ident = format_ident!(
            "__maat_compose_thunk_{}_{}",
            plugin_struct_str,
            fn_ident
        );
        // WASM1-narrow-shim — wasm32 emits a SEPARATE thunk taking
        // `&WasmGameContext` directly. The native thunk
        // (`#thunk_ident`) still has the `&dyn GameContext`
        // signature and is what `ComposeEntry::<Kind>::thunk` points
        // at on native; the wasm thunk (`#wasm_thunk_ident`) is what
        // `WasmComposeEntry::<Kind>::thunk` points at on wasm32,
        // letting the user's generic compose method monomorphize
        // at `C = WasmGameContext` for per-method DCE.
        let wasm_thunk_ident = format_ident!(
            "__maat_compose_wasm_thunk_{}_{}",
            plugin_struct_str,
            fn_ident
        );

        // Per-kind validation + thunk emission.
        let (identifier_expr, spec_fn_expr, thunk_def, entry_kind_segment) = match kind.as_str() {
            "Creation" => {
                let Some(type_id) = args.type_id else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = Creation)] requires `type_id = ...`",
                    )
                    .to_compile_error();
                };
                let Some(spec_fn) = args.spec_fn else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = Creation)] requires `spec_fn = ...`",
                    )
                    .to_compile_error();
                };
                // WASM1-narrow-shim — per-target thunk. Native
                // takes `&dyn GameContext` (feeds the
                // `Arc<dyn Fn(&dyn)>` compose registry); wasm32
                // takes `&WasmGameContext` directly so the user's
                // generic compose method monomorphizes at
                // `C = WasmGameContext` for per-method DCE.
                let thunk_def = quote! {
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #thunk_ident(
                        history: &::maat::__sdk::CreationHistory,
                        game_ctx: &dyn ::maat::__sdk::GameContext,
                        ctx: &mut ::maat::__sdk::ComposeCtx,
                    ) -> ::std::vec::Vec<::maat::__sdk::GameEvent> {
                        // WASM1-narrow-shim — `&game_ctx` (double-ref)
                        // so `C = &dyn GameContext` (Sized) on native.
                        <#plugin_path>::#fn_ident(history, &game_ctx, ctx)
                    }

                    #[cfg(target_arch = "wasm32")]
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #wasm_thunk_ident(
                        history: &::maat::__sdk::CreationHistory,
                        game_ctx: &::maat_sdk_wasm_rt::WasmGameContext,
                        ctx: &mut ::maat::__sdk::ComposeCtx,
                    ) -> ::std::vec::Vec<::maat::__sdk::GameEvent> {
                        <#plugin_path>::#fn_ident(history, game_ctx, ctx)
                    }
                };
                let id_expr = quote! { #type_id };
                let spec_expr = quote! { #spec_fn };
                (id_expr, Some(spec_expr), thunk_def, "Creation")
            }
            "LevelUp" => {
                let Some(type_id) = args.type_id else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = LevelUp)] requires `type_id = ...`",
                    )
                    .to_compile_error();
                };
                let Some(spec_fn) = args.spec_fn else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = LevelUp)] requires `spec_fn = ...`",
                    )
                    .to_compile_error();
                };
                // WASM1-narrow-shim — see Creation branch above for
                // the per-target thunk rationale.
                let thunk_def = quote! {
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #thunk_ident(
                        target_level: u32,
                        history: &::maat::__sdk::CreationHistory,
                        game_ctx: &dyn ::maat::__sdk::GameContext,
                        ctx: &mut ::maat::__sdk::ComposeCtx,
                        entity: ::maat::__sdk::EntityId,
                    ) -> ::std::vec::Vec<::maat::__sdk::GameEvent> {
                        // WASM1-narrow-shim — double-ref on native.
                        <#plugin_path>::#fn_ident(target_level, history, &game_ctx, ctx, entity)
                    }

                    #[cfg(target_arch = "wasm32")]
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #wasm_thunk_ident(
                        target_level: u32,
                        history: &::maat::__sdk::CreationHistory,
                        game_ctx: &::maat_sdk_wasm_rt::WasmGameContext,
                        ctx: &mut ::maat::__sdk::ComposeCtx,
                        entity: ::maat::__sdk::EntityId,
                    ) -> ::std::vec::Vec<::maat::__sdk::GameEvent> {
                        <#plugin_path>::#fn_ident(target_level, history, game_ctx, ctx, entity)
                    }
                };
                let id_expr = quote! { #type_id };
                let spec_expr = quote! { #spec_fn };
                (id_expr, Some(spec_expr), thunk_def, "LevelUp")
            }
            "Wizard" => {
                let Some(wizard_kind) = args.wizard_kind else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = Wizard)] requires `wizard_kind = ...`",
                    )
                    .to_compile_error();
                };
                let Some(spec_fn) = args.spec_fn else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = Wizard)] requires `spec_fn = ...`",
                    )
                    .to_compile_error();
                };
                // WASM1-narrow-shim — see Creation branch above for
                // the per-target thunk rationale.
                let thunk_def = quote! {
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #thunk_ident(
                        trigger: &::maat::__sdk::GameEvent,
                        history: &[::maat::__sdk::ResolvedGroup],
                        game_ctx: &dyn ::maat::__sdk::GameContext,
                        compose_ctx: &mut ::maat::__sdk::ComposeCtx<'_>,
                    ) -> ::maat::__sdk::ComposeResult {
                        // WASM1-narrow-shim — double-ref on native.
                        <#plugin_path>::#fn_ident(trigger, history, &game_ctx, compose_ctx)
                    }

                    #[cfg(target_arch = "wasm32")]
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #wasm_thunk_ident(
                        trigger: &::maat::__sdk::GameEvent,
                        history: &[::maat::__sdk::ResolvedGroup],
                        game_ctx: &::maat_sdk_wasm_rt::WasmGameContext,
                        compose_ctx: &mut ::maat::__sdk::ComposeCtx<'_>,
                    ) -> ::maat::__sdk::ComposeResult {
                        <#plugin_path>::#fn_ident(trigger, history, game_ctx, compose_ctx)
                    }
                };
                let id_expr = quote! { #wizard_kind };
                let spec_expr = quote! { #spec_fn };
                (id_expr, Some(spec_expr), thunk_def, "Wizard")
            }
            "DistanceMetric" => {
                let Some(key) = args.key else {
                    return syn::Error::new_spanned(
                        &fn_ident,
                        "#[compose(kind = DistanceMetric)] requires `key = ...`",
                    )
                    .to_compile_error();
                };
                // WASM1-narrow-shim — DistanceMetric thunks don't
                // take a GameContext parameter; native + wasm32
                // share the same signature. Emit both idents (the
                // wasm32 dispatch arm references `#wasm_thunk_ident`
                // uniformly across all compose kinds).
                let thunk_def = quote! {
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #thunk_ident(
                        graph: &::maat::__sdk::BoardGraph,
                        from: ::maat::__sdk::FaceId,
                        to: ::maat::__sdk::FaceId,
                    ) -> ::std::result::Result<u32, ::maat::__sdk::DistanceError> {
                        <#plugin_path>::#fn_ident(graph, from, to)
                    }

                    #[cfg(target_arch = "wasm32")]
                    #[doc(hidden)]
                    #[allow(non_snake_case)]
                    fn #wasm_thunk_ident(
                        graph: &::maat::__sdk::BoardGraph,
                        from: ::maat::__sdk::FaceId,
                        to: ::maat::__sdk::FaceId,
                    ) -> ::std::result::Result<u32, ::maat::__sdk::DistanceError> {
                        <#plugin_path>::#fn_ident(graph, from, to)
                    }
                };
                let id_expr = quote! { #key };
                (id_expr, None, thunk_def, "DistanceMetric")
            }
            other => {
                return syn::Error::new_spanned(
                    &fn_ident,
                    format!("unknown compose kind `{other}` (expected Creation, LevelUp, Wizard, DistanceMetric)"),
                )
                .to_compile_error();
            }
        };
        let _ = entry_kind_segment;

        compose_emitted.push(thunk_def);
        composes.push(ComposeInfo {
            method_name: method_name_str,
            kind,
            thunk_ident,
            wasm_thunk_ident,
            identifier_expr,
            spec_fn_expr,
        });
    }

    // Sort by method_name for stable per-plugin compose_idx.
    composes.sort_by(|a, b| a.method_name.cmp(&b.method_name));

    // Emit the `__MAAT_COMPOSES_<STRUCT>` slice + `register_composes`
    // inherent method on the plugin struct.
    let compose_slice_ident = format_ident!(
        "__MAAT_COMPOSES_{}",
        plugin_struct_str.to_uppercase()
    );

    let compose_entries: Vec<TokenStream> = composes
        .iter()
        .map(|c| {
            let method_name = &c.method_name;
            let thunk = &c.thunk_ident;
            let identifier = &c.identifier_expr;
            let spec_fn = c.spec_fn_expr.as_ref();
            match c.kind.as_str() {
                "Creation" => {
                    let spec = spec_fn.expect("Creation requires spec_fn");
                    quote! {
                        ::maat::__sdk::ComposeEntry::Creation {
                            method_name: #method_name,
                            type_id: #identifier,
                            spec_fn: #spec,
                            thunk: #thunk,
                        }
                    }
                }
                "LevelUp" => {
                    let spec = spec_fn.expect("LevelUp requires spec_fn");
                    quote! {
                        ::maat::__sdk::ComposeEntry::LevelUp {
                            method_name: #method_name,
                            type_id: #identifier,
                            spec_fn: #spec,
                            thunk: #thunk,
                        }
                    }
                }
                "Wizard" => {
                    let spec = spec_fn.expect("Wizard requires spec_fn");
                    quote! {
                        ::maat::__sdk::ComposeEntry::Wizard {
                            method_name: #method_name,
                            kind: #identifier,
                            spec_fn: #spec,
                            thunk: #thunk,
                        }
                    }
                }
                "DistanceMetric" => {
                    quote! {
                        ::maat::__sdk::ComposeEntry::DistanceMetric {
                            method_name: #method_name,
                            key: #identifier,
                            thunk: #thunk,
                        }
                    }
                }
                _ => unreachable!(),
            }
        })
        .collect();

    // Cargo refuses const initialization of an Arc-bearing value
    // (`ComposeEntry::Wizard` carries a `WizardKind` whose inner Cow
    // is not const-constructible in general). Use a `fn` returning a
    // `Vec<ComposeEntry>` for the slice — `register_composes` calls it
    // once and iterates. This is the analogue of how
    // `__MAAT_HANDLERS_<STRUCT>` is `&[HandlerEntry]` (Copy entries
    // with `&'static str` keys) but ComposeEntry is owned.
    //
    // Implementation: emit a `pub fn __maat_composes_for_<struct>() ->
    // Vec<ComposeEntry>`. `register_composes` walks the vec.
    let compose_fn_ident = format_ident!(
        "__maat_composes_for_{}",
        plugin_struct_str.to_lowercase()
    );

    // Plugin path captured for use inside the `__maat_compose` wasm32
    // export emission below.
    let plugin_path_for_compose_export = plugin_path.clone();
    let _ = &plugin_path_for_compose_export;

    let compose_emit = if composes.is_empty() {
        // No `#[compose]` methods in this `#[handlers]` block.
        // Don't emit anything — `register_composes` is only emitted
        // by the block that contains the compose methods, to avoid
        // duplicate inherent methods when a plugin has multiple
        // `#[handlers]` impl blocks (typical for crates that split
        // handlers across modules: dnd5e has 5+ such blocks).
        //
        // The trade-off: plugins must put `#[compose]` in exactly
        // one `#[handlers]` block. The block needn't contain any
        // `#[handler]`s — an empty `#[handlers]` impl with only
        // `#[compose]` methods works fine. Authors that prefer "one
        // method per file" can group composes into a dedicated
        // `compose_block.rs` with its own `#[handlers]` impl.
        TokenStream::new()
    } else {
        let compose_thunks = compose_emitted.into_iter().fold(
            TokenStream::new(),
            |mut acc, t| {
                acc.extend(t);
                acc
            },
        );

        // WASM1 Phase 3c-compose-macro sub-commit 2 — emit the
        // `__maat_compose` wasm32 export. Dispatches by (kind, fn_idx)
        // into the per-plugin compose table; each branch postcard-
        // decodes the matching wire args, calls the thunk, and
        // postcard-encodes the response.
        //
        // The kind discriminant matches `ComposeKind` in
        // `maat_sdk_types::wasm_compose` (Creation=0, LevelUp=1,
        // Wizard=2, DistanceMetric=3). The host packs kind as i32 and
        // we match on it here.
        // WASM1-narrow-shim — emit a per-method dispatch arm
        // matching `(kind, fn_idx)` to the macro-emitted
        // wasm-shape thunk. Going through `wasm_thunk_ident`
        // directly (rather than the `ComposeEntry`'s `&dyn`-shape
        // `thunk` field) is what enables per-method DCE: the
        // wasm thunk receives `&WasmGameContext` concretely so
        // each `ctx.<method>()` call inside the user's compose
        // body monomorphizes against the concrete type instead
        // of going through the `WasmGameContext` vtable.
        let wasm_method_dispatch_arms: Vec<TokenStream> = composes
            .iter()
            .enumerate()
            .map(|(idx, c)| {
                let idx_lit = idx as i32;
                let wasm_thunk = &c.wasm_thunk_ident;
                match c.kind.as_str() {
                    "Creation" => quote! {
                        (0i32, #idx_lit) => {
                            let args: ::maat_sdk_types::wasm_compose::CreationComposeArgs =
                                match ::maat_sdk_wasm_rt::postcard::from_bytes(payload) {
                                    Ok(a) => a,
                                    Err(_) => {
                                        unsafe { __MAAT_PLUGIN = Some(plugin); }
                                        return pack_err();
                                    }
                                };
                            let mut alloc = || {
                                use ::maat_sdk_types::plugin_slices::GameContext;
                                ::maat_sdk_wasm_rt::WasmGameContext::new().alloc_id()
                            };
                            let mut ctx = ::maat::__sdk::ComposeCtx {
                                alloc_id: &mut alloc,
                                by_seat: args.by_seat,
                            };
                            let game_ctx = ::maat_sdk_wasm_rt::WasmGameContext::new();
                            let events = #wasm_thunk(&args.history, &game_ctx, &mut ctx);
                            let result = ::maat_sdk_types::wasm_compose::CreationComposeResult {
                                events,
                                ctx_proposed: ::std::vec::Vec::new(),
                            };
                            ::maat_sdk_wasm_rt::postcard::to_allocvec(&result)
                                .unwrap_or_default()
                        }
                    },
                    "LevelUp" => quote! {
                        (1i32, #idx_lit) => {
                            let args: ::maat_sdk_types::wasm_compose::LevelUpComposeArgs =
                                match ::maat_sdk_wasm_rt::postcard::from_bytes(payload) {
                                    Ok(a) => a,
                                    Err(_) => {
                                        unsafe { __MAAT_PLUGIN = Some(plugin); }
                                        return pack_err();
                                    }
                                };
                            let mut alloc = || {
                                use ::maat_sdk_types::plugin_slices::GameContext;
                                ::maat_sdk_wasm_rt::WasmGameContext::new().alloc_id()
                            };
                            let mut ctx = ::maat::__sdk::ComposeCtx {
                                alloc_id: &mut alloc,
                                by_seat: args.by_seat,
                            };
                            let game_ctx = ::maat_sdk_wasm_rt::WasmGameContext::new();
                            let events = #wasm_thunk(
                                args.target_level,
                                &args.history,
                                &game_ctx,
                                &mut ctx,
                                args.entity,
                            );
                            let result = ::maat_sdk_types::wasm_compose::LevelUpComposeResult {
                                events,
                                ctx_proposed: ::std::vec::Vec::new(),
                            };
                            ::maat_sdk_wasm_rt::postcard::to_allocvec(&result)
                                .unwrap_or_default()
                        }
                    },
                    "Wizard" => quote! {
                        (2i32, #idx_lit) => {
                            let args: ::maat_sdk_types::wasm_compose::WizardComposeArgs =
                                match ::maat_sdk_wasm_rt::postcard::from_bytes(payload) {
                                    Ok(a) => a,
                                    Err(_) => {
                                        unsafe { __MAAT_PLUGIN = Some(plugin); }
                                        return pack_err();
                                    }
                                };
                            let mut alloc = || {
                                use ::maat_sdk_types::plugin_slices::GameContext;
                                ::maat_sdk_wasm_rt::WasmGameContext::new().alloc_id()
                            };
                            let mut ctx = ::maat::__sdk::ComposeCtx {
                                alloc_id: &mut alloc,
                                by_seat: args.by_seat,
                            };
                            let game_ctx = ::maat_sdk_wasm_rt::WasmGameContext::new();
                            let compose_result = #wasm_thunk(
                                &args.trigger,
                                &args.history,
                                &game_ctx,
                                &mut ctx,
                            );
                            let pipeline_result = compose_result
                                .pipeline_result
                                .map(|d| match d {
                                    ::maat::__sdk::Decision::Allow =>
                                        ::maat_sdk_types::wasm_compose::WirePipelineResult::Allow,
                                    ::maat::__sdk::Decision::Deny(reason) =>
                                        ::maat_sdk_types::wasm_compose::WirePipelineResult::Deny { reason },
                                    _ => ::maat_sdk_types::wasm_compose::WirePipelineResult::Allow,
                                });
                            let result = ::maat_sdk_types::wasm_compose::WizardComposeResult {
                                effects: compose_result.effects,
                                pipeline_result,
                            };
                            ::maat_sdk_wasm_rt::postcard::to_allocvec(&result)
                                .unwrap_or_default()
                        }
                    },
                    "DistanceMetric" => quote! {
                        (3i32, #idx_lit) => {
                            let args: ::maat_sdk_types::wasm_compose::DistanceComposeArgs =
                                match ::maat_sdk_wasm_rt::postcard::from_bytes(payload) {
                                    Ok(a) => a,
                                    Err(_) => {
                                        unsafe { __MAAT_PLUGIN = Some(plugin); }
                                        return pack_err();
                                    }
                                };
                            let graph = ::maat::__sdk::BoardGraph::default();
                            let from = ::maat::__sdk::FaceId(args.from_face as u32);
                            let to = ::maat::__sdk::FaceId(args.to_face as u32);
                            let dist = #wasm_thunk(&graph, from, to);
                            let result = ::maat_sdk_types::wasm_compose::DistanceComposeResult {
                                distance: dist.map_err(|e| ::std::format!("{e:?}")),
                            };
                            ::maat_sdk_wasm_rt::postcard::to_allocvec(&result)
                                .unwrap_or_default()
                        }
                    },
                    _ => unreachable!(),
                }
            })
            .collect();

        let wasm_compose_export = quote! {
            #[cfg(target_arch = "wasm32")]
            #[no_mangle]
            pub extern "C" fn __maat_compose(
                kind: i32,
                fn_idx: i32,
                payload_ptr: i32,
                payload_len: i32,
            ) -> i64 {
                // Sentinel: (len=0, ptr=0) on any decode / lookup
                // failure. The host maps zero-length response to an
                // empty effects path + tracing breadcrumb.
                let pack_err = || 0i64;

                // 1. Read payload bytes from plugin memory.
                let payload = unsafe {
                    core::slice::from_raw_parts(
                        payload_ptr as *const u8,
                        payload_len as usize,
                    )
                };

                // 2. Restore the plugin singleton for the call (same
                //    pattern as __maat_dispatch_handler — take, use,
                //    restore on exit).
                let plugin = unsafe {
                    __MAAT_PLUGIN
                        .take()
                        .unwrap_or_else(<#plugin_path as ::std::default::Default>::default)
                };
                let _ = &plugin; // unused — thunks are free fns

                // 3. WASM1-narrow-shim — dispatch by (kind, fn_idx)
                //    directly to the per-method wasm-shape thunk
                //    (`__maat_compose_wasm_thunk_*`). Going through
                //    these concrete-ctx thunks instead of through
                //    `ComposeEntry`'s `&dyn`-shape `thunk` field is
                //    what enables per-method DCE on the user's
                //    `ctx.<method>()` call sites.
                let response_bytes: ::std::vec::Vec<u8> = match (kind, fn_idx) {
                    #( #wasm_method_dispatch_arms )*
                    _ => {
                        unsafe { __MAAT_PLUGIN = Some(plugin); }
                        return pack_err();
                    }
                };

                // Restore the singleton.
                unsafe { __MAAT_PLUGIN = Some(plugin); }

                // 4. Copy into host-allocated buffer + pack (len, ptr).
                let len = response_bytes.len() as i32;
                let ptr = unsafe {
                    extern "C" {
                        fn __maat_alloc(size: i32) -> i32;
                    }
                    __maat_alloc(len)
                };
                if ptr <= 0 {
                    return pack_err();
                }
                unsafe {
                    core::ptr::copy_nonoverlapping(
                        response_bytes.as_ptr(),
                        ptr as *mut u8,
                        len as usize,
                    );
                }
                ((len as u64) << 32 | (ptr as u32 as u64)) as i64
            }
        };


        quote! {
            #compose_thunks

            #[doc(hidden)]
            #[allow(non_snake_case)]
            pub fn #compose_fn_ident() -> ::std::vec::Vec<::maat::__sdk::ComposeEntry> {
                ::std::vec![ #( #compose_entries ),* ]
            }

            #[doc(hidden)]
            #[allow(non_upper_case_globals)]
            pub const #compose_slice_ident: fn() -> ::std::vec::Vec<::maat::__sdk::ComposeEntry>
                = #compose_fn_ident;

            impl #plugin_path {
                /// Macro-generated helper: walks the per-plugin
                /// `ComposeEntry` table emitted by `#[compose(...)]`
                /// methods and registers each entry through the
                /// appropriate `SetupContext` method. Native targets
                /// take the `Arc<dyn Fn>`-wrapping path; wasm32
                /// targets take the indexed registration path so the
                /// host can route `__maat_compose` calls back through
                /// the function-table index.
                #[doc(hidden)]
                pub fn register_composes(
                    ctx: &mut dyn ::maat::__sdk::SetupContext,
                ) {
                    let entries = #compose_fn_ident();
                    for (i, entry) in entries.iter().enumerate() {
                        entry.dispatch_register(ctx, i as u32);
                    }
                }
            }

            #wasm_compose_export
        }
    };

    let emitted = emitted.into_iter().fold(TokenStream::new(), |mut acc, t| {
        acc.extend(t);
        acc
    });

    // WASM1-5-whitelist-minimize: emit per-plugin category-flag
    // constants so `#[plugin]`'s `manifest_static()` can assemble a
    // *minimized* `declared_host_imports` allowlist instead of the
    // full 28-entry universe. We emit two bool consts:
    //
    //   - `__MAAT_HAS_COMPOSE_<STRUCT>`        — any `#[compose]` method
    //   - `__MAAT_HAS_WIZARD_COMPOSE_<STRUCT>` — any `#[compose(kind = Wizard, ...)]`
    //
    // `has_handlers` is derived in `#[plugin]` directly from
    // `__MAAT_HANDLERS_<STRUCT>.is_empty()`.
    //
    // Multi-block contract: composes can only appear in a single
    // `#[handlers]` block per plugin (existing constraint — see the
    // `compose_emit` comment above). On wasm32 this further requires
    // a single `#[handlers]` block per plugin to avoid duplicate-
    // const collisions for the flag emit below. All current wasm
    // fixtures use a single block; documented in AUTHORING.md.
    let has_compose_lit = !composes.is_empty();
    let has_wizard_compose_lit =
        composes.iter().any(|c| c.kind == "Wizard");
    let has_compose_ident = format_ident!(
        "__MAAT_HAS_COMPOSE_{}",
        plugin_struct_str.to_uppercase()
    );
    let has_wizard_compose_ident = format_ident!(
        "__MAAT_HAS_WIZARD_COMPOSE_{}",
        plugin_struct_str.to_uppercase()
    );
    let wasm_compose_flags_emit = quote! {
        #[cfg(target_arch = "wasm32")]
        #[doc(hidden)]
        #[allow(non_upper_case_globals)]
        pub const #has_compose_ident: bool = #has_compose_lit;

        #[cfg(target_arch = "wasm32")]
        #[doc(hidden)]
        #[allow(non_upper_case_globals)]
        pub const #has_wizard_compose_ident: bool = #has_wizard_compose_lit;
    };

    // WASM1-5-whitelist-minimize-deep — body-level call-graph
    // analysis (Tier 2 / sub-trait only). Walk every method body
    // in the impl block, collect method-call names, resolve
    // through `SUBTRAIT_METHOD_TO_IMPORTS`, and emit a `&[&str]`
    // const that `#[plugin]`'s `manifest_static()` reads to
    // assemble the per-plugin sub-trait allowlist.
    //
    // The visitor walks ALL method bodies (not just `#[handler]`
    // / `#[compose]`-annotated ones) so plugin author helpers
    // that internally call `ctx.spatial()` etc. are caught.
    // False-negative surface (helpers in OTHER impl blocks /
    // free functions) is documented; in practice plugin authors
    // put helpers in the same `impl Plugin {}` block alongside
    // the handlers.
    //
    // **Why sub-trait only?** Direct `GameContext` methods are
    // reached via the `WasmGameContext` vtable, and LLVM does
    // not DCE individual vtable entries even with `lto = true`.
    // So if ANY `#[handler]` or `#[compose(kind = Wizard)]`
    // exists, the wasm module imports the full Tier 1 surface
    // (`CTX_VTABLE_BASE`) regardless of which methods the
    // author actually calls. Tier 1 inclusion is decided in
    // `#[plugin]`'s `manifest_static()` from the `has_handlers`
    // and `has_wizard_compose` flags; Tier 2 is this body-level
    // sub-trait collection.
    let deep_imports: Vec<&'static str> = collect_called_subtrait_imports(&item);
    let deep_imports_lit: Vec<TokenStream> = deep_imports
        .iter()
        .map(|s| quote! { #s })
        .collect();
    let deep_imports_ident = format_ident!(
        "__MAAT_DECLARED_CTX_IMPORTS_{}",
        plugin_struct_str.to_uppercase()
    );
    let wasm_deep_imports_emit = quote! {
        /// WASM1-5-whitelist-minimize-deep — Tier 2 sub-trait
        /// `__maat_ctx_*` host-import allowlist derived by
        /// walking each method body in the `#[handlers]` impl
        /// block. Read by `#[plugin]`'s `manifest_static()` and
        /// unioned with the always-included setup base +
        /// compose-registration (iff `__MAAT_HAS_COMPOSE_<STRUCT>`)
        /// + Tier 1 vtable base (iff handlers / Wizard compose).
        #[cfg(target_arch = "wasm32")]
        #[doc(hidden)]
        #[allow(non_upper_case_globals)]
        pub const #deep_imports_ident: &[&'static str] = &[ #( #deep_imports_lit ),* ];
    };

    quote! {
        #item
        #emitted
        #wasm_emit
        #wasm_compose_flags_emit
        #wasm_deep_imports_emit
        #compose_emit
    }
}

/// Legacy free-standing `#[handler]` attribute. Phase 5a folds method
/// handlers under `#[handlers]` (on the impl block) instead, where
/// the macro can emit free-standing items at module scope. Direct
/// `#[handler]` on a method emits a clear error directing to the new
/// shape.
pub fn handler_impl(_args: TokenStream, input: TokenStream) -> TokenStream {
    let func: Result<ItemFn, _> = parse2(input.clone());
    let span = match &func {
        Ok(f) => f.sig.ident.span(),
        Err(_) => proc_macro2::Span::call_site(),
    };
    syn::Error::new(
        span,
        "Phase 5a (S25): put `#[handler(...)]` on a `&self` method *inside* an `impl PluginStruct` \
         block annotated with `#[handlers]`. Free-standing handler functions are no longer \
         supported — handlers must be methods so they can read state from `&self`.",
    )
    .to_compile_error()
}

#[derive(Default)]
struct PluginArgs {
    canonical_id: Option<String>,
    display_name: Option<String>,
    authors: Vec<String>,
    description: Option<String>,
    license: Option<String>,
    repository: Option<String>,
    /// M2: cross-plugin dependency declarations. Shorthand form is a
    /// list of canonical-id strings; each entry expands to a
    /// `PluginDependency { canonical_id, required: true,
    /// linked_against: ([0u8;32], [0u8;32]), needs:
    /// StructuralNeeds::default() }`. The real `linked_against`
    /// hashes are filled in at install-time.
    dependencies: Vec<String>,
    /// C2-remainder: plugin-declared scenarios. Each entry is a
    /// `(name, description, entity_count)` triple expanded into a
    /// `ScenarioDeclaration`. The list surfaces in the
    /// `GET /scenarios` route after install.
    scenarios: Vec<(String, String, u32)>,
}

fn parse_plugin_args(args: TokenStream) -> syn::Result<PluginArgs> {
    let mut out = PluginArgs::default();
    for meta in parse_kvs(args)? {
        let Meta::NameValue(nv) = meta else {
            return Err(syn::Error::new_spanned(meta, "expected `key = value` form"));
        };
        let key = nv
            .path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key.as_str() {
            "canonical_id" => out.canonical_id = Some(expect_str(&nv.value)?),
            "display_name" => out.display_name = Some(expect_str(&nv.value)?),
            "description" => out.description = Some(expect_str(&nv.value)?),
            "license" => out.license = Some(expect_str(&nv.value)?),
            "repository" => out.repository = Some(expect_str(&nv.value)?),
            "authors" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.authors.push(expect_str(&el)?);
                    }
                }
                v => return Err(syn::Error::new_spanned(v, "authors must be an array of strings")),
            },
            "dependencies" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.dependencies.push(expect_str(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "dependencies must be an array of canonical_id strings (e.g. dependencies = [\"github.com/.../base-game\"])",
                    ))
                }
            },
            "scenarios" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.scenarios.push(parse_scenario_entry(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "scenarios must be an array of (name = \"…\", description = \"…\", entity_count = N) tuples",
                    ))
                }
            },
            other => {
                return Err(syn::Error::new_spanned(
                    nv.path,
                    format!("unknown plugin key `{other}`"),
                ));
            }
        }
    }
    Ok(out)
}

/// Parse one scenarios-array entry of the form
/// `(name = "…", description = "…", entity_count = N)`. The entry is
/// modelled as a tuple expression (`Expr::Tuple`) whose three
/// elements are themselves `Expr::Assign` of `Path = Literal`. The
/// keys can appear in any order; `entity_count` defaults to 0 when
/// omitted.
fn parse_scenario_entry(e: &Expr) -> syn::Result<(String, String, u32)> {
    let Expr::Tuple(tuple) = e else {
        return Err(syn::Error::new_spanned(
            e.to_token_stream(),
            "expected `(name = \"…\", description = \"…\", entity_count = N)` tuple",
        ));
    };
    let mut name: Option<String> = None;
    let mut description: Option<String> = None;
    let mut entity_count: u32 = 0;
    for field in &tuple.elems {
        let Expr::Assign(assign) = field else {
            return Err(syn::Error::new_spanned(
                field.to_token_stream(),
                "scenarios entry fields must be `key = value`",
            ));
        };
        let key_path = match &*assign.left {
            Expr::Path(ExprPath { path, .. }) => path,
            other => {
                return Err(syn::Error::new_spanned(
                    other.to_token_stream(),
                    "scenarios entry key must be an identifier",
                ));
            }
        };
        let key = key_path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key.as_str() {
            "name" => name = Some(expect_str(&assign.right)?),
            "description" => description = Some(expect_str(&assign.right)?),
            "entity_count" => {
                let Expr::Lit(ExprLit { lit: Lit::Int(li), .. }) = &*assign.right else {
                    return Err(syn::Error::new_spanned(
                        assign.right.to_token_stream(),
                        "entity_count must be an integer literal",
                    ));
                };
                entity_count = li
                    .base10_parse::<u32>()
                    .map_err(|err| syn::Error::new_spanned(li, err.to_string()))?;
            }
            other => {
                return Err(syn::Error::new_spanned(
                    key_path,
                    format!("unknown scenarios entry key `{other}` (expected name/description/entity_count)"),
                ));
            }
        }
    }
    let Some(name) = name else {
        return Err(syn::Error::new_spanned(
            e.to_token_stream(),
            "scenarios entry missing `name`",
        ));
    };
    let description = description.unwrap_or_default();
    Ok((name, description, entity_count))
}

fn expect_str(e: &Expr) -> syn::Result<String> {
    match e {
        Expr::Lit(ExprLit {
            lit: Lit::Str(s), ..
        }) => Ok(s.value()),
        _ => Err(syn::Error::new_spanned(
            e.to_token_stream(),
            "expected string literal",
        )),
    }
}

/// M2 — emit the `let dependencies: Vec<PluginDependency> = vec![...]`
/// binding that the manifest struct literal then moves by name.
///
/// `deps` is the parsed list of canonical-id strings from the
/// `dependencies = ["..."]` macro argument. Each entry expands to a
/// `PluginDependency` with `required: true`, `linked_against`
/// zero-hashes (filled in at install-time resolution), and
/// `StructuralNeeds::default()`. Empty `deps` emits an empty `Vec`
/// (back-compat with plugins that don't declare deps).
///
/// The same binding shape is consumed by the four manifest emission
/// sites in `#[plugin]` (native + wasm32), `#[capability_plugin]`,
/// and `#[view_plugin]` — they all reference `dependencies` by
/// identifier in two places: `compute_interface_hash(..., &dependencies)`
/// (so deps contribute to the interface hash via their canonical-id +
/// linked_against slots) and the `PluginManifest { dependencies, ... }`
/// struct-literal shorthand.
/// C2-remainder — emit the `let declared_scenarios: Vec<ScenarioDeclaration>`
/// binding consumed by the manifest assemblers (struct literal +
/// `compute_interface_hash` call). Empty `scenarios` emits an empty
/// `Vec`, matching pre-C2R plugins.
fn emit_scenarios_init(scenarios: &[(String, String, u32)]) -> TokenStream {
    if scenarios.is_empty() {
        quote! {
            let declared_scenarios: ::std::vec::Vec<
                ::maat::__sdk::ScenarioDeclaration,
            > = ::std::vec::Vec::new();
        }
    } else {
        let entries = scenarios.iter().map(|(name, description, count)| {
            let count_lit = proc_macro2::Literal::u32_unsuffixed(*count);
            quote! {
                ::maat::__sdk::ScenarioDeclaration {
                    name: #name.to_string(),
                    description: #description.to_string(),
                    entity_count: #count_lit,
                }
            }
        });
        quote! {
            let declared_scenarios: ::std::vec::Vec<
                ::maat::__sdk::ScenarioDeclaration,
            > = ::std::vec![ #( #entries ),* ];
        }
    }
}

fn emit_dependencies_init(deps: &[String]) -> TokenStream {
    if deps.is_empty() {
        quote! {
            let dependencies: ::std::vec::Vec<
                ::maat::__sdk::PluginDependency,
            > = ::std::vec::Vec::new();
        }
    } else {
        let entries = deps.iter().map(|cid| {
            quote! {
                ::maat::__sdk::PluginDependency {
                    // 0580: from_static is compile-time validated; if the
                    // user's literal is malformed the const-eval surfaces it.
                    canonical_id: ::maat::__sdk::CanonicalId::from_static(#cid),
                    required: true,
                    linked_against: ([0u8; 32], [0u8; 32]),
                    needs: ::maat::__sdk::StructuralNeeds::default(),
                }
            }
        });
        quote! {
            let dependencies: ::std::vec::Vec<
                ::maat::__sdk::PluginDependency,
            > = ::std::vec![ #( #entries ),* ];
        }
    }
}

/// Emit the linkme-slice walker that populates a `PluginManifest`'s
/// five `declared_*` fields (`declared_types`, `declared_handlers`,
/// `declared_capabilities`, `declared_phases`, `declared_wire_frames`).
///
/// The returned `TokenStream` is meant to be spliced inside the
/// `MANIFEST.get_or_init(|| { ... })` closure of a plugin macro
/// (`#[plugin]`, `#[capability_plugin]`, `#[view_plugin]`) BEFORE the
/// `PluginManifest { ... }` struct literal. It declares five local
/// `let mut declared_<kind>: Vec<_>` bindings that the struct literal
/// then moves into the manifest by name.
///
/// Filtering rule: `REGISTERED_HANDLERS` entries are keyed by
/// `(plugin_crate_name, plugin_struct)` because handler thunks
/// dispatch into a specific plugin's `&self` methods; the walker
/// compares both. All other registration kinds (`REGISTERED_ENTITIES`,
/// `REGISTERED_CAPABILITIES`, `REGISTERED_PHASES`,
/// `REGISTERED_WIRE_FRAMES`) carry only `plugin_crate_name` and the
/// walker filters by crate alone — multi-plugin-per-crate
/// disambiguation lives solely on the handler slice (P5, 2026-05-21).
///
/// `declared_handlers` is sorted by `method_name` so the position
/// (= `local_id`) matches the macro-emitted dispatch table. The same
/// sort is applied for capability and view plugins for consistency,
/// even though they don't currently dispatch by `local_id` — this
/// keeps the manifest deterministic regardless of plugin kind.
///
/// Wire-frame registrations: the `REGISTERED_WIRE_FRAMES` slice
/// exists but no producer-side macro currently pushes into it. The
/// walker is still wired here so that once wire-frame registration
/// lands (e.g. as a side effect of `#[derive(WireFrameHash)]`), all
/// three plugin kinds pick it up uniformly.
fn emit_manifest_decl_walker(struct_ident_str: &str) -> TokenStream {
    quote! {
        let mut declared_types: ::std::vec::Vec<
            ::maat::__sdk::TypeDeclaration,
        > = ::std::vec::Vec::new();
        for reg in ::maat::__sdk::REGISTERED_ENTITIES.iter() {
            if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                continue;
            }
            declared_types.push(::maat::__sdk::TypeDeclaration {
                type_name: reg.type_name.to_string(),
                type_id: reg.type_id,
                trait_set: reg.trait_set(),
                trait_data_signature: reg.trait_data_signature,
            });
        }

        // Collect handlers, then sort by method_name so local_id
        // ordering matches dispatch_handler's table (model plugin)
        // and stays deterministic for capability / view plugins.
        let mut handler_regs: ::std::vec::Vec<
            &::maat::__sdk::HandlerRegistration,
        > = ::std::vec::Vec::new();
        for reg in ::maat::__sdk::REGISTERED_HANDLERS.iter() {
            if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                continue;
            }
            if reg.plugin_struct != #struct_ident_str {
                continue;
            }
            handler_regs.push(reg);
        }
        handler_regs.sort_by_key(|r| r.method_name);
        let mut declared_handlers: ::std::vec::Vec<
            ::maat::__sdk::HandlerDeclaration,
        > = ::std::vec::Vec::with_capacity(handler_regs.len());
        for reg in &handler_regs {
            declared_handlers.push(::maat::__sdk::HandlerDeclaration {
                name: reg.name.to_string(),
                event_kind: reg.event_kind,
                role: reg.role,
                priority: reg.priority,
                signature: reg.signature,
            });
        }
        let mut declared_capabilities: ::std::vec::Vec<
            ::maat::__sdk::CapabilityDeclaration,
        > = ::std::vec::Vec::new();
        for reg in ::maat::__sdk::REGISTERED_CAPABILITIES.iter() {
            if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                continue;
            }
            declared_capabilities.push(
                ::maat::__sdk::CapabilityDeclaration {
                    name: reg.name.to_string(),
                    signature: reg.signature,
                },
            );
        }
        let mut declared_phases: ::std::vec::Vec<
            ::maat::__sdk::PhaseDeclaration,
        > = ::std::vec::Vec::new();
        for reg in ::maat::__sdk::REGISTERED_PHASES.iter() {
            if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                continue;
            }
            declared_phases.push(::maat::__sdk::PhaseDeclaration {
                phase_id: reg.phase_id,
                signature: reg.signature,
            });
        }
        let mut declared_wire_frames: ::std::vec::Vec<
            ::maat::__sdk::WireFrameDeclaration,
        > = ::std::vec::Vec::new();
        for reg in ::maat::__sdk::REGISTERED_WIRE_FRAMES.iter() {
            if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                continue;
            }
            declared_wire_frames.push(
                ::maat::__sdk::WireFrameDeclaration {
                    frame_name: reg.frame_name.to_string(),
                    signature: reg.signature,
                },
            );
        }
    }
}

/// `#[plugin(...)]` — emit the inherent `manifest()` method (filtering
/// the linkme slices by crate + struct) and the `ModelPlugin` impl
/// boilerplate. The annotated item is a struct definition.
///
/// Phase 5a (S25): also emits the `ModelPlugin::dispatch_handler`
/// implementation. The dispatch table is constructed lazily from
/// `REGISTERED_HANDLERS` filtered to this `(plugin_crate_name,
/// plugin_struct)` and sorted by `method_name`. The manifest builds
/// `declared_handlers` with the same sort key, so the `local_id` in
/// both stays aligned.
pub fn plugin_impl(args: TokenStream, input: TokenStream) -> TokenStream {
    let pargs = match parse_plugin_args(args) {
        Ok(p) => p,
        Err(e) => return e.to_compile_error(),
    };
    let item: ItemStruct = match parse2(input.clone()) {
        Ok(s) => s,
        Err(e) => return e.to_compile_error(),
    };
    let struct_ident = item.ident.clone();
    let struct_ident_str = struct_ident.to_string();

    // WASM1 Phase 3c — predictable name for the handler-table slice
    // emitted by `#[handlers]` in the same plugin crate. Used by both
    // the wasm32 `manifest_static()` (to populate
    // `declared_handlers`) and the wasm32 `__maat_dispatch_handler`
    // (to route by `LocalHandlerId`).
    let wasm_slice_ident_for_plugin = format_ident!(
        "__MAAT_HANDLERS_{}",
        struct_ident_str.to_uppercase()
    );

    // WASM1-5-whitelist-minimize — predictable names for the compose
    // category flags emitted by `#[handlers]` so `manifest_static()`
    // can pick a minimized `declared_host_imports` allowlist.
    let has_compose_ident_for_plugin = format_ident!(
        "__MAAT_HAS_COMPOSE_{}",
        struct_ident_str.to_uppercase()
    );
    let has_wizard_compose_ident_for_plugin = format_ident!(
        "__MAAT_HAS_WIZARD_COMPOSE_{}",
        struct_ident_str.to_uppercase()
    );

    // WASM1-5-whitelist-minimize-deep — predictable name for the
    // body-level call-graph allowlist emitted by `#[handlers]`.
    // `manifest_static()` reads this `&[&'static str]` and unions
    // it with the always-included setup base + compose-registration
    // (iff has_compose).
    let deep_imports_ident_for_plugin = format_ident!(
        "__MAAT_DECLARED_CTX_IMPORTS_{}",
        struct_ident_str.to_uppercase()
    );

    let canonical = pargs
        .canonical_id
        .clone()
        .unwrap_or_else(|| format!("local:{struct_ident_str}"));
    let display = pargs.display_name.unwrap_or_else(|| struct_ident_str.clone());
    let description = pargs.description.unwrap_or_default();
    let license = pargs.license.unwrap_or_else(|| "UNLICENSED".to_string());
    let repository = pargs.repository;
    let authors = pargs.authors;

    let repo_expr = match repository {
        Some(s) => quote! { Some(#s.to_string()) },
        None => quote! { None },
    };

    let decl_walker = emit_manifest_decl_walker(&struct_ident_str);
    let deps_init = emit_dependencies_init(&pargs.dependencies);
    let scenarios_init = emit_scenarios_init(&pargs.scenarios);

    quote! {
        #item

        // The manifest assembler + dispatch helper walk linkme slices
        // that don't exist on wasm32. Gate the impl entirely so WASM
        // clients still compile when a plugin crate happens to be in
        // their dep graph.
        #[cfg(not(target_arch = "wasm32"))]
        impl #struct_ident {
            /// Macro-generated. Walks the linkme slices, filters by
            /// `CARGO_PKG_NAME == env!("CARGO_PKG_NAME")` and
            /// `plugin_struct == "<this struct>"`, and assembles a
            /// `PluginManifest`. Cached in a `OnceLock`.
            ///
            /// Handlers are sorted by `method_name` so their position
            /// is the `local_id` used by `dispatch_handler`.
            pub fn manifest() -> &'static ::maat::__sdk::PluginManifest {
                static MANIFEST: ::std::sync::OnceLock<::maat::__sdk::PluginManifest>
                    = ::std::sync::OnceLock::new();
                MANIFEST.get_or_init(|| {
                    // 0580: from_static — compile-time validated.
                    let canonical_id = ::maat::__sdk::CanonicalId::from_static(#canonical);
                    #decl_walker

                    let content_hash = ::maat::__sdk::CanonicalId::new(#canonical)
                        .ok()
                        .map(|_| {})
                        .map(|_| {
                            // env!("METAGAMER_PLUGIN_CONTENT_HASH") is set by build.rs.
                            // Plugins without a build.rs (tests, internal tooling) get
                            // the all-zero hash; the install path treats that as a
                            // "dev" plugin but otherwise functions normally.
                            let raw: &str = option_env!("METAGAMER_PLUGIN_CONTENT_HASH")
                                .unwrap_or("");
                            decode_hash(raw)
                        })
                        .unwrap_or([0u8; 32]);
                    fn decode_hash(s: &str) -> [u8; 32] {
                        if s.len() != 64 { return [0u8; 32]; }
                        let mut out = [0u8; 32];
                        let bytes = s.as_bytes();
                        for i in 0..32 {
                            let hi = match bytes[i*2] {
                                b'0'..=b'9' => bytes[i*2] - b'0',
                                b'a'..=b'f' => bytes[i*2] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            let lo = match bytes[i*2+1] {
                                b'0'..=b'9' => bytes[i*2+1] - b'0',
                                b'a'..=b'f' => bytes[i*2+1] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2+1] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            out[i] = (hi << 4) | lo;
                        }
                        out
                    }

                    #deps_init
                    #scenarios_init
                    let interface_hash =
                        ::maat::__sdk::compute_interface_hash(
                            &declared_types,
                            &declared_handlers,
                            &declared_capabilities,
                            &declared_phases,
                            &declared_wire_frames,
                            &dependencies,
                            &declared_scenarios,
                        );

                    ::maat::__sdk::PluginManifest {
                        canonical_id,
                        display_name: #display.to_string(),
                        kind: ::maat::__sdk::PluginKind::Model,
                        authors: vec![ #( #authors.to_string() ),* ],
                        description: #description.to_string(),
                        license: ::maat::__sdk::SpdxLicenseId::new(#license),
                        repository: #repo_expr,
                        content_hash,
                        interface_hash,
                        declared_types,
                        declared_handlers,
                        declared_capabilities,
                        declared_phases,
                        declared_wire_frames,
                        dependencies,
                        phase_descriptors: ::std::vec::Vec::new(),
                        version: ::maat::__sdk::parse_semver_or_default(env!("CARGO_PKG_VERSION")),
                        price_cents: ::core::option::Option::None,
                        manifest_format_version: 1,
                        dispatch_abi_version: ::maat::__sdk::DISPATCH_ABI_V1,
                        signer_key: [0u8; 32],
                        requested_memory_bytes: ::core::option::Option::None,
                        requested_fuel_per_call: ::core::option::Option::None,
                        requested_epoch_deadline_ticks: ::core::option::Option::None,
                        declared_host_imports: ::std::vec::Vec::new(),
                        backend: ::maat::__sdk::PluginBackend::Static,
                        declared_scenarios,
                    }
                })
            }

            /// Macro-generated dispatch table — `local_id` indexes
            /// into a sorted-by-method-name vector of thunks. Aligned
            /// with `manifest().declared_handlers` which uses the
            /// same sort key.
            #[doc(hidden)]
            pub fn __maat_dispatch_table()
                -> &'static [::maat::__sdk::HandlerThunk]
            {
                static TABLE: ::std::sync::OnceLock<
                    ::std::vec::Vec<::maat::__sdk::HandlerThunk>,
                > = ::std::sync::OnceLock::new();
                TABLE.get_or_init(|| {
                    let mut regs: ::std::vec::Vec<
                        &::maat::__sdk::HandlerRegistration,
                    > = ::std::vec::Vec::new();
                    for reg in ::maat::__sdk::REGISTERED_HANDLERS.iter() {
                        if reg.plugin_crate_name != env!("CARGO_PKG_NAME") {
                            continue;
                        }
                        if reg.plugin_struct != #struct_ident_str {
                            continue;
                        }
                        regs.push(reg);
                    }
                    regs.sort_by_key(|r| r.method_name);
                    regs.iter().map(|r| r.thunk).collect()
                })
            }

        }

        // -------------------------------------------------------------
        // WASM1 Phase 3a-macro — wasm32-unknown-unknown codegen.
        //
        // The native impl above walks linkme slices that don't exist
        // on wasm32 (no global ctor support → no `distributed_slice`).
        // For wasm32 plugins we emit a parallel impl with:
        //
        //   - `manifest_static()` — builds a `PluginManifest` from
        //     the author-declared identity fields plus empty
        //     handler / entity / capability / phase / wire_frame
        //     lists. Phase 3c will populate the handler list from
        //     `#[handlers]`-collected metadata; for Phase 3a-macro
        //     `run_setup` is the only entry point exercised so the
        //     empty handler list is acceptable.
        //
        //   - `__MAAT_PLUGIN: static mut Option<Self>` — plugin
        //     singleton. `__maat_run_setup` takes it via
        //     `Option::take` and restores after the call.
        //
        //   - `__maat_start() -> (i32, i32)` — runtime-init function
        //     called by `WasmPlugin::new` immediately after
        //     instantiate. Builds the manifest, postcard-encodes it,
        //     leaks the buffer, and returns the resulting `(ptr,
        //     len)` for the host to read.
        //
        //   - `__maat_run_setup()` — the entry point
        //     `WasmPlugin::run_setup_inner` calls. Builds a
        //     `WasmSetupCtx`, primes the seat cache, and invokes
        //     the plugin's `ModelPlugin::run_setup` impl.
        //
        // The plugin struct MUST `impl Default`; we enforce that via
        // a typed function-pointer assertion inside `__maat_start`
        // (a missing `Default` impl makes the assertion fail at
        // codegen time with a "the trait `Default` is not
        // implemented" error pointing at this site).
        #[cfg(target_arch = "wasm32")]
        impl #struct_ident {
            /// Macro-generated. Returns the manifest declared via the
            /// `#[plugin(...)]` attribute. Used by `__maat_start` to
            /// produce the bytes the host reads at instantiate time.
            ///
            /// WASM1 Phase 3c populates `declared_handlers` by walking
            /// the macro-emitted `__MAAT_HANDLERS_<STRUCT>` slice
            /// (assembled by `#[handlers]`). Plugins without a
            /// `#[handlers]` block must provide an empty slice
            /// manually (the symbol IS required because the
            /// wasm32 `__maat_dispatch_handler` references it).
            /// Entity / capability / phase / wire_frame lists stay
            /// empty pending Phase 3c-compose.
            pub fn manifest_static() -> ::maat::__sdk::PluginManifest {
                // 0580: from_static — compile-time validated.
                let canonical_id = ::maat::__sdk::CanonicalId::from_static(#canonical);
                let mut declared_handlers: ::std::vec::Vec<
                    ::maat::__sdk::HandlerDeclaration,
                > = ::std::vec::Vec::with_capacity(
                    #wasm_slice_ident_for_plugin.len(),
                );
                for entry in #wasm_slice_ident_for_plugin.iter() {
                    declared_handlers.push(::maat::__sdk::HandlerDeclaration {
                        name: entry.name.to_string(),
                        event_kind: entry.event_kind,
                        role: entry.role,
                        priority: entry.priority,
                        signature: [0u8; 32],
                    });
                }
                let declared_types: ::std::vec::Vec<
                    ::maat::__sdk::TypeDeclaration,
                > = ::std::vec::Vec::new();
                let declared_capabilities: ::std::vec::Vec<
                    ::maat::__sdk::CapabilityDeclaration,
                > = ::std::vec::Vec::new();
                let declared_phases: ::std::vec::Vec<
                    ::maat::__sdk::PhaseDeclaration,
                > = ::std::vec::Vec::new();
                let declared_wire_frames: ::std::vec::Vec<
                    ::maat::__sdk::WireFrameDeclaration,
                > = ::std::vec::Vec::new();
                #deps_init
                #scenarios_init
                let interface_hash =
                    ::maat::__sdk::compute_interface_hash(
                        &declared_types,
                        &declared_handlers,
                        &declared_capabilities,
                        &declared_phases,
                        &declared_wire_frames,
                        &dependencies,
                        &declared_scenarios,
                    );
                ::maat::__sdk::PluginManifest {
                    canonical_id,
                    display_name: #display.to_string(),
                    kind: ::maat::__sdk::PluginKind::Model,
                    authors: ::std::vec![ #( #authors.to_string() ),* ],
                    description: #description.to_string(),
                    license: ::maat::__sdk::SpdxLicenseId::new(#license),
                    repository: #repo_expr,
                    content_hash: [0u8; 32],
                    interface_hash,
                    declared_types,
                    declared_handlers,
                    declared_capabilities,
                    declared_phases,
                    declared_wire_frames,
                    dependencies,
                    phase_descriptors: ::std::vec::Vec::new(),
                    version: ::maat::__sdk::parse_semver_or_default(env!("CARGO_PKG_VERSION")),
                    price_cents: ::core::option::Option::None,
                    manifest_format_version: 1,
                    dispatch_abi_version: ::maat::__sdk::DISPATCH_ABI_V1,
                    signer_key: [0u8; 32],
                    requested_memory_bytes: ::core::option::Option::None,
                    requested_fuel_per_call: ::core::option::Option::None,
                    requested_epoch_deadline_ticks: ::core::option::Option::None,
                    // WASM1-5-whitelist-minimize: per-plugin
                    // *category-based* host-import allowlist. The
                    // macro inspects the `#[handlers]` / `#[compose]`
                    // attributes that the plugin author declared and
                    // emits only the import categories the plugin
                    // can plausibly reach. Body-level analysis
                    // (parsing handler bodies to detect specific
                    // ctx.foo() calls) is the deeper follow-up
                    // (WASM1-5-whitelist-minimize-deep).
                    //
                    // Categories — derived from the canonical
                    // `extern "C"` blocks in core/sdk-wasm-rt/src/lib.rs:
                    //
                    //   Setup base — always included; powers
                    //     `__maat_run_setup` / `__maat_alloc` / etc.
                    //   Compose registration — included iff the
                    //     plugin declared any `#[compose]` method.
                    //   GameContext value reads / iterators / queries —
                    //     included iff the plugin declared any
                    //     `#[handler]` method OR a Wizard
                    //     `#[compose]` (Wizard composes get a
                    //     `&dyn GameContext` mid-call).
                    //   GameContext alloc_id — included iff the
                    //     plugin has any `#[handler]` OR `#[compose]`
                    //     (all compose kinds get alloc_id via
                    //     `ComposeCtx.alloc_id` after WASM1-3c-compose-alloc-id).
                    // WASM1-5-whitelist-minimize-deep — assemble
                    // the per-plugin allowlist from four sources:
                    //
                    //   1. **Setup base** — always included
                    //      (`__maat_alloc` + the five setup
                    //      methods). These are invoked by the
                    //      runtime / `run_setup` body. We don't
                    //      body-analyze `run_setup` (it lives in
                    //      `impl ModelPlugin for X`, not in the
                    //      `#[handlers]` block). Over-declaring
                    //      unused setup-base imports is harmless
                    //      — the loader checks declared ⊇ actual,
                    //      not declared = actual.
                    //   2. **Compose registration** — included
                    //      iff `__MAAT_HAS_COMPOSE_<STRUCT>` is
                    //      `true`. Called from `register_composes`
                    //      via `entry.dispatch_register(ctx,
                    //      compose_idx)`, which is itself called
                    //      from `run_setup`.
                    //   3. **Tier 1 vtable base** (12 direct
                    //      GameContext methods) — included iff
                    //      `has_handlers || has_wizard_compose`.
                    //      These are vtable-pulled by the
                    //      `WasmGameContext` impl in `sdk-wasm-rt`;
                    //      LLVM doesn't DCE them per-method.
                    //   4. **Tier 2 sub-trait imports** — derived
                    //      by `#[handlers]` walking every method
                    //      body (`MethodCallCollector` +
                    //      `SUBTRAIT_METHOD_TO_IMPORTS`). Emitted
                    //      as `__MAAT_DECLARED_CTX_IMPORTS_<STRUCT>:
                    //      &[&'static str]`. Sub-trait methods
                    //      are NOT vtable-pulled (they're called
                    //      on stack-allocated ZST shims), so the
                    //      wasm linker prunes unused ones.
                    declared_host_imports: {
                        let has_handlers =
                            !#wasm_slice_ident_for_plugin.is_empty();
                        let has_compose = #has_compose_ident_for_plugin;
                        let has_wizard_compose =
                            #has_wizard_compose_ident_for_plugin;
                        let deep_ctx: &[&'static str] =
                            #deep_imports_ident_for_plugin;
                        let mut imports: ::std::vec::Vec<::std::string::String> =
                            ::std::vec::Vec::with_capacity(
                                6 + 4 + 12 + deep_ctx.len(),
                            );
                        // 1. Setup base (always).
                        imports.push("__maat_alloc".to_string());
                        imports.push("__maat_set_turn_order".to_string());
                        imports.push("__maat_spawn_blueprint".to_string());
                        imports.push("__maat_set_view_template".to_string());
                        imports.push(
                            "__maat_register_entity_catalog_entry".to_string(),
                        );
                        imports.push("__maat_activate_source".to_string());
                        // 2. Compose registration (iff any #[compose]).
                        if has_compose {
                            imports.push(
                                "__maat_register_creation_spec".to_string(),
                            );
                            imports.push(
                                "__maat_register_level_up_spec".to_string(),
                            );
                            imports.push(
                                "__maat_register_wizard_spec".to_string(),
                            );
                            imports.push(
                                "__maat_register_distance_metric".to_string(),
                            );
                        }
                        // 3. Tier 1 vtable base (iff any
                        //    &dyn GameContext consumer). Includes
                        //    sub-trait imports too — sub-trait
                        //    getters return `&dyn SubTrait`, whose
                        //    vtables are also non-DCE'd by LLVM.
                        if has_handlers || has_wizard_compose {
                            // Direct GameContext methods (12).
                            imports.push(
                                "__maat_ctx_current_player".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_entity_view_for".to_string(),
                            );
                            imports.push("__maat_ctx_players".to_string());
                            imports.push("__maat_ctx_entities_at".to_string());
                            imports.push(
                                "__maat_ctx_is_owners_turn".to_string(),
                            );
                            imports.push("__maat_ctx_contained_by".to_string());
                            imports.push("__maat_ctx_seat_role_for".to_string());
                            imports.push("__maat_ctx_alloc_id".to_string());
                            imports.push("__maat_ctx_settings".to_string());
                            imports.push(
                                "__maat_ctx_this_turn_events".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_this_phase_events".to_string(),
                            );
                            imports.push("__maat_ctx_since".to_string());
                            // Sub-trait surface (6) — vtable-pulled
                            // via `&dyn PredicateQuery` etc.
                            imports.push(
                                "__maat_ctx_predicate_eval".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_vision_can_see".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_vision_perceivability".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_spatial_entities_at".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_spatial_distance".to_string(),
                            );
                            imports.push(
                                "__maat_ctx_spatial_entities_in_area"
                                    .to_string(),
                            );
                        }
                        // 4. Tier 2 body-derived sub-trait imports.
                        //    Only meaningful for plugins WITHOUT
                        //    any `&dyn GameContext` consumer (e.g.
                        //    non-Wizard compose-only plugins that
                        //    nevertheless reach a sub-trait through
                        //    some other path). For handler-bearing
                        //    plugins, these are already covered
                        //    by Tier 1; the dedup happens at the
                        //    `WasmPlugin::new` level (it builds
                        //    a HashSet from `declared_host_imports`).
                        for name in deep_ctx.iter() {
                            imports.push((*name).to_string());
                        }
                        imports
                    },
                    backend: ::maat::__sdk::PluginBackend::Wasm,
                    declared_scenarios,
                }
            }
        }

        #[cfg(target_arch = "wasm32")]
        const _: () = {
            // Enforce `impl Default for #struct_ident` at macro
            // expansion time. If the plugin author forgets the
            // derive, this line fails with a clear
            // `the trait Default is not implemented for ...` error
            // pointing at the type rather than at a generated
            // function body downstream.
            #[allow(dead_code)]
            const fn __assert_default<T: Default>() {}
            const _: () = __assert_default::<#struct_ident>();
        };

        #[cfg(target_arch = "wasm32")]
        #[allow(non_upper_case_globals)]
        static mut __MAAT_PLUGIN: Option<#struct_ident> = None;

        /// WASM1 Phase 3a-macro start function. Called by the host
        /// once after instantiate to produce the manifest blob.
        /// Returns an `i32` pointing at a 2-element `[ptr, len]`
        /// array in plugin linear memory; the host reads 8 bytes
        /// from that address to recover the manifest blob location.
        ///
        /// The manifest buffer itself is leaked (single allocation,
        /// lifetime = plugin instance, low-KiB scale). The `[ptr,
        /// len]` array is a `static mut` so its address is stable.
        ///
        /// Why the indirection rather than `-> (i32, i32)` directly?
        /// Rust's wasm32 ABI passes multi-value tuple returns via an
        /// implicit sret pointer, not as a wasm multi-value return.
        /// The wasmtime side would need to allocate the sret buffer,
        /// and we don't have an allocator. Returning a pointer to a
        /// fixed `static mut` is the path of least resistance.
        #[cfg(target_arch = "wasm32")]
        #[no_mangle]
        pub extern "C" fn __maat_start() -> i32 {
            #[allow(non_upper_case_globals)]
            static mut __MAAT_MANIFEST_OUT: [i32; 2] = [0; 2];
            let manifest = <#struct_ident>::manifest_static();
            let (p, l) =
                ::maat_sdk_wasm_rt::encode_and_leak_manifest(&manifest);
            unsafe {
                __MAAT_MANIFEST_OUT[0] = p;
                __MAAT_MANIFEST_OUT[1] = l;
                __MAAT_MANIFEST_OUT.as_ptr() as i32
            }
        }

        /// WASM1 Phase 3a-macro run-setup entry. Called by
        /// `WasmPlugin::run_setup_inner` with the active
        /// `SetupContext` aliased through host state. Takes the
        /// plugin singleton via `Option::take`, primes the
        /// `WasmSetupCtx` seat cache, invokes `ModelPlugin::run_setup`,
        /// and restores the singleton.
        #[cfg(target_arch = "wasm32")]
        #[no_mangle]
        pub extern "C" fn __maat_run_setup() {
            // SAFETY: `__MAAT_PLUGIN` is only accessed inside
            // `__maat_run_setup`, which the host invokes
            // synchronously (no re-entrancy across the host imports
            // — wasm32 is single-threaded by construction). The
            // `take`/restore pattern means at most one `&mut` lives
            // for the duration of `run_setup`.
            let mut plugin = unsafe {
                __MAAT_PLUGIN
                    .take()
                    .unwrap_or_else(<#struct_ident as ::std::default::Default>::default)
            };
            let mut ctx = ::maat_sdk_wasm_rt::WasmSetupCtx::new();
            ctx.prime_seats();
            <#struct_ident as ::maat_sdk::ModelPlugin>::run_setup(
                &mut plugin,
                &mut ctx,
            );
            unsafe { __MAAT_PLUGIN = Some(plugin); }
        }

        /// WASM1 Phase 3c handler-dispatch entry. Called by
        /// `WasmPlugin::dispatch_handler_inner` with:
        ///
        /// - `handler_id`: position in the macro-generated
        ///   `__MAAT_HANDLERS_<STRUCT>` slice (which is sorted by
        ///   `method_name` to align with the native
        ///   `manifest().declared_handlers`).
        /// - `event_ptr` / `event_len`: postcard-encoded `GameEvent`
        ///   bytes in plugin linear memory; the host wrote them
        ///   immediately before invoking this function.
        ///
        /// Returns a packed `i64`: `(response_len << 32) | response_ptr`.
        /// The plugin obtains the response buffer via `__maat_alloc`;
        /// the host reads `response_len` bytes from `response_ptr`
        /// after the call returns.
        ///
        /// Each dispatch:
        /// 1. Decodes the event.
        /// 2. Looks up the matching `HandlerEntry`.
        /// 3. Constructs a `WasmGameContext` (unit struct — free).
        /// 4. Invokes the entry's thunk through the plugin singleton.
        /// 5. Postcard-encodes the `HandlerResponse`, allocates an
        ///    output buffer, copies, packs the return.
        ///
        /// Plugin singleton lifetime note (WASM1-3c-persist, 2026-05-16):
        /// the host now keeps a per-session persistent `WasmRuntime`
        /// for both compose AND handler dispatch, so the
        /// `__MAAT_PLUGIN` singleton **survives across calls** within
        /// a session. The `take().unwrap_or_else(Default::default)`
        /// fallback below only fires on the first dispatch of a
        /// fresh runtime (or after a panic in the encode path that
        /// failed to restore the slot). Plugins MAY rely on
        /// cross-dispatch state, but because the macro dispatches
        /// with `&self`, that state must use interior mutability
        /// (`AtomicU32`, `Cell`, `RefCell`, …). New sessions get
        /// fresh singletons.
        #[cfg(target_arch = "wasm32")]
        #[no_mangle]
        pub extern "C" fn __maat_dispatch_handler(
            handler_id: i32,
            event_ptr: i32,
            event_len: i32,
        ) -> i64 {
            // Sentinel return on any decode / lookup failure: (len=0, ptr=0).
            // The host treats a zero-length response as a "decode failed"
            // surfacing it as `Decision::Allow` (sound default — the host
            // logs the anomaly and the pipeline proceeds).
            let pack_err = || 0i64;

            // 1. Decode the event.
            let event_slice = unsafe {
                core::slice::from_raw_parts(
                    event_ptr as *const u8,
                    event_len as usize,
                )
            };
            let event = match ::maat_sdk_wasm_rt::decode_event(event_slice) {
                Some(e) => e,
                None => return pack_err(),
            };

            // 2. Look up the handler entry by index.
            let entry = match #wasm_slice_ident_for_plugin.get(handler_id as usize) {
                Some(e) => e,
                None => return pack_err(),
            };

            // 3. Borrow the plugin singleton through `Option::take` to
            //    avoid holding two refs simultaneously. Restore on exit.
            //
            // SAFETY: single-threaded wasm32 + no re-entrancy across the
            // host imports. The take/restore pattern is identical to
            // `__maat_run_setup`'s.
            let plugin = unsafe {
                __MAAT_PLUGIN
                    .take()
                    .unwrap_or_else(<#struct_ident as ::std::default::Default>::default)
            };
            let ctx = ::maat_sdk_wasm_rt::WasmGameContext::new();

            // 4. Invoke the thunk. The thunk downcasts the `&dyn
            //    PluginInstance` to the concrete plugin type and
            //    dispatches to the handler method.
            let response = (entry.thunk)(
                &plugin
                    as &dyn ::maat::__sdk::PluginInstance,
                &event,
                &ctx,
            );

            // Restore the singleton BEFORE encoding so a panic in the
            // encode path leaves the slot populated. (Defensive — the
            // postcard encoder doesn't normally panic, but the bound
            // is `HandlerResponse: Serialize` which is infallible
            // except on OOM.)
            unsafe {
                __MAAT_PLUGIN = Some(plugin);
            }

            // 5. Encode and copy into a host-allocated buffer.
            let buf: ::std::vec::Vec<u8> =
                ::maat_sdk_wasm_rt::encode_response(&response);
            let len = buf.len() as i32;
            let ptr = unsafe {
                extern "C" {
                    fn __maat_alloc(size: i32) -> i32;
                }
                __maat_alloc(len)
            };
            if ptr <= 0 {
                return pack_err();
            }
            unsafe {
                core::ptr::copy_nonoverlapping(
                    buf.as_ptr(),
                    ptr as *mut u8,
                    len as usize,
                );
            }
            // Pack (len << 32) | (ptr as u32). Cast ptr through u32
            // first so a sign-extended i32 doesn't poison the low
            // 32 bits.
            ((len as u64) << 32 | (ptr as u32 as u64)) as i64
        }
    }
}

// ---------------------------------------------------------------------------
// `#[model_plugin]` — convenience attribute for `impl ModelPlugin for X`
// blocks. Generates the boilerplate `dispatch_handler` body using
// `__maat_dispatch_table()`. Required surface (manifest, run_setup,
// backend) stays author-supplied.
// ---------------------------------------------------------------------------

/// `#[model_plugin]` — attach to an `impl maat_engine::plugin::ModelPlugin
/// for StructName { ... }` block. Auto-generates `dispatch_handler` if it
/// isn't author-supplied, using the macro-generated dispatch table.
#[allow(dead_code)]
pub fn model_plugin_impl(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut item: ItemImpl = match parse2(input.clone()) {
        Ok(i) => i,
        Err(e) => return e.to_compile_error(),
    };

    // Find the implementor type ident (e.g. "Dnd5ePlugin").
    let self_ty = match &*item.self_ty {
        Type::Path(p) => p.path.segments.last().map(|s| s.ident.clone()),
        _ => None,
    };
    let Some(self_ident) = self_ty else {
        return syn::Error::new_spanned(&item.self_ty, "#[model_plugin] needs a path type")
            .to_compile_error();
    };

    let has_dispatch = item.items.iter().any(|it| match it {
        ImplItem::Fn(m) => m.sig.ident == "dispatch_handler",
        _ => false,
    });
    if !has_dispatch {
        let dispatch: ImplItem = syn::parse_quote! {
            fn dispatch_handler(
                &self,
                h: ::maat::__sdk::LocalHandlerId,
                event: &::maat::__sdk::GameEvent,
                ctx: &dyn ::maat::__sdk::GameContext,
            ) -> ::maat::__sdk::HandlerResponse {
                let table = <#self_ident>::__maat_dispatch_table();
                let idx = h.0 as usize;
                if let Some(thunk) = table.get(idx) {
                    thunk(self, event, ctx)
                } else {
                    ::maat::__sdk::HandlerResponse::from(
                        ::maat::__sdk::Decision::Allow,
                    )
                }
            }
        };
        item.items.push(dispatch);
    }

    let has_as_any = item.items.iter().any(|it| match it {
        ImplItem::Fn(m) => m.sig.ident == "as_any",
        _ => false,
    });
    if !has_as_any {
        let as_any: ImplItem = syn::parse_quote! {
            fn as_any(&self) -> &dyn ::core::any::Any { self }
        };
        item.items.push(as_any);
    }

    quote! { #item }
}

// ---------------------------------------------------------------------------
// `#[capability_plugin]` — annotate a struct that already impls
// `maat_sdk::Capability`. Emits:
//
//   1. An inherent `manifest()` method returning `&'static PluginManifest`,
//      similar to `#[plugin]`'s but with `kind = PluginKind::Capability`.
//      Uses `option_env!("METAGAMER_PLUGIN_CONTENT_HASH")` (set by build.rs)
//      for the content hash.
//   2. An `impl maat_engine::plugin::CapabilityPlugin for Self` block
//      delegating to `maat_sdk::Capability` for the typed methods.
//
// ---------------------------------------------------------------------------

#[derive(Default)]
struct CapabilityPluginArgs {
    base: PluginArgs,
    /// Optional name of an inherent method on the capability struct
    /// that the generated `CapabilityPlugin::post_install_hook` impl
    /// will delegate to. Signature must be:
    ///
    /// ```ignore
    /// fn <name>(&mut self, ctx: &dyn ::maat::__sdk::PostInstallContext)
    ///     -> ::maat::__sdk::CapabilityResult<()>
    /// ```
    ///
    /// When omitted, the default no-op impl from the trait applies.
    post_install_hook: Option<String>,
}

fn parse_capability_plugin_args(args: TokenStream) -> syn::Result<CapabilityPluginArgs> {
    let mut out = CapabilityPluginArgs::default();
    for meta in parse_kvs(args)? {
        let Meta::NameValue(nv) = meta else {
            return Err(syn::Error::new_spanned(meta, "expected `key = value` form"));
        };
        let key = nv
            .path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key.as_str() {
            "canonical_id" => out.base.canonical_id = Some(expect_str(&nv.value)?),
            "display_name" => out.base.display_name = Some(expect_str(&nv.value)?),
            "description" => out.base.description = Some(expect_str(&nv.value)?),
            "license" => out.base.license = Some(expect_str(&nv.value)?),
            "repository" => out.base.repository = Some(expect_str(&nv.value)?),
            "post_install_hook" => out.post_install_hook = Some(expect_str(&nv.value)?),
            "authors" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.authors.push(expect_str(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "authors must be an array of strings",
                    ))
                }
            },
            "dependencies" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.dependencies.push(expect_str(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "dependencies must be an array of canonical_id strings (e.g. dependencies = [\"github.com/.../base-game\"])",
                    ))
                }
            },
            "scenarios" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.scenarios.push(parse_scenario_entry(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "scenarios must be an array of (name = \"…\", description = \"…\", entity_count = N) tuples",
                    ))
                }
            },
            other => {
                return Err(syn::Error::new_spanned(
                    nv.path,
                    format!("unknown capability_plugin key `{other}`"),
                ));
            }
        }
    }
    Ok(out)
}

#[allow(dead_code)]
pub fn capability_plugin_impl(args: TokenStream, input: TokenStream) -> TokenStream {
    let pargs = match parse_capability_plugin_args(args) {
        Ok(p) => p,
        Err(e) => return e.to_compile_error(),
    };
    let item: ItemStruct = match parse2(input.clone()) {
        Ok(s) => s,
        Err(e) => return e.to_compile_error(),
    };
    let struct_ident = item.ident.clone();
    let struct_ident_str = struct_ident.to_string();

    let canonical = pargs
        .base
        .canonical_id
        .clone()
        .unwrap_or_else(|| format!("local:{struct_ident_str}"));
    let display = pargs.base.display_name.unwrap_or_else(|| struct_ident_str.clone());
    let description = pargs.base.description.unwrap_or_default();
    let license = pargs.base.license.unwrap_or_else(|| "UNLICENSED".to_string());
    let repository = pargs.base.repository;
    let authors = pargs.base.authors;

    let repo_expr = match repository {
        Some(s) => quote! { Some(#s.to_string()) },
        None => quote! { None },
    };

    // Optional `post_install_hook = "fn_name"` — when present, emit a
    // `CapabilityPlugin::post_install_hook` impl that delegates to the
    // named inherent method. When absent, the trait's default no-op
    // applies and we emit nothing.
    let post_install_impl = match pargs.post_install_hook.as_deref() {
        None => quote! {},
        Some(name) => {
            let fn_ident = syn::Ident::new(name, proc_macro2::Span::call_site());
            quote! {
                fn post_install_hook(
                    &mut self,
                    ctx: &dyn ::maat::__sdk::PostInstallContext,
                ) -> ::maat::__sdk::CapabilityResult<()> {
                    Self::#fn_ident(self, ctx)
                }
            }
        }
    };

    // Generic dispatch shared by both handle_capability_frame and
    // handle_client_capability_frame. CapabilityFrame is now a plain struct
    // { capability_id, payload } — no typed variants to match.
    let dispatch_body = quote! {
        let local: &'static str =
            <Self as ::maat_sdk::Capability>::LOCAL_NAME;
        let id_str = frame.capability_id.as_str();
        let matches = id_str == local
            || id_str.rsplit(':').next() == Some(local);
        if !matches {
            return Err(format!(
                "CapabilityFrame capability_id `{id_str}` does not match \
                 capability LOCAL_NAME `{local}`"
            ).into());
        }
        let inner: <Self as ::maat_sdk::Capability>::Frame =
            ::postcard::from_bytes(&frame.payload).map_err(
                |e| -> ::std::boxed::Box<dyn ::std::error::Error + Send + Sync> {
                    ::std::boxed::Box::new(e)
                },
            )?;
        <Self as ::maat_sdk::Capability>::handle_incoming(self, inner, ctx).await
    };

    let decl_walker = emit_manifest_decl_walker(&struct_ident_str);
    let deps_init = emit_dependencies_init(&pargs.base.dependencies);
    let scenarios_init = emit_scenarios_init(&pargs.base.scenarios);

    quote! {
        #item

        // The manifest assembler walks linkme slices that don't exist on
        // wasm32. Gate the impl entirely so WASM consumers still build.
        #[cfg(not(target_arch = "wasm32"))]
        impl #struct_ident {
            /// Macro-generated. Builds a `PluginManifest` for this capability
            /// plugin. Cached in a `OnceLock`.
            pub fn maat_manifest()
                -> &'static ::maat::__sdk::PluginManifest
            {
                static MANIFEST:
                    ::std::sync::OnceLock<::maat::__sdk::PluginManifest>
                    = ::std::sync::OnceLock::new();
                MANIFEST.get_or_init(|| {
                    // 0580: from_static — compile-time validated.
                    let canonical_id = ::maat::__sdk::CanonicalId::from_static(#canonical);
                    #decl_walker

                    let raw: &str = option_env!("METAGAMER_PLUGIN_CONTENT_HASH")
                        .unwrap_or("");
                    let content_hash = decode_hash(raw);
                    fn decode_hash(s: &str) -> [u8; 32] {
                        if s.len() != 64 { return [0u8; 32]; }
                        let mut out = [0u8; 32];
                        let bytes = s.as_bytes();
                        for i in 0..32 {
                            let hi = match bytes[i*2] {
                                b'0'..=b'9' => bytes[i*2] - b'0',
                                b'a'..=b'f' => bytes[i*2] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            let lo = match bytes[i*2+1] {
                                b'0'..=b'9' => bytes[i*2+1] - b'0',
                                b'a'..=b'f' => bytes[i*2+1] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2+1] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            out[i] = (hi << 4) | lo;
                        }
                        out
                    }

                    #deps_init
                    #scenarios_init
                    let interface_hash =
                        ::maat::__sdk::compute_interface_hash(
                            &declared_types,
                            &declared_handlers,
                            &declared_capabilities,
                            &declared_phases,
                            &declared_wire_frames,
                            &dependencies,
                            &declared_scenarios,
                        );

                    ::maat::__sdk::PluginManifest {
                        canonical_id,
                        display_name: #display.to_string(),
                        kind: ::maat::__sdk::PluginKind::Capability,
                        authors: vec![ #( #authors.to_string() ),* ],
                        description: #description.to_string(),
                        license: ::maat::__sdk::SpdxLicenseId::new(#license),
                        repository: #repo_expr,
                        content_hash,
                        interface_hash,
                        declared_types,
                        declared_handlers,
                        declared_capabilities,
                        declared_phases,
                        declared_wire_frames,
                        dependencies,
                        phase_descriptors: ::std::vec::Vec::new(),
                        version: ::maat::__sdk::parse_semver_or_default(env!("CARGO_PKG_VERSION")),
                        price_cents: ::core::option::Option::None,
                        manifest_format_version: 1,
                        dispatch_abi_version: ::maat::__sdk::DISPATCH_ABI_V1,
                        signer_key: [0u8; 32],
                        requested_memory_bytes: ::core::option::Option::None,
                        requested_fuel_per_call: ::core::option::Option::None,
                        requested_epoch_deadline_ticks: ::core::option::Option::None,
                        declared_host_imports: ::std::vec::Vec::new(),
                        backend: ::maat::__sdk::PluginBackend::Static,
                        declared_scenarios,
                    }
                })
            }
        }

        #[cfg(not(target_arch = "wasm32"))]
        #[::async_trait::async_trait]
        impl ::maat_sdk::CapabilityPlugin for #struct_ident {
            fn manifest(&self) -> &::maat::__sdk::PluginManifest {
                Self::maat_manifest()
            }

            fn backend(&self) -> ::maat::__sdk::PluginBackend {
                ::maat::__sdk::PluginBackend::Static
            }

            fn capability_id(&self) -> ::maat::__sdk::CapabilityId {
                ::maat::__sdk::CapabilityId::new(
                    <Self as ::maat_sdk::Capability>::LOCAL_NAME,
                )
            }

            async fn handle_capability_frame(
                &mut self,
                frame: ::maat_sdk::wire::CapabilityFrame,
                ctx: &mut ::maat_sdk::capability::FrameContext,
            ) -> ::maat::__sdk::CapabilityResult<()> {
                #dispatch_body
            }

            async fn handle_client_capability_frame(
                &mut self,
                frame: ::maat_sdk::wire::ClientCapabilityFrame,
                ctx: &mut ::maat_sdk::capability::FrameContext,
            ) -> ::maat::__sdk::CapabilityResult<()> {
                #dispatch_body
            }

            async fn load(
                &mut self,
                ctx: &::maat_sdk::capability::PersistContext,
            ) -> ::maat::__sdk::CapabilityResult<()> {
                <Self as ::maat_sdk::Capability>::load(self, ctx).await
            }

            async fn save(
                &self,
                ctx: &::maat_sdk::capability::PersistContext,
            ) -> ::maat::__sdk::CapabilityResult<()> {
                <Self as ::maat_sdk::Capability>::save(self, ctx).await
            }

            #post_install_impl
        }
    }
}

// ---------------------------------------------------------------------------
// `#[view_plugin]` — PG1 phase 2.
//
// Annotate the renderer struct (the one that already impls
// `maat_view_sdk::ViewPlugin`). The macro emits a sibling
// `<Name>Metadata` struct + a `ViewPluginMetadata` impl with manifest
// + content-hash machinery. Mirrors `#[capability_plugin]`.
//
// MVP: identity attrs only (canonical_id, display_name, description,
// authors, license, repository). Custom `applies_to_canonical_ids`,
// `declared_entity_coverage`, `supported_canvas_modes` go in a sibling
// `impl <Name>Metadata { ... }` block alongside the macro.
// ---------------------------------------------------------------------------

#[derive(Default)]
struct ViewPluginArgs {
    base: PluginArgs,
    /// Optional name of an inherent fn on `<Renderer>Metadata`
    /// returning `Vec<CanonicalId>` for
    /// `ViewPluginMetadata::applies_to_canonical_ids`. Default = empty.
    applies_to_fn: Option<String>,
    /// Optional name of an inherent fn returning
    /// `Vec<ViewAssetDeclaration>` for
    /// `ViewPluginMetadata::declared_entity_coverage`. Default = empty.
    coverage_fn: Option<String>,
    /// Optional name of an inherent fn returning `Vec<CanvasMode>` for
    /// `ViewPluginMetadata::supported_canvas_modes`. Default = `CanvasMode::all_2d()`.
    canvas_modes_fn: Option<String>,
}

fn parse_view_plugin_args(args: TokenStream) -> syn::Result<ViewPluginArgs> {
    let mut out = ViewPluginArgs::default();
    for meta in parse_kvs(args)? {
        let Meta::NameValue(nv) = meta else {
            return Err(syn::Error::new_spanned(meta, "expected `key = value` form"));
        };
        let key = nv
            .path
            .get_ident()
            .map(|i| i.to_string())
            .unwrap_or_default();
        match key.as_str() {
            "canonical_id" => out.base.canonical_id = Some(expect_str(&nv.value)?),
            "display_name" => out.base.display_name = Some(expect_str(&nv.value)?),
            "description" => out.base.description = Some(expect_str(&nv.value)?),
            "license" => out.base.license = Some(expect_str(&nv.value)?),
            "repository" => out.base.repository = Some(expect_str(&nv.value)?),
            "applies_to_fn" => out.applies_to_fn = Some(expect_str(&nv.value)?),
            "coverage_fn" => out.coverage_fn = Some(expect_str(&nv.value)?),
            "canvas_modes_fn" => out.canvas_modes_fn = Some(expect_str(&nv.value)?),
            "authors" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.authors.push(expect_str(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "authors must be an array of strings",
                    ))
                }
            },
            "dependencies" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.dependencies.push(expect_str(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "dependencies must be an array of canonical_id strings (e.g. dependencies = [\"github.com/.../base-game\"])",
                    ))
                }
            },
            "scenarios" => match nv.value {
                Expr::Array(ExprArray { elems, .. }) => {
                    for el in elems {
                        out.base.scenarios.push(parse_scenario_entry(&el)?);
                    }
                }
                v => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "scenarios must be an array of (name = \"…\", description = \"…\", entity_count = N) tuples",
                    ))
                }
            },
            other => {
                return Err(syn::Error::new_spanned(
                    nv.path,
                    format!("unknown view_plugin key `{other}`"),
                ));
            }
        }
    }
    Ok(out)
}

pub fn view_plugin_impl(args: TokenStream, input: TokenStream) -> TokenStream {
    let pargs = match parse_view_plugin_args(args) {
        Ok(p) => p,
        Err(e) => return e.to_compile_error(),
    };
    let item: ItemStruct = match parse2(input.clone()) {
        Ok(s) => s,
        Err(e) => return e.to_compile_error(),
    };
    let renderer_ident = item.ident.clone();
    let renderer_ident_str = renderer_ident.to_string();

    // Sibling metadata struct: <Renderer>Metadata.
    let metadata_ident = syn::Ident::new(
        &format!("{renderer_ident_str}Metadata"),
        proc_macro2::Span::call_site(),
    );

    let canonical = pargs
        .base
        .canonical_id
        .clone()
        .unwrap_or_else(|| format!("local:{renderer_ident_str}"));
    let display = pargs
        .base
        .display_name
        .unwrap_or_else(|| renderer_ident_str.clone());
    let description = pargs.base.description.unwrap_or_default();
    let license = pargs.base.license.unwrap_or_else(|| "UNLICENSED".to_string());
    let repository = pargs.base.repository;
    let authors = pargs.base.authors;

    let repo_expr = match repository {
        Some(s) => quote! { Some(#s.to_string()) },
        None => quote! { None },
    };

    // Optional method overrides: when an `<x>_fn = "fn_name"` arg is
    // present, the generated `ViewPluginMetadata::<method>` body
    // delegates to `Self::<fn_name>()`. When absent, the trait's
    // default impl applies (empty Vec for applies_to + coverage,
    // `CanvasMode::all_2d()` for canvas modes).
    let applies_to_impl = match pargs.applies_to_fn.as_deref() {
        None => quote! {},
        Some(name) => {
            let fn_ident = syn::Ident::new(name, proc_macro2::Span::call_site());
            quote! {
                fn applies_to_canonical_ids(&self)
                    -> ::std::vec::Vec<::maat::__sdk::CanonicalId>
                {
                    Self::#fn_ident()
                }
            }
        }
    };
    let coverage_impl = match pargs.coverage_fn.as_deref() {
        None => quote! {},
        Some(name) => {
            let fn_ident = syn::Ident::new(name, proc_macro2::Span::call_site());
            quote! {
                fn declared_entity_coverage(&self)
                    -> ::std::vec::Vec<::maat::types::coverage::ViewAssetDeclaration>
                {
                    Self::#fn_ident()
                }
            }
        }
    };
    let canvas_modes_impl = match pargs.canvas_modes_fn.as_deref() {
        None => quote! {},
        Some(name) => {
            let fn_ident = syn::Ident::new(name, proc_macro2::Span::call_site());
            quote! {
                fn supported_canvas_modes(&self)
                    -> ::std::vec::Vec<::maat::types::coverage::CanvasMode>
                {
                    Self::#fn_ident()
                }
            }
        }
    };

    let decl_walker = emit_manifest_decl_walker(&renderer_ident_str);
    let deps_init = emit_dependencies_init(&pargs.base.dependencies);
    let scenarios_init = emit_scenarios_init(&pargs.base.scenarios);

    quote! {
        // Pass through the renderer struct unchanged.
        #item

        // Sibling metadata struct — Send + Sync + 'static — owned by
        // the engine's view registry. The renderer struct itself may
        // be !Send (Rc-using) and stays client-side.
        #[cfg(not(target_arch = "wasm32"))]
        #[doc(hidden)]
        #[derive(Debug, Default)]
        pub struct #metadata_ident;

        #[cfg(not(target_arch = "wasm32"))]
        impl #metadata_ident {
            /// Macro-generated. Builds a `PluginManifest` for this view plugin.
            /// Cached in a `OnceLock`.
            pub fn maat_manifest()
                -> &'static ::maat::__sdk::PluginManifest
            {
                static MANIFEST:
                    ::std::sync::OnceLock<::maat::__sdk::PluginManifest>
                    = ::std::sync::OnceLock::new();
                MANIFEST.get_or_init(|| {
                    // 0580: from_static — compile-time validated.
                    let canonical_id = ::maat::__sdk::CanonicalId::from_static(#canonical);
                    #decl_walker

                    let raw: &str = option_env!("METAGAMER_PLUGIN_CONTENT_HASH")
                        .unwrap_or("");
                    let content_hash = decode_hash(raw);
                    fn decode_hash(s: &str) -> [u8; 32] {
                        if s.len() != 64 { return [0u8; 32]; }
                        let mut out = [0u8; 32];
                        let bytes = s.as_bytes();
                        for i in 0..32 {
                            let hi = match bytes[i*2] {
                                b'0'..=b'9' => bytes[i*2] - b'0',
                                b'a'..=b'f' => bytes[i*2] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            let lo = match bytes[i*2+1] {
                                b'0'..=b'9' => bytes[i*2+1] - b'0',
                                b'a'..=b'f' => bytes[i*2+1] - b'a' + 10,
                                b'A'..=b'F' => bytes[i*2+1] - b'A' + 10,
                                _ => return [0u8; 32],
                            };
                            out[i] = (hi << 4) | lo;
                        }
                        out
                    }

                    #deps_init
                    #scenarios_init
                    let interface_hash =
                        ::maat::__sdk::compute_interface_hash(
                            &declared_types,
                            &declared_handlers,
                            &declared_capabilities,
                            &declared_phases,
                            &declared_wire_frames,
                            &dependencies,
                            &declared_scenarios,
                        );

                    ::maat::__sdk::PluginManifest {
                        canonical_id,
                        display_name: #display.to_string(),
                        kind: ::maat::__sdk::PluginKind::View,
                        authors: vec![ #( #authors.to_string() ),* ],
                        description: #description.to_string(),
                        license: ::maat::__sdk::SpdxLicenseId::new(#license),
                        repository: #repo_expr,
                        content_hash,
                        interface_hash,
                        declared_types,
                        declared_handlers,
                        declared_capabilities,
                        declared_phases,
                        declared_wire_frames,
                        dependencies,
                        phase_descriptors: ::std::vec::Vec::new(),
                        version: ::maat::__sdk::parse_semver_or_default(env!("CARGO_PKG_VERSION")),
                        price_cents: ::core::option::Option::None,
                        manifest_format_version: 1,
                        dispatch_abi_version: ::maat::__sdk::DISPATCH_ABI_V1,
                        signer_key: [0u8; 32],
                        requested_memory_bytes: ::core::option::Option::None,
                        requested_fuel_per_call: ::core::option::Option::None,
                        requested_epoch_deadline_ticks: ::core::option::Option::None,
                        declared_host_imports: ::std::vec::Vec::new(),
                        backend: ::maat::__sdk::PluginBackend::Static,
                        declared_scenarios,
                    }
                })
            }
        }

        #[cfg(not(target_arch = "wasm32"))]
        impl ::maat::traits::ViewPluginMetadata for #metadata_ident {
            fn manifest(&self) -> &::maat::__sdk::PluginManifest {
                Self::maat_manifest()
            }

            fn backend(&self) -> ::maat::__sdk::PluginBackend {
                ::maat::__sdk::PluginBackend::Static
            }

            #applies_to_impl
            #coverage_impl
            #canvas_modes_impl
        }
    }
}
