# Module layout Where code goes. The folders are visible in `src/`; what is not visible is the rules that put them there. ```text cmd/ one module per `atgc ` family clients/ everything that talks to a counterpart: PDS, knot, appview, git, HTTP lexicon/ the wire: what strings and records *are*. Pure: opens no socket model/ what a record *means* to every verb that reads one config/ ~/.config/atgc: the directory, the lock, the accounts logging/ the log writers html/ every page atgc serves a browser term/ how output reaches a person or a pipe ``` ## Four rules 1. **`lexicon/` imports nothing from `clients/`.** The moment the pure layer opens a socket it stops being testable without one. This is why `identity::did_document_url` takes the PLC directory as an argument rather than looking it up. `clients/endpoints.rs` holds every compiled-in hostname and the `ATGC_*` variable that moves it; `env.rs`, beside it at the top of `src/`, holds the one reading of every `ATGC_*` switch, so that `ATGC_NO_INPUT=false` and `ATGC_USE_BOBBIN=false` cannot mean opposite things. README.md's Environment section lists all of them. 2. **Nothing outside `cmd/` imports `cmd/`.** A command is the top of the tree; anything two commands both need belongs lower down. The compiler enforces this now: items are `pub(super)` or `pub(in crate::cmd)`, and `pub(crate)` is left only where `main.rs` or a doc link reaches in. 3. **`clients/git/` owns every interaction with git.** Nothing else builds a `Command::new("git")`, which is what makes the non-interactive gate universal rather than per-caller. 4. **A rule about what a record means lives in `model/`, not in the verb that happened to need it.** See below; this is the newest rule and the one that had been broken the most times. ## `model/` and why it exists atgc is organised by verb and its bugs are about nouns. `cmd/` is over forty thousand lines, so a rule you cannot find is a rule you rewrite — and the same rules were rewritten until copies of them disagreed: | Rule | Copies | How they differed | | --- | --- | --- | | Where a pull targets | 6 | one defaulted to `main`, on the merge path | | Which state record wins | 5 | three decided state, and ordered three ways | | How many rounds a pull has | 2 | an empty array read as `0` and as `1` | | Who may write a status | 2 | pulls and issues drew the same line twice | None of that was carelessness. Two of the state orderings carried doc comments asserting they agreed with each other. The fix is not more care, it is somewhere to look: the counter-example is `cmd/stack`'s chain reader, which became a model module by accident, and chain rules stopped diverging the moment there was an obvious place for them. `lexicon/` is the wire — the NSIDs, the field names, the shape a record has on disk — and stays pure. `model/` is what a record *means*: which of two statuses is newer, where a pull lands, whether an account's write will be honoured. It may use `clients/`, because deciding standing means asking who owns a repo, but it must not know about arguments, output or exit codes. Those stay in `cmd/`, which is left to parse, call, and print. ## `config/` may import `clients/` Not a back-edge. `~/.config/atgc` does not hold opaque bytes, it holds *client specifications*: which account, which PDS, which session. Deciding whether the thing on disk is still valid is client-specific validation. Pushing those calls up into `cmd/` would make every command repeat them, to keep a diagram tidy. The constraint that remains is narrower, and it is the one that matters: **no client asks `config/` who we are.** A client takes the account, the DID or the session as an argument, from the command that already knows. It may reach for `config/dir`, which is a path, not an identity. The difference is testability: a client handed a DID can be driven with any DID, while a client that looks one up can only be driven by the machine it is running on. ## When a file becomes a folder Roughly 2,000 lines **and** somewhere to divide it. Size says look; a real dividing line says split. Splitting by verb does not count, since `pr comment` does not get its own file for being a different word. The divisions that exist are read vs write vs somebody else's pull, and knot-only vs config-only. Inside `cmd/pr/read/` there is one more, and it is the same rule applied twice over: deciding what the set of pull requests *is* is one subject, naming and printing it is another, and the lookups that turn a DID into something a column can show are a third that neither of the first two owns. Each `cmd/` module carries its own clap definitions, with help text as doc comments. There is no parallel structure mirroring the args. ## Where the `auth` split fell `auth.rs` used to be two subjects in one file at the crate root, and it is the worked example of the rule above. The OAuth client is `clients/atproto/oauth/`: `client.rs` for the jacquard wiring and the client metadata a grant is bound to, `sessions.rs` for what is on disk and what state it says an account's credentials are in, `store.rs` for jacquard's own typed view of that file, `login.rs` for the loopback callback server and the state one in-flight login owns, and `metadata.rs` for the transport wrapper that keeps the authorization server's well-known document out of the refresh critical section. The verbs are `cmd/auth.rs`, which is where every printed line and every clap definition went. The dividing line is the `config/` constraint, not the verbs. `login` and `agent_for_did` read `crate::config::account` to decide *which* DID to act for, so they are commands; the `start_auth`, `callback` and `restore` calls they make take that DID as an argument, so those are the client. Splitting by verb would have put `login` on the other side of the line and dragged the account registry into `clients/` with it.