Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
08d6205
feat(visualsign): add Diagnostic variant to SignablePayloadField
shahan-khatchadourian-anchorage Mar 31, 2026
135dd8a
feat(visualsign): add create_diagnostic_field builder
shahan-khatchadourian-anchorage Mar 31, 2026
16f3d06
test(visualsign): add Diagnostic field serialization and roundtrip tests
shahan-khatchadourian-anchorage Mar 31, 2026
1a53423
feat(solana): emit diagnostics for OOB indices in legacy transactions
shahan-khatchadourian-anchorage Mar 31, 2026
84018a3
test(solana): add diagnostic emission tests for OOB indices
shahan-khatchadourian-anchorage Mar 31, 2026
a6694be
style: apply rustfmt to diagnostic changes
shahan-khatchadourian-anchorage Mar 31, 2026
c3c918b
feat(solana): emit diagnostics for OOB indices in v0 transactions
shahan-khatchadourian-anchorage Mar 31, 2026
820f5a4
feat: configurable lint rules, boot-metric attestation, and error han…
shahan-khatchadourian-anchorage Mar 31, 2026
0e41c47
docs: add diagnostic field type and lint framework documentation
shahan-khatchadourian-anchorage Mar 31, 2026
729d7d8
feat(solana): add oob_account_index_in_skipped_instruction as separat…
Copilot Mar 31, 2026
7f293a1
fix: address code review feedback from Copilot
shahan-khatchadourian-anchorage Apr 1, 2026
4ca3d22
fix: resolve rebase conflicts and separate account index rules
shahan-khatchadourian-anchorage Apr 1, 2026
ea595d4
docs: add fixture update process for new lint rules
shahan-khatchadourian-anchorage Apr 1, 2026
ee530e9
refactor: separate display and diagnostic fixture tests
shahan-khatchadourian-anchorage Apr 1, 2026
9f57b28
fix: address PR review feedback — tracing, doc fixes, fixture rename
shahan-khatchadourian-anchorage Apr 10, 2026
340ea7b
docs: sync documentation with implementation
shahan-khatchadourian-anchorage Apr 10, 2026
f1a3e62
refactor: VisualizerContext backed by &CompiledInstruction + &[Pubkey]
shahan-khatchadourian-anchorage Apr 15, 2026
f89eb2f
refactor: update all presets to use new VisualizerContext API
shahan-khatchadourian-anchorage Apr 15, 2026
81257f2
refactor: eliminate instruction skipping, shared diagnostic scan
shahan-khatchadourian-anchorage Apr 15, 2026
a7bd5a6
refactor: eliminate instruction skipping, update tests
shahan-khatchadourian-anchorage Apr 15, 2026
2fddfe1
test: fix pipeline and integration tests for new label format
shahan-khatchadourian-anchorage Apr 15, 2026
99491b0
fix: address remaining PR review feedback
shahan-khatchadourian-anchorage Apr 16, 2026
296407d
style: apply cargo fmt
shahan-khatchadourian-anchorage Apr 16, 2026
9974105
fix: update integration tests and CLI fixtures for new diagnostic model
shahan-khatchadourian-anchorage Apr 16, 2026
ff92a53
fix: add missing clippy allow attributes on test modules after rebase
shahan-khatchadourian-anchorage Apr 16, 2026
66bd5a1
docs: update lint diagnostics documentation for refactored model
shahan-khatchadourian-anchorage Apr 16, 2026
d99c289
test: restore text fixture, membership-based JSON comparison, diagnos…
shahan-khatchadourian-anchorage Apr 16, 2026
56cc351
fix: reject unresolved accounts in token_2022 and swig_wallet parsers
shahan-khatchadourian-anchorage Apr 17, 2026
254f433
docs: fix stale examples, fixture names, and severity wording
shahan-khatchadourian-anchorage Apr 17, 2026
213ed6c
style: apply cargo fmt
shahan-khatchadourian-anchorage Apr 17, 2026
648386f
refactor: address code review feedback
shahan-khatchadourian-anchorage Apr 17, 2026
e7c597e
fix: route empty_account_keys through LintConfig severity
shahan-khatchadourian-anchorage Apr 17, 2026
ed1f75f
refactor: use BTreeMap for LintConfig overrides, .copied() for Severity
shahan-khatchadourian-anchorage Apr 17, 2026
0baec56
chore: address PR #255 review nits, drop internal planning artifact
shahan-khatchadourian-anchorage May 6, 2026
e468f86
feat: gate diagnostics behind a Cargo feature, default-on for CLI only
shahan-khatchadourian-anchorage May 6, 2026
6dfa155
Merge remote-tracking branch 'origin/main' into shahankhatch/228-lint…
shahan-khatchadourian-anchorage May 6, 2026
80b076b
feat(solana): port presets merged from main to wire-data VisualizerCo…
shahan-khatchadourian-anchorage May 6, 2026
a2d9589
test(solana): cover the diagnostics-OFF code path
shahan-khatchadourian-anchorage May 6, 2026
3f28e5d
style(swig): one match arm per variant in summarize_visualized_field
shahan-khatchadourian-anchorage May 6, 2026
2ee2ee7
revert(solana): restore "Instruction N" label across presets
shahan-khatchadourian-anchorage May 13, 2026
5d767d4
fix(solana): restore "Instruction Decoding Note" on diagnostics-OFF f…
shahan-khatchadourian-anchorage May 13, 2026
6b91873
merge: bring main into 228-lint-diagnostics-refactor
shahan-khatchadourian-anchorage May 13, 2026
664682e
fix(solana): flip CLI/integration fixture labels to "Instruction N"
shahan-khatchadourian-anchorage May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
**/target
out
docs/superpowers/
.surfpool/
205 changes: 205 additions & 0 deletions docs/contributor-guides/lint-diagnostics.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Lint Diagnostics
description: How the parser reports data quality issues as attested diagnostics
---

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

```mermaid
graph LR
A[Transaction data] --> B[Chain parser]
B --> C[Display fields]
B --> D[Diagnostics]
B --> E[Errors]
C --> F[SignablePayload]
D --> F
F --> G[Signed by ephemeral key]
```

Three categories of issues:

| Category | Where it goes | Who handles it | Example |
|----------|---------------|----------------|---------|
| **Display fields** | `SignablePayload.Fields` | Wallet UI renders them | Network name, instruction details |
| **Diagnostics** | `SignablePayload.Fields` (as `Diagnostic` variant) | Attested -- HSM/auditor can verify | OOB indices, empty account keys |
| **Errors** | `DecodeInstructionsResult.errors` | Consumer decides | No 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

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

### 2. Accept `LintConfig` in your decode function

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

### 3. Check severity and emit

```rust
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:

```rust
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

```rust
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:

| Domain | Scope |
|--------|-------|
| `transaction` | Raw transaction structure validity |
| `decode` | Instruction data interpretation |
| `account` | Account metadata and resolution |
| `wallet` | Caller-provided data quality |
| `idl` | IDL content and structure (Solana) |
| `abi` | ABI content and structure (Ethereum) |

## `LintConfig`

Controls diagnostic behavior:

```rust
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

```rust
#[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:

```bash
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.
3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
"adding-new-chain",
"contributing",
"contributor-guides/project-structure",
"contributor-guides/best-practices"
"contributor-guides/best-practices",
"contributor-guides/lint-diagnostics"
]
}
]
Expand Down
46 changes: 46 additions & 0 deletions docs/field-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ Provides expandable/collapsible content with progressive disclosure. Shows essen
| Multiple values | `list_layout` | List of recipients |
| Complex data | `preview_layout` | Detailed gas breakdown |
| Warnings | `text_v2` | "Warning: High slippage" |
| Parse diagnostics | `diagnostic` | OOB indices, data quality checks |

## Combining field types

Expand Down Expand Up @@ -450,6 +451,51 @@ Token names and other content must use ASCII equivalents (e.g., "EUR" instead of
}
```

## Diagnostic field type

### diagnostic

Reports data quality findings from the parser's lint framework. Diagnostics are attested alongside display fields in the signed payload, so the HSM/attester can verify what the parser checked and what it found.

```json
{
"Type": "diagnostic",
"Label": "transaction::oob_program_id",
"FallbackText": "warn: instruction 1: program_id_index 8 out of bounds (5 account keys)",
"Diagnostic": {
"Rule": "transaction::oob_program_id",
"Domain": "transaction",
"Level": "warn",
"Message": "instruction 1: program_id_index 8 out of bounds (5 account keys)",
"InstructionIndex": 1
}
}
```

**Properties:**
- `Rule`: Rule identifier in `domain::rule_name` format
- `Domain`: Category of the check (e.g., `transaction`, `decode`, `idl`)
- `Level`: Severity of the finding
- `ok` -- rule ran and found no issues (boot-metric attestation)
- `warn` -- data quality issue found, parsing continued
- `error` -- serious issue found
- `Message`: Human-readable description of the finding
- `InstructionIndex` (optional): Which instruction triggered the diagnostic

**Wallet handling:**
- Wallets that don't recognize `Type: "diagnostic"` can display the `FallbackText`
- Diagnostics always appear after all display fields (network, instructions, accounts)
- When `report_all_rules` is enabled (default), every rule emits a diagnostic -- `ok`, `warn`, or `error` -- so the attester can verify all expected rules ran

**Current rules:**

| Rule | Domain | Description |
|------|--------|-------------|
| `transaction::oob_program_id` | `transaction` | Instruction's program_id_index is out of bounds in account_keys |
| `transaction::oob_account_index` | `transaction` | Instruction references account indices beyond account_keys |
| `transaction::empty_account_keys` | `transaction` | Transaction has no account keys |
| `decode::visualizer_error` | `decode` | A visualizer failed to decode an instruction (always-on) |

## Future field types

Planned additions to the field type system:
Expand Down
Loading
Loading