| 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 | The same goes for a local builder that assembles a value out of a page's state for another component to consume, such as `repositoriesHref` or `hrefFor` building pagination links. The component that renders the value takes the parts and builds it once, so every page stops writing its own copy. |
| 34 | |
| 35 | ```tsx |
| 36 | // No: each page builds its own pagination links. |
| 37 | const repositoriesHref = ( |
| 38 | <CursorPagination firstHref={ |
| 39 | |
| 40 | // Yes: the pagination takes the parts. |
| 41 | <CursorPagination pathname={ |
| 42 | ``` |
| 43 | |
| 44 | 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. |
| 45 | |
| 46 | ## 3. Strings are never concatenated for layout |
| 47 | |
| 48 | No `'...' + '...'` to keep a line short. This applies to OpenAPI `description`, error messages, log lines, everything. |
| 49 | |
| 50 | ```ts |
| 51 | // No. |
| 52 | description: |
| 53 | 'Commits signed with it read as verified once the key carries an ' + |
| 54 | 'address this account has verified.', |
| 55 | |
| 56 | // Yes. |
| 57 | description: 'Commits signed with it read as verified once the key carries an address this account has verified.', |
| 58 | ``` |
| 59 | |
| 60 | Concatenation is for building a string out of parts that vary — interpolation is preferred there anyway. |
| 61 | |
| 62 | ## 4. Comments earn their place |
| 63 | |
| 64 | A comment explains why, never what. If the code says what it does and the reason is obvious, write no comment. |
| 65 | |
| 66 | 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. |
| 67 | |
| 68 | Keep: a non-obvious constraint, an invariant a reader cannot see from the code, a reason a surprising line is correct. |
| 69 | |
| 70 | ```ts |
| 71 | // No. |
| 72 | /** |
| 73 | * Directories first, then files, each group alphabetical. |
| 74 | * |
| 75 | * Submodules are grouped with directories: they are directory-shaped to a |
| 76 | * reader, whatever git calls them. |
| 77 | * |
| 78 | * Partitioning in one pass and sorting the two groups separately beats one |
| 79 | * comparator that has to check the type on every comparison, but neither is |
| 80 | * close to being worth optimizing. |
| 81 | */ |
| 82 | |
| 83 | // Yes. |
| 84 | /** Directories first, then files, each alphabetical. Submodules sort as directories. */ |
| 85 | ``` |
| 86 | |
| 87 | ## 5. No wrapped comments or strings |
| 88 | |
| 89 | One comment is one line, however long. One string literal is one line, however long. Editors wrap; source does not need to. |
| 90 | |
| 91 | ```ts |
| 92 | // No. |
| 93 | // The stored expiry doubles as the "last sent" stamp: a token issued less |
| 94 | // than a minute ago still has all but a minute of its life left. |
| 95 | |
| 96 | // Yes. |
| 97 | // 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. |
| 98 | ``` |
| 99 | |
| 100 | Multi-line block comments are allowed only when the lines are genuinely separate items, such as `@param` tags or an example. |
| 101 | |
| 102 | ## 6. ` |
| 103 | |
| 104 | A file under `services/` exports an `@Injectable()` class. Nothing else belongs there. |
| 105 | |
| 106 | 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; |
| 107 | |
| 108 | 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. |
| 109 | |
| 110 | ## 7. Production quality is the default |
| 111 | |
| 112 | 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. |
| 113 | |
| 114 | No speculative abstraction: an interface with one implementation, a factory for one product, or a configuration value that never varies is deleted on sight. |