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