Skip to main content
AcademytutorialOpenSpec tutorial series — Part 3: ADRs, the standing context

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.

TutorialOpenSpecSpec-firstADRArchitectureTutorial series
10 min read

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:

SpecADR
AnswersWhat must this feature do?How must anything be built here?
Scopeone capabilitythe whole app, or the whole fleet
Lifetimea change, then archivedstanding, evolves slowly
Formrequirements + 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-apply skill reads them from hydra's main branch.

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":

  1. Write a new adr-NNN-<topic>.md in hydra/openspec/architecture/, next number, in the compact rule format above.
  2. Keep it to a handful of judgement rules. If a line is mechanically checkable, propose a gate instead, or as well.
  3. Open it as an OpenSpec change in hydra. The decision gets reviewed like any other change before it becomes standing context.
  4. 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).