OpenSpec tutorial series — Part 3: ADRs, the standing context
Specs say what one feature must do. ADRs say how every feature must be built. This part explains Architecture Decision Records, the compact judgement-only rules an AI reads before it writes a line, and how Conduction splits them into company-wide ADRs (in hydra) and per-app ADRs (in each repo), with a model for adding and promoting them.
A spec tells you what one feature must do. But "build a search box" leaves a hundred questions unanswered that have nothing to do with search. Which data layer? Vue 2 or Vue 3? Where do modals live? Which licence header? Answering those per spec would be madness, and an AI would answer them differently every time. That is what ADRs are for. They are the standing context that every spec is built on top of. This part explains what they are, how they are written, and how Conduction splits them across the fleet.
Spec vs ADR: two different questions
These two artefacts answer two different questions. Keeping them apart is the whole trick:
| Spec | ADR | |
|---|---|---|
| Answers | What must this feature do? | How must anything be built here? |
| Scope | one capability | the whole app, or the whole fleet |
| Lifetime | a change, then archived | standing, evolves slowly |
| Form | requirements + scenarios (RFC 2119 + Gherkin) | a short list of rules |
| Example | "users can search pets by name" | "all domain data goes through OpenRegister; no custom mappers" |
A spec is read once per feature. An ADR is read before every feature. When /opsx-apply implements a change, it loads the relevant ADRs first. They are the constraints the implementation has to satisfy no matter what the spec says. The spec cannot override them. It sits inside them.
This is the same shared context a human teammate would carry in their head. Part 0 made the point: a human colleague fills the gaps with knowledge they never wrote down, and an LLM has none of that by default. ADRs are how we write that shared context down, so the agent makes the same standing calls a senior colleague would.
What an ADR actually looks like
The name "Architecture Decision Record" sounds heavy. In Conduction practice an ADR is deliberately light. It is a compact list of imperative rules, not an essay. Here is the shape of a real company-wide ADR (adr-001-data-layer.md):
## Data layer
- ALL domain data → OpenRegister objects. NO custom Entity/Mapper for domain data.
- App config → `IAppConfig`. NOT OpenRegister.
- Cross-entity references: OpenRegister relations (register+schema+objectId).
MUST NOT store foreign keys or embed full objects.
### Schema standards
- Schemas: PascalCase, schema.org vocabulary, explicit types + required flags.
- MUST NOT invent custom property names when a schema.org equivalent exists.
- Contact schemas MUST align with vCard properties (fn, email, tel, adr).
Notice what is not there: no rationale paragraphs, no diagrams, no "we considered X and Y". Each line is a rule a human or an AI can apply directly. The reasoning lives in the commit history and the change that introduced the ADR. The ADR file itself is the decision, distilled.
So the test for "does this belong in an ADR?" is simple. Can a script check it? If yes, it is a gate. If it needs a human or AI judgement call, it is an ADR.
Two tiers: company-wide and per-app
Conduction runs a whole fleet of apps (openregister, opencatalogi, openconnector, docudesk, and more). Most architecture decisions are the same across all of them: same data layer, same frontend stack, same security posture. A few are unique to one app's domain. So ADRs live at two levels, each with one canonical home:
hydra/openspec/architecture/ ← COMPANY-WIDE. Applies to every app.
adr-001-data-layer.md OpenRegister, schemas, seed data
adr-004-frontend.md Vue 2, Pinia, @conduction/nextcloud-vue
adr-005-security.md auth, CORS, input validation
adr-008-testing.md Playwright=UI, Newman=API, PHPUnit=unit
adr-014-licensing.md EUPL-1.2 on every file
adr-020-gate-scope.md gates run on the PR diff
adr-022-consume-abstractions.md apps consume OpenRegister, no wrapper CRUD
... (~30 more)
<app>/openspec/architecture/ ← PER-APP. Applies to this app only.
adr-001-<domain-decision>.md e.g. a case-management state machine
The rule of thumb is sharp. Only something truly unique to one app goes in that app's openspec/architecture/. Everything else lives company-wide in hydra/. An app's vendor-specific integration pattern or domain state machine is app-level. "We use Vue 2" is fleet-level, and must never be restated in an app.
Apps adopt company-wide ADRs, they never copy them
This is the rule that keeps the fleet coherent, and it was learned the hard way. App repos do not carry their own copies of company-wide ADRs. They adopt them. The company-wide ADRs are the source of truth, and the app's build process reads them directly.
How the build actually sees them:
- In the Hydra factory, the company-wide ADRs are baked into the builder and reviewer container images at build time, copied straight from the hydra repo.
- In an IDE or manual run, the
/opsx-applyskill reads them from hydra'smainbranch.
Either way there is exactly one authoritative copy. Why does this matter so much?
Setting up ADRs: adding and promoting
Two operations come up in practice. Both are deliberately lightweight. An ADR is just a markdown file in the right folder.
Adding a new company-wide ADR. When the fleet makes a decision that every app must follow, for example "from now on, all notifications use the x-openregister-notifications dialect":
- Write a new
adr-NNN-<topic>.mdinhydra/openspec/architecture/, next number, in the compact rule format above. - Keep it to a handful of judgement rules. If a line is mechanically checkable, propose a gate instead, or as well.
- Open it as an OpenSpec change in hydra. The decision gets reviewed like any other change before it becomes standing context.
- Once merged, every app picks it up automatically on the next build. No per-app edits.
Adding a per-app ADR. Same steps, but the file lands in <app>/openspec/architecture/ and only constrains that app. Use it for genuinely local decisions: a domain-specific workflow, a storage choice that only makes sense here.
Promoting an app ADR to fleet-wide. Sometimes a decision made in one app turns out to apply everywhere. To promote it, move the rule into a hydra/ ADR (new or existing), delete it from the app, and open it as a hydra change. This is the reverse of the stale-copy trap. You are collapsing a local decision into the single canonical home, not spreading copies.
How ADRs and gates work together
You have now met both halves of the "how" sandbox. They are complementary:
- ADRs are the judgement rules an AI reads up front and is trusted to apply ("use schema.org vocabulary", "align contacts with vCard").
- Gates are the mechanical rules a script enforces after the fact ("EUPL-1.2 header present", "no unreachable route", "every method has a
@spec").
Many gates exist precisely to enforce a decision an ADR records. Gate-22 enforces the manifest ADR. Gate-18 enforces the notifications dialect ADR. Gate-13 enforces the modal-isolation rule from the frontend ADR. The ADR is the law. The gate is the inspector. Together with the spec, which is the what, they are the three walls of the sandbox you saw in Part 4.
Test yourself
1. "Every PHP file must start with an EUPL-1.2 SPDX header." ADR or gate?
Hint
Can a script check it without any judgement?
Answer
A gate (gate-1, spdx-headers). It is mechanically checkable, a grep confirms it, so it does not belong in an ADR. The licensing choice (EUPL-1.2) is recorded in an ADR. The enforcement of the header is a gate.
2. Your app needs "all domain data through OpenRegister". Where is that decided, and do you write it in your app's openspec/architecture/?
Hint
Is this unique to your app, or true for the whole fleet?
Answer
It is a company-wide decision (ADR-001, data layer), living once in hydra/openspec/architecture/. You do not restate it in your app. Apps adopt company-wide ADRs, they never copy them. Your app's openspec/architecture/ is only for decisions unique to your app's domain.
3. Why is there a hard 80-to-120-line budget on company-wide ADRs?
Hint
Who reads them, and what happens to a long document?
Answer
Because every line competes for attention, from both human reviewers and the AI builder that reads ADRs before implementing. A bloated ADR gets skimmed and ignored. A tight one gets followed. The budget forces the question "does this rule really need judgement?", and pushes everything mechanically checkable into gates instead.
Where to next
You now have both halves of the sandbox: the what (specs) and the how (ADRs and gates).