repositoryStandards v1.1.20

ADR-040: The scale profile binds on reach, not on headcount

StatusAccepted
Date2026-08-07
Authorbodurkalukasz
Decided bythe author
Tagsprofiles, adoption, manifest, solo

Context

ADR-011 settled the mechanism - a profile field per manifest entry, self-verify --profile core|scale, views rendered from the manifest rather than hand-maintained - and it settled the principle: core keeps knowledge alive, scale coordinates people. It did not settle the trigger. The sentence that actually reaches an adopter was written later, in the profile picker (adoption.md) and repeated in the FAQ:

Start core, flip to scale when the second regular contributor arrives.

The trigger and the record name different populations. ADR-011 frames core as "every repo, even one person" and scale as "teams / enterprise", under the owner's constraint that "a solo adopter must not be asked to carry enterprise ceremony". A headcount trigger set at two makes a pair of developers the enterprise. Nothing on the scale list is what the second pair of hands changes: two people at one desk do not need a tracker bridge to know what the other is doing, curated release notes to tell each other what shipped, or a UX research cadence to agree who they are building for.

And the discount the picker promises is not the discount the manifest gives. Measured on this tree, by the tool's own count: --profile scale checks 91 entries, --profile core checks 82, and self-verify prints 9 scale-only entries skipped (--profile core). In SPEC.md, only R11's blocking coupling guard and R16's spec-guard step carry a (scale) marker. Choosing core discounts roughly a tenth of the tree and two clauses.

Worse, most of what the picker names is not in that tenth:

  • CI is described backwards. The picker says core runs its guards "locally/pre-commit" and ADR-011 says the same in parentheses. .github/workflows/spec-guard.yml is a required entry at the core profile (R16), R16 puts self-verify and spec-structure in CI for every repo with only spec-guard marked (scale), and the shipped workflow agrees with the rule rather than with the picker - it runs the coupling guard advisory at core and blocking at scale, and the full-tree coupling audit blocking at every profile. A solo adopter who reads the picker, skips CI and expects drift 0 gets drift 1 on a required entry, and finds out from a red pull request.
  • Personas and architecture are core entries. docs/personas.md (R10) and docs/ARCHITECTURE.md are required at both profiles. "Full persona roster" and "C4 discipline" are how much gets written in a file the repo carries either way - a judgment nothing measures, offered as though it were a flag.
  • Two of the named discounts were never charged. The tracker bridge is required at no profile: ADR-032 made it an optional per-capability sidecar that core never reads and whose absence is never drift. Release-notes curation has no manifest entry at any profile.

What the nine entries actually are: CONTRIBUTING.md, .github/pull_request_template.md, docs/analytics.md, docs/journeys, docs/research, docs/cycles, scripts/cycle-guard.mjs, and the spec-guard and cycle-guard guards. Read as a set they are contribution mechanics for people who cannot be told them in person, research about users nobody in the repo is, and work cycles for a team that has to agree what it committed to (ADR-028). Not one of them is triggered by a second pair of hands. Every one is triggered by somebody who is not in the room.

The field runs confirm the trigger has never been exercised anywhere near its own boundary. Every adoption inferred the profile from committer counts, and the smallest team any run in the suite records is 38 authors. Nothing has ever been adopted at two to five people, which is precisely the range the binary decides hardest.

Options considered

  • A - Keep headcount, move the number. Flip at three, or five. Cheapest possible change. Rejected: an arbitrary threshold is still the wrong variable, and it fails in both directions at once - a solo library with ten thousand users needs curated notes and a co-located team of four needs none of the scale set. Moving the number relabels the error.
  • B - A third profile for small teams. ADR-011's own "Revisit when" anticipates a third profile, but for a different audience (a regulated audit tier). Splitting a nine-entry difference into two smaller differences makes every future entry classify itself three ways, and it answers a granularity complaint with more granularity. Rejected.
  • C - Retrigger on reach, and state the measured discount instead of an impression of it. Chosen. The trigger names what each scale artifact was built for; the picker says what the flag does and does not change, so the choice is made against the mechanism rather than against a summary of it.

Decision

ADR-011's mechanism is untouched - the per-entry profile field, --profile core|scale, rendered views, the flag-plus-delta upgrade. Its principle is kept and its second half is made specific:

Core is whatever keeps knowledge alive; scale is whatever carries it to someone who is not in the room.

The trigger is a set of conditions, and headcount is not one of them. Any one of these is enough to be scale:

  • work is handed off asynchronously - somebody picks a piece up without the person who wrote it being reachable to explain it;
  • somebody contributes from outside the conversation - an outside contributor, a rotating team, a vendor, a maintainer who arrives after everyone here has gone;
  • somebody outside the repo reads execution state - a stakeholder who follows a board rather than a backlog;
  • the repo has a release audience that is not its authors;
  • it is designed for users nobody here is.

Two people at one desk shipping to each other are core, and compliant. One person shipping a library to ten thousand strangers meets the release-audience condition and is not.

The picker states the measured discount. It names the nine entries rather than characterising them, and it says plainly what the flag does not change: CI is required at core, personas and architecture are core entries whose depth is a judgment, and the tracker bridge and release-notes curation are required at no profile.

The 2-5 range gets a route, not a profile. There is no third profile and there will not be one for team size. A repo whose answers land between the two picks the route that leaves a record, both already in the mechanism:

  • declare scale and except the documents it does not carry - each in the manifest's exceptions with a reason, which self-verify reports as excepted rather than drift and keeps in the adoption denominator (R17); or
  • declare core and carry what a condition above actually triggered - carrying more than the profile requires has never been drift, and is also never verified.

The first is the paved road for the six document entries, because an exception carries a reason and a voluntary extra carries nothing. It does not extend to the other three: scripts/cycle-guard.mjs and the two guards are outside the hatch by design - waiving a live check removes it instead of recording a deviation from it - so declaring scale means accepting that R11's coupling guard blocks rather than advises. That is the difference worth deciding deliberately, and it is a smaller and sharper question than the one the headcount trigger was asking.

Reading core as "we are only two, so none of this is for us" is the one wrong answer the conditions exist to prevent.

No entry changes profile in this record. The measurement says the split may be too weak to carry what the docs put on it; it does not say which way to move, and inventing the answer from this desk is what PROF-3 is gated against.

Consequences

  • The trigger now names the thing each scale artifact was built for, so a future entry classifies itself by asking who reads it rather than by counting the team. That is the same question ADR-028 and ADR-032 already answered entry by entry - this record makes it the general test rather than a coincidence of three decisions agreeing.
  • A solo or paired adopter is told the truth about CI up front instead of discovering it from a red pull request. This makes core visibly heavier than the old picker claimed, which is the point: the previous text bought lightness with a promise the tree does not keep.
  • Cost accepted: "reach" is a judgment where "the second contributor" was a fact. A precise answer to the wrong question is worse than a judgment the picker gives conditions for, but the conditions will be argued about, and some repo will answer them wrongly.
  • Cost accepted: the picker gets longer, against ADR-011's stated fear of explaining everything twice. It is one section in one document, and it is the section whose whole job is the choice - the alternative is a shorter picker that is wrong.
  • The nine entries are now named in prose, so the manifest and the picker can disagree. docs/facts.json declares the count against the manifest, which catches an entry changing profile; it does not catch an entry being renamed. That is a real gap and it is smaller than the one it replaces.

Confirmation

  • docs/method/adoption.md and docs/faq.md state the conditions, name the nine entries and say what the flag does not change; neither still tells a core adopter that guards run locally.
  • docs/facts.json declares the scale-entry count with the manifest as its home, so facts-check fails when an entry moves profile and the prose does not.
  • Reproducible by an adopter in one command: node scripts/self-verify.mjs --profile core prints the number of entries it skipped, and it is the number the picker gives.

What this rules out

A headcount anywhere in the profile picker, in any document. A third profile for team size - ADR-011's regulated audit tier stays open on its own terms, which are about a different audience and not about how many people are in this one.

Revisit when

An adopter in the 2-5 range reads the conditions and cannot answer them without already knowing the artifact list - which would mean the conditions describe the answer rather than ask the question. Or the opposite: every repo that meets one condition meets all five, which would mean reach is a single bit after all, the binary was right, and only its label was wrong.

  • ADR-011 - revised here. The mechanism, the per-entry field and the rendered views stand exactly as decided; the trigger and the solo/enterprise framing are what this record replaces.
  • ADR-028 - cycles bind at scale, the clearest instance of the reach test being applied before it had a name.
  • ADR-032 - tracker sync is an optional extension, which is why "no tracker bridge" was never a discount.
  • R16 (CI at core), R17 and ADR-004 (exceptions are the recorded-deviation hatch), docs/open-questions/profiles.md, PROF-3 in backlog.md.