# Coding Standards

> duklog coding standards, testing requirements, and review checklist. Use when reviewing code, writing tests, or checking quality. Use when this capability is needed.

- Skill: `tomevault-io/coding-standards-8` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/coding-standards-8`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/coding-standards-8/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/coding-standards-8

---


# duklog Coding Standards

## Style
- Iterators over loops, expressions over `return`, `match` over `if let`
- No `.unwrap()`/`.expect()` in lib code (only tests and `main.rs`)
- Derive order: Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize
- Specific imports (no globs), grouped: std / external / crate-internal
- Each module has its own `thiserror` error type; propagate with `?`

## Domain Invariants
- **Never construct domain structs with struct literal syntax from outside the module** — always go through `::new()` or a dedicated constructor that enforces validation. Struct literal construction in the storage layer silently bypasses invariants (e.g., `tx_count >= 1`) that `::new()` enforces.
- **Loading from storage must enforce the same invariants as creation** — if a constructor validates a field, the deserialization path must validate it too. Check for `ok_or_else` guards that catch `None` but silently accept invalid `Some(0)` or empty-string values.

## Testing
- Every `pub fn` tested with success and failure paths
- Assert on **specific values**, not `is_ok()`/`is_empty()` — critical for mutation testing
- Test **boundary values** (e.g., activation threshold at 9, 10, 11 QSOs)
- Use **quickcheck** aggressively — default to it for any new pure function, not just validators
- Every normalization/transform function needs an idempotency property: `fn foo_is_idempotent(s: String) -> bool`
- Every normalize → validate pipeline needs a roundtrip property (construct invalid-case-but-valid-structure input, normalize, assert validates)
- Add `if !s.is_ascii() { return true; }` guard in quickcheck properties for ASCII-domain functions (callsigns, grid squares) to avoid Unicode expansion false failures
- Use `tempfile::tempdir()` for storage tests — never real paths
- Deterministic and fast; no surviving mutants per module
- 90% minimum line coverage
- For storage deserialization: test corrupt-but-parseable inputs (e.g., `tx_count: 0`, empty section) — not just missing fields

## Coverage Exclusions
- Allowed: `main.rs` setup, event loop methods requiring a real terminal
- Test `draw_*` functions with `TestBackend` render tests (not excluded from coverage)
- Keep `#[mutants::skip]` on draw functions (mutation testing visual layout isn't productive)
- Never exclude: `src/model/`, `src/adif/`, `src/storage/`, `handle_key()` methods

## TUI: When to Extract a Dedicated Widget

Extract a dedicated widget (a new concrete struct implementing a trait) when either of these is true:

1. **Multiple instances of the same specialized input appear in one form** — two RST fields on one screen is the clearest signal. Duplication of construction logic (`set_default` called twice) is the code smell.
2. **A generic type is carrying specialized behavior via flags or post-construction setters** — if `FormField` grew a `clear_on_first_input` flag that only applies to RST fields, that behavior belongs in a dedicated `RstField` type, even if there were only one RST field.

The general principle: **extract a widget to encapsulate its own logic and behavior**, not only when you have duplicates. A widget owns its invariants (defaults, reset semantics, mode-aware updates) and enforces them internally — callers should not need to know how it works.

A well-extracted widget:
- Is constructed with its domain constraints baked in (`RstField::new(label, default)`) rather than applied post-construction
- Has `reset()` semantics appropriate to the input type (RST restores to default; text clears to empty)
- Does not leak implementation details (no public flags, no caller-managed state)

## TUI: draw_* Function Structure

- High-level `draw_*` functions should **read like prose** — delegate formatting and row-building to small private helpers; the top-level function orchestrates, it does not inline per-cell logic
- When a `match` arm inside a `draw_*` function contains more than ~3 lines of data transformation (e.g., `.iter().take(n).map(|qso| Row::new(vec![...]))` with 5+ fields), extract a private helper:
  - `fn format_rst(qso: &Qso) -> String` for repeated `format!("{}/{}", qso.rst_sent, qso.rst_rcvd)`
  - `fn format_timestamp(qso: &Qso) -> String` for repeated `.format("%H:%M").to_string()`
  - `fn qso_to_row_general(qso: &Qso) -> Row` / `fn qso_to_row_pota(qso: &Qso) -> Row` per type — or a trait if the dispatch pattern warrants it
- Consider a `ToRecentRow` / similar trait with per-variant impls when the same "build a Row from a Qso" logic branches on `QsoFormType` across more than one function

## When Touching a File (Boy Scout Rule)

Before finishing any change, scan the files you modified for:

- **Dead code** — unused imports, unreachable branches, or stale comments in your diff
- **Style violations** — loops that should be iterators, `if let` chains that should be `match`, explicit `return` that should be a tail expression, in the code you touched
- **Duplicated helpers** — if the same logic appears twice in the same file, extract a shared function
- **Oversized functions** — if a function grew during this change, check whether it should be split

If you find something outside the direct scope of the task, fix it in the same PR as a clearly labeled "cleanup" commit. Do not defer cleanup to a future PR that may never come.

## ADIF/POTA Correctness
- Field format: `<FIELDNAME:length>value` (length = byte length)
- Required: `STATION_CALLSIGN`, `CALL`, `QSO_DATE` (YYYYMMDD), `TIME_ON` (HHMMSS), `BAND`, `MODE`
- Park ref format: `[A-Z]{1,3}-\d{4,5}`
- Activation threshold: 10 QSOs, single park, one UTC day

## Documentation
- Rustdoc (`///`) on all `pub` items
- When adding `adif_str()`/`from_adif_str()` methods, add a doc comment citing the spec source (e.g., the relevant file in `docs/reference/`) and explain the field name used in storage (e.g., "`APP_DUKLOG_POWER`")
- Update `docs/` when implementing or changing features:
  - `docs/user-guide.md` — screen descriptions, keybindings, workflows
  - `docs/architecture.md` — module layout, Action enum, design decisions
  - `docs/roadmap.md` — move completed phases, update remaining work
  - `docs/adif-format.md` — if ADIF fields or format changes
- No feature is complete without documentation updates

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/rubberduck203) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-14 -->

