An Architecture Decision Record (ADR) captures a single durable architectural decision together with the reasoning behind it: the context, the problem, the options that were considered, the evidence that informed the choice, the decision itself, and the consequences that follow.
ADRs exist so that wp-exelearning contributors — human and AI — can answer
"why is it built this way?" long after the fact, without archaeology through
pull request threads or chat logs. A decision recorded only inside a PR
description is easy to lose; an ADR is a first-class, long-lived document.
Guiding principles:
- Evidence before preference. Prefer a verifiable source over an assertion.
- No technical claim without a source. Cite a repository path + commit, official WordPress documentation, a benchmark, a reproducible experiment, an issue, a PR, a change document, or a previous ADR.
- Separate facts, interpretation and decision. Say what is observed, what it means, and what was decided — in that order.
- Stable IDs. An identifier is never reused and never changes once published.
- Append-only. Accepted decisions are not rewritten; they are superseded.
ADRs and change documents are complementary, not competing.
| Artifact | Answers | Lifetime |
|---|---|---|
| Change document | What will be built and how a significant change will be implemented | May become historical once implemented |
| ADR | Which durable decision was made and why | Long-lived, append-only |
| PR | The concrete code/doc changes under review | Historical review record |
A large change usually starts with a proposal and a design in its change directory. Inside that design, the decisions that will outlive the change itself — a storage layout, a file-format guarantee, a security boundary — should be extracted into ADRs or linked to existing ones. The design records how; the ADR records what was decided and why. Do not copy a whole design document into an ADR.
- …an implementation plan. That is what a change document is for.
- …a changelog entry. Use
readme.txtfor user-facing release notes.
ADRs are identified by their GitHub tracking number, not by a global counter.
The tracking number is the change's issue when it has one, and its pull
request when it does not. GitHub draws issue and pull-request numbers from a
single repository-wide sequence, so the two can never collide — in GitHub's data
model a pull request is an issue, which is why /issues/<n> resolves to a pull
request.
In this repository the tracking number is always a pull request number. Issues are disabled on
exelearning/wp-exelearning; work is coordinated in the mainexelearning/exelearningrepository, whose issue numbers come from a different sequence and therefore cannot identify a record here. Link a cross-repository issue underexternal_refs, and identify the record by the pull request that carries it.
This follows the decision taken in the main repository,
exelearning/exelearning#2232
(ADR-2232-01, Identify architecture records by tracking issue instead of a
global counter). The rationale, the rejected alternatives and the evidence live
there; this repository adopts the model and records only its own deviations —
see ../migration-map.md.
ADR-<tracking-number>-<local-sequence>-<decision-slug>.md
For example, pull request #88 could produce two decisions:
ADR-88-01-first-decision-slug.md
ADR-88-02-second-decision-slug.md
- A GitHub tracking number is required before an ADR is finalized. Here that is the pull request that carries the decision. Never open an issue solely to obtain a number — and in this repository you cannot, because issues are disabled.
<tracking-number>has no leading zeros.<local-sequence>is two digits, scoped only to that tracking number, starting at01. It is present even when a change has a single ADR, so that adding a second one later never renames the first.- A local sequence is never reused within the same tracking number, even if a record is rejected or removed.
<decision-slug>is lowercase kebab-case and names the decision, not the topic.make-distignore-single-source-of-truthis a decision;release-packagingis a topic.- The frontmatter
idmust equalADR-<tracking-number>-<local-sequence>, andtracking_issuemust equal the tracking number. CI enforces both. The field keeps the nametracking_issuebecause GitHub models a pull request as an issue; it holds whichever number identifies the change. - The document H1 must be
# <id>: <title>. - There is no global counter and no next-free-number to compute. Two branches can only collide if they share a tracking number.
- Identifiers are stable once published. If the situation around a record changes, record that in the change document — do not renumber.
- ADRs live in
docs/architecture/adr/. template.mdis the canonical template.- There is no committed index. Run
make architecture-recordsto print one, derived from frontmatter. A generated file in git conflicts on every concurrent branch, and this index is contributor-facing — it is not published docs. ../migration-map.mdmaps every retired identifier to its current path.
| Status | Meaning |
|---|---|
Proposed |
Under discussion; not yet agreed. |
Accepted |
Agreed and in force. |
Rejected |
Considered and declined. Kept for the record. |
Superseded |
Replaced by a later ADR (see superseded_by). |
A decision that is still being debated stays Proposed. It becomes Accepted
only after reviewer approval on the PR.
Status lives in the frontmatter only. Do not add a ## Status section that
repeats it — one canonical source per mutable field.
| Field | Required | Canonical source for |
|---|---|---|
id |
yes | the record's identity (must match the filename) |
title |
yes | the record's title (mirrored by the H1) |
status |
yes | lifecycle state |
date |
yes | creation date, YYYY-MM-DD |
tracking_issue |
yes | the GitHub number that owns this decision — a PR number in this repository |
legacy_id |
migrated records only | the retired identifier |
related.prs |
no | implementation / review traceability, as bare numbers |
related.changes |
no | change directories this decision belongs to |
related.adrs |
no | sibling decisions |
external_refs |
no | cross-repository issues and PRs, as full URLs |
supersedes / superseded_by |
no | decision history |
ai_assistance.tool / .model |
yes | provenance (none if unused) |
related.prs is traceability metadata: it lists every PR of this
repository that implements or reviews the decision, and it is not what identifies
the record. Its tracking number is the single value in tracking_issue, chosen
once and then stable, not the growing list in related.prs.
external_refs exists because this plugin's work is frequently coordinated from
the main eXeLearning repository or mirrored in the Moodle plugin. Those numbers
belong to other sequences, so they are stored as full URLs and never as bare
numbers.
The frontmatter deliberately records tools and links, not the names of
people — use PR links for attribution. This is why deciders and reviewers
are not part of the schema here, unlike in the main repository.
Create or update an ADR when a change introduces or modifies a durable architectural decision — one that future contributors should not have to re-litigate. In this plugin that typically includes decisions affecting:
- the storage model for ELPX packages and their metadata (attachment post meta,
the
wp-content/uploads/exelearning/extraction layout, checksums); - ELPX validation and extraction (ZIP handling, path-traversal protection, size limits);
- the embedded editor installation/build flow and where its assets are stored;
- the content proxy and the security model around serving extracted content;
- the style registry and where uploaded style packages are stored;
- WordPress integration boundaries: capabilities, nonces, REST API contracts, shortcode/block attribute contracts, Media Library integration;
- backward-compatibility guarantees for shortcodes, blocks, hooks, or stored metadata;
- any AI-assisted generation workflow or policy.
If in doubt, prefer writing a short ADR over losing the reasoning.
An ADR is worth writing, even when not strictly required, when a change would otherwise leave reviewers guessing why a non-obvious approach was chosen — for example picking one WordPress API over another, or accepting a trade-off that a later contributor might be tempted to "fix".
Do not write an ADR for:
- bug fixes that restore intended behavior;
- routine refactors with no externally observable decision;
- dependency bumps, lint/format changes, copy edits;
- translation-only or test-only changes;
- purely local implementation details with no cross-cutting impact.
Do not create an ADR per section of a design document, and do not create empty ADRs to fill a gap in the sequence — the sequence is expected to have gaps.
If a change is significant enough to need a design but does not yet lock a durable decision, start with a change document instead.
Every technical claim in an ADR should cite a verifiable source. Acceptable evidence includes:
- a repository path plus commit (e.g.
includes/class-elp-file-service.php@abc1234); - official WordPress documentation or a specification;
- a benchmark or a reproducible experiment (numbers + how to reproduce);
- a linked PR, change document, or prior ADR.
Keep the evidence inside the ADR. This repository does not maintain a
separate sources/ or experiments/ tree.
- Alternatives considered — list the realistic options, with their pros and cons, so a reader can see the decision was a choice and not an accident.
- Consequences — state the positive, negative and neutral effects honestly. A decision with no downsides is usually a decision that was not examined closely enough.
If an AI tool helped draft or research an ADR, disclose it in the frontmatter:
ai_assistance:
tool: "Claude Code" # tool / interface used
model: "claude-opus-5" # model, when relevantIf no AI tool was involved, set both fields to none. Disclosure is about
traceability, not judgement: it records how the document was produced so the
evidence can be weighed accordingly.
Accepted ADRs are append-only. Do not rewrite them except to fix typos or broken links.
To change an accepted decision:
- Create a new ADR under the tracking number that motivates the change.
- Set
supersedes: [ADR-<old-id>]in the new ADR's frontmatter. - Set
status: Supersededandsuperseded_by: [ADR-<new-id>]in the old ADR. - Run
make architecture-checkto validate the relationship.
CI rejects a one-sided relationship: both directions must be present, and a
superseded ADR must carry status: Superseded.
Refer to ADRs by their ID so links stay stable:
- From code / comments:
// See ADR-72-01: the editor ships in release packages only. - From docs:
[ADR-72-01](adr/ADR-72-01-bundle-editor-exclusively-in-release-packages.md)(adjust the relative path). - From a change document: list the ADR in
related_adrsand in the ADRs required or referenced table. - From a PR: mention
ADR-72-01and link the file.
Retired identifiers — the old four-digit ADR-NNNN / SDD-NNNN form — must not
appear in new content. CI fails on them; use
../migration-map.md to find the current identifier.
| Command | What it does |
|---|---|
make architecture-check |
Validates identifiers, metadata and cross-references. Non-zero on failure. |
make architecture-records |
Prints the ADR and change indexes, derived from frontmatter. |
Both are thin wrappers around node bin/architecture-records.mts, also available
as composer architecture-check / composer architecture-records.
make architecture-check runs as part of make check, and in CI from the
Architecture records workflow, which — unlike the main CI workflow — is not
filtered by paths-ignore and therefore also runs on documentation-only PRs.
- Identify the change's GitHub tracking number — the pull request that carries it.
- Identify the durable decision (see When an ADR is required).
- Copy
template.mdtoADR-<number>-<NN>-<decision-slug>.md, where<NN>is the next free local sequence for that tracking number only (01if it is the first). - Fill in context, problem, options, evidence, decision and consequences.
Start at
status: Proposed. - Run
make architecture-checkto validate.make architecture-recordsprints the current index if you want to read it. - Open (or reference) the PR. Reviewers discuss and, if agreed, the status moves
to
Accepted. - If a later change reverses the decision, supersede it — never edit the accepted record.
- The change has a tracking number (its pull request), and the filename uses it.
- The local sequence is the next free one for that number, starting at
01. - The slug names the decision, not the topic.
- Frontmatter
idmatches the filename;tracking_issuematches the number. - The H1 is
# <id>: <title>. - Context, problem, alternatives, decision and consequences are all present.
- Every technical claim cites a verifiable source.
- Positive, negative and neutral consequences are stated honestly.
-
statusreflects reality (Proposedwhile under discussion), and appears only in the frontmatter. -
ai_assistanceis filled in (values ornone). - Cross-repository links are in
external_refsas full URLs, not inrelated.prs. - Superseding ADRs set
supersedes/ the old ADR setssuperseded_byandstatus: Superseded. -
make architecture-checkpasses.