Skip to main content

hopper_schema/
lib.rs

1//! # Hopper Schema
2//!
3//! Schema export, ABI fingerprinting, decode tooling, and program management
4//! primitives for the Hopper framework.
5//!
6//! Provides:
7//! - Layout manifest generation (JSON-compatible metadata)
8//! - Layout fingerprint computation and comparison
9//! - Schema diff detection for migration safety
10//! - Field-level compatibility checking
11//! - Account header decoding and inspection
12//! - Segment registry inspection
13//! - Manifest registries for multi-layout programs
14//! - Program manifests for Hopper Manager (full program schema)
15//! - Field-level account decoding for account inspection
16//! - Segment migration analysis and migration planning
17
18#![no_std]
19
20pub mod accounts;
21pub mod anchor_idl;
22pub mod clientgen;
23pub mod codama;
24pub mod python_client;
25pub mod rust_client;
26
27use core::fmt;
28use hopper_core::account::HEADER_LEN;
29use hopper_core::field_map::FieldInfo;
30use hopper_runtime::layout::LayoutInfo;
31use hopper_runtime::{AccountView, LayoutContract};
32
33// Re-export receipt types for CLI consumers
34#[cfg(feature = "receipt")]
35pub use hopper_core::receipt::{
36    CompatImpact, DecodedReceipt, NarrativeRisk, Phase, ReceiptExplain, ReceiptNarrative,
37};
38// Re-export policy types for CLI consumers
39#[cfg(feature = "policy")]
40pub use hopper_core::policy::PolicyClass;
41
42// ---------------------------------------------------------------------------
43// On-chain manifest storage constants
44// ---------------------------------------------------------------------------
45
46/// PDA seed for on-chain Hopper manifest accounts.
47///
48/// Programs store their manifest JSON at:
49///   `find_program_address(&[MANIFEST_SEED], &program_id)`
50///
51/// This deterministic address allows any tool to discover a program's
52/// schema knowing only the program ID.
53pub const MANIFEST_SEED: &[u8] = b"hopper:manifest";
54
55/// 8-byte magic discriminator at the start of a manifest account.
56pub const MANIFEST_MAGIC: [u8; 8] = *b"HOPRMNFT";
57
58/// On-chain manifest account header size (bytes).
59///
60/// Layout:
61///   [0..8]   magic      : `MANIFEST_MAGIC` discriminator
62///   [8..12]  version    : u32 LE wire format version (currently 1)
63///   [12..16] data_len   : u32 LE byte count of the JSON payload
64///   [16..17] compression: u8 (0 = raw JSON, 1 = zlib-deflate)
65///   [17..20] reserved   : 3 padding bytes
66///   [20..]   payload    : JSON data (raw or compressed)
67pub const MANIFEST_HEADER_LEN: usize = 20;
68
69/// Current manifest wire format version.
70pub const MANIFEST_VERSION: u32 = 1;
71
72/// Compression tag: no compression (raw JSON).
73pub const MANIFEST_COMPRESS_NONE: u8 = 0;
74/// Compression tag: zlib-deflate compressed JSON.
75pub const MANIFEST_COMPRESS_ZLIB: u8 = 1;
76
77/// Semantic intent of a layout field.
78///
79/// Enables auto-generated UI, receipt explanations, invariant validation,
80/// and client SDKs to understand *what* each field means -- not just its type.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82#[repr(u8)]
83pub enum FieldIntent {
84    /// Token/SOL balance (lamports or token amount).
85    Balance = 0,
86    /// Public key that controls this account.
87    Authority = 1,
88    /// Unix timestamp (seconds since epoch).
89    Timestamp = 2,
90    /// Monotonic counter (nonce, sequence number).
91    Counter = 3,
92    /// Array/collection index or offset.
93    Index = 4,
94    /// Basis-point value (e.g. fee rate, slippage tolerance).
95    BasisPoints = 5,
96    /// Boolean flag stored as a byte.
97    Flag = 6,
98    /// Public key reference to another account.
99    Address = 7,
100    /// Hash or fingerprint (layout_id, merkle root, etc.).
101    Hash = 8,
102    /// PDA seed component stored on-chain.
103    PDASeed = 9,
104    /// Layout or schema version number.
105    Version = 10,
106    /// PDA bump seed.
107    Bump = 11,
108    /// Cryptographic nonce (distinct from monotonic counter).
109    Nonce = 12,
110    /// Token supply or mint total.
111    Supply = 13,
112    /// Rate limit, cap, or ceiling value.
113    Limit = 14,
114    /// Multisig or governance threshold.
115    Threshold = 15,
116    /// Owner identity (distinct from authority -- may be non-signer).
117    Owner = 16,
118    /// Delegated authority (can act on behalf of owner).
119    Delegate = 17,
120    /// State machine status / lifecycle stage.
121    Status = 18,
122    /// Application-specific field with no standard semantic.
123    Custom = 255,
124}
125
126impl FieldIntent {
127    /// Human-readable name for display.
128    pub fn name(self) -> &'static str {
129        match self {
130            Self::Balance => "balance",
131            Self::Authority => "authority",
132            Self::Timestamp => "timestamp",
133            Self::Counter => "counter",
134            Self::Index => "index",
135            Self::BasisPoints => "basis_points",
136            Self::Flag => "flag",
137            Self::Address => "address",
138            Self::Hash => "hash",
139            Self::PDASeed => "pda_seed",
140            Self::Version => "version",
141            Self::Bump => "bump",
142            Self::Nonce => "nonce",
143            Self::Supply => "supply",
144            Self::Limit => "limit",
145            Self::Threshold => "threshold",
146            Self::Owner => "owner",
147            Self::Delegate => "delegate",
148            Self::Status => "status",
149            Self::Custom => "custom",
150        }
151    }
152
153    /// Whether this field represents a monetary amount that should be
154    /// tracked for conservation invariants.
155    pub fn is_monetary(self) -> bool {
156        matches!(self, Self::Balance | Self::BasisPoints | Self::Supply)
157    }
158
159    /// Whether this field is an identity reference (authority, owner, delegate, or address).
160    pub fn is_identity(self) -> bool {
161        matches!(
162            self,
163            Self::Authority | Self::Address | Self::Owner | Self::Delegate
164        )
165    }
166
167    /// Whether this field is authority-sensitive (mutations require signer verification).
168    pub fn is_authority_sensitive(self) -> bool {
169        matches!(self, Self::Authority | Self::Owner | Self::Delegate)
170    }
171
172    /// Whether this field is immutable after initialization (bump, PDA seed, version seeds).
173    pub fn is_init_only(self) -> bool {
174        matches!(self, Self::PDASeed | Self::Bump)
175    }
176
177    /// Whether this field represents a governance or access-control parameter.
178    pub fn is_governance(self) -> bool {
179        matches!(self, Self::Threshold | Self::Limit | Self::Status)
180    }
181}
182
183impl fmt::Display for FieldIntent {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str(self.name())
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Mutation Class -- how a layout behaves under mutation
191// ---------------------------------------------------------------------------
192
193/// Classification of how a layout or segment behaves when mutated.
194///
195/// Enables Hopper to reason about mutation risk, receipt expectations,
196/// and policy requirements at the type level rather than guessing from bytes.
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198#[repr(u8)]
199pub enum MutationClass {
200    /// No writes allowed. Read-only overlay.
201    ReadOnly = 0,
202    /// New entries appended; existing data never modified.
203    AppendOnly = 1,
204    /// Existing fields modified in-place; no size change.
205    InPlace = 2,
206    /// Account may be resized (realloc) during mutation.
207    Resizing = 3,
208    /// Mutation touches authority, owner, or delegate fields.
209    AuthoritySensitive = 4,
210    /// Mutation affects balances, supply, or other financial fields.
211    Financial = 5,
212    /// Mutation changes state machine status or lifecycle stage.
213    StateTransition = 6,
214}
215
216impl MutationClass {
217    /// Human-readable name.
218    pub const fn name(self) -> &'static str {
219        match self {
220            Self::ReadOnly => "read-only",
221            Self::AppendOnly => "append-only",
222            Self::InPlace => "in-place",
223            Self::Resizing => "resizing",
224            Self::AuthoritySensitive => "authority-sensitive",
225            Self::Financial => "financial",
226            Self::StateTransition => "state-transition",
227        }
228    }
229
230    /// Whether this class involves any writes.
231    pub const fn is_mutating(self) -> bool {
232        !matches!(self, Self::ReadOnly)
233    }
234
235    /// Whether this class requires a state snapshot for receipt generation.
236    pub const fn requires_snapshot(self) -> bool {
237        !matches!(self, Self::ReadOnly)
238    }
239
240    /// Whether this class typically needs authority verification.
241    pub const fn requires_authority(self) -> bool {
242        matches!(
243            self,
244            Self::AuthoritySensitive | Self::Financial | Self::Resizing | Self::StateTransition
245        )
246    }
247}
248
249impl fmt::Display for MutationClass {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        f.write_str(self.name())
252    }
253}
254
255// ---------------------------------------------------------------------------
256// Layout Behavior -- operational metadata for a layout
257// ---------------------------------------------------------------------------
258
259/// Describes how a layout behaves under mutation, what policy it expects,
260/// and what receipt profile it should produce.
261///
262/// Attach this to a layout manifest to give Hopper's lint engine, Manager,
263/// and receipt system enough context to validate mutations semantically.
264#[derive(Clone, Copy, Debug)]
265pub struct LayoutBehavior {
266    /// Whether any instruction mutating this layout requires a signer.
267    pub requires_signer: bool,
268    /// Whether mutations affect balance/financial fields.
269    pub affects_balance: bool,
270    /// Whether mutations affect authority/owner/delegate fields.
271    pub affects_authority: bool,
272    /// Primary mutation class for this layout.
273    pub mutation_class: MutationClass,
274}
275
276impl LayoutBehavior {
277    /// A read-only layout that should never be mutated.
278    pub const READ_ONLY: Self = Self {
279        requires_signer: false,
280        affects_balance: false,
281        affects_authority: false,
282        mutation_class: MutationClass::ReadOnly,
283    };
284
285    /// Default behavior for a standard mutable account.
286    pub const STANDARD: Self = Self {
287        requires_signer: true,
288        affects_balance: false,
289        affects_authority: false,
290        mutation_class: MutationClass::InPlace,
291    };
292
293    /// Behavior for a treasury/vault layout that manages balances.
294    pub const FINANCIAL: Self = Self {
295        requires_signer: true,
296        affects_balance: true,
297        affects_authority: false,
298        mutation_class: MutationClass::Financial,
299    };
300
301    /// Behavior for an append-only journal or audit log.
302    pub const APPEND_ONLY: Self = Self {
303        requires_signer: true,
304        affects_balance: false,
305        affects_authority: false,
306        mutation_class: MutationClass::AppendOnly,
307    };
308}
309
310impl fmt::Display for LayoutBehavior {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        write!(f, "mutation={}", self.mutation_class)?;
313        if self.requires_signer {
314            write!(f, " signer")?;
315        }
316        if self.affects_balance {
317            write!(f, " balance")?;
318        }
319        if self.affects_authority {
320            write!(f, " authority")?;
321        }
322        Ok(())
323    }
324}
325
326// ---------------------------------------------------------------------------
327// Layout Stability Grade -- evolution safety scoring
328// ---------------------------------------------------------------------------
329
330/// How safe it is to evolve a layout over time.
331///
332/// Computed from field intents, segment roles, and mutation classes to help
333/// builders understand whether their layout design invites future migration
334/// pain or stays safely extensible.
335#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336#[repr(u8)]
337pub enum LayoutStabilityGrade {
338    /// Layout is safe to extend indefinitely (append-only fields, stable core).
339    Stable = 0,
340    /// Layout is actively evolving but changes are managed.
341    Evolving = 1,
342    /// Layout has fields or segments that make migration risky.
343    MigrationSensitive = 2,
344    /// Layout design makes future evolution dangerous. Refactor recommended.
345    UnsafeToEvolve = 3,
346}
347
348impl LayoutStabilityGrade {
349    /// Human-readable name.
350    pub const fn name(self) -> &'static str {
351        match self {
352            Self::Stable => "stable",
353            Self::Evolving => "evolving",
354            Self::MigrationSensitive => "migration-sensitive",
355            Self::UnsafeToEvolve => "unsafe-to-evolve",
356        }
357    }
358
359    /// Compute the stability grade for a layout manifest.
360    ///
361    /// Heuristic: counts authority-sensitive fields, financial fields,
362    /// and checks whether field ordering invites migration pain.
363    pub fn compute(manifest: &LayoutManifest) -> Self {
364        let mut authority_count = 0u16;
365        let mut financial_count = 0u16;
366        let mut init_only_count = 0u16;
367        let mut has_custom = false;
368
369        let mut i = 0;
370        while i < manifest.field_count {
371            let intent = manifest.fields[i].intent;
372            if intent.is_authority_sensitive() {
373                authority_count += 1;
374            }
375            if intent.is_monetary() {
376                financial_count += 1;
377            }
378            if intent.is_init_only() {
379                init_only_count += 1;
380            }
381            if matches!(intent, FieldIntent::Custom) {
382                has_custom = true;
383            }
384            i += 1;
385        }
386
387        // If the layout has many authority-sensitive or financial fields
388        // interleaved with generic fields, it's harder to evolve safely.
389        if authority_count > 2 && financial_count > 2 {
390            return Self::UnsafeToEvolve;
391        }
392        if authority_count > 1 || financial_count > 2 {
393            return Self::MigrationSensitive;
394        }
395        if has_custom && manifest.field_count > 8 {
396            return Self::Evolving;
397        }
398        // Init-only fields (PDA seeds, bumps) anchor the layout and
399        // make append-only extension safer.
400        if init_only_count > 0 {
401            return Self::Stable;
402        }
403        Self::Evolving
404    }
405}
406
407impl fmt::Display for LayoutStabilityGrade {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        f.write_str(self.name())
410    }
411}
412
413/// A field descriptor in a layout manifest.
414#[derive(Clone, Copy, Debug)]
415pub struct FieldDescriptor {
416    /// Field name (static str).
417    pub name: &'static str,
418    /// Canonical type name.
419    pub canonical_type: &'static str,
420    /// Byte size.
421    pub size: u16,
422    /// Byte offset from start of struct.
423    pub offset: u16,
424    /// Semantic intent (what the field *means*, not just its type).
425    pub intent: FieldIntent,
426}
427
428/// A layout manifest describing an account type.
429#[derive(Clone, Copy, Debug)]
430pub struct LayoutManifest {
431    /// Layout name.
432    pub name: &'static str,
433    /// Discriminator byte.
434    pub disc: u8,
435    /// Version byte.
436    pub version: u8,
437    /// Layout ID (8-byte fingerprint).
438    pub layout_id: [u8; 8],
439    /// Total byte size including header.
440    pub total_size: usize,
441    /// Number of fields (not counting header).
442    pub field_count: usize,
443    /// Field descriptors (static slice). Empty for legacy manifests.
444    pub fields: &'static [FieldDescriptor],
445}
446
447// -- Layout Fingerprint v2 --
448
449/// Extended layout fingerprint combining wire-level and semantic identity.
450///
451/// The **wire hash** matches the 8-byte `layout_id` stored in the account header
452/// and captures the raw byte layout (field sizes, offsets, types).
453///
454/// The **semantic hash** additionally folds in field intents, enabling detection
455/// of semantic changes that don't alter the wire format (e.g. reinterpreting a
456/// `u64` from `Balance` to `Timestamp`).
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct LayoutFingerprint {
459    /// Wire-layout hash (matches the header layout_id).
460    pub wire_hash: [u8; 8],
461    /// Semantic hash incorporating field intents, names, and roles.
462    pub semantic_hash: [u8; 8],
463}
464
465impl LayoutFingerprint {
466    /// Compute a fingerprint from a layout manifest.
467    ///
468    /// `wire_hash` is taken directly from `manifest.layout_id`.
469    /// `semantic_hash` is a deterministic FNV-1a-64 over field names, types,
470    /// sizes, offsets, and intents.
471    pub const fn from_manifest(manifest: &LayoutManifest) -> Self {
472        Self {
473            wire_hash: manifest.layout_id,
474            semantic_hash: Self::compute_semantic(manifest.fields),
475        }
476    }
477
478    /// Whether both wire and semantic fingerprints match.
479    pub const fn is_identical(&self, other: &Self) -> bool {
480        let mut i = 0;
481        while i < 8 {
482            if self.wire_hash[i] != other.wire_hash[i] {
483                return false;
484            }
485            if self.semantic_hash[i] != other.semantic_hash[i] {
486                return false;
487            }
488            i += 1;
489        }
490        true
491    }
492
493    /// Whether wire layout matches but semantics differ.
494    ///
495    /// This detects reinterpretation: same bytes on the wire, different meaning.
496    pub const fn wire_matches_but_semantics_differ(&self, other: &Self) -> bool {
497        let mut wire_eq = true;
498        let mut sem_eq = true;
499        let mut i = 0;
500        while i < 8 {
501            if self.wire_hash[i] != other.wire_hash[i] {
502                wire_eq = false;
503            }
504            if self.semantic_hash[i] != other.semantic_hash[i] {
505                sem_eq = false;
506            }
507            i += 1;
508        }
509        wire_eq && !sem_eq
510    }
511
512    /// FNV-1a-64 over field descriptors including intents.
513    const fn compute_semantic(fields: &[FieldDescriptor]) -> [u8; 8] {
514        const FNV_OFFSET: u64 = 0xcbf29ce484222325;
515        const FNV_PRIME: u64 = 0x00000100000001B3;
516
517        let mut hash = FNV_OFFSET;
518        let mut i = 0;
519        while i < fields.len() {
520            // Mix in name bytes
521            let name = fields[i].name.as_bytes();
522            let mut j = 0;
523            while j < name.len() {
524                hash ^= name[j] as u64;
525                hash = hash.wrapping_mul(FNV_PRIME);
526                j += 1;
527            }
528            // Mix in type bytes
529            let ty = fields[i].canonical_type.as_bytes();
530            j = 0;
531            while j < ty.len() {
532                hash ^= ty[j] as u64;
533                hash = hash.wrapping_mul(FNV_PRIME);
534                j += 1;
535            }
536            // Mix in size, offset
537            hash ^= fields[i].size as u64;
538            hash = hash.wrapping_mul(FNV_PRIME);
539            hash ^= fields[i].offset as u64;
540            hash = hash.wrapping_mul(FNV_PRIME);
541            // Mix in intent discriminant (the semantic component)
542            hash ^= fields[i].intent as u8 as u64;
543            hash = hash.wrapping_mul(FNV_PRIME);
544            i += 1;
545        }
546        hash.to_le_bytes()
547    }
548}
549
550impl fmt::Display for LayoutFingerprint {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        write!(f, "wire=")?;
553        let mut i = 0;
554        while i < 8 {
555            let _ = write!(f, "{:02x}", self.wire_hash[i]);
556            i += 1;
557        }
558        write!(f, " sem=")?;
559        i = 0;
560        while i < 8 {
561            let _ = write!(f, "{:02x}", self.semantic_hash[i]);
562            i += 1;
563        }
564        Ok(())
565    }
566}
567
568// -- Compatibility Checking --
569
570/// Check if two layout manifests are append-compatible.
571///
572/// Returns `true` if `newer` is a strict superset of `older`:
573/// - Same discriminator
574/// - `newer.version > older.version`
575/// - `newer.total_size >= older.total_size`
576/// - Different layout IDs (proving the change)
577#[inline]
578pub fn is_append_compatible(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
579    older.disc == newer.disc
580        && newer.version > older.version
581        && newer.total_size >= older.total_size
582        && older.layout_id != newer.layout_id
583}
584
585/// Check if migration is required between two manifests.
586///
587/// Migration is required when:
588/// - Same discriminator but different layout IDs
589/// - The newer layout is NOT append-compatible (fields changed, not just appended)
590#[inline]
591pub fn requires_migration(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
592    older.disc == newer.disc && older.layout_id != newer.layout_id
593}
594
595/// Check if accounts written by `newer` can still be parsed by code expecting `older`.
596///
597/// Backward-readable means:
598/// - Same discriminator
599/// - All fields in `older` exist in `newer` with the same name, type, and size
600/// - No fields were reordered (shared prefix is intact)
601///
602/// This is useful for progressive rollouts: if backward-readable, both V(N) and
603/// V(N+1) code can coexist, reading each other's accounts (V(N) ignores extra
604/// fields at the end).
605///
606/// Note: This does NOT mean no migration is needed -- the layout_id will still
607/// differ, so strict loaders will reject the data. This checks the *wire-level*
608/// compatibility of the prefix.
609#[inline]
610pub fn is_backward_readable(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
611    if older.disc != newer.disc {
612        return false;
613    }
614    // All older fields must exist in newer at the same positions with same types.
615    if newer.field_count < older.field_count {
616        return false;
617    }
618    let mut i = 0;
619    while i < older.field_count {
620        let old_f = &older.fields[i];
621        // Newer must have a field at the same index with matching name, type, size.
622        if i >= newer.field_count {
623            return false;
624        }
625        let new_f = &newer.fields[i];
626        if !const_str_eq(old_f.name, new_f.name)
627            || !const_str_eq(old_f.canonical_type, new_f.canonical_type)
628            || old_f.size != new_f.size
629        {
630            return false;
631        }
632        i += 1;
633    }
634    // Newer may have extra fields at the end -- that's fine for backward reading.
635    true
636}
637
638// -- Compatibility Verdict --
639
640/// Unified compatibility verdict between two layout versions.
641///
642/// Replaces ad-hoc boolean checks with a single, ranked classification.
643/// The ordering is from least disruptive to most disruptive.
644#[derive(Clone, Copy, Debug, PartialEq, Eq)]
645pub enum CompatibilityVerdict {
646    /// Layouts are byte-identical (same layout_id).
647    Identical,
648    /// Same wire layout (field count, sizes, offsets, types all match)
649    /// but `layout_id` differs. Semantic metadata changed (e.g. field
650    /// intent, layout name). Safe to read, no migration needed.
651    WireCompatible,
652    /// Same discriminator, old field prefix intact in new layout.
653    /// Old readers can still parse new accounts (they ignore the tail).
654    /// Covers both strict append (new fields only at the end) and
655    /// prefix-preserving changes. No forced migration required.
656    AppendSafe,
657    /// Breaking change: field types changed, fields removed, or prefix
658    /// altered. Full migration required before deploying new code.
659    MigrationRequired,
660    /// Different discriminators. These are fundamentally different types.
661    Incompatible,
662}
663
664impl CompatibilityVerdict {
665    /// Compute the verdict for a version transition.
666    #[inline]
667    pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
668        if older.layout_id == newer.layout_id {
669            return Self::Identical;
670        }
671        if older.disc != newer.disc {
672            return Self::Incompatible;
673        }
674        let backward = is_backward_readable(older, newer);
675        // Wire-compatible: identical wire layout but layout_id differs
676        // (only semantic metadata changed, e.g. field intent).
677        if backward
678            && older.field_count == newer.field_count
679            && older.total_size == newer.total_size
680        {
681            return Self::WireCompatible;
682        }
683        if backward {
684            Self::AppendSafe
685        } else {
686            Self::MigrationRequired
687        }
688    }
689
690    /// Human-readable name.
691    #[inline]
692    pub const fn name(self) -> &'static str {
693        match self {
694            Self::Identical => "identical",
695            Self::WireCompatible => "wire-compatible",
696            Self::AppendSafe => "append-safe",
697            Self::MigrationRequired => "migration-required",
698            Self::Incompatible => "incompatible",
699        }
700    }
701
702    /// Whether the transition is safe without any migration.
703    #[inline]
704    pub const fn is_safe(self) -> bool {
705        matches!(
706            self,
707            Self::Identical | Self::WireCompatible | Self::AppendSafe
708        )
709    }
710
711    /// Whether old readers can still parse accounts written by the new layout.
712    #[inline]
713    pub const fn is_backward_readable(self) -> bool {
714        matches!(
715            self,
716            Self::Identical | Self::WireCompatible | Self::AppendSafe
717        )
718    }
719
720    /// Whether a migration instruction is required.
721    #[inline]
722    pub const fn requires_migration(self) -> bool {
723        matches!(self, Self::MigrationRequired)
724    }
725
726    /// Refine a verdict using segment-role information.
727    ///
728    /// The base `between()` is field-level only. When a segmented account
729    /// has role metadata, this method can soften or escalate:
730    ///
731    /// * A `MigrationRequired` verdict is softened to `AppendSafe` when
732    ///   **all** changed segments are clearable or rebuildable (Cache,
733    ///   Index, Journal). Core / Audit / Extension changes stay breaking.
734    ///
735    /// * An `AppendSafe` verdict is escalated to `MigrationRequired` when
736    ///   **any** modified segment is immutable-after-init (Audit).
737    pub fn refine_with_roles<const N: usize>(self, report: &SegmentMigrationReport<N>) -> Self {
738        match self {
739            Self::MigrationRequired => {
740                // If every segment that must change is clearable or
741                // rebuildable, the migration is effectively append-safe.
742                let mut i = 0;
743                let mut all_soft = true;
744                while i < report.count {
745                    let adv = &report.advice[i];
746                    if adv.must_preserve && !adv.clearable && !adv.rebuildable {
747                        // At least one hard segment -- stay breaking.
748                        all_soft = false;
749                        break;
750                    }
751                    i += 1;
752                }
753                if all_soft && report.count > 0 {
754                    Self::AppendSafe
755                } else {
756                    self
757                }
758            }
759            Self::AppendSafe => {
760                // Escalate if any immutable (Audit) segment was touched.
761                let mut i = 0;
762                while i < report.count {
763                    if report.advice[i].immutable {
764                        return Self::MigrationRequired;
765                    }
766                    i += 1;
767                }
768                self
769            }
770            _ => self,
771        }
772    }
773}
774
775impl fmt::Display for CompatibilityVerdict {
776    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777        f.write_str(self.name())
778    }
779}
780
781// ---------------------------------------------------------------------------
782// Compatibility Explain -- human-readable verdict context
783// ---------------------------------------------------------------------------
784
785/// A structured, human-readable explanation of a compatibility verdict.
786///
787/// Goes beyond the raw verdict to tell operators *why* a transition is
788/// safe or dangerous, what segments are involved, and what action is needed.
789pub struct CompatibilityExplain {
790    /// The computed verdict.
791    pub verdict: CompatibilityVerdict,
792    /// Fields that were added in the newer layout.
793    pub added_fields: [&'static str; 16],
794    /// Number of valid entries in `added_fields`.
795    pub added_count: u8,
796    /// Fields that were removed in the newer layout (breaking).
797    pub removed_fields: [&'static str; 16],
798    /// Number of valid entries in `removed_fields`.
799    pub removed_count: u8,
800    /// Fields that were changed (type or size mismatch).
801    pub changed_fields: [&'static str; 16],
802    /// Number of valid entries in `changed_fields`.
803    pub changed_count: u8,
804    /// Whether the semantic hash changed (meaning shifted even if wire is the same).
805    pub semantic_drift: bool,
806    /// One-line human-readable summary.
807    pub summary: &'static str,
808}
809
810impl CompatibilityExplain {
811    /// Generate a full explanation from two layout manifests.
812    pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
813        let verdict = CompatibilityVerdict::between(older, newer);
814
815        let mut added = [""; 16];
816        let mut added_n = 0u8;
817        let mut removed = [""; 16];
818        let mut removed_n = 0u8;
819        let mut changed = [""; 16];
820        let mut changed_n = 0u8;
821
822        // Directly iterate manifest fields so we keep the 'static lifetime.
823        let shared = if older.field_count < newer.field_count {
824            older.field_count
825        } else {
826            newer.field_count
827        };
828
829        let mut i = 0;
830        while i < shared {
831            let old_f = &older.fields[i];
832            let new_f = &newer.fields[i];
833            let name_eq = const_str_eq(old_f.name, new_f.name);
834            let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
835            let size_eq = old_f.size == new_f.size;
836            if !(name_eq && type_eq && size_eq) {
837                if (changed_n as usize) < 16 {
838                    changed[changed_n as usize] = old_f.name;
839                    changed_n += 1;
840                }
841            }
842            i += 1;
843        }
844        // Fields only in newer (added).
845        while i < newer.field_count {
846            if (added_n as usize) < 16 {
847                added[added_n as usize] = newer.fields[i].name;
848                added_n += 1;
849            }
850            i += 1;
851        }
852        // Fields only in older (removed).
853        let mut j = shared;
854        while j < older.field_count {
855            if (removed_n as usize) < 16 {
856                removed[removed_n as usize] = older.fields[j].name;
857                removed_n += 1;
858            }
859            j += 1;
860        }
861
862        let fp_old = LayoutFingerprint::from_manifest(older);
863        let fp_new = LayoutFingerprint::from_manifest(newer);
864        let semantic_drift = fp_old.wire_matches_but_semantics_differ(&fp_new);
865
866        let summary = match verdict {
867            CompatibilityVerdict::Identical => "Layouts are byte-identical. No action needed.",
868            CompatibilityVerdict::WireCompatible => {
869                if semantic_drift {
870                    "Wire layout matches but field semantics changed. Review field intents."
871                } else {
872                    "Wire layout matches with metadata-only changes. Safe to deploy."
873                }
874            }
875            CompatibilityVerdict::AppendSafe => {
876                "New fields appended at the end. Old readers still work."
877            }
878            CompatibilityVerdict::MigrationRequired => {
879                "Breaking field changes. Migration instruction required before deploy."
880            }
881            CompatibilityVerdict::Incompatible => {
882                "Different discriminators. These are unrelated account types."
883            }
884        };
885
886        Self {
887            verdict,
888            added_fields: added,
889            added_count: added_n,
890            removed_fields: removed,
891            removed_count: removed_n,
892            changed_fields: changed,
893            changed_count: changed_n,
894            semantic_drift,
895            summary,
896        }
897    }
898}
899
900impl fmt::Display for CompatibilityExplain {
901    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
902        writeln!(f, "Verdict: {} ({})", self.verdict.name(), self.summary)?;
903        if self.added_count > 0 {
904            write!(f, "  Added:")?;
905            let mut i = 0;
906            while i < self.added_count as usize {
907                write!(f, " {}", self.added_fields[i])?;
908                i += 1;
909            }
910            writeln!(f)?;
911        }
912        if self.removed_count > 0 {
913            write!(f, "  Removed:")?;
914            let mut i = 0;
915            while i < self.removed_count as usize {
916                write!(f, " {}", self.removed_fields[i])?;
917                i += 1;
918            }
919            writeln!(f)?;
920        }
921        if self.changed_count > 0 {
922            write!(f, "  Changed:")?;
923            let mut i = 0;
924            while i < self.changed_count as usize {
925                write!(f, " {}", self.changed_fields[i])?;
926                i += 1;
927            }
928            writeln!(f)?;
929        }
930        if self.semantic_drift {
931            writeln!(
932                f,
933                "  Warning: semantic drift detected (wire matches but meaning changed)"
934            )?;
935        }
936        Ok(())
937    }
938}
939
940/// Field-level compatibility result.
941#[derive(Clone, Copy, PartialEq, Eq, Debug)]
942pub enum FieldCompat {
943    /// Field exists in both versions with identical type and size.
944    Identical,
945    /// Field exists in both but type or size changed (breaking).
946    Changed,
947    /// Field was added in the newer version (append-safe).
948    Added,
949    /// Field was removed in the newer version (breaking).
950    Removed,
951}
952
953/// Compare two manifests field-by-field.
954///
955/// Returns up to `N` field comparison results. Each entry is
956/// (field_name, FieldCompat). Checks that shared prefix fields are
957/// identical and classifies remaining fields as Added or Removed.
958#[inline]
959pub fn compare_fields<'a, const N: usize>(
960    older: &'a LayoutManifest,
961    newer: &'a LayoutManifest,
962) -> FieldCompatReport<'a, N> {
963    let mut report = FieldCompatReport {
964        entries: [FieldCompatEntry {
965            name: "",
966            status: FieldCompat::Identical,
967        }; N],
968        count: 0,
969        is_append_safe: true,
970    };
971
972    // Check shared prefix
973    let shared = if older.field_count < newer.field_count {
974        older.field_count
975    } else {
976        newer.field_count
977    };
978
979    let mut i = 0;
980    while i < shared && report.count < N {
981        let old_f = &older.fields[i];
982        let new_f = &newer.fields[i];
983
984        let name_eq = const_str_eq(old_f.name, new_f.name);
985        let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
986        let size_eq = old_f.size == new_f.size;
987
988        let status = if name_eq && type_eq && size_eq {
989            FieldCompat::Identical
990        } else {
991            report.is_append_safe = false;
992            FieldCompat::Changed
993        };
994
995        report.entries[report.count] = FieldCompatEntry {
996            name: old_f.name,
997            status,
998        };
999        report.count += 1;
1000        i += 1;
1001    }
1002
1003    // Fields only in newer (added)
1004    while i < newer.field_count && report.count < N {
1005        report.entries[report.count] = FieldCompatEntry {
1006            name: newer.fields[i].name,
1007            status: FieldCompat::Added,
1008        };
1009        report.count += 1;
1010        i += 1;
1011    }
1012
1013    // Fields only in older (removed -- breaking)
1014    let mut j = shared;
1015    while j < older.field_count && report.count < N {
1016        report.entries[report.count] = FieldCompatEntry {
1017            name: older.fields[j].name,
1018            status: FieldCompat::Removed,
1019        };
1020        report.count += 1;
1021        report.is_append_safe = false;
1022        j += 1;
1023    }
1024
1025    report
1026}
1027
1028/// A single field compatibility entry.
1029#[derive(Clone, Copy)]
1030pub struct FieldCompatEntry<'a> {
1031    pub name: &'a str,
1032    pub status: FieldCompat,
1033}
1034
1035/// Result of a field-level compatibility comparison.
1036pub struct FieldCompatReport<'a, const N: usize> {
1037    pub entries: [FieldCompatEntry<'a>; N],
1038    pub count: usize,
1039    /// True if all changes are append-only (no mutations or removals).
1040    pub is_append_safe: bool,
1041}
1042
1043impl<'a, const N: usize> FieldCompatReport<'a, N> {
1044    /// Number of field comparison entries.
1045    #[inline(always)]
1046    pub fn len(&self) -> usize {
1047        self.count
1048    }
1049
1050    /// Whether the report has no entries.
1051    #[inline(always)]
1052    pub fn is_empty(&self) -> bool {
1053        self.count == 0
1054    }
1055
1056    /// Get entry by index.
1057    #[inline(always)]
1058    pub fn get(&self, i: usize) -> Option<&FieldCompatEntry<'a>> {
1059        if i < self.count {
1060            Some(&self.entries[i])
1061        } else {
1062            None
1063        }
1064    }
1065
1066    /// Whether the schema change is append-safe (no breaking changes).
1067    #[inline(always)]
1068    pub fn is_append_safe(&self) -> bool {
1069        self.is_append_safe
1070    }
1071
1072    /// Count fields with a specific status.
1073    #[inline]
1074    pub fn count_status(&self, status: FieldCompat) -> usize {
1075        let mut n = 0;
1076        let mut i = 0;
1077        while i < self.count {
1078            if self.entries[i].status == status {
1079                n += 1;
1080            }
1081            i += 1;
1082        }
1083        n
1084    }
1085}
1086
1087// -- Account Header Decoder --
1088
1089/// Decoded account header for inspection/tooling.
1090#[derive(Clone, Copy)]
1091pub struct DecodedHeader {
1092    pub disc: u8,
1093    pub version: u8,
1094    pub flags: u16,
1095    pub layout_id: [u8; 8],
1096    pub reserved: [u8; 4],
1097}
1098
1099/// Decode an account header from raw bytes.
1100///
1101/// Works on any data that starts with a 16-byte Hopper header.
1102/// Does not validate -- just reads the bytes.
1103#[inline]
1104pub fn decode_header(data: &[u8]) -> Option<DecodedHeader> {
1105    if data.len() < HEADER_LEN {
1106        return None;
1107    }
1108    Some(DecodedHeader {
1109        disc: data[0],
1110        version: data[1],
1111        flags: u16::from_le_bytes([data[2], data[3]]),
1112        layout_id: [
1113            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
1114        ],
1115        reserved: [data[12], data[13], data[14], data[15]],
1116    })
1117}
1118
1119/// Try to identify which manifest matches an account's header.
1120///
1121/// Scans a list of manifests for matching disc + layout_id.
1122/// Returns the index and manifest if found.
1123#[inline]
1124pub fn identify_account<'a>(
1125    data: &[u8],
1126    manifests: &'a [LayoutManifest],
1127) -> Option<(usize, &'a LayoutManifest)> {
1128    let header = decode_header(data)?;
1129    let mut i = 0;
1130    while i < manifests.len() {
1131        let m = &manifests[i];
1132        if m.disc == header.disc && m.layout_id == header.layout_id {
1133            return Some((i, m));
1134        }
1135        i += 1;
1136    }
1137    None
1138}
1139
1140// -- Segment Inspector --
1141
1142/// Decoded segment entry for inspection.
1143#[derive(Clone, Copy)]
1144pub struct DecodedSegment {
1145    pub id: [u8; 4],
1146    pub offset: u32,
1147    pub size: u32,
1148    pub flags: u16,
1149    pub version: u8,
1150}
1151
1152/// Decode segment entries from a segmented account.
1153///
1154/// Returns up to `N` segments. Works on raw account data that
1155/// starts with a 16-byte header followed by the segment registry.
1156#[inline]
1157pub fn decode_segments<const N: usize>(data: &[u8]) -> Option<(usize, [DecodedSegment; N])> {
1158    let registry_start = HEADER_LEN;
1159    if data.len() < registry_start + 4 {
1160        return None;
1161    }
1162
1163    let count = u16::from_le_bytes([data[registry_start], data[registry_start + 1]]) as usize;
1164    if count > N {
1165        return None;
1166    }
1167
1168    let entries_start = registry_start + 4;
1169    let mut segments = [DecodedSegment {
1170        id: [0; 4],
1171        offset: 0,
1172        size: 0,
1173        flags: 0,
1174        version: 0,
1175    }; N];
1176
1177    let mut i = 0;
1178    while i < count {
1179        let off = entries_start + i * 16;
1180        if off + 16 > data.len() {
1181            return None;
1182        }
1183        segments[i] = DecodedSegment {
1184            id: [data[off], data[off + 1], data[off + 2], data[off + 3]],
1185            offset: u32::from_le_bytes([
1186                data[off + 4],
1187                data[off + 5],
1188                data[off + 6],
1189                data[off + 7],
1190            ]),
1191            size: u32::from_le_bytes([
1192                data[off + 8],
1193                data[off + 9],
1194                data[off + 10],
1195                data[off + 11],
1196            ]),
1197            flags: u16::from_le_bytes([data[off + 12], data[off + 13]]),
1198            version: data[off + 14],
1199        };
1200        i += 1;
1201    }
1202
1203    Some((count, segments))
1204}
1205
1206// -- Manifest Registry --
1207
1208/// A static registry of all layout manifests for a program.
1209///
1210/// Pass this to `identify_account` or CLI tooling to decode
1211/// arbitrary accounts from a program.
1212///
1213/// ```ignore
1214/// const MANIFESTS: ManifestRegistry<3> = ManifestRegistry::new(&[
1215///     VAULT_MANIFEST,
1216///     POOL_MANIFEST,
1217///     POSITION_MANIFEST,
1218/// ]);
1219///
1220/// if let Some((idx, manifest)) = MANIFESTS.identify(data) {
1221///     // Found matching layout
1222/// }
1223/// ```
1224pub struct ManifestRegistry<const N: usize> {
1225    manifests: [Option<LayoutManifest>; N],
1226    count: usize,
1227}
1228
1229impl<const N: usize> ManifestRegistry<N> {
1230    /// Create an empty registry.
1231    #[inline(always)]
1232    pub const fn empty() -> Self {
1233        Self {
1234            manifests: [None; N],
1235            count: 0,
1236        }
1237    }
1238
1239    /// Create a registry from a slice of manifests.
1240    #[inline]
1241    pub const fn from_slice(manifests: &[LayoutManifest]) -> Self {
1242        let mut reg = Self::empty();
1243        let mut i = 0;
1244        while i < manifests.len() && i < N {
1245            reg.manifests[i] = Some(manifests[i]);
1246            reg.count += 1;
1247            i += 1;
1248        }
1249        reg
1250    }
1251
1252    /// Number of registered manifests.
1253    #[inline(always)]
1254    pub const fn len(&self) -> usize {
1255        self.count
1256    }
1257
1258    /// Whether the registry has no manifests.
1259    #[inline(always)]
1260    pub const fn is_empty(&self) -> bool {
1261        self.count == 0
1262    }
1263
1264    /// Try to identify an account from header data.
1265    #[inline]
1266    pub fn identify(&self, data: &[u8]) -> Option<(usize, &LayoutManifest)> {
1267        let header = decode_header(data)?;
1268        let mut i = 0;
1269        while i < self.count {
1270            if let Some(m) = &self.manifests[i] {
1271                if m.disc == header.disc && m.layout_id == header.layout_id {
1272                    return Some((i, m));
1273                }
1274            }
1275            i += 1;
1276        }
1277        None
1278    }
1279
1280    /// Get a manifest by index.
1281    #[inline]
1282    pub fn get(&self, index: usize) -> Option<&LayoutManifest> {
1283        if index < self.count {
1284            self.manifests[index].as_ref()
1285        } else {
1286            None
1287        }
1288    }
1289
1290    /// Find a manifest by discriminator.
1291    #[inline]
1292    pub fn find_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
1293        let mut i = 0;
1294        while i < self.count {
1295            if let Some(m) = &self.manifests[i] {
1296                if m.disc == disc {
1297                    return Some(m);
1298                }
1299            }
1300            i += 1;
1301        }
1302        None
1303    }
1304
1305    /// Find a manifest by layout_id.
1306    #[inline]
1307    pub fn find_by_layout_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
1308        let mut i = 0;
1309        while i < self.count {
1310            if let Some(m) = &self.manifests[i] {
1311                if &m.layout_id == layout_id {
1312                    return Some(m);
1313                }
1314            }
1315            i += 1;
1316        }
1317        None
1318    }
1319}
1320
1321// -- Helpers --
1322
1323/// String equality check without std.
1324#[inline]
1325fn const_str_eq(a: &str, b: &str) -> bool {
1326    let a = a.as_bytes();
1327    let b = b.as_bytes();
1328    if a.len() != b.len() {
1329        return false;
1330    }
1331    let mut i = 0;
1332    while i < a.len() {
1333        if a[i] != b[i] {
1334            return false;
1335        }
1336        i += 1;
1337    }
1338    true
1339}
1340
1341// -- Migration Planner --
1342
1343/// Migration policy for a version transition.
1344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1345pub enum MigrationPolicy {
1346    /// No changes -- manifests are identical.
1347    NoOp,
1348    /// Append-only: new fields added at end. Existing data valid as-is.
1349    /// Just update header (version + layout_id). No data movement needed.
1350    AppendOnly,
1351    /// Migration required: field types/sizes changed, or fields removed.
1352    /// Must allocate new account, copy compatible prefix, zero-init new region.
1353    RequiresMigration,
1354    /// Incompatible: different discriminators or fundamental layout mismatch.
1355    Incompatible,
1356}
1357
1358/// A step in the migration plan.
1359#[derive(Clone, Copy)]
1360pub struct MigrationStep<'a> {
1361    /// Step type.
1362    pub action: MigrationAction,
1363    /// Target field name (if field-specific).
1364    pub field: &'a str,
1365    /// Byte offset for this step.
1366    pub offset: u16,
1367    /// Byte count for this step.
1368    pub size: u16,
1369}
1370
1371/// Migration action type.
1372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1373pub enum MigrationAction {
1374    /// Copy bytes from old account at (offset, size) to new account.
1375    CopyPrefix,
1376    /// Zero-initialize new region at (offset, size).
1377    ZeroInit,
1378    /// Update the account header (version + layout_id).
1379    UpdateHeader,
1380    /// Realloc the account to new size (grows in place if possible).
1381    Realloc,
1382}
1383
1384/// A generated migration plan between two layout versions.
1385///
1386/// Contains ordered steps to transform V(N) account data to V(N+1).
1387/// All steps are stack-allocated to stay `no_std`/`no_alloc`.
1388pub struct MigrationPlan<'a, const N: usize> {
1389    pub policy: MigrationPolicy,
1390    pub steps: [MigrationStep<'a>; N],
1391    pub step_count: usize,
1392    /// Old total size.
1393    pub old_size: usize,
1394    /// New total size.
1395    pub new_size: usize,
1396    /// How many bytes must be copied from old to new.
1397    pub copy_bytes: usize,
1398    /// How many bytes are newly zero-initialized.
1399    pub zero_bytes: usize,
1400    /// Whether V(N) code can still parse V(N+1) accounts (prefix-compatible).
1401    pub backward_readable: bool,
1402}
1403
1404impl<'a, const N: usize> MigrationPlan<'a, N> {
1405    /// Generate a migration plan from two manifests.
1406    ///
1407    /// Analyzes the field-level diff and produces an ordered list of
1408    /// concrete steps (copy prefix, zero-init new fields, update header).
1409    pub fn generate(older: &'a LayoutManifest, newer: &'a LayoutManifest) -> Self {
1410        let mut plan = Self {
1411            policy: MigrationPolicy::NoOp,
1412            steps: [MigrationStep {
1413                action: MigrationAction::CopyPrefix,
1414                field: "",
1415                offset: 0,
1416                size: 0,
1417            }; N],
1418            step_count: 0,
1419            old_size: older.total_size,
1420            new_size: newer.total_size,
1421            copy_bytes: 0,
1422            zero_bytes: 0,
1423            backward_readable: is_backward_readable(older, newer),
1424        };
1425
1426        // Same layout -- no-op
1427        if older.layout_id == newer.layout_id {
1428            plan.policy = MigrationPolicy::NoOp;
1429            return plan;
1430        }
1431
1432        // Different discriminators -- incompatible
1433        if older.disc != newer.disc {
1434            plan.policy = MigrationPolicy::Incompatible;
1435            return plan;
1436        }
1437
1438        // Field-level analysis
1439        let report = compare_fields::<32>(older, newer);
1440
1441        if !report.is_append_safe {
1442            plan.policy = MigrationPolicy::RequiresMigration;
1443        } else {
1444            plan.policy = MigrationPolicy::AppendOnly;
1445        }
1446
1447        // Step 1: Copy the compatible prefix (all Identical fields)
1448        let mut compatible_end: u16 = HEADER_LEN as u16;
1449        let mut i = 0;
1450        while i < report.count {
1451            if report.entries[i].status == FieldCompat::Identical {
1452                // Find matching field in older to get offset+size
1453                let mut j = 0;
1454                while j < older.field_count {
1455                    if const_str_eq(older.fields[j].name, report.entries[i].name) {
1456                        let field_end = older.fields[j].offset + older.fields[j].size;
1457                        if field_end > compatible_end {
1458                            compatible_end = field_end;
1459                        }
1460                        break;
1461                    }
1462                    j += 1;
1463                }
1464            }
1465            i += 1;
1466        }
1467
1468        if compatible_end > HEADER_LEN as u16 && plan.step_count < N {
1469            let copy_size = compatible_end - HEADER_LEN as u16;
1470            plan.steps[plan.step_count] = MigrationStep {
1471                action: MigrationAction::CopyPrefix,
1472                field: "",
1473                offset: HEADER_LEN as u16,
1474                size: copy_size,
1475            };
1476            plan.copy_bytes = copy_size as usize;
1477            plan.step_count += 1;
1478        }
1479
1480        // Step 2: Realloc if size changed
1481        if newer.total_size != older.total_size && plan.step_count < N {
1482            plan.steps[plan.step_count] = MigrationStep {
1483                action: MigrationAction::Realloc,
1484                field: "",
1485                offset: 0,
1486                size: newer.total_size as u16,
1487            };
1488            plan.step_count += 1;
1489        }
1490
1491        // Step 3: Zero-init added fields
1492        i = 0;
1493        while i < report.count {
1494            if report.entries[i].status == FieldCompat::Added && plan.step_count < N {
1495                // Find in newer manifest
1496                let mut j = 0;
1497                while j < newer.field_count {
1498                    if const_str_eq(newer.fields[j].name, report.entries[i].name) {
1499                        plan.steps[plan.step_count] = MigrationStep {
1500                            action: MigrationAction::ZeroInit,
1501                            field: report.entries[i].name,
1502                            offset: newer.fields[j].offset,
1503                            size: newer.fields[j].size,
1504                        };
1505                        plan.zero_bytes += newer.fields[j].size as usize;
1506                        plan.step_count += 1;
1507                        break;
1508                    }
1509                    j += 1;
1510                }
1511            }
1512            i += 1;
1513        }
1514
1515        // Step 4: Update header
1516        if plan.step_count < N {
1517            plan.steps[plan.step_count] = MigrationStep {
1518                action: MigrationAction::UpdateHeader,
1519                field: "",
1520                offset: 0,
1521                size: HEADER_LEN as u16,
1522            };
1523            plan.step_count += 1;
1524        }
1525
1526        plan
1527    }
1528
1529    /// Number of steps in the plan.
1530    #[inline(always)]
1531    pub fn len(&self) -> usize {
1532        self.step_count
1533    }
1534
1535    /// Whether the plan has no steps.
1536    #[inline(always)]
1537    pub fn is_empty(&self) -> bool {
1538        self.step_count == 0
1539    }
1540
1541    /// Whether this plan requires data movement (non-trivial migration).
1542    #[inline(always)]
1543    pub fn requires_data_copy(&self) -> bool {
1544        self.policy == MigrationPolicy::RequiresMigration
1545    }
1546
1547    /// Get step by index.
1548    #[inline(always)]
1549    pub fn step(&self, i: usize) -> Option<&MigrationStep<'a>> {
1550        if i < self.step_count {
1551            Some(&self.steps[i])
1552        } else {
1553            None
1554        }
1555    }
1556
1557    /// Iterator-style: iterate steps with index.
1558    #[inline]
1559    pub fn for_each_step<F: FnMut(usize, &MigrationStep<'a>)>(&self, mut f: F) {
1560        let mut i = 0;
1561        while i < self.step_count {
1562            f(i, &self.steps[i]);
1563            i += 1;
1564        }
1565    }
1566}
1567
1568// -- Segment-Role-Aware Migration Advice --
1569
1570/// Segment role classification for migration (mirrors hopper-core SegmentRole).
1571///
1572/// This is a schema-level copy so hopper-schema can reason about roles without
1573/// depending on internal details of hopper-core's segment module.
1574#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1575#[repr(u8)]
1576pub enum SegmentRoleHint {
1577    Core = 0,
1578    Extension = 1,
1579    Journal = 2,
1580    Index = 3,
1581    Cache = 4,
1582    Audit = 5,
1583    Shard = 6,
1584    Unclassified = 7,
1585}
1586
1587impl SegmentRoleHint {
1588    /// Decode role from the upper 4 bits of a segment flags field.
1589    #[inline(always)]
1590    pub fn from_flags(flags: u16) -> Self {
1591        match (flags >> 12) & 0x0F {
1592            0 => Self::Core,
1593            1 => Self::Extension,
1594            2 => Self::Journal,
1595            3 => Self::Index,
1596            4 => Self::Cache,
1597            5 => Self::Audit,
1598            6 => Self::Shard,
1599            _ => Self::Unclassified,
1600        }
1601    }
1602
1603    /// Human-readable name.
1604    #[inline(always)]
1605    pub fn name(self) -> &'static str {
1606        match self {
1607            Self::Core => "Core",
1608            Self::Extension => "Extension",
1609            Self::Journal => "Journal",
1610            Self::Index => "Index",
1611            Self::Cache => "Cache",
1612            Self::Audit => "Audit",
1613            Self::Shard => "Shard",
1614            Self::Unclassified => "Unclassified",
1615        }
1616    }
1617
1618    /// Whether data in this segment must survive migration unchanged.
1619    #[inline(always)]
1620    pub fn must_preserve(self) -> bool {
1621        matches!(
1622            self,
1623            Self::Core | Self::Extension | Self::Audit | Self::Shard
1624        )
1625    }
1626
1627    /// Whether the segment can be zeroed and rebuilt from other on-chain state.
1628    #[inline(always)]
1629    pub fn is_rebuildable(self) -> bool {
1630        matches!(self, Self::Index | Self::Cache)
1631    }
1632
1633    /// Whether the segment can be cleared during migration.
1634    #[inline(always)]
1635    pub fn is_clearable(self) -> bool {
1636        matches!(self, Self::Journal | Self::Index | Self::Cache)
1637    }
1638
1639    /// Whether the segment is append-only (no in-place mutations).
1640    #[inline(always)]
1641    pub fn is_append_only(self) -> bool {
1642        matches!(self, Self::Journal | Self::Audit)
1643    }
1644
1645    /// Whether the segment is immutable after initialization (Audit logs).
1646    #[inline(always)]
1647    pub fn is_immutable(self) -> bool {
1648        matches!(self, Self::Audit)
1649    }
1650
1651    /// Whether this segment's data must be copied during migration.
1652    ///
1653    /// Core and Audit segments contain irreplaceable state that cannot
1654    /// be rebuilt or cleared. Their bytes must survive migration intact.
1655    #[inline(always)]
1656    pub fn requires_migration_copy(self) -> bool {
1657        matches!(self, Self::Core | Self::Audit)
1658    }
1659
1660    /// Whether this segment can be safely dropped (zeroed) without data loss.
1661    ///
1662    /// Cache segments hold derived/computed values that can be rebuilt
1663    /// from other on-chain state. Dropping them is always safe.
1664    #[inline(always)]
1665    pub fn is_safe_to_drop(self) -> bool {
1666        matches!(self, Self::Cache)
1667    }
1668}
1669
1670/// Migration advice for a single segment.
1671#[derive(Clone, Copy)]
1672pub struct SegmentAdvice {
1673    /// Segment ID bytes.
1674    pub id: [u8; 4],
1675    /// Byte size.
1676    pub size: u32,
1677    /// Decoded role.
1678    pub role: SegmentRoleHint,
1679    /// Must be preserved across migration (data cannot be lost).
1680    pub must_preserve: bool,
1681    /// Can be cleared and rebuilt from other data.
1682    pub clearable: bool,
1683    /// Can be rebuilt from on-chain state (index, cache).
1684    pub rebuildable: bool,
1685    /// Append-only: only new entries allowed, no mutations.
1686    pub append_only: bool,
1687    /// Immutable after init (Audit segments).
1688    pub immutable: bool,
1689}
1690
1691/// Segment-level migration report for a segmented account.
1692///
1693/// Analyzes each segment's role and produces per-segment migration advice.
1694/// This lets the migration planner tell you which segments are safe to clear,
1695/// which must be preserved, and which can be rebuilt from other data.
1696pub struct SegmentMigrationReport<const N: usize> {
1697    pub advice: [SegmentAdvice; N],
1698    pub count: usize,
1699    /// Total bytes in segments that must be preserved.
1700    pub preserve_bytes: u32,
1701    /// Total bytes in segments that can be cleared.
1702    pub clearable_bytes: u32,
1703    /// Total bytes in segments that can be rebuilt.
1704    pub rebuildable_bytes: u32,
1705}
1706
1707impl<const N: usize> SegmentMigrationReport<N> {
1708    /// Analyze decoded segments and produce migration advice per segment.
1709    pub fn analyze(segments: &[DecodedSegment], count: usize) -> Self {
1710        let mut report = Self {
1711            advice: [SegmentAdvice {
1712                id: [0; 4],
1713                size: 0,
1714                role: SegmentRoleHint::Unclassified,
1715                must_preserve: false,
1716                clearable: false,
1717                rebuildable: false,
1718                append_only: false,
1719                immutable: false,
1720            }; N],
1721            count: 0,
1722            preserve_bytes: 0,
1723            clearable_bytes: 0,
1724            rebuildable_bytes: 0,
1725        };
1726
1727        let mut i = 0;
1728        while i < count && i < N {
1729            let seg = &segments[i];
1730            let role = SegmentRoleHint::from_flags(seg.flags);
1731
1732            report.advice[i] = SegmentAdvice {
1733                id: seg.id,
1734                size: seg.size,
1735                role,
1736                must_preserve: role.must_preserve(),
1737                clearable: role.is_clearable(),
1738                rebuildable: role.is_rebuildable(),
1739                append_only: role.is_append_only(),
1740                immutable: role.is_immutable(),
1741            };
1742
1743            if role.must_preserve() {
1744                report.preserve_bytes += seg.size;
1745            }
1746            if role.is_clearable() {
1747                report.clearable_bytes += seg.size;
1748            }
1749            if role.is_rebuildable() {
1750                report.rebuildable_bytes += seg.size;
1751            }
1752
1753            report.count += 1;
1754            i += 1;
1755        }
1756
1757        report
1758    }
1759
1760    /// Number of segments that must be preserved during migration.
1761    pub fn must_preserve_count(&self) -> usize {
1762        let mut n = 0;
1763        let mut i = 0;
1764        while i < self.count {
1765            if self.advice[i].must_preserve {
1766                n += 1;
1767            }
1768            i += 1;
1769        }
1770        n
1771    }
1772
1773    /// Number of segments that can be safely cleared during migration.
1774    pub fn clearable_count(&self) -> usize {
1775        let mut n = 0;
1776        let mut i = 0;
1777        while i < self.count {
1778            if self.advice[i].clearable {
1779                n += 1;
1780            }
1781            i += 1;
1782        }
1783        n
1784    }
1785}
1786
1787impl<const N: usize> fmt::Display for SegmentMigrationReport<N> {
1788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1789        writeln!(f, "Segment Migration Advice ({} segments):", self.count)?;
1790        let mut i = 0;
1791        while i < self.count {
1792            let a = &self.advice[i];
1793            write!(f, "  [{}] {} ({} bytes):", i, a.role.name(), a.size)?;
1794            if a.must_preserve {
1795                write!(f, " MUST-PRESERVE")?;
1796            }
1797            if a.clearable {
1798                write!(f, " clearable")?;
1799            }
1800            if a.rebuildable {
1801                write!(f, " rebuildable")?;
1802            }
1803            if a.append_only {
1804                write!(f, " append-only")?;
1805            }
1806            if a.immutable {
1807                write!(f, " immutable")?;
1808            }
1809            writeln!(f)?;
1810            i += 1;
1811        }
1812        writeln!(
1813            f,
1814            "  preserve={} bytes, clearable={} bytes, rebuildable={} bytes",
1815            self.preserve_bytes, self.clearable_bytes, self.rebuildable_bytes
1816        )?;
1817        Ok(())
1818    }
1819}
1820
1821// -- Inspection Surfaces --
1822//
1823// `core::fmt::Display` implementations for decoded types.
1824// These provide human-readable output for CLI tooling, indexers,
1825// and debugging without requiring `std`.
1826
1827impl fmt::Display for DecodedHeader {
1828    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1829        write!(
1830            f,
1831            "Header {{ disc: {}, ver: {}, flags: 0x{:04x}, layout_id: ",
1832            self.disc, self.version, self.flags,
1833        )?;
1834        write_hex(f, &self.layout_id)?;
1835        write!(f, " }}")
1836    }
1837}
1838
1839impl fmt::Debug for DecodedHeader {
1840    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1841        write!(
1842            f,
1843            "DecodedHeader {{ disc: {}, version: {}, flags: 0x{:04x}, layout_id: ",
1844            self.disc, self.version, self.flags,
1845        )?;
1846        write_hex(f, &self.layout_id)?;
1847        write!(f, ", reserved: ")?;
1848        write_hex(f, &self.reserved)?;
1849        write!(f, " }}")
1850    }
1851}
1852
1853impl fmt::Display for DecodedSegment {
1854    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1855        write!(f, "Segment {{ id: ")?;
1856        write_hex(f, &self.id)?;
1857        write!(
1858            f,
1859            ", offset: {}, size: {}, flags: 0x{:04x}, ver: {} }}",
1860            self.offset, self.size, self.flags, self.version,
1861        )
1862    }
1863}
1864
1865impl fmt::Debug for DecodedSegment {
1866    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1867        write!(f, "DecodedSegment {{ id: ")?;
1868        write_hex(f, &self.id)?;
1869        write!(
1870            f,
1871            ", offset: {}, size: {}, flags: 0x{:04x}, version: {} }}",
1872            self.offset, self.size, self.flags, self.version,
1873        )
1874    }
1875}
1876
1877impl fmt::Display for FieldCompat {
1878    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1879        match self {
1880            FieldCompat::Identical => write!(f, "identical"),
1881            FieldCompat::Changed => write!(f, "changed"),
1882            FieldCompat::Added => write!(f, "added"),
1883            FieldCompat::Removed => write!(f, "removed"),
1884        }
1885    }
1886}
1887
1888impl fmt::Display for MigrationPolicy {
1889    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1890        match self {
1891            MigrationPolicy::NoOp => write!(f, "no-op"),
1892            MigrationPolicy::AppendOnly => write!(f, "append-only"),
1893            MigrationPolicy::RequiresMigration => write!(f, "requires-migration"),
1894            MigrationPolicy::Incompatible => write!(f, "incompatible"),
1895        }
1896    }
1897}
1898
1899impl fmt::Display for MigrationAction {
1900    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1901        match self {
1902            MigrationAction::CopyPrefix => write!(f, "copy-prefix"),
1903            MigrationAction::ZeroInit => write!(f, "zero-init"),
1904            MigrationAction::UpdateHeader => write!(f, "update-header"),
1905            MigrationAction::Realloc => write!(f, "realloc"),
1906        }
1907    }
1908}
1909
1910impl<'a> fmt::Display for MigrationStep<'a> {
1911    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1912        write!(
1913            f,
1914            "{} @ offset={}, size={}",
1915            self.action, self.offset, self.size
1916        )?;
1917        if !self.field.is_empty() {
1918            write!(f, " (field: {})", self.field)?;
1919        }
1920        Ok(())
1921    }
1922}
1923
1924impl<'a, const N: usize> fmt::Display for MigrationPlan<'a, N> {
1925    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1926        writeln!(f, "MigrationPlan ({}):", self.policy)?;
1927        writeln!(
1928            f,
1929            "  old_size={}, new_size={}",
1930            self.old_size, self.new_size
1931        )?;
1932        writeln!(
1933            f,
1934            "  copy={} bytes, zero={} bytes",
1935            self.copy_bytes, self.zero_bytes
1936        )?;
1937        let mut i = 0;
1938        while i < self.step_count {
1939            writeln!(f, "  step {}: {}", i, self.steps[i])?;
1940            i += 1;
1941        }
1942        Ok(())
1943    }
1944}
1945
1946impl fmt::Display for LayoutManifest {
1947    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1948        writeln!(
1949            f,
1950            "{} v{} (disc={}, size={})",
1951            self.name, self.version, self.disc, self.total_size
1952        )?;
1953        write!(f, "  layout_id: ")?;
1954        write_hex(f, &self.layout_id)?;
1955        writeln!(f)?;
1956        let mut i = 0;
1957        while i < self.field_count {
1958            let field = &self.fields[i];
1959            writeln!(
1960                f,
1961                "  [{:>3}..{:>3}] {} : {} ({} bytes)",
1962                field.offset,
1963                field.offset + field.size,
1964                field.name,
1965                field.canonical_type,
1966                field.size,
1967            )?;
1968            i += 1;
1969        }
1970        Ok(())
1971    }
1972}
1973
1974/// Decode an account header and format it for display.
1975///
1976/// Returns `None` if data is too short.
1977pub fn format_header(data: &[u8]) -> Option<DecodedHeader> {
1978    decode_header(data)
1979}
1980
1981/// Decode segments and return a displayable segment map string.
1982///
1983/// Returns `None` if data doesn't contain a valid segment registry.
1984pub fn format_segment_map<const N: usize>(data: &[u8]) -> Option<SegmentMap<N>> {
1985    let (count, segments) = decode_segments::<N>(data)?;
1986    Some(SegmentMap { count, segments })
1987}
1988
1989/// A decoded segment map for display.
1990pub struct SegmentMap<const N: usize> {
1991    pub count: usize,
1992    pub segments: [DecodedSegment; N],
1993}
1994
1995impl<const N: usize> fmt::Display for SegmentMap<N> {
1996    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1997        writeln!(f, "Segment Map ({} segments):", self.count)?;
1998        let reg_end = HEADER_LEN + 4 + self.count * 16;
1999        writeln!(f, "  [  0..{:>3}] Header", HEADER_LEN)?;
2000        writeln!(f, "  [{:>3}..{:>3}] Registry", HEADER_LEN, reg_end)?;
2001        let mut i = 0;
2002        while i < self.count {
2003            let seg = &self.segments[i];
2004            let end = seg.offset + seg.size;
2005            write!(f, "  [{:>3}..{:>3}] Segment {} (id=", seg.offset, end, i)?;
2006            write_hex(f, &seg.id)?;
2007            writeln!(f, ", {} bytes, v{})", seg.size, seg.version)?;
2008            i += 1;
2009        }
2010        Ok(())
2011    }
2012}
2013
2014/// Write bytes as hex to a formatter (no_std compatible).
2015fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
2016    for b in bytes {
2017        write!(f, "{:02x}", b)?;
2018    }
2019    Ok(())
2020}
2021
2022// ---------------------------------------------------------------------------
2023// Program Manifest -- full program schema for Manager / tooling
2024// ---------------------------------------------------------------------------
2025
2026/// An account entry in an instruction's account list.
2027#[derive(Clone, Copy, Debug)]
2028pub struct AccountEntry {
2029    /// Account name.
2030    pub name: &'static str,
2031    /// Whether the account is writable.
2032    pub writable: bool,
2033    /// Whether the account is a signer.
2034    pub signer: bool,
2035    /// Optional layout reference name (for typed accounts).
2036    pub layout_ref: &'static str,
2037}
2038
2039/// An argument descriptor for an instruction.
2040#[derive(Clone, Copy, Debug)]
2041pub struct ArgDescriptor {
2042    /// Argument name.
2043    pub name: &'static str,
2044    /// Canonical type name.
2045    pub canonical_type: &'static str,
2046    /// Byte size.
2047    pub size: u16,
2048}
2049
2050/// How an instruction account is resolved before invocation.
2051///
2052/// The descriptor is intentionally small and string-backed so manifests can
2053/// carry resolver plans without depending on a proc-macro registry or an
2054/// allocator. Tooling can project this into richer client builders, while
2055/// no-std programs can keep it as static metadata.
2056#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2057pub enum AccountResolverKind {
2058    /// Caller must provide the account address directly.
2059    Provided,
2060    /// Account is derived from PDA seeds.
2061    Pda,
2062    /// Account is pinned to a known constant address.
2063    Constant,
2064    /// Account may be omitted by callers that do not need the path.
2065    Optional,
2066    /// Account is a program id entry.
2067    Program,
2068    /// Account is a sysvar entry.
2069    Sysvar,
2070}
2071
2072impl AccountResolverKind {
2073    /// Stable manifest spelling.
2074    pub const fn as_str(self) -> &'static str {
2075        match self {
2076            Self::Provided => "provided",
2077            Self::Pda => "pda",
2078            Self::Constant => "constant",
2079            Self::Optional => "optional",
2080            Self::Program => "program",
2081            Self::Sysvar => "sysvar",
2082        }
2083    }
2084}
2085
2086impl fmt::Display for AccountResolverKind {
2087    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2088        f.write_str(self.as_str())
2089    }
2090}
2091
2092/// Static resolver metadata for one instruction account.
2093#[derive(Clone, Copy, Debug)]
2094pub struct AccountResolverDescriptor {
2095    /// Account field name.
2096    pub account: &'static str,
2097    /// Resolver strategy.
2098    pub kind: AccountResolverKind,
2099    /// PDA seed expressions or stable seed labels.
2100    pub seeds: &'static [&'static str],
2101    /// Expected address, when known.
2102    pub expected_address: &'static str,
2103    /// Expected owner, when known.
2104    pub expected_owner: &'static str,
2105    /// Payer account used by init/realloc flows, when applicable.
2106    pub payer: &'static str,
2107    /// Layout reference for typed account resolvers.
2108    pub layout_ref: &'static str,
2109    /// Whether the account is optional.
2110    pub optional: bool,
2111}
2112
2113/// Semantic effect an instruction has on accounts or emitted artifacts.
2114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2115pub enum InstructionEffectKind {
2116    /// Reads an account without mutation.
2117    Reads,
2118    /// Mutates account data or lamports in place.
2119    Writes,
2120    /// Creates a fresh account.
2121    CreatesAccount,
2122    /// Reallocates account data.
2123    ReallocatesAccount,
2124    /// Closes an account.
2125    ClosesAccount,
2126    /// Requires a signer for authorization.
2127    RequiresSigner,
2128    /// Emits a receipt or event that clients should expect.
2129    EmitsReceipt,
2130    /// Performs a cross-program invocation.
2131    InvokesCpi,
2132}
2133
2134impl InstructionEffectKind {
2135    /// Stable manifest spelling.
2136    pub const fn as_str(self) -> &'static str {
2137        match self {
2138            Self::Reads => "reads",
2139            Self::Writes => "writes",
2140            Self::CreatesAccount => "creates_account",
2141            Self::ReallocatesAccount => "reallocates_account",
2142            Self::ClosesAccount => "closes_account",
2143            Self::RequiresSigner => "requires_signer",
2144            Self::EmitsReceipt => "emits_receipt",
2145            Self::InvokesCpi => "invokes_cpi",
2146        }
2147    }
2148}
2149
2150impl fmt::Display for InstructionEffectKind {
2151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2152        f.write_str(self.as_str())
2153    }
2154}
2155
2156/// Static effect metadata for one instruction target.
2157#[derive(Clone, Copy, Debug)]
2158pub struct InstructionEffectDescriptor {
2159    /// Effect kind.
2160    pub kind: InstructionEffectKind,
2161    /// Account, receipt, or CPI target this effect describes.
2162    pub target: &'static str,
2163    /// Layout reference for typed effects, if any.
2164    pub layout_ref: &'static str,
2165    /// Policy pack or lifecycle reason that explains the effect.
2166    pub reason: &'static str,
2167}
2168
2169impl crate::accounts::ContextAccountDescriptor {
2170    /// Derive the default resolver kind from context account metadata.
2171    pub const fn resolver_kind(&self) -> AccountResolverKind {
2172        if self.expected_address.as_bytes().len() > 0 {
2173            AccountResolverKind::Constant
2174        } else if self.seeds.len() > 0 {
2175            AccountResolverKind::Pda
2176        } else if self.optional {
2177            AccountResolverKind::Optional
2178        } else if self.kind.as_bytes().len() >= 6
2179            && self.kind.as_bytes()[0] == b'S'
2180            && self.kind.as_bytes()[1] == b'y'
2181            && self.kind.as_bytes()[2] == b's'
2182            && self.kind.as_bytes()[3] == b'v'
2183            && self.kind.as_bytes()[4] == b'a'
2184            && self.kind.as_bytes()[5] == b'r'
2185        {
2186            AccountResolverKind::Sysvar
2187        } else if self.kind.as_bytes().len() >= 7
2188            && self.kind.as_bytes()[0] == b'P'
2189            && self.kind.as_bytes()[1] == b'r'
2190            && self.kind.as_bytes()[2] == b'o'
2191            && self.kind.as_bytes()[3] == b'g'
2192            && self.kind.as_bytes()[4] == b'r'
2193            && self.kind.as_bytes()[5] == b'a'
2194            && self.kind.as_bytes()[6] == b'm'
2195        {
2196            AccountResolverKind::Program
2197        } else {
2198            AccountResolverKind::Provided
2199        }
2200    }
2201
2202    /// Project account metadata into a resolver descriptor.
2203    pub const fn resolver_descriptor(&self) -> AccountResolverDescriptor {
2204        AccountResolverDescriptor {
2205            account: self.name,
2206            kind: self.resolver_kind(),
2207            seeds: self.seeds,
2208            expected_address: self.expected_address,
2209            expected_owner: self.expected_owner,
2210            payer: self.payer,
2211            layout_ref: self.layout_ref,
2212            optional: self.optional,
2213        }
2214    }
2215
2216    /// Derive the primary semantic effect this account contributes.
2217    pub const fn primary_effect_kind(&self) -> InstructionEffectKind {
2218        match self.lifecycle {
2219            crate::accounts::AccountLifecycle::Init => InstructionEffectKind::CreatesAccount,
2220            crate::accounts::AccountLifecycle::Realloc => InstructionEffectKind::ReallocatesAccount,
2221            crate::accounts::AccountLifecycle::Close => InstructionEffectKind::ClosesAccount,
2222            crate::accounts::AccountLifecycle::Existing => {
2223                if self.writable {
2224                    InstructionEffectKind::Writes
2225                } else if self.signer {
2226                    InstructionEffectKind::RequiresSigner
2227                } else {
2228                    InstructionEffectKind::Reads
2229                }
2230            }
2231        }
2232    }
2233
2234    /// Project account metadata into an effect descriptor.
2235    pub const fn effect_descriptor(&self) -> InstructionEffectDescriptor {
2236        InstructionEffectDescriptor {
2237            kind: self.primary_effect_kind(),
2238            target: self.name,
2239            layout_ref: self.layout_ref,
2240            reason: self.policy_ref,
2241        }
2242    }
2243}
2244
2245impl crate::accounts::ContextDescriptor {
2246    /// Number of account resolver descriptors this context can expose.
2247    pub const fn resolver_count(&self) -> usize {
2248        self.accounts.len()
2249    }
2250
2251    /// Number of derived instruction effects, including receipt emission.
2252    pub const fn effect_count(&self) -> usize {
2253        self.accounts.len() + if self.receipts_expected { 1 } else { 0 }
2254    }
2255
2256    /// Find the resolver descriptor for a named account.
2257    pub fn find_resolver(&self, account: &str) -> Option<AccountResolverDescriptor> {
2258        let mut i = 0;
2259        while i < self.accounts.len() {
2260            if const_str_eq(self.accounts[i].name, account) {
2261                return Some(self.accounts[i].resolver_descriptor());
2262            }
2263            i += 1;
2264        }
2265        None
2266    }
2267
2268    /// Find the primary effect descriptor for a named account.
2269    pub fn find_effect(&self, target: &str) -> Option<InstructionEffectDescriptor> {
2270        let mut i = 0;
2271        while i < self.accounts.len() {
2272            if const_str_eq(self.accounts[i].name, target) {
2273                return Some(self.accounts[i].effect_descriptor());
2274            }
2275            i += 1;
2276        }
2277        None
2278    }
2279}
2280
2281/// Failure reason for `#[hopper::args] T::parse`.
2282///
2283/// Exposed here (rather than in `hopper-core`) because programs that use
2284/// the args derive will already depend on `hopper-schema` for `SchemaExport`.
2285/// Keeping the error type in schema avoids an extra dependency edge.
2286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2287pub enum ArgParseError {
2288    /// Not enough bytes to cover the packed struct size.
2289    TooShort {
2290        /// Bytes required.
2291        required: u16,
2292        /// Bytes available.
2293        got: u16,
2294    },
2295}
2296
2297impl fmt::Display for ArgParseError {
2298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2299        match self {
2300            ArgParseError::TooShort { required, got } => {
2301                write!(f, "args: too short (required {}, got {})", required, got)
2302            }
2303        }
2304    }
2305}
2306
2307/// Descriptor for one variant of a `#[hopper::error]` enum.
2308///
2309/// Carried in the program manifest so off-chain SDKs can map numeric error
2310/// codes back to names and. via `invariant`. to the safety check that
2311/// produced them.
2312///
2313/// ## Design notes
2314///
2315/// Hopper errors can carry the **invariant name** a variant corresponds
2316/// to, so a client that sees error `0x1001` can surface "Invariant
2317/// `balance_nonzero` failed" without needing to keep a separate lookup
2318/// table in sync with the on-chain code.
2319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2320pub struct ErrorDescriptor {
2321    /// Variant name (exactly as declared).
2322    pub name: &'static str,
2323    /// Stable numeric code emitted on failure.
2324    pub code: u32,
2325    /// Invariant this error corresponds to, or empty string if none.
2326    pub invariant: &'static str,
2327    /// Short documentation string (often copied from variant doc comments).
2328    pub doc: &'static str,
2329}
2330
2331/// Public-facing program constant.
2332///
2333/// Mirrors Anchor's `#[constant]` IDL surface. Carries the source-level
2334/// name, the stringified Rust type, and the stringified initializer
2335/// expression, so off-chain consumers can reconstruct the value
2336/// without the program crate as a build-time dependency.
2337///
2338/// Emitted by `#[hopper::constant]` as a sibling `pub const` next to
2339/// the original declaration; collected into a `&'static [ConstantDescriptor]`
2340/// slice by the program author (or by the `hopper::program!` macro) and
2341/// passed to an IDL emitter via `AnchorIdlWithConstants`.
2342#[derive(Clone, Copy, Debug)]
2343pub struct ConstantDescriptor {
2344    /// Constant name, e.g. `"MAX_DEPOSIT"`.
2345    pub name: &'static str,
2346    /// Stringified Rust type, e.g. `"u64"` or `"[u8; 32]"`.
2347    pub ty: &'static str,
2348    /// Stringified initializer expression, e.g. `"1_000_000"`.
2349    pub value: &'static str,
2350    /// Optional doc-comment text.
2351    pub docs: &'static str,
2352}
2353
2354/// A convenience wrapper holding an enum's full error table.
2355///
2356/// Programs expose their error tables to the schema via the `SchemaExport`
2357/// path; the manifest gains an `errors[]` field that aggregates across all
2358/// such registries declared in the crate.
2359#[derive(Clone, Copy, Debug)]
2360pub struct ErrorRegistry {
2361    /// Enum ident, e.g. `"VaultError"`.
2362    pub enum_name: &'static str,
2363    /// Ordered error descriptors.
2364    pub errors: &'static [ErrorDescriptor],
2365}
2366
2367impl ErrorRegistry {
2368    /// Look up an error descriptor by numeric code.
2369    pub fn find_by_code(&self, code: u32) -> Option<&ErrorDescriptor> {
2370        let mut i = 0;
2371        while i < self.errors.len() {
2372            if self.errors[i].code == code {
2373                return Some(&self.errors[i]);
2374            }
2375            i += 1;
2376        }
2377        None
2378    }
2379
2380    /// Look up the invariant name associated with a code, if any.
2381    pub fn invariant_for(&self, code: u32) -> Option<&'static str> {
2382        self.find_by_code(code).and_then(|d| {
2383            if d.invariant.is_empty() {
2384                None
2385            } else {
2386                Some(d.invariant)
2387            }
2388        })
2389    }
2390}
2391
2392/// An instruction descriptor in a program manifest.
2393#[derive(Clone, Copy, Debug)]
2394pub struct InstructionDescriptor {
2395    /// Instruction name.
2396    pub name: &'static str,
2397    /// Discriminator tag.
2398    pub tag: u8,
2399    /// Arguments.
2400    pub args: &'static [ArgDescriptor],
2401    /// Accounts.
2402    pub accounts: &'static [AccountEntry],
2403    /// Capability names.
2404    pub capabilities: &'static [&'static str],
2405    /// Policy pack name (empty if custom).
2406    pub policy_pack: &'static str,
2407    /// Whether this instruction emits a receipt.
2408    pub receipt_expected: bool,
2409}
2410
2411/// An event descriptor in a program manifest.
2412#[derive(Clone, Copy)]
2413pub struct EventDescriptor {
2414    /// Event name.
2415    pub name: &'static str,
2416    /// Event discriminator tag.
2417    pub tag: u8,
2418    /// Event fields.
2419    pub fields: &'static [FieldDescriptor],
2420}
2421
2422/// A policy descriptor in a program manifest.
2423#[derive(Clone, Copy)]
2424pub struct PolicyDescriptor {
2425    /// Policy pack name.
2426    pub name: &'static str,
2427    /// Capability names this policy covers.
2428    pub capabilities: &'static [&'static str],
2429    /// Requirement names this policy triggers.
2430    pub requirements: &'static [&'static str],
2431    /// Invariant names this policy checks.
2432    pub invariants: &'static [&'static str],
2433    /// Receipt profile expected when this policy is active.
2434    pub receipt_profile: &'static str,
2435}
2436
2437/// Extended per-layout metadata for the manifest.
2438///
2439/// Carries richer operational metadata that Hopper Manager, CLI, and
2440/// migration tooling use beyond what `LayoutManifest` provides.
2441#[derive(Clone, Copy)]
2442pub struct LayoutMetadata {
2443    /// Layout name (must match corresponding `LayoutManifest.name`).
2444    pub name: &'static str,
2445    /// Segment role descriptors (for segmented accounts).
2446    pub segment_roles: &'static [&'static str],
2447    /// Whether append-only changes to this layout are always safe.
2448    pub append_safe: bool,
2449    /// Whether changes require an explicit migration instruction.
2450    pub migration_required: bool,
2451    /// Whether derived data (index/cache segments) can be rebuilt from core.
2452    pub rebuildable: bool,
2453    /// Policy pack name that governs writes to this layout.
2454    pub policy_pack: &'static str,
2455    /// Invariant pack names that must hold for this layout.
2456    pub invariant_pack: &'static [&'static str],
2457    /// Receipt profile name for mutations on this layout.
2458    pub receipt_profile: &'static str,
2459    /// Execution phases this layout participates in.
2460    pub phase_requirements: &'static [&'static str],
2461    /// Trust profile label (e.g. "verified", "trusted", "unchecked").
2462    pub trust_profile: &'static str,
2463    /// Hints for Hopper Manager rendering.
2464    pub manager_hints: &'static [&'static str],
2465}
2466
2467/// A compatibility pair describing a known upgrade path.
2468#[derive(Clone, Copy)]
2469pub struct CompatibilityPair {
2470    /// Old layout name.
2471    pub from_layout: &'static str,
2472    /// Old version.
2473    pub from_version: u8,
2474    /// New layout name.
2475    pub to_layout: &'static str,
2476    /// New version.
2477    pub to_version: u8,
2478    /// Migration policy.
2479    pub policy: MigrationPolicy,
2480    /// Whether backward reading is supported.
2481    pub backward_readable: bool,
2482}
2483
2484/// A full program manifest for Hopper Manager and tooling.
2485///
2486/// This is the **rich internal schema** that powers `hopper manager`,
2487/// compatibility checking, migration planning, receipt rendering, and
2488/// CLI inspection. It is intentionally richer than the public IDL --
2489/// the manifest carries operational metadata that tools need but
2490/// external consumers do not.
2491///
2492/// ## Truth hierarchy
2493///
2494/// ```text
2495/// ProgramManifest  ⊃  ProgramIdl  ⊃  CodamaProjection
2496///       (rich)         (public)         (interop)
2497/// ```
2498#[derive(Clone, Copy)]
2499pub struct ProgramManifest {
2500    /// Program name.
2501    pub name: &'static str,
2502    /// Program version string.
2503    pub version: &'static str,
2504    /// Program description.
2505    pub description: &'static str,
2506    /// Layout manifests for all account types.
2507    pub layouts: &'static [LayoutManifest],
2508    /// Extended per-layout operational metadata.
2509    pub layout_metadata: &'static [LayoutMetadata],
2510    /// Instruction descriptors.
2511    pub instructions: &'static [InstructionDescriptor],
2512    /// Event descriptors.
2513    pub events: &'static [EventDescriptor],
2514    /// Policy descriptors.
2515    pub policies: &'static [PolicyDescriptor],
2516    /// Known upgrade paths between layout versions.
2517    pub compatibility_pairs: &'static [CompatibilityPair],
2518    /// Tooling / rendering hints for Manager.
2519    pub tooling_hints: &'static [&'static str],
2520    /// Context (instruction account struct) descriptors.
2521    pub contexts: &'static [crate::accounts::ContextDescriptor],
2522}
2523
2524// ---------------------------------------------------------------------------
2525// Program IDL -- public schema subset
2526// ---------------------------------------------------------------------------
2527
2528/// PDA seed hint for an instruction account.
2529#[derive(Clone, Copy)]
2530pub struct PdaSeedHint {
2531    /// Seed kind: "literal", "account", "arg".
2532    pub kind: &'static str,
2533    /// Seed value or reference name.
2534    pub value: &'static str,
2535}
2536
2537/// IDL account entry with optional PDA metadata.
2538#[derive(Clone, Copy)]
2539pub struct IdlAccountEntry {
2540    /// Account name.
2541    pub name: &'static str,
2542    /// Whether the account is writable.
2543    pub writable: bool,
2544    /// Whether the account is a signer.
2545    pub signer: bool,
2546    /// Optional layout reference name.
2547    pub layout_ref: &'static str,
2548    /// PDA seed hints (empty if not a PDA).
2549    pub pda_seeds: &'static [PdaSeedHint],
2550}
2551
2552/// IDL instruction descriptor.
2553#[derive(Clone, Copy)]
2554pub struct IdlInstructionDescriptor {
2555    /// Instruction name.
2556    pub name: &'static str,
2557    /// Discriminator tag.
2558    pub tag: u8,
2559    /// Arguments.
2560    pub args: &'static [ArgDescriptor],
2561    /// Accounts with PDA metadata.
2562    pub accounts: &'static [IdlAccountEntry],
2563}
2564
2565/// A public-facing IDL for a Hopper program.
2566///
2567/// Contains only what external consumers (clients, explorers, SDKs) need.
2568/// Does NOT contain internal policy logic, migration planner hints,
2569/// trust internals, or unsafe metadata.
2570///
2571/// Generated from (and strictly a subset of) `ProgramManifest`.
2572#[derive(Clone, Copy)]
2573pub struct ProgramIdl {
2574    /// Program name.
2575    pub name: &'static str,
2576    /// Program version string.
2577    pub version: &'static str,
2578    /// Program description.
2579    pub description: &'static str,
2580    /// Instructions with args, accounts, PDA hints.
2581    pub instructions: &'static [IdlInstructionDescriptor],
2582    /// Account layout summaries (name, disc, version, layout_id, size, fields).
2583    pub accounts: &'static [LayoutManifest],
2584    /// Event descriptors.
2585    pub events: &'static [EventDescriptor],
2586    /// Optional layout_id fingerprints per account.
2587    pub fingerprints: &'static [([u8; 8], &'static str)],
2588}
2589
2590impl ProgramIdl {
2591    /// Create an empty IDL.
2592    pub const fn empty() -> Self {
2593        Self {
2594            name: "",
2595            version: "",
2596            description: "",
2597            instructions: &[],
2598            accounts: &[],
2599            events: &[],
2600            fingerprints: &[],
2601        }
2602    }
2603
2604    /// Number of instructions.
2605    pub const fn instruction_count(&self) -> usize {
2606        self.instructions.len()
2607    }
2608
2609    /// Number of account types.
2610    pub const fn account_count(&self) -> usize {
2611        self.accounts.len()
2612    }
2613
2614    /// Find an instruction by name.
2615    pub fn find_instruction(&self, name: &str) -> Option<&IdlInstructionDescriptor> {
2616        let mut i = 0;
2617        while i < self.instructions.len() {
2618            if const_str_eq(self.instructions[i].name, name) {
2619                return Some(&self.instructions[i]);
2620            }
2621            i += 1;
2622        }
2623        None
2624    }
2625
2626    /// Find an account layout by name.
2627    pub fn find_account(&self, name: &str) -> Option<&LayoutManifest> {
2628        let mut i = 0;
2629        while i < self.accounts.len() {
2630            if const_str_eq(self.accounts[i].name, name) {
2631                return Some(&self.accounts[i]);
2632            }
2633            i += 1;
2634        }
2635        None
2636    }
2637}
2638
2639impl fmt::Display for ProgramIdl {
2640    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2641        writeln!(f, "IDL: {} {}", self.name, self.version)?;
2642        if !self.description.is_empty() {
2643            writeln!(f, "  {}", self.description)?;
2644        }
2645        writeln!(f)?;
2646        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2647        for ix in self.instructions.iter() {
2648            write!(
2649                f,
2650                "  {:>2}  {:16} args={} accounts={}",
2651                ix.tag,
2652                ix.name,
2653                ix.args.len(),
2654                ix.accounts.len()
2655            )?;
2656            writeln!(f)?;
2657        }
2658        writeln!(f)?;
2659        writeln!(f, "Accounts ({}):", self.accounts.len())?;
2660        for a in self.accounts.iter() {
2661            write!(
2662                f,
2663                "  {:16} disc={} v{} {} bytes  id=",
2664                a.name, a.disc, a.version, a.total_size
2665            )?;
2666            write_hex(f, &a.layout_id)?;
2667            writeln!(f)?;
2668        }
2669        if !self.events.is_empty() {
2670            writeln!(f)?;
2671            writeln!(f, "Events ({}):", self.events.len())?;
2672            for e in self.events.iter() {
2673                writeln!(f, "  {:>2}  {:16} fields={}", e.tag, e.name, e.fields.len())?;
2674            }
2675        }
2676        Ok(())
2677    }
2678}
2679
2680// ---------------------------------------------------------------------------
2681// Codama Projection -- ecosystem interop subset
2682// ---------------------------------------------------------------------------
2683
2684/// Codama-compatible instruction descriptor.
2685///
2686/// Only the fields needed for Codama/Kinobi IDL generation.
2687#[derive(Clone, Copy)]
2688pub struct CodamaInstruction {
2689    pub name: &'static str,
2690    pub discriminator: u8,
2691    pub args: &'static [ArgDescriptor],
2692    pub accounts: &'static [IdlAccountEntry],
2693}
2694
2695/// Codama-compatible account descriptor.
2696#[derive(Clone, Copy)]
2697pub struct CodamaAccount {
2698    pub name: &'static str,
2699    pub discriminator: u8,
2700    pub size: usize,
2701    pub fields: &'static [FieldDescriptor],
2702}
2703
2704/// Codama-compatible event descriptor.
2705#[derive(Clone, Copy)]
2706pub struct CodamaEvent {
2707    pub name: &'static str,
2708    pub discriminator: u8,
2709    pub fields: &'static [FieldDescriptor],
2710}
2711
2712/// Codama-compatible projection of a Hopper program.
2713///
2714/// This is a **bridge**, not a prison. It maps the clean public subset
2715/// of a Hopper program into a shape that Codama/Kinobi tooling can consume.
2716///
2717/// Does NOT include: internal policy logic, migration planner hints,
2718/// trust internals, unsafe metadata, segment roles, or manager hints.
2719///
2720/// ## Layering
2721///
2722/// ```text
2723/// ProgramManifest = rich truth
2724/// ProgramIdl      = public schema
2725/// CodamaProjection = compatibility projection
2726/// ```
2727#[derive(Clone, Copy)]
2728pub struct CodamaProjection {
2729    /// Program name.
2730    pub name: &'static str,
2731    /// Program version.
2732    pub version: &'static str,
2733    /// Instructions (public subset).
2734    pub instructions: &'static [CodamaInstruction],
2735    /// Account types (public subset).
2736    pub accounts: &'static [CodamaAccount],
2737    /// Events (public subset).
2738    pub events: &'static [CodamaEvent],
2739}
2740
2741impl CodamaProjection {
2742    /// Create an empty projection.
2743    pub const fn empty() -> Self {
2744        Self {
2745            name: "",
2746            version: "",
2747            instructions: &[],
2748            accounts: &[],
2749            events: &[],
2750        }
2751    }
2752}
2753
2754impl fmt::Display for CodamaProjection {
2755    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2756        writeln!(f, "Codama: {} {}", self.name, self.version)?;
2757        writeln!(f)?;
2758        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2759        for ix in self.instructions.iter() {
2760            writeln!(
2761                f,
2762                "  {:>2}  {:16} args={} accounts={}",
2763                ix.discriminator,
2764                ix.name,
2765                ix.args.len(),
2766                ix.accounts.len()
2767            )?;
2768        }
2769        writeln!(f)?;
2770        writeln!(f, "Accounts ({}):", self.accounts.len())?;
2771        for a in self.accounts.iter() {
2772            writeln!(
2773                f,
2774                "  {:16} disc={} {} bytes fields={}",
2775                a.name,
2776                a.discriminator,
2777                a.size,
2778                a.fields.len()
2779            )?;
2780        }
2781        if !self.events.is_empty() {
2782            writeln!(f)?;
2783            writeln!(f, "Events ({}):", self.events.len())?;
2784            for e in self.events.iter() {
2785                writeln!(
2786                    f,
2787                    "  {:>2}  {:16} fields={}",
2788                    e.discriminator,
2789                    e.name,
2790                    e.fields.len()
2791                )?;
2792            }
2793        }
2794        Ok(())
2795    }
2796}
2797
2798impl ProgramManifest {
2799    /// Create an empty program manifest.
2800    pub const fn empty() -> Self {
2801        Self {
2802            name: "",
2803            version: "",
2804            description: "",
2805            layouts: &[],
2806            layout_metadata: &[],
2807            instructions: &[],
2808            events: &[],
2809            policies: &[],
2810            compatibility_pairs: &[],
2811            tooling_hints: &[],
2812            contexts: &[],
2813        }
2814    }
2815
2816    /// Number of layouts.
2817    pub const fn layout_count(&self) -> usize {
2818        self.layouts.len()
2819    }
2820
2821    /// Number of instructions.
2822    pub const fn instruction_count(&self) -> usize {
2823        self.instructions.len()
2824    }
2825
2826    /// Find a layout by discriminator.
2827    pub fn find_layout_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
2828        let mut i = 0;
2829        while i < self.layouts.len() {
2830            if self.layouts[i].disc == disc {
2831                return Some(&self.layouts[i]);
2832            }
2833            i += 1;
2834        }
2835        None
2836    }
2837
2838    /// Find a layout by layout_id fingerprint.
2839    pub fn find_layout_by_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
2840        let mut i = 0;
2841        while i < self.layouts.len() {
2842            if self.layouts[i].layout_id == *layout_id {
2843                return Some(&self.layouts[i]);
2844            }
2845            i += 1;
2846        }
2847        None
2848    }
2849
2850    /// Find a layout that matches the given account data header.
2851    pub fn identify_from_data(&self, data: &[u8]) -> Option<&LayoutManifest> {
2852        let header = decode_header(data)?;
2853        // Try layout_id match first (strongest)
2854        if let Some(m) = self.find_layout_by_id(&header.layout_id) {
2855            return Some(m);
2856        }
2857        // Fall back to disc match
2858        self.find_layout_by_disc(header.disc)
2859    }
2860
2861    /// Find an instruction by tag.
2862    pub fn find_instruction(&self, tag: u8) -> Option<&InstructionDescriptor> {
2863        let mut i = 0;
2864        while i < self.instructions.len() {
2865            if self.instructions[i].tag == tag {
2866                return Some(&self.instructions[i]);
2867            }
2868            i += 1;
2869        }
2870        None
2871    }
2872
2873    /// Find a policy by name.
2874    pub fn find_policy(&self, name: &str) -> Option<&PolicyDescriptor> {
2875        let mut i = 0;
2876        while i < self.policies.len() {
2877            if self.policies[i].name == name {
2878                return Some(&self.policies[i]);
2879            }
2880            i += 1;
2881        }
2882        None
2883    }
2884
2885    /// Find extended layout metadata by layout name.
2886    pub fn find_layout_metadata(&self, name: &str) -> Option<&LayoutMetadata> {
2887        let mut i = 0;
2888        while i < self.layout_metadata.len() {
2889            if const_str_eq(self.layout_metadata[i].name, name) {
2890                return Some(&self.layout_metadata[i]);
2891            }
2892            i += 1;
2893        }
2894        None
2895    }
2896
2897    /// Find context metadata by instruction/context name.
2898    pub fn find_context(&self, name: &str) -> Option<&crate::accounts::ContextDescriptor> {
2899        let mut i = 0;
2900        while i < self.contexts.len() {
2901            if const_str_eq(self.contexts[i].name, name) {
2902                return Some(&self.contexts[i]);
2903            }
2904            i += 1;
2905        }
2906        None
2907    }
2908
2909    /// Find a compatibility pair for an upgrade path.
2910    pub fn find_compat_pair(
2911        &self,
2912        from_name: &str,
2913        from_ver: u8,
2914        to_name: &str,
2915        to_ver: u8,
2916    ) -> Option<&CompatibilityPair> {
2917        let mut i = 0;
2918        while i < self.compatibility_pairs.len() {
2919            let cp = &self.compatibility_pairs[i];
2920            if const_str_eq(cp.from_layout, from_name)
2921                && cp.from_version == from_ver
2922                && const_str_eq(cp.to_layout, to_name)
2923                && cp.to_version == to_ver
2924            {
2925                return Some(cp);
2926            }
2927            i += 1;
2928        }
2929        None
2930    }
2931}
2932
2933impl fmt::Display for ProgramManifest {
2934    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2935        writeln!(f, "Program: {} {}", self.name, self.version)?;
2936        if !self.description.is_empty() {
2937            writeln!(f, "  {}", self.description)?;
2938        }
2939        writeln!(f)?;
2940
2941        writeln!(f, "Layouts ({}):", self.layouts.len())?;
2942        for m in self.layouts.iter() {
2943            write!(
2944                f,
2945                "  {:16} v{}  disc={}  {} bytes  fingerprint=",
2946                m.name, m.version, m.disc, m.total_size
2947            )?;
2948            write_hex(f, &m.layout_id)?;
2949            // Show extended metadata if available
2950            if let Some(meta) = self.find_layout_metadata(m.name) {
2951                if !meta.trust_profile.is_empty() {
2952                    write!(f, "  trust={}", meta.trust_profile)?;
2953                }
2954                if meta.append_safe {
2955                    write!(f, "  append-safe")?;
2956                }
2957                if meta.migration_required {
2958                    write!(f, "  migration-required")?;
2959                }
2960            }
2961            writeln!(f)?;
2962        }
2963        writeln!(f)?;
2964
2965        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2966        for ix in self.instructions.iter() {
2967            write!(
2968                f,
2969                "  {:>2}  {:16} accounts={}",
2970                ix.tag,
2971                ix.name,
2972                ix.accounts.len()
2973            )?;
2974            if !ix.capabilities.is_empty() {
2975                write!(f, "  caps=")?;
2976                for (j, c) in ix.capabilities.iter().enumerate() {
2977                    if j > 0 {
2978                        write!(f, ",")?;
2979                    }
2980                    write!(f, "{}", c)?;
2981                }
2982            }
2983            if ix.receipt_expected {
2984                write!(f, "  receipt=yes")?;
2985            }
2986            if let Some(ctx) = self.find_context(ix.name) {
2987                write!(
2988                    f,
2989                    "  resolvers={} effects={}",
2990                    ctx.resolver_count(),
2991                    ctx.effect_count()
2992                )?;
2993            }
2994            writeln!(f)?;
2995        }
2996        writeln!(f)?;
2997
2998        if !self.policies.is_empty() {
2999            writeln!(f, "Policies ({}):", self.policies.len())?;
3000            for p in self.policies.iter() {
3001                write!(f, "  {:24}", p.name)?;
3002                for (j, r) in p.requirements.iter().enumerate() {
3003                    if j > 0 {
3004                        write!(f, " + ")?;
3005                    }
3006                    write!(f, "{}", r)?;
3007                }
3008                if !p.receipt_profile.is_empty() {
3009                    write!(f, "  receipt={}", p.receipt_profile)?;
3010                }
3011                writeln!(f)?;
3012            }
3013            writeln!(f)?;
3014        }
3015
3016        if !self.events.is_empty() {
3017            writeln!(f, "Events ({}):", self.events.len())?;
3018            for e in self.events.iter() {
3019                writeln!(f, "  {:>2}  {:16} fields={}", e.tag, e.name, e.fields.len())?;
3020            }
3021            writeln!(f)?;
3022        }
3023
3024        if !self.compatibility_pairs.is_empty() {
3025            writeln!(f, "Compatibility ({}):", self.compatibility_pairs.len())?;
3026            for cp in self.compatibility_pairs.iter() {
3027                writeln!(
3028                    f,
3029                    "  {} v{} -> {} v{}  {}{}",
3030                    cp.from_layout,
3031                    cp.from_version,
3032                    cp.to_layout,
3033                    cp.to_version,
3034                    cp.policy,
3035                    if cp.backward_readable {
3036                        "  backward-readable"
3037                    } else {
3038                        ""
3039                    },
3040                )?;
3041            }
3042        }
3043
3044        Ok(())
3045    }
3046}
3047
3048// ---------------------------------------------------------------------------
3049// Field-Level Account Decoder
3050// ---------------------------------------------------------------------------
3051
3052/// A decoded field value from account data.
3053pub struct DecodedField<'a> {
3054    /// Field name.
3055    pub name: &'a str,
3056    /// Canonical type.
3057    pub canonical_type: &'a str,
3058    /// Raw bytes of this field.
3059    pub raw: &'a [u8],
3060    /// Offset in account data.
3061    pub offset: u16,
3062    /// Size in bytes.
3063    pub size: u16,
3064}
3065
3066impl<'a> DecodedField<'a> {
3067    /// Format the field value as a human-readable string.
3068    ///
3069    /// Recognizes common Hopper wire types and formats them appropriately.
3070    pub fn format_value(&self, buf: &mut [u8]) -> usize {
3071        match self.canonical_type {
3072            "WireU64" | "LeU64" if self.raw.len() >= 8 => {
3073                let v = u64::from_le_bytes([
3074                    self.raw[0],
3075                    self.raw[1],
3076                    self.raw[2],
3077                    self.raw[3],
3078                    self.raw[4],
3079                    self.raw[5],
3080                    self.raw[6],
3081                    self.raw[7],
3082                ]);
3083                format_u64(v, buf)
3084            }
3085            "WireU32" | "LeU32" if self.raw.len() >= 4 => {
3086                let v =
3087                    u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) as u64;
3088                format_u64(v, buf)
3089            }
3090            "WireU16" | "LeU16" if self.raw.len() >= 2 => {
3091                let v = u16::from_le_bytes([self.raw[0], self.raw[1]]) as u64;
3092                format_u64(v, buf)
3093            }
3094            "WireBool" | "LeBool" if !self.raw.is_empty() => {
3095                if self.raw[0] != 0 {
3096                    let len = 4usize.min(buf.len());
3097                    buf[..len].copy_from_slice(&b"true"[..len]);
3098                    len
3099                } else {
3100                    let len = 5usize.min(buf.len());
3101                    buf[..len].copy_from_slice(&b"false"[..len]);
3102                    len
3103                }
3104            }
3105            "u8" if self.raw.len() == 1 => format_u64(self.raw[0] as u64, buf),
3106            _ if self.size == 32 => {
3107                // Likely an address/pubkey -- show as hex
3108                format_hex_truncated(self.raw, buf)
3109            }
3110            _ => format_hex_truncated(self.raw, buf),
3111        }
3112    }
3113}
3114
3115/// Decode all fields of an account against a layout manifest.
3116///
3117/// Returns the number of fields decoded (up to N).
3118pub fn decode_account_fields<'a, const N: usize>(
3119    data: &'a [u8],
3120    manifest: &'a LayoutManifest,
3121) -> (usize, [Option<DecodedField<'a>>; N]) {
3122    let mut fields: [Option<DecodedField<'a>>; N] = [const { None }; N];
3123    let count = manifest.field_count.min(N);
3124    let mut i = 0;
3125    while i < count {
3126        let fd = &manifest.fields[i];
3127        let start = fd.offset as usize;
3128        let end = start + fd.size as usize;
3129        if end <= data.len() {
3130            fields[i] = Some(DecodedField {
3131                name: fd.name,
3132                canonical_type: fd.canonical_type,
3133                raw: &data[start..end],
3134                offset: fd.offset,
3135                size: fd.size,
3136            });
3137        }
3138        i += 1;
3139    }
3140    (count, fields)
3141}
3142
3143/// Format a u64 as decimal into a byte buffer. Returns bytes written.
3144fn format_u64(mut v: u64, buf: &mut [u8]) -> usize {
3145    if v == 0 {
3146        if !buf.is_empty() {
3147            buf[0] = b'0';
3148            return 1;
3149        }
3150        return 0;
3151    }
3152    // Write digits in reverse
3153    let mut tmp = [0u8; 20];
3154    let mut pos = 0;
3155    while v > 0 && pos < 20 {
3156        tmp[pos] = b'0' + (v % 10) as u8;
3157        v /= 10;
3158        pos += 1;
3159    }
3160    let len = pos.min(buf.len());
3161    let mut i = 0;
3162    while i < len {
3163        buf[i] = tmp[pos - 1 - i];
3164        i += 1;
3165    }
3166    len
3167}
3168
3169/// Format bytes as hex, truncated to fit buffer. Shows first 8 + "..." if long.
3170fn format_hex_truncated(bytes: &[u8], buf: &mut [u8]) -> usize {
3171    const HEX: &[u8; 16] = b"0123456789abcdef";
3172    let max_bytes = if bytes.len() > 8 { 8 } else { bytes.len() };
3173    let mut pos = 0;
3174    // "0x" prefix
3175    if buf.len() >= 2 {
3176        buf[0] = b'0';
3177        buf[1] = b'x';
3178        pos = 2;
3179    }
3180    let mut i = 0;
3181    while i < max_bytes && pos + 1 < buf.len() {
3182        buf[pos] = HEX[(bytes[i] >> 4) as usize];
3183        buf[pos + 1] = HEX[(bytes[i] & 0xf) as usize];
3184        pos += 2;
3185        i += 1;
3186    }
3187    if bytes.len() > 8 && pos + 3 <= buf.len() {
3188        buf[pos] = b'.';
3189        buf[pos + 1] = b'.';
3190        buf[pos + 2] = b'.';
3191        pos += 3;
3192    }
3193    pos
3194}
3195
3196// ---------------------------------------------------------------------------
3197// On-Chain Schema Pointer
3198// ---------------------------------------------------------------------------
3199
3200/// On-chain account that points to a Hopper program's schema.
3201///
3202/// Stored at PDA `["hopper-schema", program_id]`. Contains hashes of
3203/// the manifest, IDL, and Codama projection, plus a URI to fetch the
3204/// full manifest. See `docs/ONCHAIN_SCHEMA_PUBLICATION.md`.
3205///
3206/// ## Wire layout (294 bytes payload + 16 bytes header = 310 bytes)
3207///
3208/// ```text
3209/// [0..16]    Hopper header (disc=255, ver=1)
3210/// [16..18]   schema_version   u16 LE
3211/// [18..20]   pointer_flags    u16 LE
3212/// [20..52]   manifest_hash    [u8; 32]
3213/// [52..84]   idl_hash         [u8; 32]
3214/// [84..116]  codama_hash      [u8; 32]
3215/// [116..118] uri_len          u16 LE
3216/// [118..310] uri              [u8; 192]
3217/// ```
3218#[repr(C)]
3219#[derive(Clone, Copy)]
3220pub struct HopperSchemaPointer {
3221    /// Schema format version (currently 1).
3222    pub schema_version: u16,
3223    /// Feature flags (HAS_MANIFEST, HAS_IDL, HAS_CODAMA, HAS_URI, ...).
3224    pub pointer_flags: u16,
3225    /// SHA-256 of the manifest JSON.
3226    pub manifest_hash: [u8; 32],
3227    /// SHA-256 of the IDL JSON.
3228    pub idl_hash: [u8; 32],
3229    /// SHA-256 of the Codama projection JSON.
3230    pub codama_hash: [u8; 32],
3231    /// Length of the URI string (0..192).
3232    pub uri_len: u16,
3233    /// UTF-8 URI pointing to the manifest (padded with zeros).
3234    pub uri: [u8; 192],
3235}
3236
3237impl HopperSchemaPointer {
3238    /// Reserved discriminator for schema pointer accounts.
3239    pub const DISC: u8 = 255;
3240
3241    /// Total payload size (excluding Hopper header).
3242    pub const PAYLOAD_LEN: usize = 2 + 2 + 32 + 32 + 32 + 2 + 192; // 294
3243
3244    /// Total account size including Hopper header.
3245    pub const ACCOUNT_LEN: usize = HEADER_LEN + Self::PAYLOAD_LEN; // 310
3246
3247    /// PDA seed prefix.
3248    pub const PDA_SEED: &'static [u8] = b"hopper-schema";
3249
3250    // Flag bits
3251    pub const FLAG_HAS_MANIFEST: u16 = 0x0001;
3252    pub const FLAG_HAS_IDL: u16 = 0x0002;
3253    pub const FLAG_HAS_CODAMA: u16 = 0x0004;
3254    pub const FLAG_HAS_URI: u16 = 0x0008;
3255    pub const FLAG_URI_IS_IPFS: u16 = 0x0010;
3256    pub const FLAG_URI_IS_ARWEAVE: u16 = 0x0020;
3257
3258    /// Get the URI as a string slice.
3259    pub fn uri_str(&self) -> &str {
3260        let len = (self.uri_len as usize).min(192);
3261        // SAFETY: We validate UTF-8 at read time.
3262        core::str::from_utf8(&self.uri[..len]).unwrap_or("")
3263    }
3264
3265    /// Check if a specific flag is set.
3266    #[inline(always)]
3267    pub fn has_flag(&self, flag: u16) -> bool {
3268        self.pointer_flags & flag != 0
3269    }
3270}
3271
3272// ---------------------------------------------------------------------------
3273// Semantic Lint Engine -- catch suspicious state patterns at build time
3274// ---------------------------------------------------------------------------
3275
3276/// A semantic lint warning produced by analyzing field intents, mutation
3277/// classes, and policy against a layout manifest.
3278#[derive(Clone, Copy, Debug)]
3279pub struct SemanticLint {
3280    /// Lint severity.
3281    pub severity: LintSeverity,
3282    /// Short machine-readable code.
3283    pub code: &'static str,
3284    /// Human-readable warning message.
3285    pub message: &'static str,
3286    /// Field name involved (empty if layout-wide).
3287    pub field: &'static str,
3288}
3289
3290/// Lint severity.
3291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3292#[repr(u8)]
3293pub enum LintSeverity {
3294    /// Informational note.
3295    Info = 0,
3296    /// Potential issue worth reviewing.
3297    Warning = 1,
3298    /// Likely correctness or security issue.
3299    Error = 2,
3300}
3301
3302impl LintSeverity {
3303    /// Human-readable label.
3304    pub const fn name(self) -> &'static str {
3305        match self {
3306            Self::Info => "info",
3307            Self::Warning => "warning",
3308            Self::Error => "error",
3309        }
3310    }
3311}
3312
3313/// Run semantic lints against a layout manifest and its behavior.
3314///
3315/// Returns the number of lint warnings produced (up to N).
3316pub fn lint_layout<const N: usize>(
3317    manifest: &LayoutManifest,
3318    behavior: &LayoutBehavior,
3319) -> (usize, [SemanticLint; N]) {
3320    let mut lints = [SemanticLint {
3321        severity: LintSeverity::Info,
3322        code: "",
3323        message: "",
3324        field: "",
3325    }; N];
3326    let mut count = 0usize;
3327
3328    let mut i = 0;
3329    while i < manifest.field_count {
3330        let field = &manifest.fields[i];
3331
3332        // Authority field mutated without signer requirement
3333        if field.intent.is_authority_sensitive()
3334            && behavior.mutation_class.is_mutating()
3335            && !behavior.requires_signer
3336        {
3337            if count < N {
3338                lints[count] = SemanticLint {
3339                    severity: LintSeverity::Error,
3340                    code: "E001",
3341                    message:
3342                        "Authority-sensitive field in mutable layout without signer requirement",
3343                    field: field.name,
3344                };
3345                count += 1;
3346            }
3347        }
3348
3349        // Financial field mutated without financial mutation class
3350        if field.intent.is_monetary()
3351            && behavior.mutation_class.is_mutating()
3352            && !matches!(behavior.mutation_class, MutationClass::Financial)
3353        {
3354            if count < N {
3355                lints[count] = SemanticLint {
3356                    severity: LintSeverity::Warning,
3357                    code: "W001",
3358                    message: "Monetary field in layout without financial mutation class",
3359                    field: field.name,
3360                };
3361                count += 1;
3362            }
3363        }
3364
3365        // Init-only field (PDA seed, bump) in a layout that isn't read-only
3366        if field.intent.is_init_only()
3367            && behavior.mutation_class.is_mutating()
3368            && !matches!(behavior.mutation_class, MutationClass::AppendOnly)
3369        {
3370            if count < N {
3371                lints[count] = SemanticLint {
3372                    severity: LintSeverity::Warning,
3373                    code: "W002",
3374                    message: "Init-only field (PDA seed or bump) in mutable layout. Consider making read-only or append-only.",
3375                    field: field.name,
3376                };
3377                count += 1;
3378            }
3379        }
3380
3381        i += 1;
3382    }
3383
3384    // Layout-wide lints
3385
3386    // Mutable layout with no signer
3387    if behavior.mutation_class.is_mutating() && !behavior.requires_signer {
3388        if count < N {
3389            lints[count] = SemanticLint {
3390                severity: LintSeverity::Warning,
3391                code: "W003",
3392                message: "Mutable layout does not require signer. Verify this is intentional.",
3393                field: "",
3394            };
3395            count += 1;
3396        }
3397    }
3398
3399    // Financial impact without balance tracking
3400    if behavior.affects_balance {
3401        let mut has_balance = false;
3402        let mut j = 0;
3403        while j < manifest.field_count {
3404            if manifest.fields[j].intent.is_monetary() {
3405                has_balance = true;
3406            }
3407            j += 1;
3408        }
3409        if !has_balance && count < N {
3410            lints[count] = SemanticLint {
3411                severity: LintSeverity::Warning,
3412                code: "W004",
3413                message: "Layout behavior declares affects_balance but no monetary fields found",
3414                field: "",
3415            };
3416            count += 1;
3417        }
3418    }
3419
3420    (count, lints)
3421}
3422
3423/// Run policy-aware semantic lints.
3424///
3425/// Complements `lint_layout` with cross-cutting checks between layout
3426/// behavior and policy classification. Call after `lint_layout` and merge
3427/// the results.
3428#[cfg(feature = "policy")]
3429pub fn lint_policy<const N: usize>(
3430    behavior: &LayoutBehavior,
3431    policy: hopper_core::policy::PolicyClass,
3432) -> (usize, [SemanticLint; N]) {
3433    let mut lints = [SemanticLint {
3434        severity: LintSeverity::Info,
3435        code: "",
3436        message: "",
3437        field: "",
3438    }; N];
3439    let mut count = 0usize;
3440
3441    // Financial mutation class without financial policy class
3442    if matches!(behavior.mutation_class, MutationClass::Financial)
3443        && !matches!(policy, hopper_core::policy::PolicyClass::Financial)
3444    {
3445        if count < N {
3446            lints[count] = SemanticLint {
3447                severity: LintSeverity::Warning,
3448                code: "W005",
3449                message: "Financial mutation class but policy class is not Financial",
3450                field: "",
3451            };
3452            count += 1;
3453        }
3454    }
3455
3456    // Financial policy class without financial mutation class
3457    if matches!(policy, hopper_core::policy::PolicyClass::Financial)
3458        && !matches!(behavior.mutation_class, MutationClass::Financial)
3459    {
3460        if count < N {
3461            lints[count] = SemanticLint {
3462                severity: LintSeverity::Warning,
3463                code: "W006",
3464                message: "Financial policy class but mutation class is not Financial",
3465                field: "",
3466            };
3467            count += 1;
3468        }
3469    }
3470
3471    (count, lints)
3472}
3473
3474impl fmt::Display for SemanticLint {
3475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3476        write!(
3477            f,
3478            "[{}] {}: {}",
3479            self.severity.name(),
3480            self.code,
3481            self.message
3482        )?;
3483        if !self.field.is_empty() {
3484            write!(f, " (field: {})", self.field)?;
3485        }
3486        Ok(())
3487    }
3488}
3489
3490// ---------------------------------------------------------------------------
3491// Protocol Operating Profile -- machine-readable program behavior map
3492// ---------------------------------------------------------------------------
3493
3494/// A machine-readable summary of a program's operational characteristics.
3495///
3496/// Generated from a `ProgramManifest` to give auditors, dashboards, explorers,
3497/// and operator tools a meaningful map of how the program behaves.
3498pub struct OperatingProfile {
3499    /// Fields classified as financial (balance, supply, basis_points).
3500    pub financial_fields: [&'static str; 16],
3501    /// Number of valid financial field entries.
3502    pub financial_count: u8,
3503    /// Fields classified as authority surfaces (authority, owner, delegate).
3504    pub authority_surfaces: [&'static str; 16],
3505    /// Number of valid authority surface entries.
3506    pub authority_count: u8,
3507    /// Segments that are append-only.
3508    pub append_only_segments: [&'static str; 8],
3509    /// Number of valid append-only segment entries.
3510    pub append_only_count: u8,
3511    /// Segments sensitive to migration.
3512    pub migration_sensitive: [&'static str; 8],
3513    /// Number of valid migration-sensitive entries.
3514    pub migration_sensitive_count: u8,
3515    /// Layout stability grades per layout.
3516    pub stability_grades: [(&'static str, LayoutStabilityGrade); 8],
3517    /// Number of valid stability grade entries.
3518    pub stability_count: u8,
3519    /// Whether the program has any financial operations.
3520    pub has_financial_ops: bool,
3521    /// Whether the program has any CPI-invoking instructions.
3522    pub has_cpi_ops: bool,
3523    /// Whether the program has migration paths defined.
3524    pub has_migration_paths: bool,
3525    /// Whether the program emits receipts.
3526    pub has_receipts: bool,
3527}
3528
3529impl OperatingProfile {
3530    /// Generate an operating profile from a program manifest.
3531    pub fn from_manifest(manifest: &ProgramManifest) -> Self {
3532        let mut profile = Self {
3533            financial_fields: [""; 16],
3534            financial_count: 0,
3535            authority_surfaces: [""; 16],
3536            authority_count: 0,
3537            append_only_segments: [""; 8],
3538            append_only_count: 0,
3539            migration_sensitive: [""; 8],
3540            migration_sensitive_count: 0,
3541            stability_grades: [("", LayoutStabilityGrade::Stable); 8],
3542            stability_count: 0,
3543            has_financial_ops: false,
3544            has_cpi_ops: false,
3545            has_migration_paths: !manifest.compatibility_pairs.is_empty(),
3546            has_receipts: false,
3547        };
3548
3549        // Scan layouts for field intents
3550        let mut li = 0;
3551        while li < manifest.layouts.len() {
3552            let layout = &manifest.layouts[li];
3553
3554            // Stability grade
3555            if (profile.stability_count as usize) < 8 {
3556                profile.stability_grades[profile.stability_count as usize] =
3557                    (layout.name, LayoutStabilityGrade::compute(layout));
3558                profile.stability_count += 1;
3559            }
3560
3561            let mut fi = 0;
3562            while fi < layout.field_count {
3563                let field = &layout.fields[fi];
3564                if field.intent.is_monetary() && (profile.financial_count as usize) < 16 {
3565                    profile.financial_fields[profile.financial_count as usize] = field.name;
3566                    profile.financial_count += 1;
3567                }
3568                if field.intent.is_authority_sensitive() && (profile.authority_count as usize) < 16
3569                {
3570                    profile.authority_surfaces[profile.authority_count as usize] = field.name;
3571                    profile.authority_count += 1;
3572                }
3573                fi += 1;
3574            }
3575            li += 1;
3576        }
3577
3578        // Scan layout metadata for segment info
3579        let mut mi = 0;
3580        while mi < manifest.layout_metadata.len() {
3581            let meta = &manifest.layout_metadata[mi];
3582            let mut si = 0;
3583            while si < meta.segment_roles.len() {
3584                let role_name = meta.segment_roles[si];
3585                if (const_str_eq(role_name, "Journal") || const_str_eq(role_name, "Audit"))
3586                    && (profile.append_only_count as usize) < 8
3587                {
3588                    profile.append_only_segments[profile.append_only_count as usize] = role_name;
3589                    profile.append_only_count += 1;
3590                }
3591                if const_str_eq(role_name, "Core")
3592                    && (profile.migration_sensitive_count as usize) < 8
3593                {
3594                    profile.migration_sensitive[profile.migration_sensitive_count as usize] =
3595                        meta.name;
3596                    profile.migration_sensitive_count += 1;
3597                }
3598                si += 1;
3599            }
3600            mi += 1;
3601        }
3602
3603        // Scan instructions for capabilities
3604        let mut ii = 0;
3605        while ii < manifest.instructions.len() {
3606            let ix = &manifest.instructions[ii];
3607            if ix.receipt_expected {
3608                profile.has_receipts = true;
3609            }
3610            let mut ci = 0;
3611            while ci < ix.capabilities.len() {
3612                if const_str_eq(ix.capabilities[ci], "MutatesTreasury") {
3613                    profile.has_financial_ops = true;
3614                }
3615                if const_str_eq(ix.capabilities[ci], "ExternalCall") {
3616                    profile.has_cpi_ops = true;
3617                }
3618                ci += 1;
3619            }
3620            ii += 1;
3621        }
3622
3623        profile
3624    }
3625}
3626
3627impl fmt::Display for OperatingProfile {
3628    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3629        writeln!(f, "Operating Profile:")?;
3630
3631        if self.financial_count > 0 {
3632            write!(f, "  Financial fields:")?;
3633            let mut i = 0;
3634            while i < self.financial_count as usize {
3635                write!(f, " {}", self.financial_fields[i])?;
3636                i += 1;
3637            }
3638            writeln!(f)?;
3639        }
3640
3641        if self.authority_count > 0 {
3642            write!(f, "  Authority surfaces:")?;
3643            let mut i = 0;
3644            while i < self.authority_count as usize {
3645                write!(f, " {}", self.authority_surfaces[i])?;
3646                i += 1;
3647            }
3648            writeln!(f)?;
3649        }
3650
3651        if self.append_only_count > 0 {
3652            write!(f, "  Append-only segments:")?;
3653            let mut i = 0;
3654            while i < self.append_only_count as usize {
3655                write!(f, " {}", self.append_only_segments[i])?;
3656                i += 1;
3657            }
3658            writeln!(f)?;
3659        }
3660
3661        if self.stability_count > 0 {
3662            writeln!(f, "  Stability grades:")?;
3663            let mut i = 0;
3664            while i < self.stability_count as usize {
3665                let (name, grade) = self.stability_grades[i];
3666                writeln!(f, "    {}: {}", name, grade.name())?;
3667                i += 1;
3668            }
3669        }
3670
3671        write!(f, "  Features:")?;
3672        if self.has_financial_ops {
3673            write!(f, " financial")?;
3674        }
3675        if self.has_cpi_ops {
3676            write!(f, " cpi")?;
3677        }
3678        if self.has_migration_paths {
3679            write!(f, " migration")?;
3680        }
3681        if self.has_receipts {
3682            write!(f, " receipts")?;
3683        }
3684        writeln!(f)?;
3685
3686        Ok(())
3687    }
3688}
3689
3690// ---------------------------------------------------------------------------
3691// Expanded IDL -- policies, compat, receipts, segments, field intents
3692// ---------------------------------------------------------------------------
3693
3694/// Extended IDL with full Hopper-native sections.
3695///
3696/// This is the **complete** program schema for Hopper-aware tools. It extends
3697/// `ProgramIdl` with policies, compatibility, receipts, segments, field intents,
3698/// and an operating profile.
3699pub struct HopperIdl {
3700    /// Base IDL (instructions, accounts, events).
3701    pub base: ProgramIdl,
3702    /// Policy descriptors.
3703    pub policies: &'static [PolicyDescriptor],
3704    /// Known upgrade paths.
3705    pub compatibility: &'static [CompatibilityPair],
3706    /// Receipt profiles keyed by name.
3707    pub receipt_profiles: &'static [ReceiptProfile],
3708    /// Segment metadata.
3709    pub segment_metadata: &'static [IdlSegmentDescriptor],
3710    /// Context (instruction account struct) descriptors.
3711    pub contexts: &'static [crate::accounts::ContextDescriptor],
3712}
3713
3714/// A receipt profile describing what a receipt for a given mutation type looks like.
3715#[derive(Clone, Copy)]
3716pub struct ReceiptProfile {
3717    /// Profile name (e.g. "default-mutation", "treasury-write").
3718    pub name: &'static str,
3719    /// Expected phase.
3720    pub expected_phase: &'static str,
3721    /// Whether balance changes are expected.
3722    pub expects_balance_change: bool,
3723    /// Whether authority changes are expected.
3724    pub expects_authority_change: bool,
3725    /// Whether journal appends are expected.
3726    pub expects_journal_append: bool,
3727    /// Minimum expected changed fields.
3728    pub min_changed_fields: u8,
3729}
3730
3731impl fmt::Display for ReceiptProfile {
3732    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3733        write!(f, "{}(phase={}", self.name, self.expected_phase)?;
3734        if self.expects_balance_change {
3735            write!(f, " balance")?;
3736        }
3737        if self.expects_authority_change {
3738            write!(f, " authority")?;
3739        }
3740        if self.expects_journal_append {
3741            write!(f, " journal")?;
3742        }
3743        if self.min_changed_fields > 0 {
3744            write!(f, " min_fields={}", self.min_changed_fields)?;
3745        }
3746        write!(f, ")")
3747    }
3748}
3749
3750/// Segment metadata for inclusion in the IDL.
3751#[derive(Clone, Copy)]
3752pub struct IdlSegmentDescriptor {
3753    /// Segment name.
3754    pub name: &'static str,
3755    /// Role name (Core, Extension, Journal, etc.).
3756    pub role: &'static str,
3757    /// Whether the segment is append-only.
3758    pub append_only: bool,
3759    /// Whether the segment is rebuildable from other data.
3760    pub rebuildable: bool,
3761    /// Whether the segment must survive migration.
3762    pub must_preserve: bool,
3763}
3764
3765impl fmt::Display for IdlSegmentDescriptor {
3766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3767        write!(f, "{}(role={}", self.name, self.role)?;
3768        if self.append_only {
3769            write!(f, " append-only")?;
3770        }
3771        if self.rebuildable {
3772            write!(f, " rebuildable")?;
3773        }
3774        if self.must_preserve {
3775            write!(f, " must-preserve")?;
3776        }
3777        write!(f, ")")
3778    }
3779}
3780
3781impl HopperIdl {
3782    /// Create an empty extended IDL.
3783    pub const fn empty() -> Self {
3784        Self {
3785            base: ProgramIdl::empty(),
3786            policies: &[],
3787            compatibility: &[],
3788            receipt_profiles: &[],
3789            segment_metadata: &[],
3790            contexts: &[],
3791        }
3792    }
3793}
3794
3795impl fmt::Display for HopperIdl {
3796    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3797        write!(f, "{}", self.base)?;
3798
3799        if !self.policies.is_empty() {
3800            writeln!(f)?;
3801            writeln!(f, "Policies ({}):", self.policies.len())?;
3802            for p in self.policies.iter() {
3803                write!(f, "  {:24}", p.name)?;
3804                for (j, r) in p.requirements.iter().enumerate() {
3805                    if j > 0 {
3806                        write!(f, " + ")?;
3807                    }
3808                    write!(f, "{}", r)?;
3809                }
3810                writeln!(f)?;
3811            }
3812        }
3813
3814        if !self.compatibility.is_empty() {
3815            writeln!(f)?;
3816            writeln!(f, "Compatibility ({}):", self.compatibility.len())?;
3817            for cp in self.compatibility.iter() {
3818                writeln!(
3819                    f,
3820                    "  {} v{} -> {} v{}  {}",
3821                    cp.from_layout, cp.from_version, cp.to_layout, cp.to_version, cp.policy,
3822                )?;
3823            }
3824        }
3825
3826        if !self.receipt_profiles.is_empty() {
3827            writeln!(f)?;
3828            writeln!(f, "Receipt Profiles ({}):", self.receipt_profiles.len())?;
3829            for rp in self.receipt_profiles.iter() {
3830                writeln!(
3831                    f,
3832                    "  {:24} phase={} balance={} authority={} journal={}",
3833                    rp.name,
3834                    rp.expected_phase,
3835                    rp.expects_balance_change,
3836                    rp.expects_authority_change,
3837                    rp.expects_journal_append,
3838                )?;
3839            }
3840        }
3841
3842        if !self.segment_metadata.is_empty() {
3843            writeln!(f)?;
3844            writeln!(f, "Segments ({}):", self.segment_metadata.len())?;
3845            for s in self.segment_metadata.iter() {
3846                write!(f, "  {:16} role={}", s.name, s.role)?;
3847                if s.append_only {
3848                    write!(f, " append-only")?;
3849                }
3850                if s.rebuildable {
3851                    write!(f, " rebuildable")?;
3852                }
3853                if s.must_preserve {
3854                    write!(f, " must-preserve")?;
3855                }
3856                writeln!(f)?;
3857            }
3858        }
3859
3860        if !self.contexts.is_empty() {
3861            writeln!(f)?;
3862            writeln!(f, "Contexts ({}):", self.contexts.len())?;
3863            for ctx in self.contexts.iter() {
3864                write!(f, "  {}", ctx)?;
3865            }
3866        }
3867
3868        Ok(())
3869    }
3870}
3871
3872// ═══════════════════════════════════════════════════════════════════════
3873//  SchemaExport -- bridge from LayoutContract + FieldMap to schema
3874// ═══════════════════════════════════════════════════════════════════════
3875
3876/// Minimal manager-readable metadata for a Hopper layout.
3877#[derive(Clone, Copy, Debug)]
3878pub struct ManagerMetadata {
3879    /// Runtime header/layout identity.
3880    pub layout: LayoutInfo,
3881    /// Field-level wire map.
3882    pub fields: &'static [FieldInfo],
3883}
3884
3885/// Unified schema payload tying runtime identity to rich manifest metadata.
3886#[derive(Clone, Copy, Debug)]
3887pub struct SchemaBundle {
3888    pub manager: ManagerMetadata,
3889    pub manifest: LayoutManifest,
3890}
3891
3892/// Trait for layout types that can export their full schema information.
3893///
3894/// This creates a single source of truth linking runtime layout contracts
3895/// (discriminator, version, layout_id, size) with field-level metadata
3896/// (names, offsets, sizes). The exported information powers:
3897///
3898/// - Manager metadata (on-chain or off-chain program inspection)
3899/// - IDL generation (Codama, Hopper IDL, client SDKs)
3900/// - Schema diff and migration safety checking
3901/// - Client code generation with typed field access
3902///
3903/// Implementors provide both a runtime-facing view (`layout_info`, `field_map`)
3904/// and a higher-level schema manifest for richer tooling.
3905pub trait SchemaExport: LayoutContract {
3906    /// Runtime header/layout identity.
3907    #[inline(always)]
3908    fn layout_info() -> LayoutInfo {
3909        <Self as LayoutContract>::layout_info_static()
3910    }
3911
3912    /// Field-level wire map used by manager and client tooling.
3913    #[inline(always)]
3914    fn field_map() -> &'static [FieldInfo] {
3915        <Self as LayoutContract>::fields()
3916    }
3917
3918    /// Combined runtime metadata payload for manager-facing inspection.
3919    #[inline(always)]
3920    fn manager_metadata() -> ManagerMetadata {
3921        ManagerMetadata {
3922            layout: Self::layout_info(),
3923            fields: Self::field_map(),
3924        }
3925    }
3926
3927    /// Combined runtime and manifest metadata payload.
3928    #[inline(always)]
3929    fn schema_bundle() -> SchemaBundle {
3930        SchemaBundle {
3931            manager: Self::manager_metadata(),
3932            manifest: Self::layout_manifest(),
3933        }
3934    }
3935
3936    /// Rich schema manifest for diffing, linting, and client generation.
3937    fn layout_manifest() -> LayoutManifest;
3938}
3939
3940/// Bridge from a live `AccountView` to the schema bundle of a concrete layout type.
3941pub trait AccountSchemaExt {
3942    /// Return manager metadata if the account header matches `T`.
3943    fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata>;
3944
3945    /// Return the full schema bundle if the account header matches `T`.
3946    fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle>;
3947}
3948
3949impl AccountSchemaExt for AccountView {
3950    #[inline]
3951    fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata> {
3952        let info = self.layout_info()?;
3953        if info.matches::<T>() {
3954            Some(T::manager_metadata())
3955        } else {
3956            None
3957        }
3958    }
3959
3960    #[inline]
3961    fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle> {
3962        let info = self.layout_info()?;
3963        if info.matches::<T>() {
3964            Some(T::schema_bundle())
3965        } else {
3966            None
3967        }
3968    }
3969}
3970
3971// -- Migration Plan Tests --
3972
3973#[cfg(test)]
3974mod tests {
3975    use super::*;
3976
3977    const V1_FIELDS: &[FieldDescriptor] = &[
3978        FieldDescriptor {
3979            name: "authority",
3980            canonical_type: "[u8;32]",
3981            size: 32,
3982            offset: 16,
3983            intent: FieldIntent::Custom,
3984        },
3985        FieldDescriptor {
3986            name: "balance",
3987            canonical_type: "WireU64",
3988            size: 8,
3989            offset: 48,
3990            intent: FieldIntent::Custom,
3991        },
3992    ];
3993
3994    const V2_FIELDS: &[FieldDescriptor] = &[
3995        FieldDescriptor {
3996            name: "authority",
3997            canonical_type: "[u8;32]",
3998            size: 32,
3999            offset: 16,
4000            intent: FieldIntent::Custom,
4001        },
4002        FieldDescriptor {
4003            name: "balance",
4004            canonical_type: "WireU64",
4005            size: 8,
4006            offset: 48,
4007            intent: FieldIntent::Custom,
4008        },
4009        FieldDescriptor {
4010            name: "bump",
4011            canonical_type: "u8",
4012            size: 1,
4013            offset: 56,
4014            intent: FieldIntent::Custom,
4015        },
4016    ];
4017
4018    const V1_MANIFEST: LayoutManifest = LayoutManifest {
4019        name: "Vault",
4020        disc: 1,
4021        version: 1,
4022        layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4023        total_size: 56,
4024        field_count: 2,
4025        fields: V1_FIELDS,
4026    };
4027
4028    const V2_MANIFEST: LayoutManifest = LayoutManifest {
4029        name: "Vault",
4030        disc: 1,
4031        version: 2,
4032        layout_id: [10, 20, 30, 40, 50, 60, 70, 80],
4033        total_size: 57,
4034        field_count: 3,
4035        fields: V2_FIELDS,
4036    };
4037
4038    #[test]
4039    fn no_op_for_identical() {
4040        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V1_MANIFEST);
4041        assert_eq!(plan.policy, MigrationPolicy::NoOp);
4042        assert_eq!(plan.step_count, 0);
4043    }
4044
4045    #[test]
4046    fn append_only_migration() {
4047        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V2_MANIFEST);
4048        assert_eq!(plan.policy, MigrationPolicy::AppendOnly);
4049        assert!(plan.step_count >= 3); // copy + realloc + zero-init + header
4050        assert_eq!(plan.old_size, 56);
4051        assert_eq!(plan.new_size, 57);
4052        assert!(plan.copy_bytes > 0);
4053        assert!(plan.zero_bytes > 0);
4054
4055        // First step should be CopyPrefix
4056        assert_eq!(plan.steps[0].action, MigrationAction::CopyPrefix);
4057        // Should have a ZeroInit for the "bump" field
4058        let mut found_zero = false;
4059        let mut i = 0;
4060        while i < plan.step_count {
4061            if plan.steps[i].action == MigrationAction::ZeroInit {
4062                assert_eq!(plan.steps[i].field, "bump");
4063                assert_eq!(plan.steps[i].size, 1);
4064                found_zero = true;
4065            }
4066            i += 1;
4067        }
4068        assert!(found_zero);
4069    }
4070
4071    #[test]
4072    fn incompatible_different_disc() {
4073        let other = LayoutManifest {
4074            disc: 99,
4075            ..V2_MANIFEST
4076        };
4077        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &other);
4078        assert_eq!(plan.policy, MigrationPolicy::Incompatible);
4079    }
4080
4081    #[test]
4082    fn breaking_change_detected() {
4083        let changed_fields: &[FieldDescriptor] = &[
4084            FieldDescriptor {
4085                name: "authority",
4086                canonical_type: "WireU64",
4087                size: 8,
4088                offset: 16,
4089                intent: FieldIntent::Custom,
4090            },
4091            FieldDescriptor {
4092                name: "balance",
4093                canonical_type: "WireU64",
4094                size: 8,
4095                offset: 24,
4096                intent: FieldIntent::Custom,
4097            },
4098        ];
4099        let breaking = LayoutManifest {
4100            name: "Vault",
4101            disc: 1,
4102            version: 2,
4103            layout_id: [99; 8],
4104            total_size: 32,
4105            field_count: 2,
4106            fields: changed_fields,
4107        };
4108        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &breaking);
4109        assert_eq!(plan.policy, MigrationPolicy::RequiresMigration);
4110    }
4111
4112    // -----------------------------------------------------------------------
4113    // CompatibilityVerdict tests
4114    // -----------------------------------------------------------------------
4115
4116    #[test]
4117    fn verdict_identical() {
4118        let v = CompatibilityVerdict::between(&V1_MANIFEST, &V1_MANIFEST);
4119        assert_eq!(v, CompatibilityVerdict::Identical);
4120        assert!(v.is_safe());
4121        assert!(v.is_backward_readable());
4122        assert!(!v.requires_migration());
4123    }
4124
4125    #[test]
4126    fn verdict_append_safe() {
4127        let v = CompatibilityVerdict::between(&V1_MANIFEST, &V2_MANIFEST);
4128        assert_eq!(v, CompatibilityVerdict::AppendSafe);
4129        assert!(v.is_safe());
4130        assert!(v.is_backward_readable());
4131        assert!(!v.requires_migration());
4132    }
4133
4134    #[test]
4135    fn verdict_migration_required() {
4136        let changed_fields: &[FieldDescriptor] = &[
4137            FieldDescriptor {
4138                name: "authority",
4139                canonical_type: "WireU64",
4140                size: 8,
4141                offset: 16,
4142                intent: FieldIntent::Custom,
4143            },
4144            FieldDescriptor {
4145                name: "balance",
4146                canonical_type: "WireU64",
4147                size: 8,
4148                offset: 24,
4149                intent: FieldIntent::Custom,
4150            },
4151        ];
4152        let breaking = LayoutManifest {
4153            name: "Vault",
4154            disc: 1,
4155            version: 2,
4156            layout_id: [99; 8],
4157            total_size: 32,
4158            field_count: 2,
4159            fields: changed_fields,
4160        };
4161        let v = CompatibilityVerdict::between(&V1_MANIFEST, &breaking);
4162        assert_eq!(v, CompatibilityVerdict::MigrationRequired);
4163        assert!(!v.is_safe());
4164        assert!(!v.is_backward_readable());
4165        assert!(v.requires_migration());
4166    }
4167
4168    #[test]
4169    fn verdict_wire_compatible() {
4170        // Same disc, same fields (count + prefix), same total_size, but different layout_id.
4171        let semantic_variant = LayoutManifest {
4172            layout_id: [77; 8], // different layout_id
4173            ..V1_MANIFEST       // same disc, fields, total_size
4174        };
4175        let v = CompatibilityVerdict::between(&V1_MANIFEST, &semantic_variant);
4176        assert_eq!(v, CompatibilityVerdict::WireCompatible);
4177        assert!(v.is_safe());
4178        assert!(v.is_backward_readable());
4179        assert!(!v.requires_migration());
4180    }
4181
4182    #[test]
4183    fn verdict_incompatible() {
4184        let other = LayoutManifest {
4185            disc: 99,
4186            ..V2_MANIFEST
4187        };
4188        let v = CompatibilityVerdict::between(&V1_MANIFEST, &other);
4189        assert_eq!(v, CompatibilityVerdict::Incompatible);
4190        assert!(!v.is_safe());
4191    }
4192
4193    #[test]
4194    fn verdict_names() {
4195        assert_eq!(CompatibilityVerdict::Identical.name(), "identical");
4196        assert_eq!(
4197            CompatibilityVerdict::WireCompatible.name(),
4198            "wire-compatible"
4199        );
4200        assert_eq!(CompatibilityVerdict::AppendSafe.name(), "append-safe");
4201        assert_eq!(
4202            CompatibilityVerdict::MigrationRequired.name(),
4203            "migration-required"
4204        );
4205        assert_eq!(CompatibilityVerdict::Incompatible.name(), "incompatible");
4206    }
4207
4208    #[test]
4209    fn segment_advice_core_must_preserve() {
4210        let segs = [DecodedSegment {
4211            id: [1, 0, 0, 0],
4212            offset: 36,
4213            size: 100,
4214            flags: 0x0000, // Core = upper 4 bits 0
4215            version: 1,
4216        }];
4217        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4218        assert_eq!(report.count, 1);
4219        assert_eq!(report.advice[0].role, SegmentRoleHint::Core);
4220        assert!(report.advice[0].must_preserve);
4221        assert!(!report.advice[0].clearable);
4222        assert_eq!(report.preserve_bytes, 100);
4223    }
4224
4225    #[test]
4226    fn segment_advice_journal_clearable() {
4227        let segs = [DecodedSegment {
4228            id: [2, 0, 0, 0],
4229            offset: 136,
4230            size: 256,
4231            flags: 0x2000, // Journal = upper 4 bits 2
4232            version: 1,
4233        }];
4234        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4235        assert_eq!(report.advice[0].role, SegmentRoleHint::Journal);
4236        assert!(report.advice[0].clearable);
4237        assert!(report.advice[0].append_only);
4238        assert!(!report.advice[0].must_preserve);
4239        assert_eq!(report.clearable_bytes, 256);
4240    }
4241
4242    #[test]
4243    fn segment_advice_cache_rebuildable() {
4244        let segs = [DecodedSegment {
4245            id: [3, 0, 0, 0],
4246            offset: 400,
4247            size: 64,
4248            flags: 0x4000, // Cache = upper 4 bits 4
4249            version: 1,
4250        }];
4251        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4252        assert_eq!(report.advice[0].role, SegmentRoleHint::Cache);
4253        assert!(report.advice[0].clearable);
4254        assert!(report.advice[0].rebuildable);
4255    }
4256
4257    #[test]
4258    fn segment_advice_audit_immutable() {
4259        let segs = [DecodedSegment {
4260            id: [4, 0, 0, 0],
4261            offset: 200,
4262            size: 32,
4263            flags: 0x5000, // Audit = upper 4 bits 5
4264            version: 1,
4265        }];
4266        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4267        assert_eq!(report.advice[0].role, SegmentRoleHint::Audit);
4268        assert!(report.advice[0].must_preserve);
4269        assert!(report.advice[0].immutable);
4270        assert!(report.advice[0].append_only);
4271        assert!(!report.advice[0].clearable);
4272    }
4273
4274    #[test]
4275    fn segment_advice_mixed_report() {
4276        let segs = [
4277            DecodedSegment {
4278                id: [1, 0, 0, 0],
4279                offset: 36,
4280                size: 100,
4281                flags: 0x0000,
4282                version: 1,
4283            },
4284            DecodedSegment {
4285                id: [2, 0, 0, 0],
4286                offset: 136,
4287                size: 200,
4288                flags: 0x2000,
4289                version: 1,
4290            },
4291            DecodedSegment {
4292                id: [3, 0, 0, 0],
4293                offset: 336,
4294                size: 64,
4295                flags: 0x4000,
4296                version: 1,
4297            },
4298        ];
4299        let report = SegmentMigrationReport::<8>::analyze(&segs, 3);
4300        assert_eq!(report.count, 3);
4301        assert_eq!(report.must_preserve_count(), 1);
4302        assert_eq!(report.clearable_count(), 2);
4303        assert_eq!(report.preserve_bytes, 100);
4304        assert_eq!(report.clearable_bytes, 264);
4305        assert_eq!(report.rebuildable_bytes, 64);
4306    }
4307
4308    #[test]
4309    fn segment_role_hint_requires_migration_copy() {
4310        assert!(SegmentRoleHint::Core.requires_migration_copy());
4311        assert!(SegmentRoleHint::Audit.requires_migration_copy());
4312        assert!(!SegmentRoleHint::Extension.requires_migration_copy());
4313        assert!(!SegmentRoleHint::Journal.requires_migration_copy());
4314        assert!(!SegmentRoleHint::Index.requires_migration_copy());
4315        assert!(!SegmentRoleHint::Cache.requires_migration_copy());
4316        assert!(!SegmentRoleHint::Shard.requires_migration_copy());
4317    }
4318
4319    #[test]
4320    fn segment_role_hint_is_safe_to_drop() {
4321        assert!(SegmentRoleHint::Cache.is_safe_to_drop());
4322        assert!(!SegmentRoleHint::Core.is_safe_to_drop());
4323        assert!(!SegmentRoleHint::Extension.is_safe_to_drop());
4324        assert!(!SegmentRoleHint::Journal.is_safe_to_drop());
4325        assert!(!SegmentRoleHint::Index.is_safe_to_drop());
4326        assert!(!SegmentRoleHint::Audit.is_safe_to_drop());
4327        assert!(!SegmentRoleHint::Shard.is_safe_to_drop());
4328    }
4329
4330    // -----------------------------------------------------------------------
4331    // Program Manifest tests
4332    // -----------------------------------------------------------------------
4333
4334    static PM_LAYOUTS: &[LayoutManifest] = &[
4335        LayoutManifest {
4336            name: "Vault",
4337            disc: 1,
4338            version: 1,
4339            layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4340            total_size: 57,
4341            field_count: 0,
4342            fields: &[],
4343        },
4344        LayoutManifest {
4345            name: "Config",
4346            disc: 2,
4347            version: 1,
4348            layout_id: [8, 7, 6, 5, 4, 3, 2, 1],
4349            total_size: 43,
4350            field_count: 0,
4351            fields: &[],
4352        },
4353    ];
4354
4355    static PM_INSTRUCTIONS: &[InstructionDescriptor] = &[
4356        InstructionDescriptor {
4357            name: "deposit",
4358            tag: 1,
4359            args: &[],
4360            accounts: &[],
4361            capabilities: &["MutatesState"],
4362            policy_pack: "TREASURY_WRITE",
4363            receipt_expected: true,
4364        },
4365        InstructionDescriptor {
4366            name: "withdraw",
4367            tag: 2,
4368            args: &[],
4369            accounts: &[],
4370            capabilities: &["MutatesState", "TransfersTokens"],
4371            policy_pack: "TREASURY_WRITE",
4372            receipt_expected: true,
4373        },
4374    ];
4375
4376    static PM_POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
4377        name: "TREASURY_WRITE",
4378        capabilities: &["MutatesState"],
4379        requirements: &["SignerAuthority"],
4380        invariants: &[],
4381        receipt_profile: "default-mutation",
4382    }];
4383
4384    #[test]
4385    fn program_manifest_find_layout_by_disc() {
4386        let prog = ProgramManifest {
4387            name: "test",
4388            version: "0.1.0",
4389            description: "",
4390            layouts: PM_LAYOUTS,
4391            layout_metadata: &[],
4392            instructions: &[],
4393            events: &[],
4394            policies: &[],
4395            compatibility_pairs: &[],
4396            tooling_hints: &[],
4397            contexts: &[],
4398        };
4399        assert_eq!(prog.layout_count(), 2);
4400        assert!(prog.find_layout_by_disc(1).is_some());
4401        assert_eq!(prog.find_layout_by_disc(1).unwrap().name, "Vault");
4402        assert!(prog.find_layout_by_disc(2).is_some());
4403        assert!(prog.find_layout_by_disc(3).is_none());
4404    }
4405
4406    #[test]
4407    fn program_manifest_find_layout_by_id() {
4408        let prog = ProgramManifest {
4409            name: "test",
4410            version: "0.1.0",
4411            description: "",
4412            layouts: PM_LAYOUTS,
4413            layout_metadata: &[],
4414            instructions: &[],
4415            events: &[],
4416            policies: &[],
4417            compatibility_pairs: &[],
4418            tooling_hints: &[],
4419            contexts: &[],
4420        };
4421        let id = [1, 2, 3, 4, 5, 6, 7, 8];
4422        assert!(prog.find_layout_by_id(&id).is_some());
4423        let bad_id = [0, 0, 0, 0, 0, 0, 0, 0];
4424        assert!(prog.find_layout_by_id(&bad_id).is_none());
4425    }
4426
4427    #[test]
4428    fn program_manifest_identify_from_data() {
4429        static ID_LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
4430            name: "Vault",
4431            disc: 1,
4432            version: 1,
4433            layout_id: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
4434            total_size: 57,
4435            field_count: 0,
4436            fields: &[],
4437        }];
4438        let prog = ProgramManifest {
4439            name: "test",
4440            version: "0.1.0",
4441            description: "",
4442            layouts: ID_LAYOUTS,
4443            layout_metadata: &[],
4444            instructions: &[],
4445            events: &[],
4446            policies: &[],
4447            compatibility_pairs: &[],
4448            tooling_hints: &[],
4449            contexts: &[],
4450        };
4451        // Build a 16-byte header: disc=1, version=1, flags=0, layout_id
4452        let mut data = [0u8; 57];
4453        data[0] = 1; // disc
4454        data[1] = 1; // version
4455        data[4..12].copy_from_slice(&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]);
4456        let result = prog.identify_from_data(&data);
4457        assert!(result.is_some());
4458        assert_eq!(result.unwrap().name, "Vault");
4459    }
4460
4461    #[test]
4462    fn program_manifest_find_instruction() {
4463        let prog = ProgramManifest {
4464            name: "test",
4465            version: "0.1.0",
4466            description: "",
4467            layouts: &[],
4468            layout_metadata: &[],
4469            instructions: PM_INSTRUCTIONS,
4470            events: &[],
4471            policies: &[],
4472            compatibility_pairs: &[],
4473            tooling_hints: &[],
4474            contexts: &[],
4475        };
4476        assert_eq!(prog.instruction_count(), 2);
4477        assert_eq!(prog.find_instruction(1).unwrap().name, "deposit");
4478        assert_eq!(prog.find_instruction(2).unwrap().name, "withdraw");
4479        assert!(prog.find_instruction(3).is_none());
4480    }
4481
4482    #[test]
4483    fn context_resolvers_and_effects_are_derived_from_manifest_metadata() {
4484        static CTX_ACCOUNTS: &[crate::accounts::ContextAccountDescriptor] = &[
4485            crate::accounts::ContextAccountDescriptor {
4486                name: "authority",
4487                kind: "Signer",
4488                writable: false,
4489                signer: true,
4490                layout_ref: "",
4491                policy_ref: "AUTHORITY",
4492                seeds: &[],
4493                optional: false,
4494                lifecycle: crate::accounts::AccountLifecycle::Existing,
4495                payer: "",
4496                init_space: 0,
4497                has_one: &[],
4498                expected_address: "",
4499                expected_owner: "",
4500            },
4501            crate::accounts::ContextAccountDescriptor {
4502                name: "vault",
4503                kind: "HopperAccount",
4504                writable: true,
4505                signer: false,
4506                layout_ref: "Vault",
4507                policy_ref: "TREASURY_WRITE",
4508                seeds: &["b\"vault\"", "authority"],
4509                optional: false,
4510                lifecycle: crate::accounts::AccountLifecycle::Init,
4511                payer: "authority",
4512                init_space: 128,
4513                has_one: &[],
4514                expected_address: "",
4515                expected_owner: "",
4516            },
4517        ];
4518        static CONTEXTS: &[crate::accounts::ContextDescriptor] =
4519            &[crate::accounts::ContextDescriptor {
4520                name: "deposit",
4521                accounts: CTX_ACCOUNTS,
4522                policies: &["TREASURY_WRITE"],
4523                receipts_expected: true,
4524                mutation_classes: &["Financial"],
4525            }];
4526
4527        let resolver = CONTEXTS[0].find_resolver("vault").unwrap();
4528        assert_eq!(resolver.kind, AccountResolverKind::Pda);
4529        assert_eq!(resolver.seeds.len(), 2);
4530        assert_eq!(resolver.payer, "authority");
4531
4532        let effect = CONTEXTS[0].find_effect("vault").unwrap();
4533        assert_eq!(effect.kind, InstructionEffectKind::CreatesAccount);
4534        assert_eq!(effect.layout_ref, "Vault");
4535        assert_eq!(CONTEXTS[0].effect_count(), 3);
4536
4537        let prog = ProgramManifest {
4538            name: "test",
4539            version: "0.1.0",
4540            description: "",
4541            layouts: &[],
4542            layout_metadata: &[],
4543            instructions: PM_INSTRUCTIONS,
4544            events: &[],
4545            policies: &[],
4546            compatibility_pairs: &[],
4547            tooling_hints: &[],
4548            contexts: CONTEXTS,
4549        };
4550        assert!(prog.find_context("deposit").is_some());
4551        extern crate alloc;
4552        use alloc::format;
4553        let rendered = format!("{}", prog);
4554        assert!(rendered.contains("resolvers=2 effects=3"));
4555    }
4556
4557    #[test]
4558    fn program_manifest_find_policy() {
4559        let prog = ProgramManifest {
4560            name: "test",
4561            version: "0.1.0",
4562            description: "",
4563            layouts: &[],
4564            layout_metadata: &[],
4565            instructions: &[],
4566            events: &[],
4567            policies: PM_POLICIES,
4568            compatibility_pairs: &[],
4569            tooling_hints: &[],
4570            contexts: &[],
4571        };
4572        assert!(prog.find_policy("TREASURY_WRITE").is_some());
4573        assert!(prog.find_policy("NONEXISTENT").is_none());
4574    }
4575
4576    #[test]
4577    fn decode_account_fields_basic() {
4578        static DECODE_FIELDS: &[FieldDescriptor] = &[
4579            FieldDescriptor {
4580                name: "balance",
4581                canonical_type: "WireU64",
4582                size: 8,
4583                offset: 16,
4584                intent: FieldIntent::Custom,
4585            },
4586            FieldDescriptor {
4587                name: "bump",
4588                canonical_type: "u8",
4589                size: 1,
4590                offset: 24,
4591                intent: FieldIntent::Custom,
4592            },
4593        ];
4594        static DECODE_MANIFEST: LayoutManifest = LayoutManifest {
4595            name: "Test",
4596            disc: 1,
4597            version: 1,
4598            layout_id: [0; 8],
4599            total_size: 25,
4600            field_count: 2,
4601            fields: DECODE_FIELDS,
4602        };
4603        let mut data = [0u8; 25];
4604        let balance_bytes = 1000u64.to_le_bytes();
4605        data[16..24].copy_from_slice(&balance_bytes);
4606        data[24] = 254;
4607
4608        let (count, decoded) = decode_account_fields::<8>(&data, &DECODE_MANIFEST);
4609        assert_eq!(count, 2);
4610        assert!(decoded[0].is_some());
4611        assert_eq!(decoded[0].as_ref().unwrap().name, "balance");
4612        assert!(decoded[1].is_some());
4613        assert_eq!(decoded[1].as_ref().unwrap().name, "bump");
4614        assert_eq!(decoded[1].as_ref().unwrap().raw[0], 254);
4615    }
4616
4617    #[test]
4618    fn decoded_field_format_wire_u64() {
4619        let raw = 42u64.to_le_bytes();
4620        let field = DecodedField {
4621            name: "balance",
4622            canonical_type: "WireU64",
4623            raw: &raw,
4624            offset: 16,
4625            size: 8,
4626        };
4627        let mut buf = [0u8; 32];
4628        let len = field.format_value(&mut buf);
4629        assert_eq!(&buf[..len], b"42");
4630    }
4631
4632    #[test]
4633    fn decoded_field_format_wire_u32() {
4634        let raw = 65535u32.to_le_bytes();
4635        let field = DecodedField {
4636            name: "count",
4637            canonical_type: "WireU32",
4638            raw: &raw,
4639            offset: 0,
4640            size: 4,
4641        };
4642        let mut buf = [0u8; 32];
4643        let len = field.format_value(&mut buf);
4644        assert_eq!(&buf[..len], b"65535");
4645    }
4646
4647    #[test]
4648    fn decoded_field_format_bool() {
4649        let raw_true = [1u8];
4650        let field = DecodedField {
4651            name: "frozen",
4652            canonical_type: "WireBool",
4653            raw: &raw_true,
4654            offset: 0,
4655            size: 1,
4656        };
4657        let mut buf = [0u8; 32];
4658        let len = field.format_value(&mut buf);
4659        assert_eq!(&buf[..len], b"true");
4660
4661        let raw_false = [0u8];
4662        let field2 = DecodedField {
4663            name: "frozen",
4664            canonical_type: "WireBool",
4665            raw: &raw_false,
4666            offset: 0,
4667            size: 1,
4668        };
4669        let len = field2.format_value(&mut buf);
4670        assert_eq!(&buf[..len], b"false");
4671    }
4672
4673    #[test]
4674    fn decoded_field_format_address() {
4675        let raw = [0xABu8; 32];
4676        let field = DecodedField {
4677            name: "authority",
4678            canonical_type: "[u8;32]",
4679            raw: &raw,
4680            offset: 0,
4681            size: 32,
4682        };
4683        let mut buf = [0u8; 64];
4684        let len = field.format_value(&mut buf);
4685        let s = core::str::from_utf8(&buf[..len]).unwrap();
4686        assert!(s.starts_with("0x"));
4687        assert!(s.ends_with("..."));
4688    }
4689
4690    #[test]
4691    fn format_u64_basic() {
4692        let mut buf = [0u8; 32];
4693        let len = super::format_u64(12345, &mut buf);
4694        assert_eq!(&buf[..len], b"12345");
4695
4696        let len = super::format_u64(0, &mut buf);
4697        assert_eq!(&buf[..len], b"0");
4698
4699        let len = super::format_u64(u64::MAX, &mut buf);
4700        let expected = b"18446744073709551615";
4701        assert_eq!(&buf[..len], &expected[..]);
4702    }
4703
4704    #[test]
4705    fn format_hex_truncated_short() {
4706        let mut buf = [0u8; 64];
4707        let len = super::format_hex_truncated(&[0xAB, 0xCD], &mut buf);
4708        assert_eq!(&buf[..len], b"0xabcd");
4709    }
4710
4711    #[test]
4712    fn format_hex_truncated_long() {
4713        let mut buf = [0u8; 64];
4714        let data = [0xFFu8; 32];
4715        let len = super::format_hex_truncated(&data, &mut buf);
4716        let s = core::str::from_utf8(&buf[..len]).unwrap();
4717        assert!(s.starts_with("0x"));
4718        assert!(s.ends_with("..."));
4719        assert_eq!(len, 21); // 0x + 16 hex chars + ...
4720    }
4721
4722    #[test]
4723    fn program_manifest_display() {
4724        let prog = ProgramManifest {
4725            name: "test_program",
4726            version: "0.1.0",
4727            description: "A test",
4728            layouts: PM_LAYOUTS,
4729            layout_metadata: &[],
4730            instructions: PM_INSTRUCTIONS,
4731            events: &[],
4732            policies: PM_POLICIES,
4733            compatibility_pairs: &[],
4734            tooling_hints: &[],
4735            contexts: &[],
4736        };
4737        extern crate alloc;
4738        use alloc::format;
4739        let s = format!("{}", prog);
4740        assert!(s.contains("test_program"));
4741        assert!(s.contains("Vault"));
4742        assert!(s.contains("deposit"));
4743        assert!(s.contains("MutatesState"));
4744        assert!(s.contains("TREASURY_WRITE"));
4745        assert!(s.contains("SignerAuthority"));
4746    }
4747
4748    #[test]
4749    fn program_manifest_empty() {
4750        let prog = ProgramManifest::empty();
4751        assert_eq!(prog.layout_count(), 0);
4752        assert_eq!(prog.instruction_count(), 0);
4753        assert!(prog.find_layout_by_disc(0).is_none());
4754        assert!(prog.find_instruction(0).is_none());
4755        assert!(prog.identify_from_data(&[0u8; 16]).is_none());
4756    }
4757
4758    // -----------------------------------------------------------------------
4759    // Malformed input torture tests
4760    // -----------------------------------------------------------------------
4761
4762    #[test]
4763    fn decode_header_empty_buffer() {
4764        assert!(decode_header(&[]).is_none());
4765    }
4766
4767    #[test]
4768    fn decode_header_one_byte() {
4769        assert!(decode_header(&[0xFF]).is_none());
4770    }
4771
4772    #[test]
4773    fn decode_header_fifteen_bytes() {
4774        assert!(decode_header(&[0u8; 15]).is_none());
4775    }
4776
4777    #[test]
4778    fn decode_header_exact_sixteen() {
4779        let h = decode_header(&[0u8; 16]);
4780        assert!(h.is_some());
4781        let h = h.unwrap();
4782        assert_eq!(h.disc, 0);
4783        assert_eq!(h.version, 0);
4784    }
4785
4786    #[test]
4787    fn decode_header_large_buffer() {
4788        let data = [0xABu8; 1024];
4789        let h = decode_header(&data).unwrap();
4790        assert_eq!(h.disc, 0xAB);
4791        assert_eq!(h.version, 0xAB);
4792    }
4793
4794    #[test]
4795    fn decode_segments_too_short() {
4796        // Needs header (16) + registry header (4) minimum
4797        assert!(decode_segments::<8>(&[0u8; 19]).is_none());
4798    }
4799
4800    #[test]
4801    fn decode_segments_zero_count() {
4802        // 16 header + 4 registry header with count=0
4803        let mut data = [0u8; 20];
4804        data[16] = 0; // count low byte
4805        data[17] = 0; // count high byte
4806        let result = decode_segments::<8>(&data);
4807        assert!(result.is_some());
4808        let (n, _) = result.unwrap();
4809        assert_eq!(n, 0);
4810    }
4811
4812    #[test]
4813    fn compare_fields_identical_empty() {
4814        let a = LayoutManifest {
4815            name: "A",
4816            disc: 1,
4817            version: 1,
4818            layout_id: [0; 8],
4819            total_size: 16,
4820            field_count: 0,
4821            fields: &[],
4822        };
4823        let b = LayoutManifest {
4824            name: "B",
4825            disc: 1,
4826            version: 1,
4827            layout_id: [0; 8],
4828            total_size: 16,
4829            field_count: 0,
4830            fields: &[],
4831        };
4832        let report = compare_fields::<8>(&a, &b);
4833        assert_eq!(report.count, 0);
4834        assert!(report.is_append_safe);
4835    }
4836
4837    static SINGLE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4838        name: "x",
4839        canonical_type: "u8",
4840        size: 1,
4841        offset: 16,
4842        intent: FieldIntent::Custom,
4843    }];
4844
4845    #[test]
4846    fn compare_fields_all_removed() {
4847        let a = LayoutManifest {
4848            name: "A",
4849            disc: 1,
4850            version: 1,
4851            layout_id: [1; 8],
4852            total_size: 17,
4853            field_count: 1,
4854            fields: SINGLE_FIELD,
4855        };
4856        let b = LayoutManifest {
4857            name: "B",
4858            disc: 1,
4859            version: 2,
4860            layout_id: [2; 8],
4861            total_size: 16,
4862            field_count: 0,
4863            fields: &[],
4864        };
4865        let report = compare_fields::<8>(&a, &b);
4866        assert_eq!(report.count, 1);
4867        assert!(!report.is_append_safe);
4868    }
4869
4870    static OLD_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4871        name: "x",
4872        canonical_type: "u8",
4873        size: 1,
4874        offset: 16,
4875        intent: FieldIntent::Custom,
4876    }];
4877    static NEW_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4878        name: "x",
4879        canonical_type: "u16",
4880        size: 2,
4881        offset: 16,
4882        intent: FieldIntent::Custom,
4883    }];
4884
4885    #[test]
4886    fn compare_fields_type_change_detected() {
4887        let a = LayoutManifest {
4888            name: "A",
4889            disc: 1,
4890            version: 1,
4891            layout_id: [1; 8],
4892            total_size: 17,
4893            field_count: 1,
4894            fields: OLD_TYPE_FIELD,
4895        };
4896        let b = LayoutManifest {
4897            name: "B",
4898            disc: 1,
4899            version: 2,
4900            layout_id: [2; 8],
4901            total_size: 18,
4902            field_count: 1,
4903            fields: NEW_TYPE_FIELD,
4904        };
4905        let report = compare_fields::<8>(&a, &b);
4906        assert_eq!(report.entries[0].status, FieldCompat::Changed);
4907        assert!(!report.is_append_safe);
4908    }
4909
4910    #[test]
4911    fn verdict_different_disc_is_incompatible() {
4912        let a = LayoutManifest {
4913            name: "A",
4914            disc: 1,
4915            version: 1,
4916            layout_id: [1; 8],
4917            total_size: 16,
4918            field_count: 0,
4919            fields: &[],
4920        };
4921        let b = LayoutManifest {
4922            name: "B",
4923            disc: 2,
4924            version: 1,
4925            layout_id: [2; 8],
4926            total_size: 16,
4927            field_count: 0,
4928            fields: &[],
4929        };
4930        assert_eq!(
4931            CompatibilityVerdict::between(&a, &b),
4932            CompatibilityVerdict::Incompatible
4933        );
4934    }
4935
4936    #[test]
4937    fn verdict_same_id_is_identical() {
4938        let a = LayoutManifest {
4939            name: "A",
4940            disc: 1,
4941            version: 1,
4942            layout_id: [9; 8],
4943            total_size: 16,
4944            field_count: 0,
4945            fields: &[],
4946        };
4947        assert_eq!(
4948            CompatibilityVerdict::between(&a, &a),
4949            CompatibilityVerdict::Identical
4950        );
4951    }
4952
4953    #[test]
4954    fn compatibility_explain_between_identical() {
4955        let a = LayoutManifest {
4956            name: "A",
4957            disc: 1,
4958            version: 1,
4959            layout_id: [9; 8],
4960            total_size: 16,
4961            field_count: 0,
4962            fields: &[],
4963        };
4964        let exp = CompatibilityExplain::between(&a, &a);
4965        assert_eq!(exp.verdict, CompatibilityVerdict::Identical);
4966        assert_eq!(exp.added_count, 0);
4967        assert_eq!(exp.removed_count, 0);
4968        assert!(!exp.semantic_drift);
4969    }
4970
4971    static APPEND_OLD: &[FieldDescriptor] = &[FieldDescriptor {
4972        name: "a",
4973        canonical_type: "u8",
4974        size: 1,
4975        offset: 16,
4976        intent: FieldIntent::Custom,
4977    }];
4978    static APPEND_NEW: &[FieldDescriptor] = &[
4979        FieldDescriptor {
4980            name: "a",
4981            canonical_type: "u8",
4982            size: 1,
4983            offset: 16,
4984            intent: FieldIntent::Custom,
4985        },
4986        FieldDescriptor {
4987            name: "b",
4988            canonical_type: "u8",
4989            size: 1,
4990            offset: 17,
4991            intent: FieldIntent::Custom,
4992        },
4993    ];
4994
4995    #[test]
4996    fn compatibility_explain_append_counts_fields() {
4997        let older = LayoutManifest {
4998            name: "T",
4999            disc: 1,
5000            version: 1,
5001            layout_id: [1; 8],
5002            total_size: 17,
5003            field_count: 1,
5004            fields: APPEND_OLD,
5005        };
5006        let newer = LayoutManifest {
5007            name: "T",
5008            disc: 1,
5009            version: 2,
5010            layout_id: [2; 8],
5011            total_size: 18,
5012            field_count: 2,
5013            fields: APPEND_NEW,
5014        };
5015        let exp = CompatibilityExplain::between(&older, &newer);
5016        assert_eq!(exp.verdict, CompatibilityVerdict::AppendSafe);
5017        assert_eq!(exp.added_count, 1);
5018        assert_eq!(exp.added_fields[0], "b");
5019    }
5020
5021    #[test]
5022    fn layout_fingerprint_deterministic() {
5023        let m = LayoutManifest {
5024            name: "X",
5025            disc: 1,
5026            version: 1,
5027            layout_id: [5; 8],
5028            total_size: 16,
5029            field_count: 0,
5030            fields: &[],
5031        };
5032        let fp1 = LayoutFingerprint::from_manifest(&m);
5033        let fp2 = LayoutFingerprint::from_manifest(&m);
5034        assert_eq!(fp1.wire_hash, fp2.wire_hash);
5035        assert_eq!(fp1.semantic_hash, fp2.semantic_hash);
5036    }
5037
5038    static FP_CUSTOM: &[FieldDescriptor] = &[FieldDescriptor {
5039        name: "x",
5040        canonical_type: "u8",
5041        size: 1,
5042        offset: 16,
5043        intent: FieldIntent::Custom,
5044    }];
5045    static FP_BALANCE: &[FieldDescriptor] = &[FieldDescriptor {
5046        name: "x",
5047        canonical_type: "u8",
5048        size: 1,
5049        offset: 16,
5050        intent: FieldIntent::Balance,
5051    }];
5052
5053    #[test]
5054    fn layout_fingerprint_differs_on_intent_change() {
5055        let m1 = LayoutManifest {
5056            name: "T",
5057            disc: 1,
5058            version: 1,
5059            layout_id: [1; 8],
5060            total_size: 17,
5061            field_count: 1,
5062            fields: FP_CUSTOM,
5063        };
5064        let m2 = LayoutManifest {
5065            name: "T",
5066            disc: 1,
5067            version: 1,
5068            layout_id: [1; 8],
5069            total_size: 17,
5070            field_count: 1,
5071            fields: FP_BALANCE,
5072        };
5073        let fp1 = LayoutFingerprint::from_manifest(&m1);
5074        let fp2 = LayoutFingerprint::from_manifest(&m2);
5075        assert_eq!(fp1.wire_hash, fp2.wire_hash);
5076        assert_ne!(fp1.semantic_hash, fp2.semantic_hash);
5077    }
5078
5079    static LINT_AUTH_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
5080        name: "auth",
5081        canonical_type: "[u8;32]",
5082        size: 32,
5083        offset: 16,
5084        intent: FieldIntent::Authority,
5085    }];
5086
5087    #[test]
5088    fn lint_layout_authority_without_signer() {
5089        let m = LayoutManifest {
5090            name: "T",
5091            disc: 1,
5092            version: 1,
5093            layout_id: [0; 8],
5094            total_size: 48,
5095            field_count: 1,
5096            fields: LINT_AUTH_FIELD,
5097        };
5098        // Use a mutating behavior WITHOUT signer to trigger E001
5099        let behavior = LayoutBehavior {
5100            requires_signer: false,
5101            affects_balance: false,
5102            affects_authority: true,
5103            mutation_class: MutationClass::InPlace,
5104        };
5105        let (n, lints) = lint_layout::<8>(&m, &behavior);
5106        assert!(n >= 1);
5107        assert_eq!(lints[0].code, "E001");
5108    }
5109
5110    #[test]
5111    fn lint_layout_clean_passes() {
5112        let m = LayoutManifest {
5113            name: "T",
5114            disc: 1,
5115            version: 1,
5116            layout_id: [0; 8],
5117            total_size: 48,
5118            field_count: 1,
5119            fields: LINT_AUTH_FIELD,
5120        };
5121        let behavior = LayoutBehavior {
5122            requires_signer: true,
5123            affects_balance: false,
5124            affects_authority: true,
5125            mutation_class: MutationClass::AuthoritySensitive,
5126        };
5127        let (n, _) = lint_layout::<8>(&m, &behavior);
5128        assert_eq!(n, 0);
5129    }
5130
5131    #[test]
5132    fn mutation_class_properties() {
5133        assert!(!MutationClass::ReadOnly.is_mutating());
5134        assert!(MutationClass::InPlace.is_mutating());
5135        assert!(MutationClass::Financial.requires_snapshot());
5136        assert!(MutationClass::AuthoritySensitive.requires_authority());
5137        assert!(!MutationClass::AppendOnly.requires_authority());
5138    }
5139
5140    static SEED_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
5141        name: "seed",
5142        canonical_type: "[u8;32]",
5143        size: 32,
5144        offset: 16,
5145        intent: FieldIntent::PDASeed,
5146    }];
5147
5148    #[test]
5149    fn layout_stability_grade_stable_with_init_only() {
5150        let m = LayoutManifest {
5151            name: "T",
5152            disc: 1,
5153            version: 1,
5154            layout_id: [0; 8],
5155            total_size: 48,
5156            field_count: 1,
5157            fields: SEED_FIELD,
5158        };
5159        assert_eq!(
5160            LayoutStabilityGrade::compute(&m),
5161            LayoutStabilityGrade::Stable
5162        );
5163    }
5164
5165    #[test]
5166    fn layout_stability_grade_evolving_with_custom() {
5167        let m = LayoutManifest {
5168            name: "T",
5169            disc: 1,
5170            version: 1,
5171            layout_id: [0; 8],
5172            total_size: 17,
5173            field_count: 1,
5174            fields: SINGLE_FIELD,
5175        };
5176        assert_eq!(
5177            LayoutStabilityGrade::compute(&m),
5178            LayoutStabilityGrade::Evolving
5179        );
5180    }
5181
5182    static GRADE_HEAVY: &[FieldDescriptor] = &[
5183        FieldDescriptor {
5184            name: "auth1",
5185            canonical_type: "[u8;32]",
5186            size: 32,
5187            offset: 16,
5188            intent: FieldIntent::Authority,
5189        },
5190        FieldDescriptor {
5191            name: "auth2",
5192            canonical_type: "[u8;32]",
5193            size: 32,
5194            offset: 48,
5195            intent: FieldIntent::Owner,
5196        },
5197        FieldDescriptor {
5198            name: "auth3",
5199            canonical_type: "[u8;32]",
5200            size: 32,
5201            offset: 80,
5202            intent: FieldIntent::Delegate,
5203        },
5204        FieldDescriptor {
5205            name: "bal1",
5206            canonical_type: "WireU64",
5207            size: 8,
5208            offset: 112,
5209            intent: FieldIntent::Balance,
5210        },
5211        FieldDescriptor {
5212            name: "bal2",
5213            canonical_type: "WireU64",
5214            size: 8,
5215            offset: 120,
5216            intent: FieldIntent::Supply,
5217        },
5218        FieldDescriptor {
5219            name: "bal3",
5220            canonical_type: "WireU64",
5221            size: 8,
5222            offset: 128,
5223            intent: FieldIntent::Balance,
5224        },
5225    ];
5226
5227    #[test]
5228    fn layout_stability_grade_unsafe_to_evolve_heavy() {
5229        let m = LayoutManifest {
5230            name: "T",
5231            disc: 1,
5232            version: 1,
5233            layout_id: [0; 8],
5234            total_size: 136,
5235            field_count: 6,
5236            fields: GRADE_HEAVY,
5237        };
5238        let grade = LayoutStabilityGrade::compute(&m);
5239        assert_eq!(grade, LayoutStabilityGrade::UnsafeToEvolve);
5240    }
5241
5242    #[test]
5243    fn field_intent_new_variants_coverage() {
5244        assert_eq!(FieldIntent::PDASeed.name(), "pda_seed");
5245        assert_eq!(FieldIntent::Version.name(), "version");
5246        assert_eq!(FieldIntent::Bump.name(), "bump");
5247        assert_eq!(FieldIntent::Status.name(), "status");
5248        assert!(FieldIntent::Owner.is_authority_sensitive());
5249        assert!(FieldIntent::Delegate.is_authority_sensitive());
5250        assert!(FieldIntent::Threshold.is_governance());
5251        assert!(FieldIntent::Bump.is_init_only());
5252        assert!(FieldIntent::PDASeed.is_init_only());
5253        assert!(FieldIntent::Supply.is_monetary());
5254    }
5255
5256    #[test]
5257    fn refine_verdict_softens_with_rebuildable_segments() {
5258        let advice = [
5259            SegmentAdvice {
5260                id: [0; 4],
5261                size: 100,
5262                role: SegmentRoleHint::Cache,
5263                must_preserve: false,
5264                clearable: true,
5265                rebuildable: true,
5266                append_only: false,
5267                immutable: false,
5268            },
5269            SegmentAdvice {
5270                id: [0; 4],
5271                size: 0,
5272                role: SegmentRoleHint::Unclassified,
5273                must_preserve: false,
5274                clearable: false,
5275                rebuildable: false,
5276                append_only: false,
5277                immutable: false,
5278            },
5279        ];
5280        let report = SegmentMigrationReport {
5281            advice,
5282            count: 1,
5283            preserve_bytes: 0,
5284            clearable_bytes: 100,
5285            rebuildable_bytes: 100,
5286        };
5287        let refined = CompatibilityVerdict::MigrationRequired.refine_with_roles(&report);
5288        assert_eq!(refined, CompatibilityVerdict::AppendSafe);
5289    }
5290
5291    #[test]
5292    fn refine_verdict_escalates_with_immutable_segment() {
5293        let advice = [SegmentAdvice {
5294            id: [0; 4],
5295            size: 50,
5296            role: SegmentRoleHint::Audit,
5297            must_preserve: true,
5298            clearable: false,
5299            rebuildable: false,
5300            append_only: true,
5301            immutable: true,
5302        }];
5303        let report = SegmentMigrationReport {
5304            advice,
5305            count: 1,
5306            preserve_bytes: 50,
5307            clearable_bytes: 0,
5308            rebuildable_bytes: 0,
5309        };
5310        let refined = CompatibilityVerdict::AppendSafe.refine_with_roles(&report);
5311        assert_eq!(refined, CompatibilityVerdict::MigrationRequired);
5312    }
5313
5314    #[test]
5315    #[cfg(feature = "policy")]
5316    fn lint_policy_financial_mismatch() {
5317        let behavior = LayoutBehavior {
5318            requires_signer: true,
5319            affects_balance: true,
5320            affects_authority: false,
5321            mutation_class: MutationClass::Financial,
5322        };
5323        let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Write);
5324        assert!(n >= 1);
5325        assert_eq!(lints[0].code, "W005");
5326    }
5327
5328    #[test]
5329    #[cfg(feature = "policy")]
5330    fn lint_policy_reverse_mismatch() {
5331        let behavior = LayoutBehavior {
5332            requires_signer: true,
5333            affects_balance: false,
5334            affects_authority: false,
5335            mutation_class: MutationClass::InPlace,
5336        };
5337        let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5338        assert!(n >= 1);
5339        assert_eq!(lints[0].code, "W006");
5340    }
5341
5342    #[test]
5343    #[cfg(feature = "policy")]
5344    fn lint_policy_clean_when_aligned() {
5345        let behavior = LayoutBehavior {
5346            requires_signer: true,
5347            affects_balance: true,
5348            affects_authority: false,
5349            mutation_class: MutationClass::Financial,
5350        };
5351        let (n, _) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5352        assert_eq!(n, 0);
5353    }
5354
5355    #[test]
5356    fn display_field_intent() {
5357        extern crate alloc;
5358        use alloc::format;
5359        assert_eq!(format!("{}", FieldIntent::Balance), "balance");
5360        assert_eq!(format!("{}", FieldIntent::Authority), "authority");
5361    }
5362
5363    #[test]
5364    fn display_mutation_class() {
5365        extern crate alloc;
5366        use alloc::format;
5367        assert_eq!(format!("{}", MutationClass::Financial), "financial");
5368        assert_eq!(format!("{}", MutationClass::ReadOnly), "read-only");
5369    }
5370
5371    #[test]
5372    fn display_layout_stability_grade() {
5373        extern crate alloc;
5374        use alloc::format;
5375        assert_eq!(format!("{}", LayoutStabilityGrade::Stable), "stable");
5376        assert_eq!(
5377            format!("{}", LayoutStabilityGrade::UnsafeToEvolve),
5378            "unsafe-to-evolve"
5379        );
5380    }
5381
5382    #[test]
5383    fn display_compatibility_verdict() {
5384        extern crate alloc;
5385        use alloc::format;
5386        assert_eq!(format!("{}", CompatibilityVerdict::Identical), "identical");
5387        assert_eq!(
5388            format!("{}", CompatibilityVerdict::MigrationRequired),
5389            "migration-required"
5390        );
5391    }
5392
5393    #[test]
5394    fn display_layout_fingerprint() {
5395        extern crate alloc;
5396        use alloc::format;
5397        let fp = LayoutFingerprint {
5398            wire_hash: [0xAB, 0xCD, 0, 0, 0, 0, 0, 0],
5399            semantic_hash: [0, 0, 0, 0, 0, 0, 0xFF, 0x01],
5400        };
5401        let s = format!("{}", fp);
5402        assert!(s.starts_with("wire=abcd"));
5403        assert!(s.contains("sem="));
5404        assert!(s.ends_with("ff01"));
5405    }
5406
5407    #[test]
5408    fn display_receipt_profile() {
5409        extern crate alloc;
5410        use alloc::format;
5411        let rp = ReceiptProfile {
5412            name: "test",
5413            expected_phase: "Mutate",
5414            expects_balance_change: true,
5415            expects_authority_change: false,
5416            expects_journal_append: false,
5417            min_changed_fields: 2,
5418        };
5419        let s = format!("{}", rp);
5420        assert!(s.contains("test"));
5421        assert!(s.contains("Mutate"));
5422        assert!(s.contains("balance"));
5423        assert!(s.contains("min_fields=2"));
5424    }
5425
5426    #[test]
5427    fn display_idl_segment_descriptor() {
5428        extern crate alloc;
5429        use alloc::format;
5430        let sd = IdlSegmentDescriptor {
5431            name: "core",
5432            role: "Core",
5433            append_only: false,
5434            rebuildable: false,
5435            must_preserve: true,
5436        };
5437        let s = format!("{}", sd);
5438        assert!(s.contains("core"));
5439        assert!(s.contains("Core"));
5440        assert!(s.contains("must-preserve"));
5441        assert!(!s.contains("append-only"));
5442    }
5443}