| name | coding-standards |
| description | duklog coding standards, testing requirements, and review checklist. Use when reviewing code, writing tests, or checking quality. |
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:
- 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.
- 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