| 1 | # Code standards |
| 2 | |
| 3 | Rules for every file in this repository. They are not style preferences; a change that breaks one of them is a change that gets sent back. |
| 4 | |
| 5 | ## 1. DRY and SOLID |
| 6 | |
| 7 | Write a thing once. Two call sites doing the same keyset pagination, the same binary-content sniff, the same permission check means one function and two callers. |
| 8 | |
| 9 | Duplication that is about to diverge is not duplication — do not merge two things that happen to look alike today. |
| 10 | |
| 11 | Each unit does one thing: a service owns a domain, a module wires it, a controller maps HTTP to it, a DTO describes the wire. A service that spawns `git` and also decides HTTP status codes is two things. |
| 12 | |
| 13 | Depend on the narrow thing. A function that needs a git directory takes a git directory, not the repository row it could be read from. |
| 14 | |
| 15 | ## 2. No reshaping functions |
| 16 | |
| 17 | `toCommitDTO`, `toCommentDTO`, `toSummaryOf |
| 18 | |
| 19 | Either the function that produces the data produces the exact fields the caller needs, or it produces nothing and the caller builds the object where it is returned. |
| 20 | |
| 21 | ```ts |
| 22 | // No: the row is fetched, then renamed. |
| 23 | function toCommitSummary(commit: |
| 24 | return { sha: |
| 25 | } |
| 26 | |
| 27 | // Yes: the query selects the fields the response is made of. |
| 28 | const [commit] = |
| 29 | .select({ sha: |
| 30 | .from(schema.commit |
| 31 | ``` |
| 32 | |
| 33 | Value conversion is not reshaping: parsing a string into a `Date`, turning a service name into a binary name, formatting a colour for an `<input>`. Those stay. |
| 34 | |
| 35 | ## 3. Strings are never concatenated for layout |
| 36 | |
| 37 | No `'...' + '...'` to keep a line short. This applies to OpenAPI `description`, error messages, log lines, everything. |
| 38 | |
| 39 | ```ts |
| 40 | // No. |
| 41 | description: |
| 42 | 'Commits signed with it read as verified once the key carries an ' + |
| 43 | 'address this account has verified.', |
| 44 | |
| 45 | // Yes. |
| 46 | description: 'Commits signed with it read as verified once the key carries an address this account has verified.', |
| 47 | ``` |
| 48 | |
| 49 | Concatenation is for building a string out of parts that vary — interpolation is preferred there anyway. |
| 50 | |
| 51 | ## 4. Comments earn their place |
| 52 | |
| 53 | A comment explains why, never what. If the code says what it does and the reason is obvious, write no comment. |
| 54 | |
| 55 | Ban: paragraph essays above a function, restating the signature, narrating each step, explaining the language, apologising for a decision, or telling a story about the alternative that was rejected — that belongs in `docs/`, as a numbered decision. |
| 56 | |
| 57 | Keep: a non-obvious constraint, an invariant a reader cannot see from the code, a reason a surprising line is correct. |
| 58 | |
| 59 | ```ts |
| 60 | // No. |
| 61 | /** |
| 62 | * Directories first, then files, each group alphabetical. |
| 63 | * |
| 64 | * Submodules are grouped with directories: they are directory-shaped to a |
| 65 | * reader, whatever git calls them. |
| 66 | * |
| 67 | * Partitioning in one pass and sorting the two groups separately beats one |
| 68 | * comparator that has to check the type on every comparison, but neither is |
| 69 | * close to being worth optimizing. |
| 70 | */ |
| 71 | |
| 72 | // Yes. |
| 73 | /** Directories first, then files, each alphabetical. Submodules sort as directories. */ |
| 74 | ``` |
| 75 | |
| 76 | ## 5. No wrapped comments or strings |
| 77 | |
| 78 | One comment is one line, however long. One string literal is one line, however long. Editors wrap; source does not need to. |
| 79 | |
| 80 | ```ts |
| 81 | // No. |
| 82 | // The stored expiry doubles as the "last sent" stamp: a token issued less |
| 83 | // than a minute ago still has all but a minute of its life left. |
| 84 | |
| 85 | // Yes. |
| 86 | // The stored expiry doubles as the "last sent" stamp: a token issued less than a minute ago still has all but a minute of its life left. |
| 87 | ``` |
| 88 | |
| 89 | Multi-line block comments are allowed only when the lines are genuinely separate items, such as `@param` tags or an example. |
| 90 | |
| 91 | ## 6. ` |
| 92 | |
| 93 | A file under `services/` exports an `@Injectable()` class. Nothing else belongs there. |
| 94 | |
| 95 | Pure functions, types, error classes, codecs and test fakes live under `lib/`, mirroring the same folder names. `lib/git/tree/list-tree.ts` is a function; |
| 96 | |
| 97 | The same applies elsewhere: a folder is named for what it holds, and a file that does not fit the name moves rather than stretching it. |
| 98 | |
| 99 | ## 7. Production quality is the default |
| 100 | |
| 101 | Every branch is reachable and reached in tests, or the branch does not exist. Errors carry a cause the caller can act on. Nothing is left `any`. Nothing ships commented out. Trust boundaries validate, and the check lives where the boundary is, not at each call site. |
| 102 | |
| 103 | No speculative abstraction: an interface with one implementation, a factory for one product, or a configuration value that never varies is deleted on sight. |