Architecture Decision Record
beta · Family decision-docs · Phase develop · Sizes lean, full · ~2,000 tokens
Records one architecturally significant decision: the forces that made it necessary, the options genuinely considered, the option chosen, and the consequences accepted. Immutable once accepted; superseded by a new record rather than edited.
Fast reference for using the ADR bundle. For the full reasoning, history, and sources, read
adr_companion.md.
When to use
Section titled “When to use”- A decision is expensive or slow to reverse: a persistence engine, a data model, a public API shape, an auth scheme, a language or framework, a hard dependency.
- Reasonable engineers disagreed, and the disagreement is worth preserving.
- A future reader will predictably ask “why is it like this, and did they consider X?”
- The decision looks wrong from outside but was deliberate. This is the highest-value record you will ever write.
- You are backfilling a decision made months ago that the team keeps re-litigating. Legitimate, and explicitly endorsed practice. Date it when the decision was made, and say it was reconstructed.
When NOT to use
Section titled “When NOT to use”- The decision is cheap to reverse. Use the reversibility test, not a significance test: if undoing it next quarter costs an afternoon, it does not need a record. This is the single most useful filter, and skipping it is how a decisions directory fills with noise.
- The decision has not been made yet. You want an RFC or a design proposal. An RFC requests input; an ADR records an outcome. Run the RFC, then write the ADR when the call is made.
- It is an implementation detail. A variable name, a minor refactor, a library version bump.
- It is already stated in a rulebook. If your methodology or contributing guide already mandates it, an ADR restating it adds a second source of truth, which is worse than none.
- You are writing it so nobody can be blamed later. That is not a record, and everyone can tell.
Pick a variant
Section titled “Pick a variant”- Lean (default, and it is not a compromise): the three sections MADR marks mandatory. Most decisions are recorded honestly in half a page. A short record that gets written beats a thorough one that does not.
- Full: when the decision is contested, crosses teams, carries regulatory or safety weight, or you expect to defend it to an auditor. Adds Decision Drivers, Confirmation, Pros and Cons of the Options, and More Information.
Grow lean into full by adding sections; never reorder the shared ones. The full variant is a strict superset.
The scaling signal is the contestedness of the decision, not the importance of the system.
Quality rubric (self-grade before you commit)
Section titled “Quality rubric (self-grade before you commit)”- The title names the decision, not the topic. A stranger scanning the folder knows what was chosen.
- The context describes the situation, not the argument you won. It was not written backwards from the conclusion.
- The context records conditions that could stop being true, so a future reader can tell whether the decision has expired.
- “Do nothing” appears in Considered Options, or its absence is deliberate.
- No straw men. Every option listed was genuinely on the table.
- The outcome is in active voice (“We will …”), and a human being is named in
decision-makers. - There is at least one real negative consequence, and it is one someone would actually act on. “Small learning curve” does not count.
- The chosen option has honest cons listed, not just the rejected ones.
- (Full) Decision Drivers exist and are falsifiable: at least one option fails at least one driver. If every option passes every driver, the real reason is still unwritten.
- (Full) Confirmation names a real check, or honestly states that nothing enforces this.
-
statusis correct, and if this supersedes an earlier record, that record has been updated to point here. - Filed at
docs/internal/decisions/NNNN-kebab-title.mdwith a fresh, never-reused number. - Every guidance comment deleted; no placeholders remain.
Named anti-patterns (the usual wrecks)
Section titled “Named anti-patterns (the usual wrecks)”- Approval theater. Records written after the fact for decisions nobody contested, so the folder looks disciplined. They bury the records that matter.
- The CYA record. Written to spread accountability rather than inform. The tells are passive
voice and a
decision-makersfield that is either empty or lists the entire org chart. - Decision without alternatives. The costliest of these, whatever its frequency. The record cannot answer the only question it will ever be asked.
- Stale status. A record still marked
acceptedfor something ripped out two years ago is not merely useless; it actively misleads whoever finds it. - Scope bloat. Everything becomes an ADR, and the architecture gets harder to see. The artifact inverts its own purpose.
- All-upside consequences. If the decision cost nothing, it was not a decision.
- The wiki. Records stored anywhere but the repo are reliably records nobody reads.
Coming from Nygard’s format?
Section titled “Coming from Nygard’s format?”Every Nygard section maps into MADR without loss. You are not rewriting anything, you are renaming.
| Nygard (2011) | MADR (this template) |
|---|---|
| Title | Title (H1) |
| Status | status in YAML frontmatter |
| Context | Context and Problem Statement |
| Decision | Decision Outcome |
| Consequences | Consequences (under Decision Outcome) |
| (not in Nygard) | Considered Options, and the rest of the full variant |
The one substantive addition is that MADR makes Considered Options a mandatory section, where Nygard’s five sections have no dedicated home for the alternatives at all. Promoting them to a required list of their own is the most useful thing MADR changed, because the alternatives are the section teams silently drop, and a record without them cannot answer the only question it will ever be asked. (That is this bundle’s reading of the difference between the two formats, not a claim about what Nygard intended.)
Compatibility note: the develop-adr skill
Section titled “Compatibility note: the develop-adr skill”This bundle’s pairs_with is develop-adr in
pm-skills, which drafts an ADR interactively.
Be aware that the two currently differ. The skill’s bundled template follows Nygard’s format
(Status / Context / Decision / Consequences / Alternatives Considered / References). This bundle
follows MADR v4, which is the format the wider product-on-purpose organization has standardized
on for its own decision records, and which its project scaffolding expects at
docs/internal/decisions/.
Until they converge, use the mapping table above: content drafted by the skill transfers into this template section for section. If you are recording a decision inside a product-on-purpose repository, use this template, because it is the format the org’s tooling and conventions expect.
(This divergence was found while building this bundle, and it is logged as a finding for pm-skills rather than silently patched. It is exactly the kind of drift a template library exists to surface.)
The artifacts
Section titled “The artifacts”adr_template-lean.md · ~2,000 tokens
---status: "{{status}}"date: "{{date}}"decision-makers: ["{{decision_makers}}"]doc_type: adrsize: leansource_template: adrsource_template_version: 0.1.0---
<!--LEAN ADR. The three sections MADR marks mandatory, and nothing else. This is the right default:most decisions are recorded honestly in half a page, and a short record that gets written beats athorough one that does not. To grow this into a full ADR, ADD sections (see adr_template-full.md);never rename or reorder the ones below, because the full variant is a strict superset of this one.
WHERE THIS FILE GOES docs/internal/decisions/NNNN-short-kebab-title.mdNumbered sequentially from 0001. Numbers are never reused, not even for a record that ends uprejected or superseded. The number is an address, not a ranking.
IMMUTABILITY, THE RULE THAT MAKES THIS ARTIFACT WORTH ANYTHINGOnce this record is accepted, do not rewrite the decision. Write a NEW record and set this one'sstatus to "superseded by ADR-NNNN". A decisions folder that silently rewrites itself is just awiki with extra steps. The trail is the product.(One narrow exception, and only one: a factual error in the record, as opposed to a change in thedecision, is corrected in place with the correction dated and the error named. Seeadr_companion.md section 6 for why that line sits exactly there.)
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into adr_companion.md for the deep reasoning), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid.2. Replace each {{placeholder}} with your content.3. If a section does not apply, write "N/A" and one line of why, rather than deleting it.4. Before you ship: self-grade against adr_guide.md, then DELETE every HTML comment. They are guidance, not content.-->
# {{title}}
<!-- WHAT A short title naming BOTH the problem solved and the option chosen, in plain words. Phrased as a statement, not a question. WHY The title is the only part of this record most people will ever read. A decisions directory is browsed as a list of filenames; a title that names a topic instead of a decision is invisible in that list. Deep dive: adr_companion.md section 3 (Anatomy > Title). ASK What was the problem? What did we choose? Scanning the folder, would a stranger know? GOOD "The meta declares the size contract; single-size bundles are a legal shape" WEAK "Database decision" (a topic, not a decision; nobody can act on it) TRAP Naming the topic rather than the decision. "Caching" is a folder name. "Use Redis for the session cache, accepting a new operational dependency" is a record. -->
## Context and Problem Statement
<!-- WHAT The forces in play, and the problem they create, in two or three sentences or a short story. Make the SCOPE explicit: name the components or boundaries this decision binds. WHY Context is what expires. The decision may stay right while the reason for it quietly stops being true, and the only way a future reader can tell is if you wrote down the conditions you were under. This section is the record's shelf-life label. Deep dive: adr_companion.md section 3 (Anatomy > Context and Problem Statement). ASK What forces are pressing (technical, business, organizational)? What constraint, deadline, or skill gap is real? What breaks if we do nothing? What does this bind? GOOD "Seven of the 27 Tier-1 types are single-size, so the next bundle we build is likely to be one, and the gate cannot pass it: two checks hardcode the assumption that every bundle ships exactly lean and full." WEAK "We need to decide on our caching strategy." (restates that a decision exists; names no force, no constraint, and nothing that would ever stop being true) TRAP Writing the context AFTER the decision, as justification. Then it records the argument you won, not the situation you were in, and it teaches a future reader nothing. -->
{{context_and_problem}}
## Considered Options
<!-- WHAT The options genuinely on the table. Titles only here; the reason the winner won goes in Decision Outcome below. (The full variant adds a "Pros and Cons of the Options" section for weighing each one properly. If you find you need it, you want that variant.) WHY The rejected options are the reason this artifact exists. A record that lists only the winner cannot answer the one question it will actually be asked, which is "did you think about X?" Without the alternatives, a future team re-litigates from zero. Deep dive: adr_companion.md section 3 (Anatomy > Considered Options), and the anti-pattern in section 7 (recording the decision but not the alternatives). ASK What else was seriously on the table? What was the obvious option we rejected, and the one a newcomer will ask about in six months? Is "do nothing" one of them? GOOD * Make the meta the size contract, enforced in both directions * Waive the checks whenever a variant file is absent * Require every type to ship two variants, padding out a full for single-size types WEAK A single bullet, or a list padded with options nobody would ever have chosen. TRAP Straw men. Two decoys and a winner is not a decision record, it is a press release. If an option was never real, leave it out and say so if it comes up. -->
* {{option_1}}* {{option_2}}* {{option_3}}
## Decision Outcome
<!-- WHAT The option chosen, and the justification, in active voice: "We will ...", not "It was decided ...". Then the consequences, honestly, both directions. WHY Active voice puts a decision-maker back into the sentence. Passive voice ("it was decided") is how a record becomes accountability theater: everyone signed, nobody chose. Deep dive: adr_companion.md section 3 (Anatomy > Decision Outcome); the responsibility-deflection failure is in section 7. ASK Which option, and because of what? What gets harder now? What are we knowingly paying? GOOD "Chosen option: the meta is the contract, because it lets the gate tell a deliberately single-size bundle apart from a half-built one, which is the only thing a governance gate exists to do." WEAK "After discussion, option A was selected as the best fit." (no agent, no reason) TRAP Listing only good consequences. Nygard is explicit that ALL consequences are listed, not just the positive ones. A record with no cost is a record nobody believes. -->
Chosen option: "{{chosen_option}}", because {{justification}}.
### Consequences
<!-- WHAT What becomes easier, and what becomes harder. Name the price you agreed to pay. WHY The negative consequences are the highest-value lines in the whole record. They are what a future reader checks against reality to decide whether the decision still holds. Deep dive: adr_companion.md section 3 (Anatomy > Consequences). ASK What is now harder or more expensive? What risk did we accept on purpose? What would have to become true for this to have been the wrong call? GOOD Bad, because "sizes_available is now load-bearing: a bundle whose meta omits it fails the gate rather than being skipped. That is a deliberate behavior change. A contract you can silently omit is not a contract." WEAK "Bad, because there is a small learning curve." (a cost nobody would ever act on) TRAP A "Bad, because" line so weightless it is really a "Good" in disguise. If you cannot name a real cost, you have probably not understood the decision yet. -->
* Good, because {{positive_consequence}}* Bad, because {{negative_consequence}}adr_template-full.md · ~3,300 tokens
---status: "{{status}}"date: "{{date}}"decision-makers: ["{{decision_makers}}"]consulted: ["{{consulted}}"]informed: ["{{informed}}"]doc_type: adrsize: fullsource_template: adrsource_template_version: 0.1.0---
<!--FULL ADR. Every section MADR defines, mandatory and optional. Use this weight only when thedecision earns it: it is hard to reverse, it is contested, it crosses teams, it carriesregulatory or safety weight, or you expect to be asked to defend it.
Most decisions do not earn it. The lean variant (adr_template-lean.md) is the default, andreaching for this one by reflex is how a decisions folder fills up with ceremony nobody reads.The full variant is a strict superset of lean: the shared sections keep their names and theirorder, and this file only ADDS.
WHERE THIS FILE GOES docs/internal/decisions/NNNN-short-kebab-title.mdNumbered sequentially from 0001. Numbers are never reused, not even for a record that ends uprejected or superseded. The number is an address, not a ranking.
IMMUTABILITY, THE RULE THAT MAKES THIS ARTIFACT WORTH ANYTHINGOnce this record is accepted, do not rewrite the decision. Write a NEW record and set this one'sstatus to "superseded by ADR-NNNN". A decisions folder that silently rewrites itself is just awiki with extra steps. The trail is the product.(One narrow exception, and only one: a factual error in the record, as opposed to a change in thedecision, is corrected in place with the correction dated and the error named. Seeadr_companion.md section 6 for why that line sits exactly there.)
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into adr_companion.md for the deep reasoning), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid.2. Replace each {{placeholder}} with your content.3. If a section does not apply, write "N/A" and one line of why, rather than deleting it.4. Before you ship: self-grade against adr_guide.md, then DELETE every HTML comment. They are guidance, not content.-->
# {{title}}
<!-- WHAT A short title naming BOTH the problem solved and the option chosen, in plain words. Phrased as a statement, not a question. WHY The title is the only part of this record most people will ever read. A decisions directory is browsed as a list of filenames; a title that names a topic instead of a decision is invisible in that list. Deep dive: adr_companion.md section 3 (Anatomy > Title). ASK What was the problem? What did we choose? Scanning the folder, would a stranger know? GOOD "The meta declares the size contract; single-size bundles are a legal shape" WEAK "Database decision" (a topic, not a decision; nobody can act on it) TRAP Naming the topic rather than the decision. "Caching" is a folder name. "Use Redis for the session cache, accepting a new operational dependency" is a record. -->
## Context and Problem Statement
<!-- WHAT The forces in play, and the problem they create, in two or three sentences or a short story. Make the SCOPE explicit: name the components or boundaries this decision binds. WHY Context is what expires. The decision may stay right while the reason for it quietly stops being true, and the only way a future reader can tell is if you wrote down the conditions you were under. This section is the record's shelf-life label. Deep dive: adr_companion.md section 3 (Anatomy > Context and Problem Statement). ASK What forces are pressing (technical, business, organizational)? What constraint, deadline, or skill gap is real? What breaks if we do nothing? What does this bind? GOOD "Seven of the 27 Tier-1 types are single-size, so the next bundle we build is likely to be one, and the gate cannot pass it: two checks hardcode the assumption that every bundle ships exactly lean and full." WEAK "We need to decide on our caching strategy." (restates that a decision exists; names no force, no constraint, and nothing that would ever stop being true) TRAP Writing the context AFTER the decision, as justification. Then it records the argument you won, not the situation you were in, and it teaches a future reader nothing. -->
{{context_and_problem}}
## Decision Drivers
<!-- WHAT The criteria the decision is actually being judged against: the qualities you must have, the constraints you cannot break, the forces you must resolve. WHY Drivers are what turn an opinion into a decision. Stated up front, they are the yardstick every option gets measured with, and they are what stops the comparison below from being a rationalization of the answer you already liked. Deep dive: adr_companion.md section 3 (Anatomy > Decision Drivers). ASK What quality must this protect (latency, cost, auditability, reversibility)? What constraint is non-negotiable? Which driver wins if two of them collide? GOOD * "The gate must be able to distinguish a deliberately single-size bundle from a half-built one. This is the k.o. criterion: an option that fails it is out." * "No empty variant padded out of obligation (see ADR 0002, the variant model)." WEAK "It should be good and maintainable." TRAP Writing drivers that no option could ever fail. If every option satisfies every driver, the drivers are decoration and the real reason is still unwritten. Say which driver is the knockout. -->
* {{decision_driver_1}}* {{decision_driver_2}}
## Considered Options
<!-- WHAT The options genuinely on the table. Titles only here; the reasoning lives below in "Pros and Cons of the Options". WHY The rejected options are the reason this artifact exists. A record that lists only the winner cannot answer the one question it will actually be asked, which is "did you think about X?" Without the alternatives, a future team re-litigates from zero. Deep dive: adr_companion.md section 3 (Anatomy > Considered Options), and the anti-pattern in section 7 (recording the decision but not the alternatives). ASK What else was seriously on the table? What was the obvious option we rejected, and the one a newcomer will ask about in six months? Is "do nothing" one of them? GOOD * Make the meta the size contract, enforced in both directions * Waive the checks whenever a variant file is absent * Require every type to ship two variants, padding out a full for single-size types WEAK A single bullet, or a list padded with options nobody would ever have chosen. TRAP Straw men. Two decoys and a winner is not a decision record, it is a press release. If an option was never real, leave it out and say so if it comes up. -->
* {{option_1}}* {{option_2}}* {{option_3}}
## Decision Outcome
<!-- WHAT The option chosen, and the justification, in active voice: "We will ...", not "It was decided ...". WHY Active voice puts a decision-maker back into the sentence. Passive voice ("it was decided") is how a record becomes accountability theater: everyone signed, nobody chose. Deep dive: adr_companion.md section 3 (Anatomy > Decision Outcome); the responsibility-deflection failure is in section 7. ASK Which option, and against which driver did it win? Who is accountable for this? GOOD "Chosen option: the meta is the contract, because it lets the gate tell a deliberately single-size bundle apart from a half-built one, which is the only thing a governance gate exists to do." WEAK "After discussion, option A was selected as the best fit." (no agent, no reason) TRAP Justifying the winner without ever referring back to a driver. If the reason it won is not one of the criteria you listed above, one of the two sections is lying. -->
Chosen option: "{{chosen_option}}", because {{justification}}.
### Consequences
<!-- WHAT What becomes easier, and what becomes harder. Name the price you agreed to pay. WHY The negative consequences are the highest-value lines in the whole record. They are what a future reader checks against reality to decide whether the decision still holds. Nygard is explicit: ALL consequences are listed, not just the positive ones. Deep dive: adr_companion.md section 3 (Anatomy > Consequences). ASK What is now harder or more expensive? What risk did we accept on purpose? What would have to become true for this to have been the wrong call? GOOD Bad, because "sizes_available is now load-bearing: a bundle whose meta omits it fails the gate rather than being skipped. That is a deliberate behavior change. A contract you can silently omit is not a contract." WEAK "Bad, because there is a small learning curve." (a cost nobody would ever act on) TRAP A "Bad, because" line so weightless it is really a "Good" in disguise. If you cannot name a real cost, you have probably not understood the decision yet. -->
* Good, because {{positive_consequence}}* Bad, because {{negative_consequence}}
### Confirmation
<!-- WHAT How anyone can check that the implementation actually complies with this decision. Name the fitness function: the test, the lint rule, the CI check, the review step. WHY This is the section that separates a decision that binds from a decision that merely happened. An ADR nobody checks is a comment; an ADR a machine checks is a constraint. It is also the section teams skip most often, which is why so many decisions folders document an architecture the codebase no longer has. Deep dive: adr_companion.md section 3 (Anatomy > Confirmation) and section 6 (the AI-era argument for machine-checkable decisions). ASK What automated check would fail if someone violated this tomorrow? If none can exist, what manual review step catches it, and who runs it? GOOD "CI check F in tools/check-bundles.py enforces this on every push: sizes_available must exist, be non-empty, and use exactly one vocabulary. Verified against six fixtures, including a bundle with an undeclared stray variant file, which fails." WEAK "The team will keep this in mind during code review." TRAP Leaving this blank because no automated check exists. If the honest answer is "nothing enforces this", write THAT down. It is a real and useful finding about how much the decision is actually worth. -->
{{confirmation}}
## Pros and Cons of the Options
<!-- WHAT Each option, weighed against the decision drivers above. Including the one you chose. WHY This is the audit trail of the thinking, and it is what makes the record defensible rather than merely declarative. Weighing the WINNER honestly (it has real cons, or it would not have been a decision) is what signals the analysis was real. Deep dive: adr_companion.md section 3 (Anatomy > Pros and Cons of the Options). ASK For each option: what does it buy, what does it cost, and which driver kills it? GOOD "Good, because it needs no new dependency. Bad, because it cannot distinguish intent from incompletion, which is the knockout driver. Rejected on that ground alone." WEAK Every rejected option getting a single "Bad, because it is worse." TRAP A winner with no cons listed. It tells the reader you were selling, not deciding, and it is the fastest way to lose their trust in the rest of the record. -->
### {{option_1}}
* Good, because {{option_1_pro}}* Bad, because {{option_1_con}}
### {{option_2}}
* Good, because {{option_2_pro}}* Bad, because {{option_2_con}}
## More Information
<!-- WHAT Whatever a future reader needs and cannot get from the sections above: links to related records, the evidence behind the call, the team agreement, and when this should be revisited. WHY This is where you set the record's expiry. A decision made under conditions that will predictably change should say so, with a trigger, so that the next team knows whether they are reading history or law. Deep dive: adr_companion.md section 3 (Anatomy > More Information). ASK Which records does this supersede, refine, or depend on? What evidence backs it? What event should make us revisit it? GOOD "Refines ADR 0002 (the variant model), which flagged this exact loose end in its own consequences. Revisit if a third size vocabulary is ever proposed." WEAK A bare link dump with no statement of the relationship. TRAP Using this as a junk drawer. If a link matters to the decision, say in one clause WHY it matters; if it does not, leave it out. -->
{{more_information}}---status: accepteddate: "2026-07-12"decision-makers: [jprisant, claude]consulted: []informed: []doc_type: adrsize: fullsource_template: adrsource_template_version: 0.1.0---
# The meta declares the size contract; single-size bundles are a legal shape
## Context and Problem Statement
An earlier decision, ADR 0002 (the variant model), settled that a document type ships the number ofsize variants it earns rather than a fixed two, and it left a loose end sitting in its ownconsequences: a note that single-size types would need `sizes_available: [lean]` with the nestingcheck waived, and that the refinement should be recorded as its own decision when the firstsingle-size bundle was actually built.
That moment arrived. Seven of the 27 Tier-1 types in the master catalog are single-size, so the nextbundle we build is likely to be one, and the governance gate cannot pass it. Two of its checkshardcode the assumption that every bundle ships exactly `lean` and `full`:
- **Check A (files)** requires all eight canonical files, including both variant files, so a single-size bundle fails with `missing: template-full.md`.- **Check C (nesting)** hard-fails when either variant file is absent, because it has nothing to compare.
The obvious fix is to relax both checks whenever a variant file is missing. That fix is wrong, andseeing why is the whole decision: it silently converts a real defect (a bundle that was *supposed* toship two variants and only shipped one) into a passing run. The gate would lose the ability to tell"deliberately single-size" apart from "half-built," which is the one thing a governance gate existsto do.
This decision binds `tools/check-bundles.py` and the `sizes_available` field in every bundle's`<type>_meta.yaml`.
## Decision Drivers
* **The gate must distinguish intent from incompletion.** This is the knockout criterion. Any option that leaves a half-built bundle passing green is out, regardless of its other merits.* **No empty variant padded out of obligation.** ADR 0002 (the variant model) settled this, and a fix that contradicts a standing decision is not a fix.* **Drift must be catchable in both directions.** A missing declared file and an undeclared present file are both forms of the bundle disagreeing with itself.* **The gate stays pure standard library.** ADR 0008 (gate as a local Python script) committed to no third-party dependencies, so no option may require a YAML parser.
## Considered Options
* **Option A:** Make `sizes_available` the size contract, and have the gate enforce the files on disk against it exactly, in both directions.* **Option B:** Waive the nesting and file checks whenever a variant file is absent.* **Option C:** Require every type to ship two variants, padding out a `full` for single-size types.
## Decision Outcome
Chosen option: **"Make `sizes_available` the size contract"**, because it is the only option thatsatisfies the knockout driver. The gate can distinguish a deliberately single-size bundle from ahalf-built one for the simplest possible reason: the bundle now *says* which it is, and the gateholds it to its word.
`sizes_available` stops being a field the gate cross-checks as an afterthought. It becomes thedeclaration of what the bundle **is**, and every structural check keys off it:
- **A (files):** the six core files, plus exactly one variant file per declared size. A declared variant that is missing fails. A variant file present on disk that the meta never declared **also** fails.- **C (nesting):** each variant must be an ordered subset of the next larger one (`lean` in `full`, or `s` in `m` in `l`). For a single-size bundle the rule is **vacuous, not waived**, and the gate says so out loud: `single-variant bundle {lean}; nesting rule not applicable`.- **F (meta):** `sizes_available` must exist, be non-empty, and use exactly one size vocabulary. `lean`/`full` and `s`/`m`/`l` are the two legal vocabularies and may not be mixed.
### Consequences
* Good, because a single-size bundle is now a **declared shape** rather than a hole in the checks.* Good, because the reverse direction is now caught. A stale `template-full.md` could previously linger in a bundle whose meta said lean-only and nothing would ever notice. This was a real gap, and it was found by making this change rather than by a user.* Good, because three-weight (`s`/`m`/`l`) bundles are now supported end to end, including transitive nesting, which the variant model always permitted but the gate never checked.* Bad, because **`sizes_available` is now load-bearing**. A bundle whose meta omits it fails the gate rather than being skipped. That is a deliberate behavior change and it will break any bundle authored before this record. A contract you can silently omit is not a contract, so the cost is accepted.* Bad, because the gate now **parses YAML with a regular expression**. The pure-stdlib driver forbids a YAML dependency, so `sizes_available` is read with a pattern that handles an inline list and a block list and would mis-read an exotic-but-legal YAML construction. This is a real fragility in the option we chose, and the honest mitigation is that it is confined to one function (`parse_sizes`) and would be replaced the moment the gate is allowed a dependency.* Consequence for the rulebook: **a bundle is no longer always eight files.** It is six core files plus one per declared size. Two variants gives eight (the common case), one gives seven, three gives nine. The methodology now states the rule rather than the common case, which is a better rule anyway.
### Confirmation
Enforced automatically, on every push to `main` and every pull request, by`.github/workflows/ci.yml`, which runs `tools/check-bundles.py`. Checks A, C, and F above are thefitness function, and a violation fails the build rather than producing a warning.
Verified at the time of the change against six purpose-built fixtures, each asserting a distinctfailure mode rather than only the happy path:
| Fixture | Shape | Expected ||---|---|---|| `aa-single` | declares `[lean]`, ships one variant | pass || `bb-sml` | declares `[s, m, l]`, nests transitively | pass || `cc-stray` | declares `[lean]`, but a `template-full.md` sits on disk | fail || `dd-missing` | declares `[lean, full]`, ships only `lean` | fail || `ee-mixed` | declares `[lean, m]`, mixing vocabularies | fail || `ff-badnest` | `lean` sections are not an ordered subset of `full` | fail |
All four bundles existing at the time continued to pass unchanged, which is the regression evidencethat the contract was tightened without moving the goalposts under work already done.
## Pros and Cons of the Options
### Make `sizes_available` the size contract
* Good, because the gate can finally distinguish intent from incompletion. This is the knockout driver, and this is the only option that clears it.* Good, because it catches undeclared stray files, a direction of drift no option else even considered.* Good, because it generalizes: a `s`/`m`/`l` bundle needs no further gate work.* Bad, because it makes a metadata field load-bearing, so a meta typo becomes a build failure.* Bad, because it forces regex-parsing of YAML under the pure-stdlib constraint (see Consequences).
### Waive the checks whenever a variant file is absent
* Good, because it is a three-line change and ships in ten minutes.* Bad, because it **fails the knockout driver outright**: a half-built bundle and a deliberately single-size bundle become indistinguishable, both passing green. Rejected on this ground alone, and no other merit could have rescued it.* Bad, because it makes the gate quieter over time. Every future variant mistake would pass in silence, and a gate that cannot fail is decoration.
### Require every type to ship two variants
* Good, because it keeps the gate's existing logic completely untouched.* Bad, because it directly contradicts ADR 0002 (the variant model), which settled that a type ships the variants it earns. Re-litigating a standing decision to avoid changing a script is the tail wagging the dog.* Bad, because a padded `full` variant that duplicates its `lean` is a lie told to a linter. It would teach every future author that the way past the gate is to produce a file nobody needs.
## More Information
Refines **ADR 0002 (the variant model)**, which flagged this exact loose end in its own consequencesand deferred it to the first single-size bundle. Constrained by **ADR 0008 (gate as a local Pythonscript)**, whose no-dependencies rule is directly responsible for the regex-parsing cost acceptedabove.
Revisit if either of two things becomes true: a third size vocabulary is proposed (the current designhardcodes two), or the gate is permitted a third-party dependency, at which point `parse_sizes`should be replaced with a real YAML parse and the second "Bad" consequence disappears.Provenance
Section titled “Provenance”The reasoning, the history and every source, in the repository:
- Companion - the long-form argument: why these sections, where the sources disagree, and what the bundle refuses to claim
- History - what changed in this bundle, and when
- Research log - every source consulted, with what each one actually supports
- Catalog metadata - the machine-readable record this page is generated from
Catalog record: 9 sections across 1 format(s).