Architectural priorities
The ranking
The owner sets this order, and it is strict.
correctness > simplicity > performance > shipping speed
When two of these collide, the higher one wins and the lower one yields.
How to use it
Most trade-offs answer themselves here. An agent that faces “this is technically wrong, and nobody would notice” reads the ranking and fixes it. Before the ranking existed, that agent guessed, and the guesses produced both over-engineering and blocking on the wrong things.
Escalate on cost, not on principle. Apply the ranking when the fix is cheap. Ask the owner when the fix is expensive, when the defect is unreachable in practice, or when “leave it and note it” is a real answer.
To escalate, name the severity, name the cost of the fix, and give a recommendation. The owner sets the bar. Reserve blocking language for what is unsafe to ship. AGENTS.md carries the longer version under “Recommendations and the quality bar”.
Performance has two states
Measure a performance problem, and it becomes a bug. It then sits under correctness, and the correctness rules apply to it.
Leave it unmeasured, and it is speculation. It then stays third, below simplicity.
Run the query, read the plan, or time the request before you argue for performance. This split settles two of the collisions below.
Collisions
Correctness against simplicity
Correctness wins. Accept more code when the simpler form can be wrong.
.php-cs-fixer.dist.php shows the shape. ignoreVCSIgnored(true) was one line,
and it matched zero files inside a gitignored worktree. just cs then fixed
nothing, and the style leg of just ci passed with nothing inspected. The file
now carries an explicit exclude list and a guard that throws on zero files
(e15e07bd).
Correctness against performance
Correctness wins. A slower path that is right beats a faster path that is wrong.
The HTML sanitizer shows the shape. ef0f4555 removes style and title from
the parsed tree instead of with a regex pre-pass. An unclosed <style> swallows
the rest of the document, and only a parser resolves that. A parser costs more
than a regex, and nobody measured the difference, because correctness settled
the choice on its own.
This pair has the weakest grounding of the six. No commit in this repository weighs a measured performance number against a correctness cost. The rule holds, and the example only shows correctness winning without a contest.
Correctness against shipping speed
Correctness wins. Ship late, and ship right.
The pull request gate says so. working-with-prs tells you to fix every failure
the gate reports, including the failures that pre-date your change. The only
acceptable response to a flaky test is a fix.
Decision answers show that the owner sets where the rule applies. The first
decision form refused a stale submission, so a reviewer on an old version got an
error and re-read the block. SaveDecisionAnswerHandler uses last-write-wins
instead. It moves the picks onto the latest version by option label, and it
refuses only a label that the latest version removed. A decision has one shared
answer, and today the document owner is the only person who answers it, so the
refusal protected nobody. The owner made that call. AGENTS.md
still lists the old refusal as an example of a judgement about rigour.
Simplicity against performance
Simplicity wins while the performance cost is unmeasured.
Search indexing runs synchronously inside the command handlers. A queue or a
database trigger takes the work off the write path. DocumentSearchIndexer keeps
it in the handlers, because that is what makes it greppable. Nobody has measured
a write that is too slow.
Measure the cost, and the answer flips, because a measured problem is a bug.
ad5de641 accepts real complexity for a measured number. A per-row regconfig
kept the search off idx_documents_search_vector, which cost 8.6 ms against
0.5 ms on 200,000 rows. The fix emits one query branch per language and adds a
composite index.
Simplicity against shipping speed
Simplicity wins. Make the change easy, then make the easy change.
The audit subsystem landed as one branch in 39129f49. 1183917f then moved the
audit trail out into ubermuda/audit-bundle, and 08e2c182 did the same for the
health checks. Each extraction delayed other work, and each left one boundary in
place of a module that keeps growing.
Performance against shipping speed
Performance wins when it is measured. A measured problem is a bug, and a bug outranks getting the feature out.
ad5de641 again. Full-text search already worked, and the branch was ready. The
measurement held it until the query plan used the index.
An unmeasured performance worry raises no collision at all. Note it, ship, and measure later.
Architecture Decision Records
Write an ADR on judgement. No class of change requires one. A new dependency, a schema change and a cross-module boundary are each a reason to consider an ADR. None of them compels one.
Write one when a future reader asks why, and the code cannot answer. Skip one when the commit message or a docblock carries the reason well enough.
The records live in docs/adr/, and docs/adr/index.md lists them.
- Put the file at
docs/adr/NNNN-short-slug.md, and take the next number. - Give it the front matter every page under
docs/has,titleanddescription. - Use the sections that
docs/adr/index.mdnames. - Name the options you rejected, and name the cost the decision accepts.
- Add the record to the list in
docs/adr/index.md.
A page under docs/ publishes to the public documentation site by default. The
loader skips a file name that starts with _, and it skips the internal paths
that website/src/content.config.ts excludes. A directory name that starts with
_ does not hide the files inside it. Add the record to the “Decision records”
group in website/astro.config.mjs, or the page is reachable only by its URL. Write every
ADR as public text.
Decisions with no record
The reasoning for these four lives in pull request comments and in docblocks. They are candidates for an early ADR. Nobody has to write them.
- Adopting
martin-georgiev/postgresql-for-doctrinein place of hand-rolled Doctrine classes. The library bounds PHP at^8.2 <8.6, and the project accepted that bound. - Keeping search indexing synchronous.
- Saving a decision answer with last-write-wins instead of refusing a stale one.
- Moving the HTML sanitizer to an explicit per-element allowlist.