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/// Failure reason for `#[hopper::args] T::parse`.
2051///
2052/// Exposed here (rather than in `hopper-core`) because programs that use
2053/// the args derive will already depend on `hopper-schema` for `SchemaExport`.
2054/// Keeping the error type in schema avoids an extra dependency edge.
2055#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2056pub enum ArgParseError {
2057    /// Not enough bytes to cover the packed struct size.
2058    TooShort {
2059        /// Bytes required.
2060        required: u16,
2061        /// Bytes available.
2062        got: u16,
2063    },
2064}
2065
2066impl fmt::Display for ArgParseError {
2067    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2068        match self {
2069            ArgParseError::TooShort { required, got } => {
2070                write!(f, "args: too short (required {}, got {})", required, got)
2071            }
2072        }
2073    }
2074}
2075
2076/// Descriptor for one variant of a `#[hopper::error]` enum.
2077///
2078/// Carried in the program manifest so off-chain SDKs can map numeric error
2079/// codes back to names and. via `invariant`. to the safety check that
2080/// produced them.
2081///
2082/// ## Design notes
2083///
2084/// Hopper errors can carry the **invariant name** a variant corresponds
2085/// to, so a client that sees error `0x1001` can surface "Invariant
2086/// `balance_nonzero` failed" without needing to keep a separate lookup
2087/// table in sync with the on-chain code.
2088#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2089pub struct ErrorDescriptor {
2090    /// Variant name (exactly as declared).
2091    pub name: &'static str,
2092    /// Stable numeric code emitted on failure.
2093    pub code: u32,
2094    /// Invariant this error corresponds to, or empty string if none.
2095    pub invariant: &'static str,
2096    /// Short documentation string (often copied from variant doc comments).
2097    pub doc: &'static str,
2098}
2099
2100/// Public-facing program constant.
2101///
2102/// Mirrors Anchor's `#[constant]` IDL surface. Carries the source-level
2103/// name, the stringified Rust type, and the stringified initializer
2104/// expression, so off-chain consumers can reconstruct the value
2105/// without the program crate as a build-time dependency.
2106///
2107/// Emitted by `#[hopper::constant]` as a sibling `pub const` next to
2108/// the original declaration; collected into a `&'static [ConstantDescriptor]`
2109/// slice by the program author (or by the `hopper::program!` macro) and
2110/// passed to an IDL emitter via `AnchorIdlWithConstants`.
2111#[derive(Clone, Copy, Debug)]
2112pub struct ConstantDescriptor {
2113    /// Constant name, e.g. `"MAX_DEPOSIT"`.
2114    pub name: &'static str,
2115    /// Stringified Rust type, e.g. `"u64"` or `"[u8; 32]"`.
2116    pub ty: &'static str,
2117    /// Stringified initializer expression, e.g. `"1_000_000"`.
2118    pub value: &'static str,
2119    /// Optional doc-comment text.
2120    pub docs: &'static str,
2121}
2122
2123/// A convenience wrapper holding an enum's full error table.
2124///
2125/// Programs expose their error tables to the schema via the `SchemaExport`
2126/// path; the manifest gains an `errors[]` field that aggregates across all
2127/// such registries declared in the crate.
2128#[derive(Clone, Copy, Debug)]
2129pub struct ErrorRegistry {
2130    /// Enum ident, e.g. `"VaultError"`.
2131    pub enum_name: &'static str,
2132    /// Ordered error descriptors.
2133    pub errors: &'static [ErrorDescriptor],
2134}
2135
2136impl ErrorRegistry {
2137    /// Look up an error descriptor by numeric code.
2138    pub fn find_by_code(&self, code: u32) -> Option<&ErrorDescriptor> {
2139        let mut i = 0;
2140        while i < self.errors.len() {
2141            if self.errors[i].code == code {
2142                return Some(&self.errors[i]);
2143            }
2144            i += 1;
2145        }
2146        None
2147    }
2148
2149    /// Look up the invariant name associated with a code, if any.
2150    pub fn invariant_for(&self, code: u32) -> Option<&'static str> {
2151        self.find_by_code(code).and_then(|d| {
2152            if d.invariant.is_empty() {
2153                None
2154            } else {
2155                Some(d.invariant)
2156            }
2157        })
2158    }
2159}
2160
2161/// An instruction descriptor in a program manifest.
2162#[derive(Clone, Copy, Debug)]
2163pub struct InstructionDescriptor {
2164    /// Instruction name.
2165    pub name: &'static str,
2166    /// Discriminator tag.
2167    pub tag: u8,
2168    /// Arguments.
2169    pub args: &'static [ArgDescriptor],
2170    /// Accounts.
2171    pub accounts: &'static [AccountEntry],
2172    /// Capability names.
2173    pub capabilities: &'static [&'static str],
2174    /// Policy pack name (empty if custom).
2175    pub policy_pack: &'static str,
2176    /// Whether this instruction emits a receipt.
2177    pub receipt_expected: bool,
2178}
2179
2180/// An event descriptor in a program manifest.
2181#[derive(Clone, Copy)]
2182pub struct EventDescriptor {
2183    /// Event name.
2184    pub name: &'static str,
2185    /// Event discriminator tag.
2186    pub tag: u8,
2187    /// Event fields.
2188    pub fields: &'static [FieldDescriptor],
2189}
2190
2191/// A policy descriptor in a program manifest.
2192#[derive(Clone, Copy)]
2193pub struct PolicyDescriptor {
2194    /// Policy pack name.
2195    pub name: &'static str,
2196    /// Capability names this policy covers.
2197    pub capabilities: &'static [&'static str],
2198    /// Requirement names this policy triggers.
2199    pub requirements: &'static [&'static str],
2200    /// Invariant names this policy checks.
2201    pub invariants: &'static [&'static str],
2202    /// Receipt profile expected when this policy is active.
2203    pub receipt_profile: &'static str,
2204}
2205
2206/// Extended per-layout metadata for the manifest.
2207///
2208/// Carries richer operational metadata that Hopper Manager, CLI, and
2209/// migration tooling use beyond what `LayoutManifest` provides.
2210#[derive(Clone, Copy)]
2211pub struct LayoutMetadata {
2212    /// Layout name (must match corresponding `LayoutManifest.name`).
2213    pub name: &'static str,
2214    /// Segment role descriptors (for segmented accounts).
2215    pub segment_roles: &'static [&'static str],
2216    /// Whether append-only changes to this layout are always safe.
2217    pub append_safe: bool,
2218    /// Whether changes require an explicit migration instruction.
2219    pub migration_required: bool,
2220    /// Whether derived data (index/cache segments) can be rebuilt from core.
2221    pub rebuildable: bool,
2222    /// Policy pack name that governs writes to this layout.
2223    pub policy_pack: &'static str,
2224    /// Invariant pack names that must hold for this layout.
2225    pub invariant_pack: &'static [&'static str],
2226    /// Receipt profile name for mutations on this layout.
2227    pub receipt_profile: &'static str,
2228    /// Execution phases this layout participates in.
2229    pub phase_requirements: &'static [&'static str],
2230    /// Trust profile label (e.g. "verified", "trusted", "unchecked").
2231    pub trust_profile: &'static str,
2232    /// Hints for Hopper Manager rendering.
2233    pub manager_hints: &'static [&'static str],
2234}
2235
2236/// A compatibility pair describing a known upgrade path.
2237#[derive(Clone, Copy)]
2238pub struct CompatibilityPair {
2239    /// Old layout name.
2240    pub from_layout: &'static str,
2241    /// Old version.
2242    pub from_version: u8,
2243    /// New layout name.
2244    pub to_layout: &'static str,
2245    /// New version.
2246    pub to_version: u8,
2247    /// Migration policy.
2248    pub policy: MigrationPolicy,
2249    /// Whether backward reading is supported.
2250    pub backward_readable: bool,
2251}
2252
2253/// A full program manifest for Hopper Manager and tooling.
2254///
2255/// This is the **rich internal schema** that powers `hopper manager`,
2256/// compatibility checking, migration planning, receipt rendering, and
2257/// CLI inspection. It is intentionally richer than the public IDL --
2258/// the manifest carries operational metadata that tools need but
2259/// external consumers do not.
2260///
2261/// ## Truth hierarchy
2262///
2263/// ```text
2264/// ProgramManifest  ⊃  ProgramIdl  ⊃  CodamaProjection
2265///       (rich)         (public)         (interop)
2266/// ```
2267#[derive(Clone, Copy)]
2268pub struct ProgramManifest {
2269    /// Program name.
2270    pub name: &'static str,
2271    /// Program version string.
2272    pub version: &'static str,
2273    /// Program description.
2274    pub description: &'static str,
2275    /// Layout manifests for all account types.
2276    pub layouts: &'static [LayoutManifest],
2277    /// Extended per-layout operational metadata.
2278    pub layout_metadata: &'static [LayoutMetadata],
2279    /// Instruction descriptors.
2280    pub instructions: &'static [InstructionDescriptor],
2281    /// Event descriptors.
2282    pub events: &'static [EventDescriptor],
2283    /// Policy descriptors.
2284    pub policies: &'static [PolicyDescriptor],
2285    /// Known upgrade paths between layout versions.
2286    pub compatibility_pairs: &'static [CompatibilityPair],
2287    /// Tooling / rendering hints for Manager.
2288    pub tooling_hints: &'static [&'static str],
2289    /// Context (instruction account struct) descriptors.
2290    pub contexts: &'static [crate::accounts::ContextDescriptor],
2291}
2292
2293// ---------------------------------------------------------------------------
2294// Program IDL -- public schema subset
2295// ---------------------------------------------------------------------------
2296
2297/// PDA seed hint for an instruction account.
2298#[derive(Clone, Copy)]
2299pub struct PdaSeedHint {
2300    /// Seed kind: "literal", "account", "arg".
2301    pub kind: &'static str,
2302    /// Seed value or reference name.
2303    pub value: &'static str,
2304}
2305
2306/// IDL account entry with optional PDA metadata.
2307#[derive(Clone, Copy)]
2308pub struct IdlAccountEntry {
2309    /// Account name.
2310    pub name: &'static str,
2311    /// Whether the account is writable.
2312    pub writable: bool,
2313    /// Whether the account is a signer.
2314    pub signer: bool,
2315    /// Optional layout reference name.
2316    pub layout_ref: &'static str,
2317    /// PDA seed hints (empty if not a PDA).
2318    pub pda_seeds: &'static [PdaSeedHint],
2319}
2320
2321/// IDL instruction descriptor.
2322#[derive(Clone, Copy)]
2323pub struct IdlInstructionDescriptor {
2324    /// Instruction name.
2325    pub name: &'static str,
2326    /// Discriminator tag.
2327    pub tag: u8,
2328    /// Arguments.
2329    pub args: &'static [ArgDescriptor],
2330    /// Accounts with PDA metadata.
2331    pub accounts: &'static [IdlAccountEntry],
2332}
2333
2334/// A public-facing IDL for a Hopper program.
2335///
2336/// Contains only what external consumers (clients, explorers, SDKs) need.
2337/// Does NOT contain internal policy logic, migration planner hints,
2338/// trust internals, or unsafe metadata.
2339///
2340/// Generated from (and strictly a subset of) `ProgramManifest`.
2341#[derive(Clone, Copy)]
2342pub struct ProgramIdl {
2343    /// Program name.
2344    pub name: &'static str,
2345    /// Program version string.
2346    pub version: &'static str,
2347    /// Program description.
2348    pub description: &'static str,
2349    /// Instructions with args, accounts, PDA hints.
2350    pub instructions: &'static [IdlInstructionDescriptor],
2351    /// Account layout summaries (name, disc, version, layout_id, size, fields).
2352    pub accounts: &'static [LayoutManifest],
2353    /// Event descriptors.
2354    pub events: &'static [EventDescriptor],
2355    /// Optional layout_id fingerprints per account.
2356    pub fingerprints: &'static [([u8; 8], &'static str)],
2357}
2358
2359impl ProgramIdl {
2360    /// Create an empty IDL.
2361    pub const fn empty() -> Self {
2362        Self {
2363            name: "",
2364            version: "",
2365            description: "",
2366            instructions: &[],
2367            accounts: &[],
2368            events: &[],
2369            fingerprints: &[],
2370        }
2371    }
2372
2373    /// Number of instructions.
2374    pub const fn instruction_count(&self) -> usize {
2375        self.instructions.len()
2376    }
2377
2378    /// Number of account types.
2379    pub const fn account_count(&self) -> usize {
2380        self.accounts.len()
2381    }
2382
2383    /// Find an instruction by name.
2384    pub fn find_instruction(&self, name: &str) -> Option<&IdlInstructionDescriptor> {
2385        let mut i = 0;
2386        while i < self.instructions.len() {
2387            if const_str_eq(self.instructions[i].name, name) {
2388                return Some(&self.instructions[i]);
2389            }
2390            i += 1;
2391        }
2392        None
2393    }
2394
2395    /// Find an account layout by name.
2396    pub fn find_account(&self, name: &str) -> Option<&LayoutManifest> {
2397        let mut i = 0;
2398        while i < self.accounts.len() {
2399            if const_str_eq(self.accounts[i].name, name) {
2400                return Some(&self.accounts[i]);
2401            }
2402            i += 1;
2403        }
2404        None
2405    }
2406}
2407
2408impl fmt::Display for ProgramIdl {
2409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2410        writeln!(f, "IDL: {} {}", self.name, self.version)?;
2411        if !self.description.is_empty() {
2412            writeln!(f, "  {}", self.description)?;
2413        }
2414        writeln!(f)?;
2415        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2416        for ix in self.instructions.iter() {
2417            write!(
2418                f,
2419                "  {:>2}  {:16} args={} accounts={}",
2420                ix.tag,
2421                ix.name,
2422                ix.args.len(),
2423                ix.accounts.len()
2424            )?;
2425            writeln!(f)?;
2426        }
2427        writeln!(f)?;
2428        writeln!(f, "Accounts ({}):", self.accounts.len())?;
2429        for a in self.accounts.iter() {
2430            write!(
2431                f,
2432                "  {:16} disc={} v{} {} bytes  id=",
2433                a.name, a.disc, a.version, a.total_size
2434            )?;
2435            write_hex(f, &a.layout_id)?;
2436            writeln!(f)?;
2437        }
2438        if !self.events.is_empty() {
2439            writeln!(f)?;
2440            writeln!(f, "Events ({}):", self.events.len())?;
2441            for e in self.events.iter() {
2442                writeln!(f, "  {:>2}  {:16} fields={}", e.tag, e.name, e.fields.len())?;
2443            }
2444        }
2445        Ok(())
2446    }
2447}
2448
2449// ---------------------------------------------------------------------------
2450// Codama Projection -- ecosystem interop subset
2451// ---------------------------------------------------------------------------
2452
2453/// Codama-compatible instruction descriptor.
2454///
2455/// Only the fields needed for Codama/Kinobi IDL generation.
2456#[derive(Clone, Copy)]
2457pub struct CodamaInstruction {
2458    pub name: &'static str,
2459    pub discriminator: u8,
2460    pub args: &'static [ArgDescriptor],
2461    pub accounts: &'static [IdlAccountEntry],
2462}
2463
2464/// Codama-compatible account descriptor.
2465#[derive(Clone, Copy)]
2466pub struct CodamaAccount {
2467    pub name: &'static str,
2468    pub discriminator: u8,
2469    pub size: usize,
2470    pub fields: &'static [FieldDescriptor],
2471}
2472
2473/// Codama-compatible event descriptor.
2474#[derive(Clone, Copy)]
2475pub struct CodamaEvent {
2476    pub name: &'static str,
2477    pub discriminator: u8,
2478    pub fields: &'static [FieldDescriptor],
2479}
2480
2481/// Codama-compatible projection of a Hopper program.
2482///
2483/// This is a **bridge**, not a prison. It maps the clean public subset
2484/// of a Hopper program into a shape that Codama/Kinobi tooling can consume.
2485///
2486/// Does NOT include: internal policy logic, migration planner hints,
2487/// trust internals, unsafe metadata, segment roles, or manager hints.
2488///
2489/// ## Layering
2490///
2491/// ```text
2492/// ProgramManifest = rich truth
2493/// ProgramIdl      = public schema
2494/// CodamaProjection = compatibility projection
2495/// ```
2496#[derive(Clone, Copy)]
2497pub struct CodamaProjection {
2498    /// Program name.
2499    pub name: &'static str,
2500    /// Program version.
2501    pub version: &'static str,
2502    /// Instructions (public subset).
2503    pub instructions: &'static [CodamaInstruction],
2504    /// Account types (public subset).
2505    pub accounts: &'static [CodamaAccount],
2506    /// Events (public subset).
2507    pub events: &'static [CodamaEvent],
2508}
2509
2510impl CodamaProjection {
2511    /// Create an empty projection.
2512    pub const fn empty() -> Self {
2513        Self {
2514            name: "",
2515            version: "",
2516            instructions: &[],
2517            accounts: &[],
2518            events: &[],
2519        }
2520    }
2521}
2522
2523impl fmt::Display for CodamaProjection {
2524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2525        writeln!(f, "Codama: {} {}", self.name, self.version)?;
2526        writeln!(f)?;
2527        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2528        for ix in self.instructions.iter() {
2529            writeln!(
2530                f,
2531                "  {:>2}  {:16} args={} accounts={}",
2532                ix.discriminator,
2533                ix.name,
2534                ix.args.len(),
2535                ix.accounts.len()
2536            )?;
2537        }
2538        writeln!(f)?;
2539        writeln!(f, "Accounts ({}):", self.accounts.len())?;
2540        for a in self.accounts.iter() {
2541            writeln!(
2542                f,
2543                "  {:16} disc={} {} bytes fields={}",
2544                a.name,
2545                a.discriminator,
2546                a.size,
2547                a.fields.len()
2548            )?;
2549        }
2550        if !self.events.is_empty() {
2551            writeln!(f)?;
2552            writeln!(f, "Events ({}):", self.events.len())?;
2553            for e in self.events.iter() {
2554                writeln!(
2555                    f,
2556                    "  {:>2}  {:16} fields={}",
2557                    e.discriminator,
2558                    e.name,
2559                    e.fields.len()
2560                )?;
2561            }
2562        }
2563        Ok(())
2564    }
2565}
2566
2567impl ProgramManifest {
2568    /// Create an empty program manifest.
2569    pub const fn empty() -> Self {
2570        Self {
2571            name: "",
2572            version: "",
2573            description: "",
2574            layouts: &[],
2575            layout_metadata: &[],
2576            instructions: &[],
2577            events: &[],
2578            policies: &[],
2579            compatibility_pairs: &[],
2580            tooling_hints: &[],
2581            contexts: &[],
2582        }
2583    }
2584
2585    /// Number of layouts.
2586    pub const fn layout_count(&self) -> usize {
2587        self.layouts.len()
2588    }
2589
2590    /// Number of instructions.
2591    pub const fn instruction_count(&self) -> usize {
2592        self.instructions.len()
2593    }
2594
2595    /// Find a layout by discriminator.
2596    pub fn find_layout_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
2597        let mut i = 0;
2598        while i < self.layouts.len() {
2599            if self.layouts[i].disc == disc {
2600                return Some(&self.layouts[i]);
2601            }
2602            i += 1;
2603        }
2604        None
2605    }
2606
2607    /// Find a layout by layout_id fingerprint.
2608    pub fn find_layout_by_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
2609        let mut i = 0;
2610        while i < self.layouts.len() {
2611            if self.layouts[i].layout_id == *layout_id {
2612                return Some(&self.layouts[i]);
2613            }
2614            i += 1;
2615        }
2616        None
2617    }
2618
2619    /// Find a layout that matches the given account data header.
2620    pub fn identify_from_data(&self, data: &[u8]) -> Option<&LayoutManifest> {
2621        let header = decode_header(data)?;
2622        // Try layout_id match first (strongest)
2623        if let Some(m) = self.find_layout_by_id(&header.layout_id) {
2624            return Some(m);
2625        }
2626        // Fall back to disc match
2627        self.find_layout_by_disc(header.disc)
2628    }
2629
2630    /// Find an instruction by tag.
2631    pub fn find_instruction(&self, tag: u8) -> Option<&InstructionDescriptor> {
2632        let mut i = 0;
2633        while i < self.instructions.len() {
2634            if self.instructions[i].tag == tag {
2635                return Some(&self.instructions[i]);
2636            }
2637            i += 1;
2638        }
2639        None
2640    }
2641
2642    /// Find a policy by name.
2643    pub fn find_policy(&self, name: &str) -> Option<&PolicyDescriptor> {
2644        let mut i = 0;
2645        while i < self.policies.len() {
2646            if self.policies[i].name == name {
2647                return Some(&self.policies[i]);
2648            }
2649            i += 1;
2650        }
2651        None
2652    }
2653
2654    /// Find extended layout metadata by layout name.
2655    pub fn find_layout_metadata(&self, name: &str) -> Option<&LayoutMetadata> {
2656        let mut i = 0;
2657        while i < self.layout_metadata.len() {
2658            if const_str_eq(self.layout_metadata[i].name, name) {
2659                return Some(&self.layout_metadata[i]);
2660            }
2661            i += 1;
2662        }
2663        None
2664    }
2665
2666    /// Find a compatibility pair for an upgrade path.
2667    pub fn find_compat_pair(
2668        &self,
2669        from_name: &str,
2670        from_ver: u8,
2671        to_name: &str,
2672        to_ver: u8,
2673    ) -> Option<&CompatibilityPair> {
2674        let mut i = 0;
2675        while i < self.compatibility_pairs.len() {
2676            let cp = &self.compatibility_pairs[i];
2677            if const_str_eq(cp.from_layout, from_name)
2678                && cp.from_version == from_ver
2679                && const_str_eq(cp.to_layout, to_name)
2680                && cp.to_version == to_ver
2681            {
2682                return Some(cp);
2683            }
2684            i += 1;
2685        }
2686        None
2687    }
2688}
2689
2690impl fmt::Display for ProgramManifest {
2691    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2692        writeln!(f, "Program: {} {}", self.name, self.version)?;
2693        if !self.description.is_empty() {
2694            writeln!(f, "  {}", self.description)?;
2695        }
2696        writeln!(f)?;
2697
2698        writeln!(f, "Layouts ({}):", self.layouts.len())?;
2699        for m in self.layouts.iter() {
2700            write!(
2701                f,
2702                "  {:16} v{}  disc={}  {} bytes  fingerprint=",
2703                m.name, m.version, m.disc, m.total_size
2704            )?;
2705            write_hex(f, &m.layout_id)?;
2706            // Show extended metadata if available
2707            if let Some(meta) = self.find_layout_metadata(m.name) {
2708                if !meta.trust_profile.is_empty() {
2709                    write!(f, "  trust={}", meta.trust_profile)?;
2710                }
2711                if meta.append_safe {
2712                    write!(f, "  append-safe")?;
2713                }
2714                if meta.migration_required {
2715                    write!(f, "  migration-required")?;
2716                }
2717            }
2718            writeln!(f)?;
2719        }
2720        writeln!(f)?;
2721
2722        writeln!(f, "Instructions ({}):", self.instructions.len())?;
2723        for ix in self.instructions.iter() {
2724            write!(
2725                f,
2726                "  {:>2}  {:16} accounts={}",
2727                ix.tag,
2728                ix.name,
2729                ix.accounts.len()
2730            )?;
2731            if !ix.capabilities.is_empty() {
2732                write!(f, "  caps=")?;
2733                for (j, c) in ix.capabilities.iter().enumerate() {
2734                    if j > 0 {
2735                        write!(f, ",")?;
2736                    }
2737                    write!(f, "{}", c)?;
2738                }
2739            }
2740            if ix.receipt_expected {
2741                write!(f, "  receipt=yes")?;
2742            }
2743            writeln!(f)?;
2744        }
2745        writeln!(f)?;
2746
2747        if !self.policies.is_empty() {
2748            writeln!(f, "Policies ({}):", self.policies.len())?;
2749            for p in self.policies.iter() {
2750                write!(f, "  {:24}", p.name)?;
2751                for (j, r) in p.requirements.iter().enumerate() {
2752                    if j > 0 {
2753                        write!(f, " + ")?;
2754                    }
2755                    write!(f, "{}", r)?;
2756                }
2757                if !p.receipt_profile.is_empty() {
2758                    write!(f, "  receipt={}", p.receipt_profile)?;
2759                }
2760                writeln!(f)?;
2761            }
2762            writeln!(f)?;
2763        }
2764
2765        if !self.events.is_empty() {
2766            writeln!(f, "Events ({}):", self.events.len())?;
2767            for e in self.events.iter() {
2768                writeln!(f, "  {:>2}  {:16} fields={}", e.tag, e.name, e.fields.len())?;
2769            }
2770            writeln!(f)?;
2771        }
2772
2773        if !self.compatibility_pairs.is_empty() {
2774            writeln!(f, "Compatibility ({}):", self.compatibility_pairs.len())?;
2775            for cp in self.compatibility_pairs.iter() {
2776                writeln!(
2777                    f,
2778                    "  {} v{} -> {} v{}  {}{}",
2779                    cp.from_layout,
2780                    cp.from_version,
2781                    cp.to_layout,
2782                    cp.to_version,
2783                    cp.policy,
2784                    if cp.backward_readable {
2785                        "  backward-readable"
2786                    } else {
2787                        ""
2788                    },
2789                )?;
2790            }
2791        }
2792
2793        Ok(())
2794    }
2795}
2796
2797// ---------------------------------------------------------------------------
2798// Field-Level Account Decoder
2799// ---------------------------------------------------------------------------
2800
2801/// A decoded field value from account data.
2802pub struct DecodedField<'a> {
2803    /// Field name.
2804    pub name: &'a str,
2805    /// Canonical type.
2806    pub canonical_type: &'a str,
2807    /// Raw bytes of this field.
2808    pub raw: &'a [u8],
2809    /// Offset in account data.
2810    pub offset: u16,
2811    /// Size in bytes.
2812    pub size: u16,
2813}
2814
2815impl<'a> DecodedField<'a> {
2816    /// Format the field value as a human-readable string.
2817    ///
2818    /// Recognizes common Hopper wire types and formats them appropriately.
2819    pub fn format_value(&self, buf: &mut [u8]) -> usize {
2820        match self.canonical_type {
2821            "WireU64" | "LeU64" if self.raw.len() >= 8 => {
2822                let v = u64::from_le_bytes([
2823                    self.raw[0],
2824                    self.raw[1],
2825                    self.raw[2],
2826                    self.raw[3],
2827                    self.raw[4],
2828                    self.raw[5],
2829                    self.raw[6],
2830                    self.raw[7],
2831                ]);
2832                format_u64(v, buf)
2833            }
2834            "WireU32" | "LeU32" if self.raw.len() >= 4 => {
2835                let v =
2836                    u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) as u64;
2837                format_u64(v, buf)
2838            }
2839            "WireU16" | "LeU16" if self.raw.len() >= 2 => {
2840                let v = u16::from_le_bytes([self.raw[0], self.raw[1]]) as u64;
2841                format_u64(v, buf)
2842            }
2843            "WireBool" | "LeBool" if !self.raw.is_empty() => {
2844                if self.raw[0] != 0 {
2845                    let len = 4usize.min(buf.len());
2846                    buf[..len].copy_from_slice(&b"true"[..len]);
2847                    len
2848                } else {
2849                    let len = 5usize.min(buf.len());
2850                    buf[..len].copy_from_slice(&b"false"[..len]);
2851                    len
2852                }
2853            }
2854            "u8" if self.raw.len() == 1 => format_u64(self.raw[0] as u64, buf),
2855            _ if self.size == 32 => {
2856                // Likely an address/pubkey -- show as hex
2857                format_hex_truncated(self.raw, buf)
2858            }
2859            _ => format_hex_truncated(self.raw, buf),
2860        }
2861    }
2862}
2863
2864/// Decode all fields of an account against a layout manifest.
2865///
2866/// Returns the number of fields decoded (up to N).
2867pub fn decode_account_fields<'a, const N: usize>(
2868    data: &'a [u8],
2869    manifest: &'a LayoutManifest,
2870) -> (usize, [Option<DecodedField<'a>>; N]) {
2871    let mut fields: [Option<DecodedField<'a>>; N] = [const { None }; N];
2872    let count = manifest.field_count.min(N);
2873    let mut i = 0;
2874    while i < count {
2875        let fd = &manifest.fields[i];
2876        let start = fd.offset as usize;
2877        let end = start + fd.size as usize;
2878        if end <= data.len() {
2879            fields[i] = Some(DecodedField {
2880                name: fd.name,
2881                canonical_type: fd.canonical_type,
2882                raw: &data[start..end],
2883                offset: fd.offset,
2884                size: fd.size,
2885            });
2886        }
2887        i += 1;
2888    }
2889    (count, fields)
2890}
2891
2892/// Format a u64 as decimal into a byte buffer. Returns bytes written.
2893fn format_u64(mut v: u64, buf: &mut [u8]) -> usize {
2894    if v == 0 {
2895        if !buf.is_empty() {
2896            buf[0] = b'0';
2897            return 1;
2898        }
2899        return 0;
2900    }
2901    // Write digits in reverse
2902    let mut tmp = [0u8; 20];
2903    let mut pos = 0;
2904    while v > 0 && pos < 20 {
2905        tmp[pos] = b'0' + (v % 10) as u8;
2906        v /= 10;
2907        pos += 1;
2908    }
2909    let len = pos.min(buf.len());
2910    let mut i = 0;
2911    while i < len {
2912        buf[i] = tmp[pos - 1 - i];
2913        i += 1;
2914    }
2915    len
2916}
2917
2918/// Format bytes as hex, truncated to fit buffer. Shows first 8 + "..." if long.
2919fn format_hex_truncated(bytes: &[u8], buf: &mut [u8]) -> usize {
2920    const HEX: &[u8; 16] = b"0123456789abcdef";
2921    let max_bytes = if bytes.len() > 8 { 8 } else { bytes.len() };
2922    let mut pos = 0;
2923    // "0x" prefix
2924    if buf.len() >= 2 {
2925        buf[0] = b'0';
2926        buf[1] = b'x';
2927        pos = 2;
2928    }
2929    let mut i = 0;
2930    while i < max_bytes && pos + 1 < buf.len() {
2931        buf[pos] = HEX[(bytes[i] >> 4) as usize];
2932        buf[pos + 1] = HEX[(bytes[i] & 0xf) as usize];
2933        pos += 2;
2934        i += 1;
2935    }
2936    if bytes.len() > 8 && pos + 3 <= buf.len() {
2937        buf[pos] = b'.';
2938        buf[pos + 1] = b'.';
2939        buf[pos + 2] = b'.';
2940        pos += 3;
2941    }
2942    pos
2943}
2944
2945// ---------------------------------------------------------------------------
2946// On-Chain Schema Pointer
2947// ---------------------------------------------------------------------------
2948
2949/// On-chain account that points to a Hopper program's schema.
2950///
2951/// Stored at PDA `["hopper-schema", program_id]`. Contains hashes of
2952/// the manifest, IDL, and Codama projection, plus a URI to fetch the
2953/// full manifest. See `docs/ONCHAIN_SCHEMA_PUBLICATION.md`.
2954///
2955/// ## Wire layout (294 bytes payload + 16 bytes header = 310 bytes)
2956///
2957/// ```text
2958/// [0..16]    Hopper header (disc=255, ver=1)
2959/// [16..18]   schema_version   u16 LE
2960/// [18..20]   pointer_flags    u16 LE
2961/// [20..52]   manifest_hash    [u8; 32]
2962/// [52..84]   idl_hash         [u8; 32]
2963/// [84..116]  codama_hash      [u8; 32]
2964/// [116..118] uri_len          u16 LE
2965/// [118..310] uri              [u8; 192]
2966/// ```
2967#[repr(C)]
2968#[derive(Clone, Copy)]
2969pub struct HopperSchemaPointer {
2970    /// Schema format version (currently 1).
2971    pub schema_version: u16,
2972    /// Feature flags (HAS_MANIFEST, HAS_IDL, HAS_CODAMA, HAS_URI, ...).
2973    pub pointer_flags: u16,
2974    /// SHA-256 of the manifest JSON.
2975    pub manifest_hash: [u8; 32],
2976    /// SHA-256 of the IDL JSON.
2977    pub idl_hash: [u8; 32],
2978    /// SHA-256 of the Codama projection JSON.
2979    pub codama_hash: [u8; 32],
2980    /// Length of the URI string (0..192).
2981    pub uri_len: u16,
2982    /// UTF-8 URI pointing to the manifest (padded with zeros).
2983    pub uri: [u8; 192],
2984}
2985
2986impl HopperSchemaPointer {
2987    /// Reserved discriminator for schema pointer accounts.
2988    pub const DISC: u8 = 255;
2989
2990    /// Total payload size (excluding Hopper header).
2991    pub const PAYLOAD_LEN: usize = 2 + 2 + 32 + 32 + 32 + 2 + 192; // 294
2992
2993    /// Total account size including Hopper header.
2994    pub const ACCOUNT_LEN: usize = HEADER_LEN + Self::PAYLOAD_LEN; // 310
2995
2996    /// PDA seed prefix.
2997    pub const PDA_SEED: &'static [u8] = b"hopper-schema";
2998
2999    // Flag bits
3000    pub const FLAG_HAS_MANIFEST: u16 = 0x0001;
3001    pub const FLAG_HAS_IDL: u16 = 0x0002;
3002    pub const FLAG_HAS_CODAMA: u16 = 0x0004;
3003    pub const FLAG_HAS_URI: u16 = 0x0008;
3004    pub const FLAG_URI_IS_IPFS: u16 = 0x0010;
3005    pub const FLAG_URI_IS_ARWEAVE: u16 = 0x0020;
3006
3007    /// Get the URI as a string slice.
3008    pub fn uri_str(&self) -> &str {
3009        let len = (self.uri_len as usize).min(192);
3010        // SAFETY: We validate UTF-8 at read time.
3011        core::str::from_utf8(&self.uri[..len]).unwrap_or("")
3012    }
3013
3014    /// Check if a specific flag is set.
3015    #[inline(always)]
3016    pub fn has_flag(&self, flag: u16) -> bool {
3017        self.pointer_flags & flag != 0
3018    }
3019}
3020
3021// ---------------------------------------------------------------------------
3022// Semantic Lint Engine -- catch suspicious state patterns at build time
3023// ---------------------------------------------------------------------------
3024
3025/// A semantic lint warning produced by analyzing field intents, mutation
3026/// classes, and policy against a layout manifest.
3027#[derive(Clone, Copy, Debug)]
3028pub struct SemanticLint {
3029    /// Lint severity.
3030    pub severity: LintSeverity,
3031    /// Short machine-readable code.
3032    pub code: &'static str,
3033    /// Human-readable warning message.
3034    pub message: &'static str,
3035    /// Field name involved (empty if layout-wide).
3036    pub field: &'static str,
3037}
3038
3039/// Lint severity.
3040#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3041#[repr(u8)]
3042pub enum LintSeverity {
3043    /// Informational note.
3044    Info = 0,
3045    /// Potential issue worth reviewing.
3046    Warning = 1,
3047    /// Likely correctness or security issue.
3048    Error = 2,
3049}
3050
3051impl LintSeverity {
3052    /// Human-readable label.
3053    pub const fn name(self) -> &'static str {
3054        match self {
3055            Self::Info => "info",
3056            Self::Warning => "warning",
3057            Self::Error => "error",
3058        }
3059    }
3060}
3061
3062/// Run semantic lints against a layout manifest and its behavior.
3063///
3064/// Returns the number of lint warnings produced (up to N).
3065pub fn lint_layout<const N: usize>(
3066    manifest: &LayoutManifest,
3067    behavior: &LayoutBehavior,
3068) -> (usize, [SemanticLint; N]) {
3069    let mut lints = [SemanticLint {
3070        severity: LintSeverity::Info,
3071        code: "",
3072        message: "",
3073        field: "",
3074    }; N];
3075    let mut count = 0usize;
3076
3077    let mut i = 0;
3078    while i < manifest.field_count {
3079        let field = &manifest.fields[i];
3080
3081        // Authority field mutated without signer requirement
3082        if field.intent.is_authority_sensitive()
3083            && behavior.mutation_class.is_mutating()
3084            && !behavior.requires_signer
3085        {
3086            if count < N {
3087                lints[count] = SemanticLint {
3088                    severity: LintSeverity::Error,
3089                    code: "E001",
3090                    message:
3091                        "Authority-sensitive field in mutable layout without signer requirement",
3092                    field: field.name,
3093                };
3094                count += 1;
3095            }
3096        }
3097
3098        // Financial field mutated without financial mutation class
3099        if field.intent.is_monetary()
3100            && behavior.mutation_class.is_mutating()
3101            && !matches!(behavior.mutation_class, MutationClass::Financial)
3102        {
3103            if count < N {
3104                lints[count] = SemanticLint {
3105                    severity: LintSeverity::Warning,
3106                    code: "W001",
3107                    message: "Monetary field in layout without financial mutation class",
3108                    field: field.name,
3109                };
3110                count += 1;
3111            }
3112        }
3113
3114        // Init-only field (PDA seed, bump) in a layout that isn't read-only
3115        if field.intent.is_init_only()
3116            && behavior.mutation_class.is_mutating()
3117            && !matches!(behavior.mutation_class, MutationClass::AppendOnly)
3118        {
3119            if count < N {
3120                lints[count] = SemanticLint {
3121                    severity: LintSeverity::Warning,
3122                    code: "W002",
3123                    message: "Init-only field (PDA seed or bump) in mutable layout. Consider making read-only or append-only.",
3124                    field: field.name,
3125                };
3126                count += 1;
3127            }
3128        }
3129
3130        i += 1;
3131    }
3132
3133    // Layout-wide lints
3134
3135    // Mutable layout with no signer
3136    if behavior.mutation_class.is_mutating() && !behavior.requires_signer {
3137        if count < N {
3138            lints[count] = SemanticLint {
3139                severity: LintSeverity::Warning,
3140                code: "W003",
3141                message: "Mutable layout does not require signer. Verify this is intentional.",
3142                field: "",
3143            };
3144            count += 1;
3145        }
3146    }
3147
3148    // Financial impact without balance tracking
3149    if behavior.affects_balance {
3150        let mut has_balance = false;
3151        let mut j = 0;
3152        while j < manifest.field_count {
3153            if manifest.fields[j].intent.is_monetary() {
3154                has_balance = true;
3155            }
3156            j += 1;
3157        }
3158        if !has_balance && count < N {
3159            lints[count] = SemanticLint {
3160                severity: LintSeverity::Warning,
3161                code: "W004",
3162                message: "Layout behavior declares affects_balance but no monetary fields found",
3163                field: "",
3164            };
3165            count += 1;
3166        }
3167    }
3168
3169    (count, lints)
3170}
3171
3172/// Run policy-aware semantic lints.
3173///
3174/// Complements `lint_layout` with cross-cutting checks between layout
3175/// behavior and policy classification. Call after `lint_layout` and merge
3176/// the results.
3177#[cfg(feature = "policy")]
3178pub fn lint_policy<const N: usize>(
3179    behavior: &LayoutBehavior,
3180    policy: hopper_core::policy::PolicyClass,
3181) -> (usize, [SemanticLint; N]) {
3182    let mut lints = [SemanticLint {
3183        severity: LintSeverity::Info,
3184        code: "",
3185        message: "",
3186        field: "",
3187    }; N];
3188    let mut count = 0usize;
3189
3190    // Financial mutation class without financial policy class
3191    if matches!(behavior.mutation_class, MutationClass::Financial)
3192        && !matches!(policy, hopper_core::policy::PolicyClass::Financial)
3193    {
3194        if count < N {
3195            lints[count] = SemanticLint {
3196                severity: LintSeverity::Warning,
3197                code: "W005",
3198                message: "Financial mutation class but policy class is not Financial",
3199                field: "",
3200            };
3201            count += 1;
3202        }
3203    }
3204
3205    // Financial policy class without financial mutation class
3206    if matches!(policy, hopper_core::policy::PolicyClass::Financial)
3207        && !matches!(behavior.mutation_class, MutationClass::Financial)
3208    {
3209        if count < N {
3210            lints[count] = SemanticLint {
3211                severity: LintSeverity::Warning,
3212                code: "W006",
3213                message: "Financial policy class but mutation class is not Financial",
3214                field: "",
3215            };
3216            count += 1;
3217        }
3218    }
3219
3220    (count, lints)
3221}
3222
3223impl fmt::Display for SemanticLint {
3224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3225        write!(
3226            f,
3227            "[{}] {}: {}",
3228            self.severity.name(),
3229            self.code,
3230            self.message
3231        )?;
3232        if !self.field.is_empty() {
3233            write!(f, " (field: {})", self.field)?;
3234        }
3235        Ok(())
3236    }
3237}
3238
3239// ---------------------------------------------------------------------------
3240// Protocol Operating Profile -- machine-readable program behavior map
3241// ---------------------------------------------------------------------------
3242
3243/// A machine-readable summary of a program's operational characteristics.
3244///
3245/// Generated from a `ProgramManifest` to give auditors, dashboards, explorers,
3246/// and operator tools a meaningful map of how the program behaves.
3247pub struct OperatingProfile {
3248    /// Fields classified as financial (balance, supply, basis_points).
3249    pub financial_fields: [&'static str; 16],
3250    /// Number of valid financial field entries.
3251    pub financial_count: u8,
3252    /// Fields classified as authority surfaces (authority, owner, delegate).
3253    pub authority_surfaces: [&'static str; 16],
3254    /// Number of valid authority surface entries.
3255    pub authority_count: u8,
3256    /// Segments that are append-only.
3257    pub append_only_segments: [&'static str; 8],
3258    /// Number of valid append-only segment entries.
3259    pub append_only_count: u8,
3260    /// Segments sensitive to migration.
3261    pub migration_sensitive: [&'static str; 8],
3262    /// Number of valid migration-sensitive entries.
3263    pub migration_sensitive_count: u8,
3264    /// Layout stability grades per layout.
3265    pub stability_grades: [(&'static str, LayoutStabilityGrade); 8],
3266    /// Number of valid stability grade entries.
3267    pub stability_count: u8,
3268    /// Whether the program has any financial operations.
3269    pub has_financial_ops: bool,
3270    /// Whether the program has any CPI-invoking instructions.
3271    pub has_cpi_ops: bool,
3272    /// Whether the program has migration paths defined.
3273    pub has_migration_paths: bool,
3274    /// Whether the program emits receipts.
3275    pub has_receipts: bool,
3276}
3277
3278impl OperatingProfile {
3279    /// Generate an operating profile from a program manifest.
3280    pub fn from_manifest(manifest: &ProgramManifest) -> Self {
3281        let mut profile = Self {
3282            financial_fields: [""; 16],
3283            financial_count: 0,
3284            authority_surfaces: [""; 16],
3285            authority_count: 0,
3286            append_only_segments: [""; 8],
3287            append_only_count: 0,
3288            migration_sensitive: [""; 8],
3289            migration_sensitive_count: 0,
3290            stability_grades: [("", LayoutStabilityGrade::Stable); 8],
3291            stability_count: 0,
3292            has_financial_ops: false,
3293            has_cpi_ops: false,
3294            has_migration_paths: !manifest.compatibility_pairs.is_empty(),
3295            has_receipts: false,
3296        };
3297
3298        // Scan layouts for field intents
3299        let mut li = 0;
3300        while li < manifest.layouts.len() {
3301            let layout = &manifest.layouts[li];
3302
3303            // Stability grade
3304            if (profile.stability_count as usize) < 8 {
3305                profile.stability_grades[profile.stability_count as usize] =
3306                    (layout.name, LayoutStabilityGrade::compute(layout));
3307                profile.stability_count += 1;
3308            }
3309
3310            let mut fi = 0;
3311            while fi < layout.field_count {
3312                let field = &layout.fields[fi];
3313                if field.intent.is_monetary() && (profile.financial_count as usize) < 16 {
3314                    profile.financial_fields[profile.financial_count as usize] = field.name;
3315                    profile.financial_count += 1;
3316                }
3317                if field.intent.is_authority_sensitive() && (profile.authority_count as usize) < 16
3318                {
3319                    profile.authority_surfaces[profile.authority_count as usize] = field.name;
3320                    profile.authority_count += 1;
3321                }
3322                fi += 1;
3323            }
3324            li += 1;
3325        }
3326
3327        // Scan layout metadata for segment info
3328        let mut mi = 0;
3329        while mi < manifest.layout_metadata.len() {
3330            let meta = &manifest.layout_metadata[mi];
3331            let mut si = 0;
3332            while si < meta.segment_roles.len() {
3333                let role_name = meta.segment_roles[si];
3334                if (const_str_eq(role_name, "Journal") || const_str_eq(role_name, "Audit"))
3335                    && (profile.append_only_count as usize) < 8
3336                {
3337                    profile.append_only_segments[profile.append_only_count as usize] = role_name;
3338                    profile.append_only_count += 1;
3339                }
3340                if const_str_eq(role_name, "Core")
3341                    && (profile.migration_sensitive_count as usize) < 8
3342                {
3343                    profile.migration_sensitive[profile.migration_sensitive_count as usize] =
3344                        meta.name;
3345                    profile.migration_sensitive_count += 1;
3346                }
3347                si += 1;
3348            }
3349            mi += 1;
3350        }
3351
3352        // Scan instructions for capabilities
3353        let mut ii = 0;
3354        while ii < manifest.instructions.len() {
3355            let ix = &manifest.instructions[ii];
3356            if ix.receipt_expected {
3357                profile.has_receipts = true;
3358            }
3359            let mut ci = 0;
3360            while ci < ix.capabilities.len() {
3361                if const_str_eq(ix.capabilities[ci], "MutatesTreasury") {
3362                    profile.has_financial_ops = true;
3363                }
3364                if const_str_eq(ix.capabilities[ci], "ExternalCall") {
3365                    profile.has_cpi_ops = true;
3366                }
3367                ci += 1;
3368            }
3369            ii += 1;
3370        }
3371
3372        profile
3373    }
3374}
3375
3376impl fmt::Display for OperatingProfile {
3377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3378        writeln!(f, "Operating Profile:")?;
3379
3380        if self.financial_count > 0 {
3381            write!(f, "  Financial fields:")?;
3382            let mut i = 0;
3383            while i < self.financial_count as usize {
3384                write!(f, " {}", self.financial_fields[i])?;
3385                i += 1;
3386            }
3387            writeln!(f)?;
3388        }
3389
3390        if self.authority_count > 0 {
3391            write!(f, "  Authority surfaces:")?;
3392            let mut i = 0;
3393            while i < self.authority_count as usize {
3394                write!(f, " {}", self.authority_surfaces[i])?;
3395                i += 1;
3396            }
3397            writeln!(f)?;
3398        }
3399
3400        if self.append_only_count > 0 {
3401            write!(f, "  Append-only segments:")?;
3402            let mut i = 0;
3403            while i < self.append_only_count as usize {
3404                write!(f, " {}", self.append_only_segments[i])?;
3405                i += 1;
3406            }
3407            writeln!(f)?;
3408        }
3409
3410        if self.stability_count > 0 {
3411            writeln!(f, "  Stability grades:")?;
3412            let mut i = 0;
3413            while i < self.stability_count as usize {
3414                let (name, grade) = self.stability_grades[i];
3415                writeln!(f, "    {}: {}", name, grade.name())?;
3416                i += 1;
3417            }
3418        }
3419
3420        write!(f, "  Features:")?;
3421        if self.has_financial_ops {
3422            write!(f, " financial")?;
3423        }
3424        if self.has_cpi_ops {
3425            write!(f, " cpi")?;
3426        }
3427        if self.has_migration_paths {
3428            write!(f, " migration")?;
3429        }
3430        if self.has_receipts {
3431            write!(f, " receipts")?;
3432        }
3433        writeln!(f)?;
3434
3435        Ok(())
3436    }
3437}
3438
3439// ---------------------------------------------------------------------------
3440// Expanded IDL -- policies, compat, receipts, segments, field intents
3441// ---------------------------------------------------------------------------
3442
3443/// Extended IDL with full Hopper-native sections.
3444///
3445/// This is the **complete** program schema for Hopper-aware tools. It extends
3446/// `ProgramIdl` with policies, compatibility, receipts, segments, field intents,
3447/// and an operating profile.
3448pub struct HopperIdl {
3449    /// Base IDL (instructions, accounts, events).
3450    pub base: ProgramIdl,
3451    /// Policy descriptors.
3452    pub policies: &'static [PolicyDescriptor],
3453    /// Known upgrade paths.
3454    pub compatibility: &'static [CompatibilityPair],
3455    /// Receipt profiles keyed by name.
3456    pub receipt_profiles: &'static [ReceiptProfile],
3457    /// Segment metadata.
3458    pub segment_metadata: &'static [IdlSegmentDescriptor],
3459    /// Context (instruction account struct) descriptors.
3460    pub contexts: &'static [crate::accounts::ContextDescriptor],
3461}
3462
3463/// A receipt profile describing what a receipt for a given mutation type looks like.
3464#[derive(Clone, Copy)]
3465pub struct ReceiptProfile {
3466    /// Profile name (e.g. "default-mutation", "treasury-write").
3467    pub name: &'static str,
3468    /// Expected phase.
3469    pub expected_phase: &'static str,
3470    /// Whether balance changes are expected.
3471    pub expects_balance_change: bool,
3472    /// Whether authority changes are expected.
3473    pub expects_authority_change: bool,
3474    /// Whether journal appends are expected.
3475    pub expects_journal_append: bool,
3476    /// Minimum expected changed fields.
3477    pub min_changed_fields: u8,
3478}
3479
3480impl fmt::Display for ReceiptProfile {
3481    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3482        write!(f, "{}(phase={}", self.name, self.expected_phase)?;
3483        if self.expects_balance_change {
3484            write!(f, " balance")?;
3485        }
3486        if self.expects_authority_change {
3487            write!(f, " authority")?;
3488        }
3489        if self.expects_journal_append {
3490            write!(f, " journal")?;
3491        }
3492        if self.min_changed_fields > 0 {
3493            write!(f, " min_fields={}", self.min_changed_fields)?;
3494        }
3495        write!(f, ")")
3496    }
3497}
3498
3499/// Segment metadata for inclusion in the IDL.
3500#[derive(Clone, Copy)]
3501pub struct IdlSegmentDescriptor {
3502    /// Segment name.
3503    pub name: &'static str,
3504    /// Role name (Core, Extension, Journal, etc.).
3505    pub role: &'static str,
3506    /// Whether the segment is append-only.
3507    pub append_only: bool,
3508    /// Whether the segment is rebuildable from other data.
3509    pub rebuildable: bool,
3510    /// Whether the segment must survive migration.
3511    pub must_preserve: bool,
3512}
3513
3514impl fmt::Display for IdlSegmentDescriptor {
3515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3516        write!(f, "{}(role={}", self.name, self.role)?;
3517        if self.append_only {
3518            write!(f, " append-only")?;
3519        }
3520        if self.rebuildable {
3521            write!(f, " rebuildable")?;
3522        }
3523        if self.must_preserve {
3524            write!(f, " must-preserve")?;
3525        }
3526        write!(f, ")")
3527    }
3528}
3529
3530impl HopperIdl {
3531    /// Create an empty extended IDL.
3532    pub const fn empty() -> Self {
3533        Self {
3534            base: ProgramIdl::empty(),
3535            policies: &[],
3536            compatibility: &[],
3537            receipt_profiles: &[],
3538            segment_metadata: &[],
3539            contexts: &[],
3540        }
3541    }
3542}
3543
3544impl fmt::Display for HopperIdl {
3545    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3546        write!(f, "{}", self.base)?;
3547
3548        if !self.policies.is_empty() {
3549            writeln!(f)?;
3550            writeln!(f, "Policies ({}):", self.policies.len())?;
3551            for p in self.policies.iter() {
3552                write!(f, "  {:24}", p.name)?;
3553                for (j, r) in p.requirements.iter().enumerate() {
3554                    if j > 0 {
3555                        write!(f, " + ")?;
3556                    }
3557                    write!(f, "{}", r)?;
3558                }
3559                writeln!(f)?;
3560            }
3561        }
3562
3563        if !self.compatibility.is_empty() {
3564            writeln!(f)?;
3565            writeln!(f, "Compatibility ({}):", self.compatibility.len())?;
3566            for cp in self.compatibility.iter() {
3567                writeln!(
3568                    f,
3569                    "  {} v{} -> {} v{}  {}",
3570                    cp.from_layout, cp.from_version, cp.to_layout, cp.to_version, cp.policy,
3571                )?;
3572            }
3573        }
3574
3575        if !self.receipt_profiles.is_empty() {
3576            writeln!(f)?;
3577            writeln!(f, "Receipt Profiles ({}):", self.receipt_profiles.len())?;
3578            for rp in self.receipt_profiles.iter() {
3579                writeln!(
3580                    f,
3581                    "  {:24} phase={} balance={} authority={} journal={}",
3582                    rp.name,
3583                    rp.expected_phase,
3584                    rp.expects_balance_change,
3585                    rp.expects_authority_change,
3586                    rp.expects_journal_append,
3587                )?;
3588            }
3589        }
3590
3591        if !self.segment_metadata.is_empty() {
3592            writeln!(f)?;
3593            writeln!(f, "Segments ({}):", self.segment_metadata.len())?;
3594            for s in self.segment_metadata.iter() {
3595                write!(f, "  {:16} role={}", s.name, s.role)?;
3596                if s.append_only {
3597                    write!(f, " append-only")?;
3598                }
3599                if s.rebuildable {
3600                    write!(f, " rebuildable")?;
3601                }
3602                if s.must_preserve {
3603                    write!(f, " must-preserve")?;
3604                }
3605                writeln!(f)?;
3606            }
3607        }
3608
3609        if !self.contexts.is_empty() {
3610            writeln!(f)?;
3611            writeln!(f, "Contexts ({}):", self.contexts.len())?;
3612            for ctx in self.contexts.iter() {
3613                write!(f, "  {}", ctx)?;
3614            }
3615        }
3616
3617        Ok(())
3618    }
3619}
3620
3621// ═══════════════════════════════════════════════════════════════════════
3622//  SchemaExport -- bridge from LayoutContract + FieldMap to schema
3623// ═══════════════════════════════════════════════════════════════════════
3624
3625/// Minimal manager-readable metadata for a Hopper layout.
3626#[derive(Clone, Copy, Debug)]
3627pub struct ManagerMetadata {
3628    /// Runtime header/layout identity.
3629    pub layout: LayoutInfo,
3630    /// Field-level wire map.
3631    pub fields: &'static [FieldInfo],
3632}
3633
3634/// Unified schema payload tying runtime identity to rich manifest metadata.
3635#[derive(Clone, Copy, Debug)]
3636pub struct SchemaBundle {
3637    pub manager: ManagerMetadata,
3638    pub manifest: LayoutManifest,
3639}
3640
3641/// Trait for layout types that can export their full schema information.
3642///
3643/// This creates a single source of truth linking runtime layout contracts
3644/// (discriminator, version, layout_id, size) with field-level metadata
3645/// (names, offsets, sizes). The exported information powers:
3646///
3647/// - Manager metadata (on-chain or off-chain program inspection)
3648/// - IDL generation (Codama, Hopper IDL, client SDKs)
3649/// - Schema diff and migration safety checking
3650/// - Client code generation with typed field access
3651///
3652/// Implementors provide both a runtime-facing view (`layout_info`, `field_map`)
3653/// and a higher-level schema manifest for richer tooling.
3654pub trait SchemaExport: LayoutContract {
3655    /// Runtime header/layout identity.
3656    #[inline(always)]
3657    fn layout_info() -> LayoutInfo {
3658        <Self as LayoutContract>::layout_info_static()
3659    }
3660
3661    /// Field-level wire map used by manager and client tooling.
3662    #[inline(always)]
3663    fn field_map() -> &'static [FieldInfo] {
3664        <Self as LayoutContract>::fields()
3665    }
3666
3667    /// Combined runtime metadata payload for manager-facing inspection.
3668    #[inline(always)]
3669    fn manager_metadata() -> ManagerMetadata {
3670        ManagerMetadata {
3671            layout: Self::layout_info(),
3672            fields: Self::field_map(),
3673        }
3674    }
3675
3676    /// Combined runtime and manifest metadata payload.
3677    #[inline(always)]
3678    fn schema_bundle() -> SchemaBundle {
3679        SchemaBundle {
3680            manager: Self::manager_metadata(),
3681            manifest: Self::layout_manifest(),
3682        }
3683    }
3684
3685    /// Rich schema manifest for diffing, linting, and client generation.
3686    fn layout_manifest() -> LayoutManifest;
3687}
3688
3689/// Bridge from a live `AccountView` to the schema bundle of a concrete layout type.
3690pub trait AccountSchemaExt {
3691    /// Return manager metadata if the account header matches `T`.
3692    fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata>;
3693
3694    /// Return the full schema bundle if the account header matches `T`.
3695    fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle>;
3696}
3697
3698impl AccountSchemaExt for AccountView {
3699    #[inline]
3700    fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata> {
3701        let info = self.layout_info()?;
3702        if info.matches::<T>() {
3703            Some(T::manager_metadata())
3704        } else {
3705            None
3706        }
3707    }
3708
3709    #[inline]
3710    fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle> {
3711        let info = self.layout_info()?;
3712        if info.matches::<T>() {
3713            Some(T::schema_bundle())
3714        } else {
3715            None
3716        }
3717    }
3718}
3719
3720// -- Migration Plan Tests --
3721
3722#[cfg(test)]
3723mod tests {
3724    use super::*;
3725
3726    const V1_FIELDS: &[FieldDescriptor] = &[
3727        FieldDescriptor {
3728            name: "authority",
3729            canonical_type: "[u8;32]",
3730            size: 32,
3731            offset: 16,
3732            intent: FieldIntent::Custom,
3733        },
3734        FieldDescriptor {
3735            name: "balance",
3736            canonical_type: "WireU64",
3737            size: 8,
3738            offset: 48,
3739            intent: FieldIntent::Custom,
3740        },
3741    ];
3742
3743    const V2_FIELDS: &[FieldDescriptor] = &[
3744        FieldDescriptor {
3745            name: "authority",
3746            canonical_type: "[u8;32]",
3747            size: 32,
3748            offset: 16,
3749            intent: FieldIntent::Custom,
3750        },
3751        FieldDescriptor {
3752            name: "balance",
3753            canonical_type: "WireU64",
3754            size: 8,
3755            offset: 48,
3756            intent: FieldIntent::Custom,
3757        },
3758        FieldDescriptor {
3759            name: "bump",
3760            canonical_type: "u8",
3761            size: 1,
3762            offset: 56,
3763            intent: FieldIntent::Custom,
3764        },
3765    ];
3766
3767    const V1_MANIFEST: LayoutManifest = LayoutManifest {
3768        name: "Vault",
3769        disc: 1,
3770        version: 1,
3771        layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
3772        total_size: 56,
3773        field_count: 2,
3774        fields: V1_FIELDS,
3775    };
3776
3777    const V2_MANIFEST: LayoutManifest = LayoutManifest {
3778        name: "Vault",
3779        disc: 1,
3780        version: 2,
3781        layout_id: [10, 20, 30, 40, 50, 60, 70, 80],
3782        total_size: 57,
3783        field_count: 3,
3784        fields: V2_FIELDS,
3785    };
3786
3787    #[test]
3788    fn no_op_for_identical() {
3789        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V1_MANIFEST);
3790        assert_eq!(plan.policy, MigrationPolicy::NoOp);
3791        assert_eq!(plan.step_count, 0);
3792    }
3793
3794    #[test]
3795    fn append_only_migration() {
3796        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V2_MANIFEST);
3797        assert_eq!(plan.policy, MigrationPolicy::AppendOnly);
3798        assert!(plan.step_count >= 3); // copy + realloc + zero-init + header
3799        assert_eq!(plan.old_size, 56);
3800        assert_eq!(plan.new_size, 57);
3801        assert!(plan.copy_bytes > 0);
3802        assert!(plan.zero_bytes > 0);
3803
3804        // First step should be CopyPrefix
3805        assert_eq!(plan.steps[0].action, MigrationAction::CopyPrefix);
3806        // Should have a ZeroInit for the "bump" field
3807        let mut found_zero = false;
3808        let mut i = 0;
3809        while i < plan.step_count {
3810            if plan.steps[i].action == MigrationAction::ZeroInit {
3811                assert_eq!(plan.steps[i].field, "bump");
3812                assert_eq!(plan.steps[i].size, 1);
3813                found_zero = true;
3814            }
3815            i += 1;
3816        }
3817        assert!(found_zero);
3818    }
3819
3820    #[test]
3821    fn incompatible_different_disc() {
3822        let other = LayoutManifest {
3823            disc: 99,
3824            ..V2_MANIFEST
3825        };
3826        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &other);
3827        assert_eq!(plan.policy, MigrationPolicy::Incompatible);
3828    }
3829
3830    #[test]
3831    fn breaking_change_detected() {
3832        let changed_fields: &[FieldDescriptor] = &[
3833            FieldDescriptor {
3834                name: "authority",
3835                canonical_type: "WireU64",
3836                size: 8,
3837                offset: 16,
3838                intent: FieldIntent::Custom,
3839            },
3840            FieldDescriptor {
3841                name: "balance",
3842                canonical_type: "WireU64",
3843                size: 8,
3844                offset: 24,
3845                intent: FieldIntent::Custom,
3846            },
3847        ];
3848        let breaking = LayoutManifest {
3849            name: "Vault",
3850            disc: 1,
3851            version: 2,
3852            layout_id: [99; 8],
3853            total_size: 32,
3854            field_count: 2,
3855            fields: changed_fields,
3856        };
3857        let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &breaking);
3858        assert_eq!(plan.policy, MigrationPolicy::RequiresMigration);
3859    }
3860
3861    // -----------------------------------------------------------------------
3862    // CompatibilityVerdict tests
3863    // -----------------------------------------------------------------------
3864
3865    #[test]
3866    fn verdict_identical() {
3867        let v = CompatibilityVerdict::between(&V1_MANIFEST, &V1_MANIFEST);
3868        assert_eq!(v, CompatibilityVerdict::Identical);
3869        assert!(v.is_safe());
3870        assert!(v.is_backward_readable());
3871        assert!(!v.requires_migration());
3872    }
3873
3874    #[test]
3875    fn verdict_append_safe() {
3876        let v = CompatibilityVerdict::between(&V1_MANIFEST, &V2_MANIFEST);
3877        assert_eq!(v, CompatibilityVerdict::AppendSafe);
3878        assert!(v.is_safe());
3879        assert!(v.is_backward_readable());
3880        assert!(!v.requires_migration());
3881    }
3882
3883    #[test]
3884    fn verdict_migration_required() {
3885        let changed_fields: &[FieldDescriptor] = &[
3886            FieldDescriptor {
3887                name: "authority",
3888                canonical_type: "WireU64",
3889                size: 8,
3890                offset: 16,
3891                intent: FieldIntent::Custom,
3892            },
3893            FieldDescriptor {
3894                name: "balance",
3895                canonical_type: "WireU64",
3896                size: 8,
3897                offset: 24,
3898                intent: FieldIntent::Custom,
3899            },
3900        ];
3901        let breaking = LayoutManifest {
3902            name: "Vault",
3903            disc: 1,
3904            version: 2,
3905            layout_id: [99; 8],
3906            total_size: 32,
3907            field_count: 2,
3908            fields: changed_fields,
3909        };
3910        let v = CompatibilityVerdict::between(&V1_MANIFEST, &breaking);
3911        assert_eq!(v, CompatibilityVerdict::MigrationRequired);
3912        assert!(!v.is_safe());
3913        assert!(!v.is_backward_readable());
3914        assert!(v.requires_migration());
3915    }
3916
3917    #[test]
3918    fn verdict_wire_compatible() {
3919        // Same disc, same fields (count + prefix), same total_size, but different layout_id.
3920        let semantic_variant = LayoutManifest {
3921            layout_id: [77; 8], // different layout_id
3922            ..V1_MANIFEST       // same disc, fields, total_size
3923        };
3924        let v = CompatibilityVerdict::between(&V1_MANIFEST, &semantic_variant);
3925        assert_eq!(v, CompatibilityVerdict::WireCompatible);
3926        assert!(v.is_safe());
3927        assert!(v.is_backward_readable());
3928        assert!(!v.requires_migration());
3929    }
3930
3931    #[test]
3932    fn verdict_incompatible() {
3933        let other = LayoutManifest {
3934            disc: 99,
3935            ..V2_MANIFEST
3936        };
3937        let v = CompatibilityVerdict::between(&V1_MANIFEST, &other);
3938        assert_eq!(v, CompatibilityVerdict::Incompatible);
3939        assert!(!v.is_safe());
3940    }
3941
3942    #[test]
3943    fn verdict_names() {
3944        assert_eq!(CompatibilityVerdict::Identical.name(), "identical");
3945        assert_eq!(
3946            CompatibilityVerdict::WireCompatible.name(),
3947            "wire-compatible"
3948        );
3949        assert_eq!(CompatibilityVerdict::AppendSafe.name(), "append-safe");
3950        assert_eq!(
3951            CompatibilityVerdict::MigrationRequired.name(),
3952            "migration-required"
3953        );
3954        assert_eq!(CompatibilityVerdict::Incompatible.name(), "incompatible");
3955    }
3956
3957    #[test]
3958    fn segment_advice_core_must_preserve() {
3959        let segs = [DecodedSegment {
3960            id: [1, 0, 0, 0],
3961            offset: 36,
3962            size: 100,
3963            flags: 0x0000, // Core = upper 4 bits 0
3964            version: 1,
3965        }];
3966        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
3967        assert_eq!(report.count, 1);
3968        assert_eq!(report.advice[0].role, SegmentRoleHint::Core);
3969        assert!(report.advice[0].must_preserve);
3970        assert!(!report.advice[0].clearable);
3971        assert_eq!(report.preserve_bytes, 100);
3972    }
3973
3974    #[test]
3975    fn segment_advice_journal_clearable() {
3976        let segs = [DecodedSegment {
3977            id: [2, 0, 0, 0],
3978            offset: 136,
3979            size: 256,
3980            flags: 0x2000, // Journal = upper 4 bits 2
3981            version: 1,
3982        }];
3983        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
3984        assert_eq!(report.advice[0].role, SegmentRoleHint::Journal);
3985        assert!(report.advice[0].clearable);
3986        assert!(report.advice[0].append_only);
3987        assert!(!report.advice[0].must_preserve);
3988        assert_eq!(report.clearable_bytes, 256);
3989    }
3990
3991    #[test]
3992    fn segment_advice_cache_rebuildable() {
3993        let segs = [DecodedSegment {
3994            id: [3, 0, 0, 0],
3995            offset: 400,
3996            size: 64,
3997            flags: 0x4000, // Cache = upper 4 bits 4
3998            version: 1,
3999        }];
4000        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4001        assert_eq!(report.advice[0].role, SegmentRoleHint::Cache);
4002        assert!(report.advice[0].clearable);
4003        assert!(report.advice[0].rebuildable);
4004    }
4005
4006    #[test]
4007    fn segment_advice_audit_immutable() {
4008        let segs = [DecodedSegment {
4009            id: [4, 0, 0, 0],
4010            offset: 200,
4011            size: 32,
4012            flags: 0x5000, // Audit = upper 4 bits 5
4013            version: 1,
4014        }];
4015        let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4016        assert_eq!(report.advice[0].role, SegmentRoleHint::Audit);
4017        assert!(report.advice[0].must_preserve);
4018        assert!(report.advice[0].immutable);
4019        assert!(report.advice[0].append_only);
4020        assert!(!report.advice[0].clearable);
4021    }
4022
4023    #[test]
4024    fn segment_advice_mixed_report() {
4025        let segs = [
4026            DecodedSegment {
4027                id: [1, 0, 0, 0],
4028                offset: 36,
4029                size: 100,
4030                flags: 0x0000,
4031                version: 1,
4032            },
4033            DecodedSegment {
4034                id: [2, 0, 0, 0],
4035                offset: 136,
4036                size: 200,
4037                flags: 0x2000,
4038                version: 1,
4039            },
4040            DecodedSegment {
4041                id: [3, 0, 0, 0],
4042                offset: 336,
4043                size: 64,
4044                flags: 0x4000,
4045                version: 1,
4046            },
4047        ];
4048        let report = SegmentMigrationReport::<8>::analyze(&segs, 3);
4049        assert_eq!(report.count, 3);
4050        assert_eq!(report.must_preserve_count(), 1);
4051        assert_eq!(report.clearable_count(), 2);
4052        assert_eq!(report.preserve_bytes, 100);
4053        assert_eq!(report.clearable_bytes, 264);
4054        assert_eq!(report.rebuildable_bytes, 64);
4055    }
4056
4057    #[test]
4058    fn segment_role_hint_requires_migration_copy() {
4059        assert!(SegmentRoleHint::Core.requires_migration_copy());
4060        assert!(SegmentRoleHint::Audit.requires_migration_copy());
4061        assert!(!SegmentRoleHint::Extension.requires_migration_copy());
4062        assert!(!SegmentRoleHint::Journal.requires_migration_copy());
4063        assert!(!SegmentRoleHint::Index.requires_migration_copy());
4064        assert!(!SegmentRoleHint::Cache.requires_migration_copy());
4065        assert!(!SegmentRoleHint::Shard.requires_migration_copy());
4066    }
4067
4068    #[test]
4069    fn segment_role_hint_is_safe_to_drop() {
4070        assert!(SegmentRoleHint::Cache.is_safe_to_drop());
4071        assert!(!SegmentRoleHint::Core.is_safe_to_drop());
4072        assert!(!SegmentRoleHint::Extension.is_safe_to_drop());
4073        assert!(!SegmentRoleHint::Journal.is_safe_to_drop());
4074        assert!(!SegmentRoleHint::Index.is_safe_to_drop());
4075        assert!(!SegmentRoleHint::Audit.is_safe_to_drop());
4076        assert!(!SegmentRoleHint::Shard.is_safe_to_drop());
4077    }
4078
4079    // -----------------------------------------------------------------------
4080    // Program Manifest tests
4081    // -----------------------------------------------------------------------
4082
4083    static PM_LAYOUTS: &[LayoutManifest] = &[
4084        LayoutManifest {
4085            name: "Vault",
4086            disc: 1,
4087            version: 1,
4088            layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4089            total_size: 57,
4090            field_count: 0,
4091            fields: &[],
4092        },
4093        LayoutManifest {
4094            name: "Config",
4095            disc: 2,
4096            version: 1,
4097            layout_id: [8, 7, 6, 5, 4, 3, 2, 1],
4098            total_size: 43,
4099            field_count: 0,
4100            fields: &[],
4101        },
4102    ];
4103
4104    static PM_INSTRUCTIONS: &[InstructionDescriptor] = &[
4105        InstructionDescriptor {
4106            name: "deposit",
4107            tag: 1,
4108            args: &[],
4109            accounts: &[],
4110            capabilities: &["MutatesState"],
4111            policy_pack: "TREASURY_WRITE",
4112            receipt_expected: true,
4113        },
4114        InstructionDescriptor {
4115            name: "withdraw",
4116            tag: 2,
4117            args: &[],
4118            accounts: &[],
4119            capabilities: &["MutatesState", "TransfersTokens"],
4120            policy_pack: "TREASURY_WRITE",
4121            receipt_expected: true,
4122        },
4123    ];
4124
4125    static PM_POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
4126        name: "TREASURY_WRITE",
4127        capabilities: &["MutatesState"],
4128        requirements: &["SignerAuthority"],
4129        invariants: &[],
4130        receipt_profile: "default-mutation",
4131    }];
4132
4133    #[test]
4134    fn program_manifest_find_layout_by_disc() {
4135        let prog = ProgramManifest {
4136            name: "test",
4137            version: "0.1.0",
4138            description: "",
4139            layouts: PM_LAYOUTS,
4140            layout_metadata: &[],
4141            instructions: &[],
4142            events: &[],
4143            policies: &[],
4144            compatibility_pairs: &[],
4145            tooling_hints: &[],
4146            contexts: &[],
4147        };
4148        assert_eq!(prog.layout_count(), 2);
4149        assert!(prog.find_layout_by_disc(1).is_some());
4150        assert_eq!(prog.find_layout_by_disc(1).unwrap().name, "Vault");
4151        assert!(prog.find_layout_by_disc(2).is_some());
4152        assert!(prog.find_layout_by_disc(3).is_none());
4153    }
4154
4155    #[test]
4156    fn program_manifest_find_layout_by_id() {
4157        let prog = ProgramManifest {
4158            name: "test",
4159            version: "0.1.0",
4160            description: "",
4161            layouts: PM_LAYOUTS,
4162            layout_metadata: &[],
4163            instructions: &[],
4164            events: &[],
4165            policies: &[],
4166            compatibility_pairs: &[],
4167            tooling_hints: &[],
4168            contexts: &[],
4169        };
4170        let id = [1, 2, 3, 4, 5, 6, 7, 8];
4171        assert!(prog.find_layout_by_id(&id).is_some());
4172        let bad_id = [0, 0, 0, 0, 0, 0, 0, 0];
4173        assert!(prog.find_layout_by_id(&bad_id).is_none());
4174    }
4175
4176    #[test]
4177    fn program_manifest_identify_from_data() {
4178        static ID_LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
4179            name: "Vault",
4180            disc: 1,
4181            version: 1,
4182            layout_id: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
4183            total_size: 57,
4184            field_count: 0,
4185            fields: &[],
4186        }];
4187        let prog = ProgramManifest {
4188            name: "test",
4189            version: "0.1.0",
4190            description: "",
4191            layouts: ID_LAYOUTS,
4192            layout_metadata: &[],
4193            instructions: &[],
4194            events: &[],
4195            policies: &[],
4196            compatibility_pairs: &[],
4197            tooling_hints: &[],
4198            contexts: &[],
4199        };
4200        // Build a 16-byte header: disc=1, version=1, flags=0, layout_id
4201        let mut data = [0u8; 57];
4202        data[0] = 1; // disc
4203        data[1] = 1; // version
4204        data[4..12].copy_from_slice(&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]);
4205        let result = prog.identify_from_data(&data);
4206        assert!(result.is_some());
4207        assert_eq!(result.unwrap().name, "Vault");
4208    }
4209
4210    #[test]
4211    fn program_manifest_find_instruction() {
4212        let prog = ProgramManifest {
4213            name: "test",
4214            version: "0.1.0",
4215            description: "",
4216            layouts: &[],
4217            layout_metadata: &[],
4218            instructions: PM_INSTRUCTIONS,
4219            events: &[],
4220            policies: &[],
4221            compatibility_pairs: &[],
4222            tooling_hints: &[],
4223            contexts: &[],
4224        };
4225        assert_eq!(prog.instruction_count(), 2);
4226        assert_eq!(prog.find_instruction(1).unwrap().name, "deposit");
4227        assert_eq!(prog.find_instruction(2).unwrap().name, "withdraw");
4228        assert!(prog.find_instruction(3).is_none());
4229    }
4230
4231    #[test]
4232    fn program_manifest_find_policy() {
4233        let prog = ProgramManifest {
4234            name: "test",
4235            version: "0.1.0",
4236            description: "",
4237            layouts: &[],
4238            layout_metadata: &[],
4239            instructions: &[],
4240            events: &[],
4241            policies: PM_POLICIES,
4242            compatibility_pairs: &[],
4243            tooling_hints: &[],
4244            contexts: &[],
4245        };
4246        assert!(prog.find_policy("TREASURY_WRITE").is_some());
4247        assert!(prog.find_policy("NONEXISTENT").is_none());
4248    }
4249
4250    #[test]
4251    fn decode_account_fields_basic() {
4252        static DECODE_FIELDS: &[FieldDescriptor] = &[
4253            FieldDescriptor {
4254                name: "balance",
4255                canonical_type: "WireU64",
4256                size: 8,
4257                offset: 16,
4258                intent: FieldIntent::Custom,
4259            },
4260            FieldDescriptor {
4261                name: "bump",
4262                canonical_type: "u8",
4263                size: 1,
4264                offset: 24,
4265                intent: FieldIntent::Custom,
4266            },
4267        ];
4268        static DECODE_MANIFEST: LayoutManifest = LayoutManifest {
4269            name: "Test",
4270            disc: 1,
4271            version: 1,
4272            layout_id: [0; 8],
4273            total_size: 25,
4274            field_count: 2,
4275            fields: DECODE_FIELDS,
4276        };
4277        let mut data = [0u8; 25];
4278        let balance_bytes = 1000u64.to_le_bytes();
4279        data[16..24].copy_from_slice(&balance_bytes);
4280        data[24] = 254;
4281
4282        let (count, decoded) = decode_account_fields::<8>(&data, &DECODE_MANIFEST);
4283        assert_eq!(count, 2);
4284        assert!(decoded[0].is_some());
4285        assert_eq!(decoded[0].as_ref().unwrap().name, "balance");
4286        assert!(decoded[1].is_some());
4287        assert_eq!(decoded[1].as_ref().unwrap().name, "bump");
4288        assert_eq!(decoded[1].as_ref().unwrap().raw[0], 254);
4289    }
4290
4291    #[test]
4292    fn decoded_field_format_wire_u64() {
4293        let raw = 42u64.to_le_bytes();
4294        let field = DecodedField {
4295            name: "balance",
4296            canonical_type: "WireU64",
4297            raw: &raw,
4298            offset: 16,
4299            size: 8,
4300        };
4301        let mut buf = [0u8; 32];
4302        let len = field.format_value(&mut buf);
4303        assert_eq!(&buf[..len], b"42");
4304    }
4305
4306    #[test]
4307    fn decoded_field_format_wire_u32() {
4308        let raw = 65535u32.to_le_bytes();
4309        let field = DecodedField {
4310            name: "count",
4311            canonical_type: "WireU32",
4312            raw: &raw,
4313            offset: 0,
4314            size: 4,
4315        };
4316        let mut buf = [0u8; 32];
4317        let len = field.format_value(&mut buf);
4318        assert_eq!(&buf[..len], b"65535");
4319    }
4320
4321    #[test]
4322    fn decoded_field_format_bool() {
4323        let raw_true = [1u8];
4324        let field = DecodedField {
4325            name: "frozen",
4326            canonical_type: "WireBool",
4327            raw: &raw_true,
4328            offset: 0,
4329            size: 1,
4330        };
4331        let mut buf = [0u8; 32];
4332        let len = field.format_value(&mut buf);
4333        assert_eq!(&buf[..len], b"true");
4334
4335        let raw_false = [0u8];
4336        let field2 = DecodedField {
4337            name: "frozen",
4338            canonical_type: "WireBool",
4339            raw: &raw_false,
4340            offset: 0,
4341            size: 1,
4342        };
4343        let len = field2.format_value(&mut buf);
4344        assert_eq!(&buf[..len], b"false");
4345    }
4346
4347    #[test]
4348    fn decoded_field_format_address() {
4349        let raw = [0xABu8; 32];
4350        let field = DecodedField {
4351            name: "authority",
4352            canonical_type: "[u8;32]",
4353            raw: &raw,
4354            offset: 0,
4355            size: 32,
4356        };
4357        let mut buf = [0u8; 64];
4358        let len = field.format_value(&mut buf);
4359        let s = core::str::from_utf8(&buf[..len]).unwrap();
4360        assert!(s.starts_with("0x"));
4361        assert!(s.ends_with("..."));
4362    }
4363
4364    #[test]
4365    fn format_u64_basic() {
4366        let mut buf = [0u8; 32];
4367        let len = super::format_u64(12345, &mut buf);
4368        assert_eq!(&buf[..len], b"12345");
4369
4370        let len = super::format_u64(0, &mut buf);
4371        assert_eq!(&buf[..len], b"0");
4372
4373        let len = super::format_u64(u64::MAX, &mut buf);
4374        let expected = b"18446744073709551615";
4375        assert_eq!(&buf[..len], &expected[..]);
4376    }
4377
4378    #[test]
4379    fn format_hex_truncated_short() {
4380        let mut buf = [0u8; 64];
4381        let len = super::format_hex_truncated(&[0xAB, 0xCD], &mut buf);
4382        assert_eq!(&buf[..len], b"0xabcd");
4383    }
4384
4385    #[test]
4386    fn format_hex_truncated_long() {
4387        let mut buf = [0u8; 64];
4388        let data = [0xFFu8; 32];
4389        let len = super::format_hex_truncated(&data, &mut buf);
4390        let s = core::str::from_utf8(&buf[..len]).unwrap();
4391        assert!(s.starts_with("0x"));
4392        assert!(s.ends_with("..."));
4393        assert_eq!(len, 21); // 0x + 16 hex chars + ...
4394    }
4395
4396    #[test]
4397    fn program_manifest_display() {
4398        let prog = ProgramManifest {
4399            name: "test_program",
4400            version: "0.1.0",
4401            description: "A test",
4402            layouts: PM_LAYOUTS,
4403            layout_metadata: &[],
4404            instructions: PM_INSTRUCTIONS,
4405            events: &[],
4406            policies: PM_POLICIES,
4407            compatibility_pairs: &[],
4408            tooling_hints: &[],
4409            contexts: &[],
4410        };
4411        extern crate alloc;
4412        use alloc::format;
4413        let s = format!("{}", prog);
4414        assert!(s.contains("test_program"));
4415        assert!(s.contains("Vault"));
4416        assert!(s.contains("deposit"));
4417        assert!(s.contains("MutatesState"));
4418        assert!(s.contains("TREASURY_WRITE"));
4419        assert!(s.contains("SignerAuthority"));
4420    }
4421
4422    #[test]
4423    fn program_manifest_empty() {
4424        let prog = ProgramManifest::empty();
4425        assert_eq!(prog.layout_count(), 0);
4426        assert_eq!(prog.instruction_count(), 0);
4427        assert!(prog.find_layout_by_disc(0).is_none());
4428        assert!(prog.find_instruction(0).is_none());
4429        assert!(prog.identify_from_data(&[0u8; 16]).is_none());
4430    }
4431
4432    // -----------------------------------------------------------------------
4433    // Malformed input torture tests
4434    // -----------------------------------------------------------------------
4435
4436    #[test]
4437    fn decode_header_empty_buffer() {
4438        assert!(decode_header(&[]).is_none());
4439    }
4440
4441    #[test]
4442    fn decode_header_one_byte() {
4443        assert!(decode_header(&[0xFF]).is_none());
4444    }
4445
4446    #[test]
4447    fn decode_header_fifteen_bytes() {
4448        assert!(decode_header(&[0u8; 15]).is_none());
4449    }
4450
4451    #[test]
4452    fn decode_header_exact_sixteen() {
4453        let h = decode_header(&[0u8; 16]);
4454        assert!(h.is_some());
4455        let h = h.unwrap();
4456        assert_eq!(h.disc, 0);
4457        assert_eq!(h.version, 0);
4458    }
4459
4460    #[test]
4461    fn decode_header_large_buffer() {
4462        let data = [0xABu8; 1024];
4463        let h = decode_header(&data).unwrap();
4464        assert_eq!(h.disc, 0xAB);
4465        assert_eq!(h.version, 0xAB);
4466    }
4467
4468    #[test]
4469    fn decode_segments_too_short() {
4470        // Needs header (16) + registry header (4) minimum
4471        assert!(decode_segments::<8>(&[0u8; 19]).is_none());
4472    }
4473
4474    #[test]
4475    fn decode_segments_zero_count() {
4476        // 16 header + 4 registry header with count=0
4477        let mut data = [0u8; 20];
4478        data[16] = 0; // count low byte
4479        data[17] = 0; // count high byte
4480        let result = decode_segments::<8>(&data);
4481        assert!(result.is_some());
4482        let (n, _) = result.unwrap();
4483        assert_eq!(n, 0);
4484    }
4485
4486    #[test]
4487    fn compare_fields_identical_empty() {
4488        let a = LayoutManifest {
4489            name: "A",
4490            disc: 1,
4491            version: 1,
4492            layout_id: [0; 8],
4493            total_size: 16,
4494            field_count: 0,
4495            fields: &[],
4496        };
4497        let b = LayoutManifest {
4498            name: "B",
4499            disc: 1,
4500            version: 1,
4501            layout_id: [0; 8],
4502            total_size: 16,
4503            field_count: 0,
4504            fields: &[],
4505        };
4506        let report = compare_fields::<8>(&a, &b);
4507        assert_eq!(report.count, 0);
4508        assert!(report.is_append_safe);
4509    }
4510
4511    static SINGLE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4512        name: "x",
4513        canonical_type: "u8",
4514        size: 1,
4515        offset: 16,
4516        intent: FieldIntent::Custom,
4517    }];
4518
4519    #[test]
4520    fn compare_fields_all_removed() {
4521        let a = LayoutManifest {
4522            name: "A",
4523            disc: 1,
4524            version: 1,
4525            layout_id: [1; 8],
4526            total_size: 17,
4527            field_count: 1,
4528            fields: SINGLE_FIELD,
4529        };
4530        let b = LayoutManifest {
4531            name: "B",
4532            disc: 1,
4533            version: 2,
4534            layout_id: [2; 8],
4535            total_size: 16,
4536            field_count: 0,
4537            fields: &[],
4538        };
4539        let report = compare_fields::<8>(&a, &b);
4540        assert_eq!(report.count, 1);
4541        assert!(!report.is_append_safe);
4542    }
4543
4544    static OLD_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4545        name: "x",
4546        canonical_type: "u8",
4547        size: 1,
4548        offset: 16,
4549        intent: FieldIntent::Custom,
4550    }];
4551    static NEW_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4552        name: "x",
4553        canonical_type: "u16",
4554        size: 2,
4555        offset: 16,
4556        intent: FieldIntent::Custom,
4557    }];
4558
4559    #[test]
4560    fn compare_fields_type_change_detected() {
4561        let a = LayoutManifest {
4562            name: "A",
4563            disc: 1,
4564            version: 1,
4565            layout_id: [1; 8],
4566            total_size: 17,
4567            field_count: 1,
4568            fields: OLD_TYPE_FIELD,
4569        };
4570        let b = LayoutManifest {
4571            name: "B",
4572            disc: 1,
4573            version: 2,
4574            layout_id: [2; 8],
4575            total_size: 18,
4576            field_count: 1,
4577            fields: NEW_TYPE_FIELD,
4578        };
4579        let report = compare_fields::<8>(&a, &b);
4580        assert_eq!(report.entries[0].status, FieldCompat::Changed);
4581        assert!(!report.is_append_safe);
4582    }
4583
4584    #[test]
4585    fn verdict_different_disc_is_incompatible() {
4586        let a = LayoutManifest {
4587            name: "A",
4588            disc: 1,
4589            version: 1,
4590            layout_id: [1; 8],
4591            total_size: 16,
4592            field_count: 0,
4593            fields: &[],
4594        };
4595        let b = LayoutManifest {
4596            name: "B",
4597            disc: 2,
4598            version: 1,
4599            layout_id: [2; 8],
4600            total_size: 16,
4601            field_count: 0,
4602            fields: &[],
4603        };
4604        assert_eq!(
4605            CompatibilityVerdict::between(&a, &b),
4606            CompatibilityVerdict::Incompatible
4607        );
4608    }
4609
4610    #[test]
4611    fn verdict_same_id_is_identical() {
4612        let a = LayoutManifest {
4613            name: "A",
4614            disc: 1,
4615            version: 1,
4616            layout_id: [9; 8],
4617            total_size: 16,
4618            field_count: 0,
4619            fields: &[],
4620        };
4621        assert_eq!(
4622            CompatibilityVerdict::between(&a, &a),
4623            CompatibilityVerdict::Identical
4624        );
4625    }
4626
4627    #[test]
4628    fn compatibility_explain_between_identical() {
4629        let a = LayoutManifest {
4630            name: "A",
4631            disc: 1,
4632            version: 1,
4633            layout_id: [9; 8],
4634            total_size: 16,
4635            field_count: 0,
4636            fields: &[],
4637        };
4638        let exp = CompatibilityExplain::between(&a, &a);
4639        assert_eq!(exp.verdict, CompatibilityVerdict::Identical);
4640        assert_eq!(exp.added_count, 0);
4641        assert_eq!(exp.removed_count, 0);
4642        assert!(!exp.semantic_drift);
4643    }
4644
4645    static APPEND_OLD: &[FieldDescriptor] = &[FieldDescriptor {
4646        name: "a",
4647        canonical_type: "u8",
4648        size: 1,
4649        offset: 16,
4650        intent: FieldIntent::Custom,
4651    }];
4652    static APPEND_NEW: &[FieldDescriptor] = &[
4653        FieldDescriptor {
4654            name: "a",
4655            canonical_type: "u8",
4656            size: 1,
4657            offset: 16,
4658            intent: FieldIntent::Custom,
4659        },
4660        FieldDescriptor {
4661            name: "b",
4662            canonical_type: "u8",
4663            size: 1,
4664            offset: 17,
4665            intent: FieldIntent::Custom,
4666        },
4667    ];
4668
4669    #[test]
4670    fn compatibility_explain_append_counts_fields() {
4671        let older = LayoutManifest {
4672            name: "T",
4673            disc: 1,
4674            version: 1,
4675            layout_id: [1; 8],
4676            total_size: 17,
4677            field_count: 1,
4678            fields: APPEND_OLD,
4679        };
4680        let newer = LayoutManifest {
4681            name: "T",
4682            disc: 1,
4683            version: 2,
4684            layout_id: [2; 8],
4685            total_size: 18,
4686            field_count: 2,
4687            fields: APPEND_NEW,
4688        };
4689        let exp = CompatibilityExplain::between(&older, &newer);
4690        assert_eq!(exp.verdict, CompatibilityVerdict::AppendSafe);
4691        assert_eq!(exp.added_count, 1);
4692        assert_eq!(exp.added_fields[0], "b");
4693    }
4694
4695    #[test]
4696    fn layout_fingerprint_deterministic() {
4697        let m = LayoutManifest {
4698            name: "X",
4699            disc: 1,
4700            version: 1,
4701            layout_id: [5; 8],
4702            total_size: 16,
4703            field_count: 0,
4704            fields: &[],
4705        };
4706        let fp1 = LayoutFingerprint::from_manifest(&m);
4707        let fp2 = LayoutFingerprint::from_manifest(&m);
4708        assert_eq!(fp1.wire_hash, fp2.wire_hash);
4709        assert_eq!(fp1.semantic_hash, fp2.semantic_hash);
4710    }
4711
4712    static FP_CUSTOM: &[FieldDescriptor] = &[FieldDescriptor {
4713        name: "x",
4714        canonical_type: "u8",
4715        size: 1,
4716        offset: 16,
4717        intent: FieldIntent::Custom,
4718    }];
4719    static FP_BALANCE: &[FieldDescriptor] = &[FieldDescriptor {
4720        name: "x",
4721        canonical_type: "u8",
4722        size: 1,
4723        offset: 16,
4724        intent: FieldIntent::Balance,
4725    }];
4726
4727    #[test]
4728    fn layout_fingerprint_differs_on_intent_change() {
4729        let m1 = LayoutManifest {
4730            name: "T",
4731            disc: 1,
4732            version: 1,
4733            layout_id: [1; 8],
4734            total_size: 17,
4735            field_count: 1,
4736            fields: FP_CUSTOM,
4737        };
4738        let m2 = LayoutManifest {
4739            name: "T",
4740            disc: 1,
4741            version: 1,
4742            layout_id: [1; 8],
4743            total_size: 17,
4744            field_count: 1,
4745            fields: FP_BALANCE,
4746        };
4747        let fp1 = LayoutFingerprint::from_manifest(&m1);
4748        let fp2 = LayoutFingerprint::from_manifest(&m2);
4749        assert_eq!(fp1.wire_hash, fp2.wire_hash);
4750        assert_ne!(fp1.semantic_hash, fp2.semantic_hash);
4751    }
4752
4753    static LINT_AUTH_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4754        name: "auth",
4755        canonical_type: "[u8;32]",
4756        size: 32,
4757        offset: 16,
4758        intent: FieldIntent::Authority,
4759    }];
4760
4761    #[test]
4762    fn lint_layout_authority_without_signer() {
4763        let m = LayoutManifest {
4764            name: "T",
4765            disc: 1,
4766            version: 1,
4767            layout_id: [0; 8],
4768            total_size: 48,
4769            field_count: 1,
4770            fields: LINT_AUTH_FIELD,
4771        };
4772        // Use a mutating behavior WITHOUT signer to trigger E001
4773        let behavior = LayoutBehavior {
4774            requires_signer: false,
4775            affects_balance: false,
4776            affects_authority: true,
4777            mutation_class: MutationClass::InPlace,
4778        };
4779        let (n, lints) = lint_layout::<8>(&m, &behavior);
4780        assert!(n >= 1);
4781        assert_eq!(lints[0].code, "E001");
4782    }
4783
4784    #[test]
4785    fn lint_layout_clean_passes() {
4786        let m = LayoutManifest {
4787            name: "T",
4788            disc: 1,
4789            version: 1,
4790            layout_id: [0; 8],
4791            total_size: 48,
4792            field_count: 1,
4793            fields: LINT_AUTH_FIELD,
4794        };
4795        let behavior = LayoutBehavior {
4796            requires_signer: true,
4797            affects_balance: false,
4798            affects_authority: true,
4799            mutation_class: MutationClass::AuthoritySensitive,
4800        };
4801        let (n, _) = lint_layout::<8>(&m, &behavior);
4802        assert_eq!(n, 0);
4803    }
4804
4805    #[test]
4806    fn mutation_class_properties() {
4807        assert!(!MutationClass::ReadOnly.is_mutating());
4808        assert!(MutationClass::InPlace.is_mutating());
4809        assert!(MutationClass::Financial.requires_snapshot());
4810        assert!(MutationClass::AuthoritySensitive.requires_authority());
4811        assert!(!MutationClass::AppendOnly.requires_authority());
4812    }
4813
4814    static SEED_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4815        name: "seed",
4816        canonical_type: "[u8;32]",
4817        size: 32,
4818        offset: 16,
4819        intent: FieldIntent::PDASeed,
4820    }];
4821
4822    #[test]
4823    fn layout_stability_grade_stable_with_init_only() {
4824        let m = LayoutManifest {
4825            name: "T",
4826            disc: 1,
4827            version: 1,
4828            layout_id: [0; 8],
4829            total_size: 48,
4830            field_count: 1,
4831            fields: SEED_FIELD,
4832        };
4833        assert_eq!(
4834            LayoutStabilityGrade::compute(&m),
4835            LayoutStabilityGrade::Stable
4836        );
4837    }
4838
4839    #[test]
4840    fn layout_stability_grade_evolving_with_custom() {
4841        let m = LayoutManifest {
4842            name: "T",
4843            disc: 1,
4844            version: 1,
4845            layout_id: [0; 8],
4846            total_size: 17,
4847            field_count: 1,
4848            fields: SINGLE_FIELD,
4849        };
4850        assert_eq!(
4851            LayoutStabilityGrade::compute(&m),
4852            LayoutStabilityGrade::Evolving
4853        );
4854    }
4855
4856    static GRADE_HEAVY: &[FieldDescriptor] = &[
4857        FieldDescriptor {
4858            name: "auth1",
4859            canonical_type: "[u8;32]",
4860            size: 32,
4861            offset: 16,
4862            intent: FieldIntent::Authority,
4863        },
4864        FieldDescriptor {
4865            name: "auth2",
4866            canonical_type: "[u8;32]",
4867            size: 32,
4868            offset: 48,
4869            intent: FieldIntent::Owner,
4870        },
4871        FieldDescriptor {
4872            name: "auth3",
4873            canonical_type: "[u8;32]",
4874            size: 32,
4875            offset: 80,
4876            intent: FieldIntent::Delegate,
4877        },
4878        FieldDescriptor {
4879            name: "bal1",
4880            canonical_type: "WireU64",
4881            size: 8,
4882            offset: 112,
4883            intent: FieldIntent::Balance,
4884        },
4885        FieldDescriptor {
4886            name: "bal2",
4887            canonical_type: "WireU64",
4888            size: 8,
4889            offset: 120,
4890            intent: FieldIntent::Supply,
4891        },
4892        FieldDescriptor {
4893            name: "bal3",
4894            canonical_type: "WireU64",
4895            size: 8,
4896            offset: 128,
4897            intent: FieldIntent::Balance,
4898        },
4899    ];
4900
4901    #[test]
4902    fn layout_stability_grade_unsafe_to_evolve_heavy() {
4903        let m = LayoutManifest {
4904            name: "T",
4905            disc: 1,
4906            version: 1,
4907            layout_id: [0; 8],
4908            total_size: 136,
4909            field_count: 6,
4910            fields: GRADE_HEAVY,
4911        };
4912        let grade = LayoutStabilityGrade::compute(&m);
4913        assert_eq!(grade, LayoutStabilityGrade::UnsafeToEvolve);
4914    }
4915
4916    #[test]
4917    fn field_intent_new_variants_coverage() {
4918        assert_eq!(FieldIntent::PDASeed.name(), "pda_seed");
4919        assert_eq!(FieldIntent::Version.name(), "version");
4920        assert_eq!(FieldIntent::Bump.name(), "bump");
4921        assert_eq!(FieldIntent::Status.name(), "status");
4922        assert!(FieldIntent::Owner.is_authority_sensitive());
4923        assert!(FieldIntent::Delegate.is_authority_sensitive());
4924        assert!(FieldIntent::Threshold.is_governance());
4925        assert!(FieldIntent::Bump.is_init_only());
4926        assert!(FieldIntent::PDASeed.is_init_only());
4927        assert!(FieldIntent::Supply.is_monetary());
4928    }
4929
4930    #[test]
4931    fn refine_verdict_softens_with_rebuildable_segments() {
4932        let advice = [
4933            SegmentAdvice {
4934                id: [0; 4],
4935                size: 100,
4936                role: SegmentRoleHint::Cache,
4937                must_preserve: false,
4938                clearable: true,
4939                rebuildable: true,
4940                append_only: false,
4941                immutable: false,
4942            },
4943            SegmentAdvice {
4944                id: [0; 4],
4945                size: 0,
4946                role: SegmentRoleHint::Unclassified,
4947                must_preserve: false,
4948                clearable: false,
4949                rebuildable: false,
4950                append_only: false,
4951                immutable: false,
4952            },
4953        ];
4954        let report = SegmentMigrationReport {
4955            advice,
4956            count: 1,
4957            preserve_bytes: 0,
4958            clearable_bytes: 100,
4959            rebuildable_bytes: 100,
4960        };
4961        let refined = CompatibilityVerdict::MigrationRequired.refine_with_roles(&report);
4962        assert_eq!(refined, CompatibilityVerdict::AppendSafe);
4963    }
4964
4965    #[test]
4966    fn refine_verdict_escalates_with_immutable_segment() {
4967        let advice = [SegmentAdvice {
4968            id: [0; 4],
4969            size: 50,
4970            role: SegmentRoleHint::Audit,
4971            must_preserve: true,
4972            clearable: false,
4973            rebuildable: false,
4974            append_only: true,
4975            immutable: true,
4976        }];
4977        let report = SegmentMigrationReport {
4978            advice,
4979            count: 1,
4980            preserve_bytes: 50,
4981            clearable_bytes: 0,
4982            rebuildable_bytes: 0,
4983        };
4984        let refined = CompatibilityVerdict::AppendSafe.refine_with_roles(&report);
4985        assert_eq!(refined, CompatibilityVerdict::MigrationRequired);
4986    }
4987
4988    #[test]
4989    #[cfg(feature = "policy")]
4990    fn lint_policy_financial_mismatch() {
4991        let behavior = LayoutBehavior {
4992            requires_signer: true,
4993            affects_balance: true,
4994            affects_authority: false,
4995            mutation_class: MutationClass::Financial,
4996        };
4997        let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Write);
4998        assert!(n >= 1);
4999        assert_eq!(lints[0].code, "W005");
5000    }
5001
5002    #[test]
5003    #[cfg(feature = "policy")]
5004    fn lint_policy_reverse_mismatch() {
5005        let behavior = LayoutBehavior {
5006            requires_signer: true,
5007            affects_balance: false,
5008            affects_authority: false,
5009            mutation_class: MutationClass::InPlace,
5010        };
5011        let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5012        assert!(n >= 1);
5013        assert_eq!(lints[0].code, "W006");
5014    }
5015
5016    #[test]
5017    #[cfg(feature = "policy")]
5018    fn lint_policy_clean_when_aligned() {
5019        let behavior = LayoutBehavior {
5020            requires_signer: true,
5021            affects_balance: true,
5022            affects_authority: false,
5023            mutation_class: MutationClass::Financial,
5024        };
5025        let (n, _) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5026        assert_eq!(n, 0);
5027    }
5028
5029    #[test]
5030    fn display_field_intent() {
5031        extern crate alloc;
5032        use alloc::format;
5033        assert_eq!(format!("{}", FieldIntent::Balance), "balance");
5034        assert_eq!(format!("{}", FieldIntent::Authority), "authority");
5035    }
5036
5037    #[test]
5038    fn display_mutation_class() {
5039        extern crate alloc;
5040        use alloc::format;
5041        assert_eq!(format!("{}", MutationClass::Financial), "financial");
5042        assert_eq!(format!("{}", MutationClass::ReadOnly), "read-only");
5043    }
5044
5045    #[test]
5046    fn display_layout_stability_grade() {
5047        extern crate alloc;
5048        use alloc::format;
5049        assert_eq!(format!("{}", LayoutStabilityGrade::Stable), "stable");
5050        assert_eq!(
5051            format!("{}", LayoutStabilityGrade::UnsafeToEvolve),
5052            "unsafe-to-evolve"
5053        );
5054    }
5055
5056    #[test]
5057    fn display_compatibility_verdict() {
5058        extern crate alloc;
5059        use alloc::format;
5060        assert_eq!(format!("{}", CompatibilityVerdict::Identical), "identical");
5061        assert_eq!(
5062            format!("{}", CompatibilityVerdict::MigrationRequired),
5063            "migration-required"
5064        );
5065    }
5066
5067    #[test]
5068    fn display_layout_fingerprint() {
5069        extern crate alloc;
5070        use alloc::format;
5071        let fp = LayoutFingerprint {
5072            wire_hash: [0xAB, 0xCD, 0, 0, 0, 0, 0, 0],
5073            semantic_hash: [0, 0, 0, 0, 0, 0, 0xFF, 0x01],
5074        };
5075        let s = format!("{}", fp);
5076        assert!(s.starts_with("wire=abcd"));
5077        assert!(s.contains("sem="));
5078        assert!(s.ends_with("ff01"));
5079    }
5080
5081    #[test]
5082    fn display_receipt_profile() {
5083        extern crate alloc;
5084        use alloc::format;
5085        let rp = ReceiptProfile {
5086            name: "test",
5087            expected_phase: "Mutate",
5088            expects_balance_change: true,
5089            expects_authority_change: false,
5090            expects_journal_append: false,
5091            min_changed_fields: 2,
5092        };
5093        let s = format!("{}", rp);
5094        assert!(s.contains("test"));
5095        assert!(s.contains("Mutate"));
5096        assert!(s.contains("balance"));
5097        assert!(s.contains("min_fields=2"));
5098    }
5099
5100    #[test]
5101    fn display_idl_segment_descriptor() {
5102        extern crate alloc;
5103        use alloc::format;
5104        let sd = IdlSegmentDescriptor {
5105            name: "core",
5106            role: "Core",
5107            append_only: false,
5108            rebuildable: false,
5109            must_preserve: true,
5110        };
5111        let s = format!("{}", sd);
5112        assert!(s.contains("core"));
5113        assert!(s.contains("Core"));
5114        assert!(s.contains("must-preserve"));
5115        assert!(!s.contains("append-only"));
5116    }
5117}