Skip to main content

Documentation Index

Fetch the complete documentation index at: https://anchoragedigital-shahankhatch-228-lint-diagnostics-refactor.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

The lint framework allows chain parsers to report data quality issues as structured diagnostics that are attested alongside display fields in the signed payload. This replaces silent data dropping with transparent, machine-readable reporting.

Architecture

Three categories of issues:
CategoryWhere it goesWho handles itExample
Display fieldsSignablePayload.FieldsWallet UI renders themNetwork name, instruction details
DiagnosticsSignablePayload.Fields (as Diagnostic variant)Attested — HSM/auditor can verifyOOB indices, empty account keys
ErrorsDecodeInstructionsResult.errorsConsumer decidesNo visualizer found

The diagnostics Cargo feature

Diagnostic emission is gated behind a diagnostics Cargo feature on visualsign, visualsign-solana, and parser_cli. The default builds:
  • parser_cli enables diagnostics (default-on); CLI users see the full diagnostic detail.
  • parser_app and parser_grpc_server do not enable it. Their SignablePayload shape is stable for HSMs and wallets that derive a metadata digest from it.
When the feature is off, decode_instructions returns Result<Vec<AnnotatedPayloadField>, VisualSignError> instead of a struct with separate diagnostic and error vectors. Empty account_keys and per-instruction visualizer errors abort the decode with Err; out-of-bounds indices flow through to the catch-all unknown_program visualizer with no diagnostic emission. Paired *.diagnostics.expected fixtures only matter when the feature is on. The CI Makefile splits invocations to defeat Cargo feature unification: cargo {build,test,clippy} --workspace --exclude parser_cli covers the OFF path; -p parser_cli and -p visualsign-solana --features diagnostics --lib cover the ON path.

Adding a diagnostic to a chain parser

1. Import the builder

use visualsign::field_builders::create_diagnostic_field;
use visualsign::lint::LintConfig;

2. Accept LintConfig in your decode function

pub fn decode_instructions(
    transaction: &MyTransaction,
    lint_config: &LintConfig,
) -> DecodeResult {

3. Check severity and emit

let severity = lint_config.severity_for(
    "transaction::my_rule",
    visualsign::lint::Severity::Warn,
);

if !matches!(severity, visualsign::lint::Severity::Allow) {
    diagnostics.push(create_diagnostic_field(
        "transaction::my_rule",
        "transaction",
        severity.clone(),
        &format!("description of what went wrong"),
        Some(instruction_index as u32),
    ));
}
create_diagnostic_field automatically emits tracing::warn! for warn and error-level diagnostics, giving operators production log visibility without any extra code in chain parsers.

4. Emit ok-level diagnostics for rules that pass

When report_all_rules is enabled, rules that find no issues still report:
if issue_count == 0 && lint_config.should_report_ok("transaction::my_rule") {
    diagnostics.push(create_diagnostic_field(
        "transaction::my_rule",
        "transaction",
        visualsign::lint::Severity::Ok,
        &format!("all {} items checked successfully", total),
        None,
    ));
}
This provides boot-metric-style attestation — the verifier can confirm every expected rule ran.

5. Return results separately

DecodeInstructionsResult {
    fields,      // display fields for the wallet UI
    errors,      // per-instruction parser errors
    diagnostics, // data quality diagnostics for attestation
}
The caller (visualsign.rs) appends diagnostics after all display fields.

Rule naming conventions

Rules follow the domain::rule_name format:
  • transaction::oob_program_id — instruction’s program_id_index is out of bounds in account_keys
  • transaction::oob_account_index — instruction references out-of-bounds account index in account_keys
  • transaction::empty_account_keys — transaction has no account keys
  • decode::visualizer_error — a visualizer failed to decode an instruction (always-on, not configurable via LintConfig)
Domains reflect who owns the problem:
DomainScope
transactionRaw transaction structure validity
decodeInstruction data interpretation
accountAccount metadata and resolution
walletCaller-provided data quality
idlIDL content and structure (Solana)
abiABI content and structure (Ethereum)

LintConfig

Controls diagnostic behavior:
use visualsign::lint::{LintConfig, Severity};

// Default: all rules at default severity, ok-level diagnostics enabled
let config = LintConfig::default();

// Custom: override specific rules
let config = LintConfig {
    overrides: HashMap::from([
        ("transaction::oob_account_index".to_string(), Severity::Allow),
    ]),
    report_all_rules: true,
};
Severity levels:
  • Ok — rule ran and found no issues
  • Warn — data quality issue found, parsing continued
  • Error — serious issue found
  • Allow — rule suppressed, no diagnostic emitted

Deterministic serialization

Diagnostic fields follow the same deterministic serialization rules as all other SignablePayloadField variants:
  • Alphabetical key ordering at every nesting level
  • ASCII-only content
  • Optional fields omitted when None (e.g., InstructionIndex)
This ensures diagnostics are covered by the same signing and attestation flow as display fields.

Testing diagnostics

#[test]
fn test_my_rule_emits_diagnostic() {
    let config = LintConfig::default();
    let result = decode_instructions(&tx, &registry, &config);

    let warns: Vec<_> = result.diagnostics
        .iter()
        .filter_map(|f| match &f.signable_payload_field {
            SignablePayloadField::Diagnostic { diagnostic, .. }
                if diagnostic.level == "warn" => Some(diagnostic),
            _ => None,
        })
        .collect();

    assert_eq!(warns.len(), 1);
    assert_eq!(warns[0].rule, "transaction::my_rule");
}

Updating fixtures and snapshots when adding rules

Adding a new rule that emits ok-level diagnostics changes the output of every transaction parse. You must update:
  1. CLI fixtures — regenerate the *.display.expected fixtures and the matching *.diagnostics.expected fixtures by running the CLI against the fixture inputs:
    cargo run --bin parser_cli -- $(cat src/parser/cli/tests/fixtures/solana-json.input | tr '\n' ' ') > src/parser/cli/tests/fixtures/solana-json.display.expected
    cargo run --bin parser_cli -- $(cat src/parser/cli/tests/fixtures/solana-text.input | tr '\n' ' ') > src/parser/cli/tests/fixtures/solana-text.display.expected
    
    For JSON fixtures, filter diagnostics from the display expected file and update the diagnostics expected file separately.
  2. Integration test expected JSON — update src/integration/tests/parser.rs to include the new diagnostic fields in the expected_sp JSON
  3. Field count assertions — tests that assert payload.fields.len() (e.g., swig_wallet tests) need their counts updated to include the new ok-level diagnostics
  4. Fuzz and proptest — run cargo test -p visualsign-solana --test fuzz_idl_parsing and --test pipeline_integration to verify no regressions
Run make -C src fmt && make -C src lint && make -C src test to verify everything passes before pushing.