Add ARCHITECTURE.md and apply it to this PR's code
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.
Applied it to this PR's own code:
- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
approve out of main.rs into a new pipeline.rs. main.rs's own main() had
grown to 278 lines and zero tests by treating "it's just the entry
point" as an excuse to skip separating logic from wiring; now main.rs
is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
pending-state -> what to do), replacing dispatch logic that was
previously inlined into a function that also made the real network/
build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
each stating that module's one job in a sentence, per the "deep
modules" principle the spec argues for.
Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.
Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:42:18 +00:00
# pkgwatch — code organization
Status: written 2026-09-17, once the build/publish pipeline PR gave this
project enough real code to have actual conventions worth writing down,
instead of guessing at them in advance.
This is distinct from `SPEC.md` , which is the product design (what
pkgwatch does and why). This file is about how the *code* implementing
that design is organized, so it stays readable as it grows past PoC size
instead of quietly accumulating debt. Researched against current industry
practice rather than asserted from habit — see Further reading.
## Principles
1. **One module, one job — and say what it is, up front.**
Ousterhout's "deep modules": the best modules expose a lot of
functionality through a simple interface, hiding the complexity behind
it. The two failure modes he names — *change amplification* (one
conceptual change forces edits in many places) and *obscurity* (a
reader can't tell where responsibility lives) — are both symptoms of
modules that don't have one clear job.
**Rule** : every `src/*.rs` file opens with a `//!` doc comment stating
its one responsibility in a sentence. If it can't be one sentence, the
module is doing too much.
**Example already here** : `builder.rs` 's job is "turn an
already-downloaded, already-verified artifact into a built package." It
hides PKGBUILD templating, upstream-string validation, and the
`makepkg` invocation behind one `build()` call — none of that leaks to
callers.
2. **Organize by pipeline stage (feature), not by technical layer.**
The package-by-feature vs. package-by-layer research is consistent:
feature-based grouping gives high cohesion within a module and low
coupling between modules; layer-based grouping (`models/`, `utils/` ,
`helpers/` ) tends toward the opposite, and a single feature change ends
up touching files scattered across every layer.
**Rule** : modules are named after what they do in the pipeline
2026-09-20 09:08:25 +00:00
(`source`, `fetcher` , `verifier` , `builder` , `sanity` , `publisher` ,
`state` ), not generic buckets. (`source` is a directory module: the
`ReleaseSource` trait and one file per host, re-exported from its
`mod.rs` so the rest of the crate never names a host's file.) A new pipeline stage gets a new module
Add ARCHITECTURE.md and apply it to this PR's code
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.
Applied it to this PR's own code:
- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
approve out of main.rs into a new pipeline.rs. main.rs's own main() had
grown to 278 lines and zero tests by treating "it's just the entry
point" as an excuse to skip separating logic from wiring; now main.rs
is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
pending-state -> what to do), replacing dispatch logic that was
previously inlined into a function that also made the real network/
build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
each stating that module's one job in a sentence, per the "deep
modules" principle the spec argues for.
Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.
Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:42:18 +00:00
named after the stage, not a method bolted onto an existing one.
**Anti-example to keep watching for** : a `utils.rs` grab-bag. `hash.rs`
could look like one but isn't — it exists for exactly one piece of
shared logic (`sha256_hex`) that two real stages (`verifier`,
`builder` ) both need, not as a place to dump unrelated helpers.
3. **Separate pure decision logic from I/O ("functional core, imperative
shell").**
A function that decides *and* does in the same body can't be tested
without standing up everything the "does" half touches — often a
network call, a subprocess, or the filesystem. Pulling the decision out
into its own pure function makes it trivially unit-testable and makes
the I/O half thin enough that it obviously matches the decision.
**Applied this PR** : `pipeline::process_package` 's tier dispatch
(publish now / still pending / newly pending / verification failed) was
originally inline in a function that also made the real network and
build calls. Pulled out into `decide_tier_action` , a pure function with
its own unit tests covering all four outcomes, no I/O involved.
4. **Every network, subprocess, filesystem-root, or environment boundary
is injectable.**
Same testability goal as #3 , applied to the specific ways this program
reaches outside itself. A consistent shape beats ad hoc mocking invented
per call site.
2026-09-20 08:57:14 +00:00
**Already in force** : `GithubEndpoints` /`ForgejoEndpoints` (the
`ReleaseSource` implementations, whose API root is what fetcher/verifier
take), `repo_add_bin` and the pacman.conf path (publisher), `PKGWATCH_REPO_DIR`
Add ARCHITECTURE.md and apply it to this PR's code
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.
Applied it to this PR's own code:
- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
approve out of main.rs into a new pipeline.rs. main.rs's own main() had
grown to 278 lines and zero tests by treating "it's just the entry
point" as an excuse to skip separating logic from wiring; now main.rs
is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
pending-state -> what to do), replacing dispatch logic that was
previously inlined into a function that also made the real network/
build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
each stating that module's one job in a sentence, per the "deep
modules" principle the spec argues for.
Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.
Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:42:18 +00:00
(main, for manual dry runs against a scratch repo instead of the real
one). A new external call follows the same shape: production code calls
a thin wrapper with the real default; tests call the parameterized
version with a fake.
5. ** `main.rs` is a dispatcher, not the program.**
Found by looking at this project's own `main.rs` : it grew to 278 lines
and zero tests over the course of one PR, because "it's just the entry
point" is an easy excuse to skip separating logic from wiring — even
though Rust doesn't actually stop you from unit-testing a binary
crate's `main.rs` . The Rust community convention of splitting
entry-point parsing from application logic exists precisely so the
logic ends up somewhere it's normal to test.
**Rule** : `main.rs` may parse `argv` , build shared clients, and print
output. It must not contain a pipeline decision, a network/subprocess
call, or anything with a test worth writing — that belongs in
`pipeline.rs` .
**Applied this PR** : moved `process_package` , `fetch_and_verify` ,
`build_and_publish` , `run_review` , and `approve` out of `main.rs` into a
new `pipeline.rs` , leaving `main.rs` as argument dispatch only.
6. **Validate at the boundary, once — don't scatter checks.**
Already stated project-wide (see the user's global instructions: don't
validate scenarios that can't happen, validate at system boundaries).
**Example already here** : `builder.rs` 's `validate_pkgname` /
`validate_pkgver` /`validate_shell_safe` run once, at PKGBUILD-generation
time, against every upstream-controlled string — not sprinkled through
whatever code happens to produce those strings.
7. **Don't build generality the currently-tracked packages don't need.**
Already the load-bearing design principle in `SPEC.md` ("a small fixed
set of PKGBUILD shapes," "extend when a third real shape shows up").
Restated here because it's also a tech-debt principle in its own right:
speculative abstraction is debt too — every future reader has to
understand it whether or not it's ever exercised.
8. **Every non-obvious structural decision gets one sentence of "why,"
inline.**
Standard tech-debt-prevention advice is to keep Architecture Decision
Records; a single-crate personal tool doesn't need a `docs/adr/`
directory, but the same information — why this way and not the obvious
alternative — needs to live somewhere a future reader will actually see
it: the doc comment on the thing itself.
2026-09-20 09:08:25 +00:00
**Example already here** : `source/github.rs` 's doc comment on
2026-09-20 08:57:14 +00:00
`GithubEndpoints::latest_release` explains why the newest Atom-feed entry isn't
Add ARCHITECTURE.md and apply it to this PR's code
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.
Applied it to this PR's own code:
- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
approve out of main.rs into a new pipeline.rs. main.rs's own main() had
grown to 278 lines and zero tests by treating "it's just the entry
point" as an excuse to skip separating logic from wiring; now main.rs
is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
pending-state -> what to do), replacing dispatch logic that was
previously inlined into a function that also made the real network/
build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
each stating that module's one job in a sentence, per the "deep
modules" principle the spec argues for.
Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.
Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:42:18 +00:00
trusted outright (scaleway-cli's `-dbg1` tag has no real Release behind
it) — the reasoning lives right next to the code it justifies, not in a
commit message or a separate design doc no one will find later.
## What's machine-enforced vs. what isn't
`cargo make ci` (format, clippy, cognitive-complexity threshold, coverage,
audit) mechanically enforces what's checkable: style, a handful of lint
categories, a complexity ceiling, and that coverage doesn't quietly
regress. It does **not** enforce module cohesion, naming, or "is this
logic in the right module" — those stay code-review questions. Worth
being honest about that boundary rather than implying CI catches
everything above.
## Further reading
- [A Philosophy of Software Design — deep modules & information hiding, summary ](https://medium.com/swlh/a-philosophy-of-software-design-by-john-ousterhout-4a00d0ff9f1c )
- [Package by feature vs. package by layer ](https://medium.com/@felixnjunge78/package-by-feature-vs-package-by-layer-which-one-wins-11ee03921fed )
- [Coupling and cohesion as the foundations of a maintainable codebase ](https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d )
- [Rust module and crate organization best practices ](https://softwarepatternslexicon.com/rust/idiomatic-rust-patterns/module-and-crate-organization-best-practices/ )
- [Reducing technical debt in 2026 — IBM ](https://www.ibm.com/think/insights/reduce-technical-debt )