Frequencies, bands, modes, callsigns, grid squares and the country file live in Nonemm.Core. Nonemm.Contests holds the scoring engine and CQ WW and CQ WPX. Nonemm.Storage writes N1MM's DXLOG schema, and Nonemm.Formats writes Cabrillo 3.0 and reads and writes ADIF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
name, description
| name | description |
|---|---|
| code-style | Language-neutral code style rules - comments, file and folder layout, module dependencies, constants vs literals, OOP vs FP, errors and async, naming, language-specific notes, tests. Load when writing or reviewing code in any language. |
Code style
Sections 2 to 10 of the project style guide. Section 1 (prose) and section 11
(the finishing checklist) are in CLAUDE.md and load every session.
Examples are JS-flavoured pseudocode using one running scenario: a service that serves a product catalog over HTTP, backed by a database and a cache. The rules are language-neutral. Translate the syntax; keep the rule.
2. Comments
Default: no comment. Code that reads clearly needs none. Write one only when one of these is true:
- The why isn't visible from the code (a workaround, a spec quirk, a performance trade-off).
- The mechanics are genuinely non-obvious (a subtle index, an ordering requirement).
- The signature needs a type or unit the code doesn't state.
- It's a
TODO/FIXMEwith an actual next step.
Exported symbols should have a one-line doc comment when it tells the reader
something the name and signature don't. A should, not a must: don't write
/** Returns the base URL. */ above getBaseUrl().
Keep comments to one line where possible. Lowercase, informal and short is fine:
this.pending = []; // indexed by position in the batch
// group by host so we send one request per host
const perHost = new Map();
Never write a comment that narrates your thought process, restates the line
below it, or explains a design philosophy. Those belong in a design note under
docs/ or in the PR description, not in the source.
// BAD — restates the code
// Loop over the users and collect their IDs
const ids = users.map(u => u.id);
// BAD — internal monologue
// The user will most likely be scanning this list top to bottom, so what they
// really want here is for the newest items to feel immediately reachable, which
// means we sort descending and let the eye land on the first row.
sortDescending(items, 'createdAt');
// GOOD — explains a non-obvious constraint
// the API returns oldest-first; the UI expects the opposite
sortDescending(items, 'createdAt');
Doc comments only where they add something the declaration doesn't. In a typed
language the signature already states most of it, so delete any parameter doc
that repeats the type. Keep one like this — the type string doesn't say the
value is a root URL, and the example shows the expected format:
/**
* @param baseUrl the site root, e.g. https://example.com/
*/
3. Files and folders
One folder per concern. Extensions vary by language; the shape doesn't:
src/
main entry point, process setup
http/
index creates the server, registers the routes
AuthMiddleware
catalog/
Product
ProductRepository
db/
index
util/
hashing
- Group folders by subject, not by shape.
catalog/holds everything about products. Don't createhelpers/orconstants/folders. - A shared types or declarations folder is for declarations, not domain types. Ambient declarations, generated API clients and types used across several folders belong there. A type used by one module stays in that module.
- A module's entry point file is an entry point, not a barrel. It creates what the module provides and exports the function the caller calls. Don't fill it with re-exports of the folder's internals.
- One class per file, named after the class. Small related types can share a file, but the file name must match what's inside.
- Free functions go in a lowercase-named file describing the topic
(
hashing,formatting), not a catch-allutils. - Keep files small. Over ~300 lines, check whether part of the file is a separate concern and split it out.
- Don't put a class, unrelated helper functions and a constants block in the same file. Split them into three files.
- Design notes go in
docs/. Anything explaining why the system is shaped the way it is — decisions, trade-offs, rejected alternatives — goes in a file underdocs/, not in a source comment.
If a piece of code is only ever used in relation to one class or module, put it
there. Product.getImageUrl() is a method on Product, not a
buildProductImageUrl(product) in util/. Don't make a file per method.
Move code into a shared module when it gets a second caller, not before.
4. Dependencies between modules
Dependencies point in one direction: entry point → wiring → workers → the modules at the end of the chain.
main → http/index → catalog/ProductRepository → util/hashing
→ catalog/Product
→ db/index
- No cycles. If module A imports B and B imports A, fix it one of two ways: move the shared code into a third module both can import, or move the method into the module where the data it uses is defined.
- Give the modules at the end of the chain no project imports.
hashingandProductimport only third-party packages, so you can move them or test them on their own. - Pass dependencies in as constructor arguments.
ProductRepositorytakes the database and cache clients in its constructor;AuthMiddlewaretakes averifyTokencallback. Neither imports the config or a global singleton.
// BAD — reaches for a module-level singleton and the global config
class ProductRepository {
find(id) {
return cache.get(config.keyPrefix + id); // both imported at the top
}
}
// GOOD — dependencies passed in by the caller
class ProductRepository {
constructor(cache, keyPrefix) { ... }
find(id) {
return this.cache.get(this.keyPrefix + id);
}
}
- Import the config only in the entry point and the wiring files. Pass the values down as arguments. No other module reads it.
- Declare each value and each rule in one file. The list of upstream hosts is
in
http/indexand nowhere else. If the same literal or the same logic appears in two files, delete one and import it from the other.
Single responsibility, practically. Ask: can I describe this file's job in one sentence, without "and"?
Product: holds the fields of one catalog item.ProductRepository: loads products from the database.AuthMiddleware: checks the Authorization header.hashing: turns an object into a stable hash.
Not everything needs its own module. A helper called from one place can stay a local function in that file. Split a file when it covers two subjects that don't share state.
5. Constants vs literals
Extract a value when it is repeated, configurable, or not understandable from the value alone. Otherwise inline it.
// BAD
const METRES_PER_KILOMETRE = 1000;
const CACHE_KEY_PREFIX = 'product:';
// GOOD — used once, obvious in place
const km = metres / 1000;
const key = 'product:' + hashObject(query);
- Unit conversions,
0,1,-1,100for percent: inline. - A magic number whose meaning isn't obvious (
0o666,86_400, a retry limit someone will want to tune): name it, or add a short comment. - A value used in three places: declare it once in the module that uses it most, and import it from there. Not in a global constants file.
- A value that must stay in sync with something outside the file (a server limit, a CSS breakpoint): name it, and say in a comment what it matches.
- Anything deployment-specific (URLs, ports, keys, limits): config file.
6. OOP and FP
Applies in every language that has both. Pick one per module and stay with it.
- Class when there is state that persists between calls, or several methods over the same data: a queue that holds pending items, a client that holds a connection.
- Plain function when it's a transform with no state:
hashObject,formatPrice. - Don't mix them in one file. Move exported free functions out of a file that defines a class.
- No class with only static methods. Export the functions from a module, or use whatever the language offers for free functions.
- No inheritance unless the subclass is a kind of the base class. Otherwise
pass a function or an object in, as
AuthMiddlewaretakesverifyToken.
State and mutation
- Don't mutate function arguments. Return a new value instead.
- Don't export mutable module-level or global state. If state is shared, put it in a class and pass the instance to whoever needs it.
- Know which of your language's collection operations mutate in place and which
return a copy. Sorting and reversing usually mutate:
sort(),reverse(),splice()andpush()in JS,list.sort()andlist.append()in Python,std::sortin C++,Collections.sortin Java. Copy first when the caller still needs the original. - Watch for shared references when creating collections. Filling n slots with one
empty collection puts the same object in every slot, so writing to one writes
to all. This bites in JS (
new Array(n).fill([])), Python ([[]] * n) and anywhere else that copies the reference rather than the value. Construct each element separately.
7. Errors and async
- Don't swallow errors. A
catchblock either handles the failure (retry, documented fallback) or rethrows. Logging and continuing is not handling it. - The caller must be able to tell a failure from an empty result. Don't return an empty list, null or nothing for both. Throw, or return a result type that states which happened.
- Catch around the statement that can fail, not around the whole function body.
- Put the values needed to debug in the message: the URL, the id, the status
code. Not
'Request failed'. - Rethrow with added context or not at all.
catch (e) { throw e; }is noise. Preserve the original error as the cause where the language supports it. - No unobserved async results. Every promise, future or task is awaited, returned, or has an error handler attached. Turn on the linter rule for this if the language has one.
- Don't mix callbacks and promises in one code path.
// BAD — the caller gets nothing back and cannot tell why
async function fetchProducts(url) {
try {
return await post(url, body);
} catch (e) {
console.error('fetch rejected', e);
}
}
// GOOD — the error propagates, with the context to debug it
async function fetchProducts(url) {
try {
return await post(url, body);
} catch (cause) {
throw new Error(`product request to ${url} failed`, { cause });
}
}
8. Naming
Follow the dominant convention of the language you're in: PascalCase classes
with camelCase members in TS, Java and C#; snake_case in Python, Rust and C.
Don't import another language's casing. On top of that:
- No Hungarian notation and no
Iprefix on interfaces. - Constant casing only for values that never change.
- File names match the main export: the class name for a class, a topic name for a group of functions.
- Functions start with a verb:
buildUrl(), noturlBuilder(). Booleans start withis,hasorshould. - Don't repeat the containing type in a member name:
Product.getImageUrl(), notProduct.getProductImageUrl(). - No invented abbreviations.
id,url,db,req/resare fine because everyone reads them;prdctorrqSndrare not.
9. Language-specific notes
Replace this section with the rules for the language in use. TypeScript, as a worked example:
- No
any. Useunknownat boundaries and narrow. - Define types in the module that uses them. A shared
types.tsonly for types genuinely used across several folders. - Prefer
typefor unions and object shapes,interfacefor things meant to be implemented. - Don't add a wrapper type until it prevents a real mistake.
stringis fine for a URL until you have two kinds of string that could be swapped. strict: trueintsconfig.json. Don't disable a check for one file.- No
ascasts to force a shape. Parse external data into a typed value at the boundary and let the check fail there. - No non-null assertion (
!) without a comment saying why the value can't be null. readonlyon fields and parameters that are never reassigned;as constfor literal tables.- Exhaustive
switchon a union: add adefaultthat assigns tonever, so adding a case breaks the build.
The equivalents elsewhere: const correctness and RAII in C++, final and
Optional in Java, strict types and typed properties in PHP, type hints checked
by a type checker in Python.
10. Tests
- Test through the module's entry point, not its internals. A test that calls a private method breaks on every refactor.
- One behavior per test, named after what it asserts.
- No mocking framework for our own code. Pass a fake in through the constructor. That is what section 4 is for.
- Test files sit next to the code they test, following the language's convention for test file names.
- A bug fix includes the test that reproduces the bug.
- Don't test getters, types, or that a library works.