Skip to content

feat(badges): ship corpus-size and ARB queue badges as recipes over existing output - #127

Merged
mbeacom merged 12 commits into
mainfrom
mbeacom-adr-0025-adoption-badge
Aug 12, 2026
Merged

feat(badges): ship corpus-size and ARB queue badges as recipes over existing output#127
mbeacom merged 12 commits into
mainfrom
mbeacom-adr-0025-adoption-badge

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Adds two badges to adrkit — corpus size and ARB queue depth — as recipes over output the tool already produces, plus a published guide. Ratifies ADR-0025.

What ships

Both badges render a number through shields.io from JSON your own repository publishes:

Badge Source Claims
ADRs: 25 $.checked from adr lint --json how many decisions are on record
ARB queue: 6 pending $.totalItems from adr queue --format json how many await review

Plus a badges guide wired into the sidebar, and scripts/check-doc-cli-versions.ts (+20 tests) in clean-clone-builds so the recipe's pinned CLI version cannot silently rot at a release.

No new CLI surface. Both files are the commands' verbatim output — adrkit defines no badge format, and an adr badge command would be public API maintained forever to reformat fields that already exist.

A static "uses adrkit" badge is deliberately not offered. It asserts something a reader cannot check and carries no state; a count proves the same thing for the same cost. The pass/fail badge is GitHub's, not adrkit's — documented as a pattern, with attribution, rather than presented as a third adrkit badge.

Two mechanisms, deliberately

adrkit emits both reports from its site buildsite.yml already rebuilds on docs/adr/**, so they are generated into site/public/ (gitignored, exactly like the served schema) and served from adrkit.dev. No workflow holds a write token and there is no stored artifact to fall behind.

The published recipe commits the files, because most repositories have no site to piggyback on, and names the alternatives for a protected default branch rather than shipping a snippet that fails where nobody looks. The cost — adrkit no longer dogfoods the exact snippet it publishes — is recorded in the ADR.

Refused, on posture

  • No hosted badge endpoint. Uptime dependency in every adopter's README, a computed surface on the origin ADR-0011 froze as static, and badge renders as a de-facto record of who uses adrkit.
  • No adr badge command.

Trade-offs recorded rather than glossed

  • Dropping the static image drops the offline escape hatch, so MADR-0008's one portable objection — reliance on an online renderer — is now unanswered. A number cannot be a committed SVG without being wrong as soon as the corpus changes.
  • $.checked counts records at any status on purpose: a superseded decision is still a decision that was recorded, and filtering to "active" would make the number a judgement rather than a count.
  • Depth, not deadlines: buildQueueReport selects by status: 'proposed', so depth is a pure function of the corpus. SLA state advances with the calendar and would need a scheduled rebuild and a daily bot commit to stay true.

Review

Three independent five-lens deep-review rounds (adversarial, architect, consumer, operator, supply-chain-security), each adjudicated. Rounds 1–2 found flaws in the original design; round 3 found flaws in round 2's fixes, twice — all verified by execution rather than reading:

  • The published recipe could never bootstrap: git diff --quiet exits 0 on an untracked file, so an adopter's first run committed nothing and the badge 404'd forever.
  • The workflow could never push here: main requires self-dogfood, which is gated on pull_request, and github-actions[bot] is not a bypass actor.
  • The CLI-pin guard was wrong four times. npx @adrkit/cli@0.6.0. — a pin ending a sentence — was reported stale, which blocks every merge in a required check; npx -y @adrkit/cli@0.1.0 didn't match at all. Rebuilt rather than patched a fifth time; 20 tests pin all five failure modes.
  • The PR-branch recipe force-pushed every run, silently destroying review commits on the open PR.
  • An earlier draft claimed the corpus-status badge detects stopped regeneration — it does not, since it reports the workflow running adr check, which never touches the queue report.

Notes

  • No publishable source changes. The only packages/ files are queue/types.ts (a JSDoc compatibility note) and lint.test.ts.
  • lint.test.ts no longer hardcodes the corpus count — it had been bumped 21 → 22 → 23 → 24 across four PRs and conflicted on every rebase.
  • QueueReport.totalItems now carries an external-consumer obligation, recorded on the field and in docs/RELEASING.md.
  • README badges are deliberately absent — the served JSON does not exist until this merges and the site publishes, so they would render error tiles on the front page. They land in a follow-up, tracked as action item 1.

Gates: 1994 tests, typecheck, lint 8/8, check:deps, check:doc-pins, corpus 25 records clean, site build serving both reports.

mbeacom and others added 10 commits August 11, 2026 22:09
…tus badge

Records the badge question as three distinct claims — adoption, status, and
certification — and ships only the one verifiable by the viewer.

Ships a static adoption badge (shields static URL plus a committable SVG),
documents the GitHub Actions workflow badge as the honest status badge, defers
`adr badge` behind four stated constraints, and refuses a hosted badge endpoint.

The refusal is on posture, not effort: a computed endpoint contradicts ADR-0004
and puts a dynamic surface on the origin ADR-0011 froze as static and immutable,
while making badge renders a de-facto telemetry stream.

Status: proposed — surfaces in `adr queue` as async tier, deadline 2027-02-09.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
The vendored MADR record weighs a status badge *inside each ADR file* as an
alternative to a `status:` frontmatter field. Its objections are about per-record
proliferation and markdown-source readability, neither of which transfers to a
single repository-level badge.

Only the online-service dependency carries over, and that is answered by shipping
a committable SVG rather than by declining to ship a badge. Cited as adjacent
prior art, not as support for the decision.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
…ting output

Amends ADR-0022, whose earlier draft deferred a computed badge on the premise
that it required a new CLI surface. Measurement falsified that premise:

- QueueReport v1 already emits `totalItems`, `asOf`, and `corpusFingerprint`.
- shields `dynamic/json` reads it with no adrkit code (observed rendering
  `adrkit version: 0.4.0` from this repo's raw package.json).
- Its failure modes render honestly: `resource not found`, `no result`.
- Queue depth is asOf-independent — buildQueueReport selects on
  `status === 'proposed'` — so regeneration on corpus change suffices and no
  daily bot commit is needed. Deadline-derived badges would not be, which is
  why the recipe badges `$.totalItems` and nothing else.

Ships: a committable adoption SVG whose colors are converted from the site's
own oklch tokens (#cb492d / #1d1311), a workflow regenerating .adrkit/queue.json
on corpus change, a badges guide, and the queue badge on this README.

Refuses, on posture rather than effort: an `adr badge` command (it would be
public API to reformat a field already emitted) and a hosted endpoint (uptime
dependency, a computed surface on the origin ADR-0011 froze as static, and
usage telemetry as a side effect of rendering).

Also fixes a hardcoded corpus count in lint.test.ts that broke on this record.
The count is now derived from the corpus contract rather than bumped, and was
observed failing under a +1 mutation before being restored (ADR-0016).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
… entry

PR #106 landed ADR-0022 (scan inbound markers in check and CI) and PR #109 is
teed up as ADR-0023, so this record takes 0024. Renames the file and updates the
`id`, the heading, and every cross-reference in the workflow comment and the
badges guide.

Regenerates .adrkit/queue.json against the rebased corpus: depth is now 8, since
the newly landed ADR-0022 is itself proposed.

The rebase conflicted on packages/cli/test/lint.test.ts exactly as predicted —
#106 had bumped the hardcoded corpus count from 21 to 22. Resolved in favor of
deriving the count, which removes the recurring bump rather than repeating it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
…the site build

A five-lens review found the shipped design broken in two independent ways, both
verified by execution rather than reading.

The workflow could never have worked here. The `main` ruleset requires the
`self-dogfood` check, `self-dogfood` is gated on `github.event_name ==
'pull_request'`, and `github-actions[bot]` is not a bypass actor — so every bot
push would be rejected and the badge would freeze at whatever this PR committed.

The published recipe could never have bootstrapped. `git diff --quiet -- <path>`
exits 0 for an untracked file, so `&& exit 0` fired on an adopter's first run and
nothing was ever committed; `git commit -am` would not have staged it either.
Both reproduced in a scratch repository, and the corrected first-run /
no-change / changed paths were executed before being written down.

adrkit now emits `queue.json` as a site build artifact. `site.yml` already
rebuilds on `docs/adr/**`, so the file is generated into `site/public/`
(gitignored, exactly like the served schema) and the badge reads
adrkit.dev/queue.json. No write token, no bot commit, and no stored artifact that
can fall behind — which also removes the committed-generated-file precedent the
review flagged as departing from `schema/adr.schema.json` and `packages/ci/dist`.

The adopter recipe keeps committing the file, because most repositories have no
site to piggyback on, and now SHA-pins `actions/checkout`, pins the `@adrkit/cli`
version inside a write-capable job, validates the JSON before publishing it, and
names the alternatives for a protected default branch.

Also corrects a false claim the record made about itself: the corpus-status badge
does not detect stopped regeneration, since it reports the workflow running
`adr check`, which never touches the queue report. Records the `totalItems`
external-badge constraint on the field and in RELEASING.md, namespaces the SVG
clipPath id, and fixes the `color=blue` / `cb492d` contradiction.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
…ersion

v0.5.0 shipped while this branch was open, which made the published recipe's
`npx @adrkit/cli@0.4.0` pin stale. Bumped to 0.5.0 — and, because a pinned
literal in a doc is exactly the value that rots silently at the next release,
added a guard rather than trusting review to catch it. `sync-schema --check` now
fails when a docs page pins a version other than the root `package.json`, which
was observed failing on a reverted 0.4.0 pin before being kept. `docs/RELEASING.md`
names the bump as a release step so the failure is anticipated rather than
discovered as a red site deploy.

Also retargets two citations the release invalidated: ADR-0021 is now superseded,
so the marker-cannot-lie rule is cited as ADR-0021 -> 0022/0023 rather than as
current authority, and the `adrkit version: 0.4.0` shields observation is marked
as the version observed at the time rather than reading as a present claim.

The rebase conflicted on the corpus count in lint.test.ts for the third time
(22 -> 23 on main); resolved to the derived form again, which is the last time
that conflict can occur. package.json is restored from main and re-edited as a
single added line — an earlier `json.dumps` rewrite had escaped the em-dash in
`description` to `\u2014`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
… findings

PR #123/#124 landed a different ADR-0024 (measured scan extent) and v0.6.0
shipped, so this record takes 0025 and the recipe pins 0.6.0. Only this record's
own references moved; the ADR-0024 citations in packages/**, CHANGELOG, and the
CLI source belong to the scan-extent record and are untouched.

The site deploy no longer fails on a corpus finding. `adr queue` documents exit 1
as "report emitted with error-severity corpus findings" — the report is complete
and publishable — so treating it as fatal let a corpus problem block the canonical
schema at its `$id`, which ADR-0011 promises to serve. Exit >=2 still fails, and
the emitted JSON is validated before the build proceeds.

The version guard added last commit was wrong in the case it existed for: its
`\d+\.\d+\.\d+` pattern matched the `0.6.0` inside `0.6.0-rc.1` and passed. The
first fix — a negative lookahead — was worse: the token stopped matching at all,
so a pre-release pin passed silently. Both were caught by running the guard
rather than reading it. It now captures the whole version token and compares it
in full, and is scoped to executable `npx`/`bunx` invocations so prose naming an
older release cannot block a deploy. Observed passing on 0.6.0 and on a prose
mention, failing on 0.6.0-rc.1, 0.5.0, and `latest`.

Adds `package.json` to the site trigger so a release bump re-runs the pin guard
in the same push; extends this record's `affects` to the queue types and the
guard script so `adr check` routes a maintainer to the `totalItems` obligation
the record creates; and fills three gaps in the published guide — the
protected-branch aside now shows the actual YAML for the branch and pull-request
routes, the pin carries an ageing warning, and the disclosure note covers the
`deciders` values that travel in queue.json.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Pressure-testing where the guard belongs produced a clearer answer than
"leave it or move it". The repo's convention for a guard is not just a
single-purpose script — every one in `scripts/` has a colocated `.test.ts` and a
required-check CI step, while `site/scripts/` holds untested generators. This
guard had no tests and had been wrong twice in two commits, which is the case
for the convention rather than an exception to it.

It also compares the root `package.json` against site content, so it is a
repo-wide concern rather than a site-internal one; and a stale pin is a content
defect that should block a merge, not block publishing the canonical schema at
its `$id`. Extraction does not weaken enforcement: `check:doc-pins` runs in
`clean-clone-builds`, which is a required status check, so a stale pin cannot
reach main. `sync-schema.ts` is back to the single purpose its header claims.

The nine new tests pin both historical failures — the `\d+\.\d+\.\d+` pattern
that matched the `0.6.0` inside `0.6.0-rc.1`, and the anchored variant that made
the token stop matching and pass silently — plus the prose-vs-recipe distinction
and the global-regex `lastIndex` reset.

Defers the README badge to a follow-up. `https://adrkit.dev/queue.json` does not
exist until the site publishes once, so merging the badge now renders an error
tile on this project's own front page — the exact failure this record exists to
avoid. The record and changelog no longer claim a badge that is not there, and
the action item now tracks adding it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
The guard had been wrong four times, and round 3 found the last two by running
it rather than reading it: `npx @adrkit/cli@0.6.0.` — a pin ending a sentence —
was reported stale, which fails a REQUIRED check and blocks every merge in the
repository; and `npx -y @adrkit/cli@0.1.0` did not match at all, so a badly
stale pin passed. A fifth regex patch was the wrong answer, so the matcher was
rebuilt around the two properties that kept breaking: trailing punctuation is
trimmed before comparison, and runner flags are tolerated between `npx`/`bunx`
and the package. It also hands out a fresh RegExp per scan, because `matchAll`
seeds its matcher from the source's `lastIndex` and a shared global instance
could silently start mid-string. Twenty tests now pin all five failure modes,
including the latent one.

Two fixes land in the recipe published for adopters. The pull-request variant
force-pushed its branch on every run, silently destroying any review commits on
the open PR, and `|| true` swallowed a missing-permission failure so the
workflow went green while never opening a PR. It now pushes without `--force`
and creates a PR only when none is open. The whole final step is shown rather
than a fragment, because the previous snippet put a step-level `env:` inside a
`run:` block for anyone who followed "replace `git push` with" literally. The
`--force` that remains on the artifact branch is explained, and the badge URL's
percent-encoded `%2Fmain%2F` is named as the place a branch substitution goes.

The site deploy no longer depends on the queue tool at all: an `adr queue` exit
of 2 now warns and publishes without `queue.json` instead of aborting the job,
so a queue regression can no longer strand the canonical schema at its `$id`.

Also corrects a renumber miss — `site/.gitignore` cited ADR-0024, which is a real
but unrelated record — and extends this record's `affects` to `.github/workflows/ci.yml`
and `package.json`, so `adr explain .github/workflows/ci.yml` now names the
decision that mandates the guard living there. Verified by running it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Accepts the badges record after three independent review rounds. Follows the
convention set by ADR-0022/0023/0024: `status` moves to `accepted` and
`provenance.ratifiedBy` is set, while `date` stays the drafting date.

Ratification is what removes the record from its own queue — depth drops from 7
to 6, which is the projection this decision publishes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Copilot AI balanced review requested due to automatic review settings August 12, 2026 02:21
@mbeacom mbeacom self-assigned this Aug 12, 2026
@mbeacom mbeacom added the enhancement New feature or request label Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds adoption and ARB queue-depth badges as recipes over existing adrkit output, avoiding new CLI or hosted-service APIs.

Changes:

  • Adds badge assets, documentation, navigation, and ADR-0025.
  • Generates /queue.json during site deployment.
  • Adds CI validation for pinned documentation CLI versions.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
site/src/content/docs/ci.mdx Links CI guidance to badge recipes.
site/src/content/docs/badges.mdx Documents adoption, status, and queue badges.
site/public/badge/adrkit.svg Adds the vendorable adoption badge.
site/astro.config.mjs Adds Badges navigation.
site/.gitignore Ignores generated queue output.
scripts/check-doc-cli-versions.ts Detects stale documented CLI pins.
scripts/check-doc-cli-versions.test.ts Tests CLI-pin detection.
packages/core/src/queue/types.ts Documents totalItems compatibility.
packages/cli/test/lint.test.ts Derives expected corpus count dynamically.
package.json Adds queue-generation and pin-check scripts.
docs/RELEASING.md Records badge compatibility and release steps.
docs/adr/0025-ship-badges-as-recipes-over-existing-output.md Ratifies the badge architecture.
CHANGELOG.md Documents the new badge recipes.
.github/workflows/site.yml Generates and deploys queue.json.
.github/workflows/ci.yml Runs the documentation pin guard.
Suppressed comments (1)

site/src/content/docs/badges.mdx:158

  • A second run will usually fail as a non-fast-forward push while adr-queue-refresh still exists: every run starts and commits from main, so the existing branch's bot or review commits are not ancestors of this new commit. Fetch/check out the existing refresh branch and integrate current main before regenerating, or use a new branch per update, so an open PR can actually be refreshed without discarding review commits.
          # No --force: the branch may carry review commits, and overwriting them
          # silently is worse than a failed run you can see.
          git push -u origin HEAD:adr-queue-refresh

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread site/src/content/docs/badges.mdx Outdated
# reads as a bug in your tooling. Fail here instead of committing it.
- name: Validate before publishing
run: |
python3 -c "import json,sys; r=json.load(open('.adrkit/queue.json.tmp')); sys.exit(0 if isinstance(r.get('totalItems'), int) else 'totalItems missing')"
Comment thread .github/workflows/site.yml Outdated
Comment on lines +81 to +86
- name: Emit the ARB queue report for the badge
working-directory: .
run: |
bun install --frozen-lockfile
set +e
bun run packages/cli/src/index.ts queue --format json > /tmp/queue.json
Comment thread scripts/check-doc-cli-versions.ts Outdated
* and the package. Prose that merely names an older release is not a recipe and
* must not fail a required check.
*/
const CLI_PIN_SOURCE = String.raw`(?:npx|bunx)(?:\s+-{1,2}[A-Za-z][\w-]*)*\s+@adrkit/cli@([^\s\`'"]+)`;
The static `ADRs | adrkit` badge failed this record's own test. The Context
argues a badge must not assert what a reader cannot check, and "this repository
uses adrkit" is exactly that: unverifiable from the image, carrying no state,
and the only badge here a reader learned nothing from.

`adr lint --json` already emits `{ checked, findings }`, so `$.checked` reports
how many decisions are on record — the same adoption signal with the evidence
attached, for the same cost and still no new CLI surface. Both published
artifacts stay verbatim command output; adrkit defines no badge format.

Two consequences are recorded rather than glossed. Dropping the static image
drops the offline escape hatch, so MADR-0008's one portable objection — reliance
on an online renderer — is now unanswered: a number cannot be a committed SVG
without being wrong as soon as the corpus changes. And the pass/fail badge is
GitHub's, not adrkit's; the guide now says so instead of presenting three badges
as though adrkit contributed all of them.

`$.checked` counts records at any status on purpose. A superseded decision is
still a decision that was recorded, and filtering to "active" would make the
number a judgement rather than a count.

Verified: the site build serves both reports (checked=25, totalItems=6), the
badge/ directory is gone from the output, and shields renders the new label and
color parameters against a live JSON source.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom mbeacom changed the title feat(badges): ship adoption and ARB queue badges as recipes over existing output feat(badges): ship corpus-size and ARB queue badges as recipes over existing output Aug 12, 2026
…alidation

The reviewer found a third false negative in the pin guard: `npx
--cache=/tmp/x @adrkit/cli@0.5.0` did not match, because the option grammar
accepted only bare flags. An unrecognised option makes the whole match fail, and
a guard that matches nothing reports nothing — so a stale pin bypassed a required
check. The grammar now accepts `--opt=value`, with three regression cases.

The site deploy no longer filters pushes by path. `queue.json` and `lint.json`
are projections of main produced by the CLI, so the deploy has to re-run when
the projector changes, not only when the corpus does. Enumerating the
generator's dependencies is fragile — one missed path leaves adrkit.dev serving
numbers from an older implementation, silently — so every push to main deploys.
Pull requests keep their path filter, since there the build is only a check.

Both publishers now validate the field the badge actually reads rather than that
the file merely parses. A structurally valid but wrong report would otherwise
render `no result` and read as a bug in adrkit. The check is `type(x) is int`
rather than `isinstance`, because bool subclasses int in Python and isinstance
would accept `{"totalItems": true}` — the reviewer's point, which applied to the
recipe's earlier validation and would have re-applied here.

Verified by execution: the helper publishes on int, skips on bool, missing key,
and truncation, and still publishes on exit 1 with a complete report.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom
mbeacom merged commit 478ea29 into main Aug 12, 2026
10 checks passed
@mbeacom
mbeacom deleted the mbeacom-adr-0025-adoption-badge branch August 12, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants