status: Accepted date: 2026-08-10
ADR-047: the adoption ping is informed, not asked, and carries the minimum that still means something
Context
Nothing in this repo answers "how many repos actually run this standard" - the honest answer today is "unknown." PRODUCT.md claims real-world mechanics and the README claims field-run adoption, but a skeptic evaluating the project has no count to check either claim against, only the fixtures this repo wrote about itself (see EXHIBIT-1 in backlog.md).
A passive, zero-code option was considered first: align-to-standards already leaves a .standards-version marker in every aligned repo, so a GitHub code search for that filename would return a real count without any new code. It was rejected as the primary source because most adopting repos are private - code search only sees public ones, so the number would be a floor, not a count, and would silently under-report by an unknown margin.
A consent-gated ping (ask "can we send this?", wait for yes/no) was also considered and rejected. The owner had already watched a related idea - asking adopters for feedback/opinion at the end of a run - talked out of existence in the same conversation on the grounds that almost nobody answers a voluntary prompt. A yes/no gate on the ping has the identical failure mode: it adds a decision point that costs the adopter nothing to skip, so it would skip. The same reasoning kills a self-registration list (an ADOPTERS.md an adopter PRs themselves, the pattern Kubernetes/Envoy use) - it is the same voluntary action in a different shape.
What survives that filter is a ping that fires automatically and is disclosed, not requested - the model Homebrew, Next.js and the .NET CLI use for their own telemetry: on by default, named plainly in the output, switched off by one environment variable for whoever wants that. That model only stays honest if the payload genuinely could not identify who sent it - a disclosure covers a fact, not a permission slip, so the fact it discloses has to already be harmless.
Decision
align-to-standards sends one ping when a run reaches a completed state (drift 0, or an explicit terminal outcome for a run that did not fully align), and it tells the adopter this is happening rather than asking:
Sending an anonymous signal about this adoption (stack, standard version, final drift) - no repo name, no code content. Turn it off with
REPOSTDS_NO_TELEMETRY=1.
(English, like every other artifact in this repo (AGENTS.md's working-language rule) - align-to-standards runs against any adopter's repo, not only the maintainer's own.)
The payload is exactly this, nothing more:
{
"event": "adoption_completed",
"stack": "node",
"standards_version": "1.4.0",
"drift": 0,
"fully_aligned": true,
"date": "2026-08-10"
}
No repo name, no org, no URL, no free-text field - a free-text field would eventually carry whatever an adopter typed, which reopens the exact identifiability question this record exists to close. date is day-granularity, not a timestamp - an exact time is one more axis an adopter's own activity log could be correlated against. No field persists across pings (no client-generated UUID, no repeat-visit token), because a stable identifier that is not a name is still a way to notice "this is the same repo again," which is a form of tracking the minimal-payload decision is meant to rule out.
REPOSTDS_NO_TELEMETRY=1 (checked before the request is built) skips the ping entirely. This is what makes "inform, don't ask" defensible rather than sneaky: the disclosure names the exact fields sent, and anyone who wants zero participation gets a single, documented switch.
Endpoint, v1: the account's free *.workers.dev address, not a subdomain of repositorystandards.com. That was the original plan - NS-delegate just stats. so the rest of the domain's DNS in home.pl stays untouched - but Cloudflare's self-serve "connect a domain" flow refused a bare subdomain outright ("provide the root domain, not a subdomain"), live-tested rather than assumed. Getting a custom subdomain would mean either an Enterprise plan or moving repositorystandards.com's whole zone to Cloudflare, both bigger and riskier than this record's scope. The workers.dev address costs nothing, touches no existing DNS, and is reachable the moment it's deployed; a nicer hostname is deferred, not abandoned. Live at https://stats.repositorystandards.workers.dev - the account's workers.dev subdomain was renamed from its default (account-name-derived) value to repositorystandards first, since this account's only other tenant (stayget.com) uses it for DNS, not Workers, so nothing else shared the old name. GET / returns {"count": N}; POST / accepts the payload below - no /adoption path, the worker name alone (stats) already says what it is.
Storage: Cloudflare D1, one append-only table. A plain KV counter was considered and rejected once the payload grew past a single number - drift and fully_aligned are worth querying later ("what's the drift distribution across real adoptions"), which a bare counter throws away.
Consequences
- A new small service exists outside this repo's own CI: a Worker + a D1 database, deployed by the maintainer via
wrangler, not built bychecks.ymlorpages.yml. align-to-standards's completion step gains the notice text and thecurlcall, gated by the env var check. This is repo-own (zone 1): it changes what the maintainer's own copy of the skill does at the end of a run, not what the shipped standard requires of an adopter. An adopter who forks or vendors the skill without this repo's Worker URL simply has a call that fails silently or is stripped - nothing in R1-R25 depends on it.- No dashboard reads this data yet. Turning the raw D1 rows into a number
EXHIBIT-1or the site can point at is separate follow-up work, not scoped by this record. - The custom-domain question (
stats.repositorystandards.com, or migrating the whole zone to Cloudflare) stays open for later, once the free address has proven the mechanism works.
Compliance
align-to-standards's completion step prints the disclosure text and issues the POST unless REPOSTDS_NO_TELEMETRY is set; the POST body matches the schema above field-for-field, with no additional keys; the Worker persists only those fields, no request metadata (IP, headers) alongside them.
Revisit when
- The free
workers.devaddress has proven the mechanism works - the custom-domain question (astats.subdomain, or moving the whole zone to Cloudflare) is explicitly left open for that point, not abandoned. - The payload ever needs a field beyond the fixed schema (stack, standard version, drift, fully_aligned, date) - any addition, especially a free-text one, reopens the identifiability question this record was written to close.
Related
- ADR-045 - a different mechanism for a different purpose.
record-runis consent-gated, carries far more detail, and feeds the human-prompting corpus to improve the product. This ping is automatic-and- disclosed (not consent-gated), carries the minimum, and only feeds a count. Neither touches the other's storage.
