# ADR-0012: Versioning and release Status: Accepted Date: 2026-09-18 Deciders: Wesley Cormier The versioning scheme, the publication channel and the changelog tool are all decided. The tool is **release-please**, settled by the owner on 2026-09-20 (first amendment at the foot of this record) and wired as of the third; its first run, on 2026-09-22, stopped at an organisation setting (fifth amendment), and its second put `@standarx/nav@0.1.0` on npm the same day, with a provenance attestation (sixth amendment). From that amendment on, the publish step authenticates through npm trusted publishing; `0.2.0` went out through it on 2026-09-23, and the token that published `0.1.0` is revoked (seventh amendment). ## Context standarnav was unreleased when this was decided: `package.json` was at version `0.0.0` and nothing was published on npm — `0.1.0` has been on the registry since 2026-09-22 (sixth amendment). Everything about releasing had to be decided rather than copied: there was no release workflow to inherit, no changeset flow left standing and no publication channel beyond packed tarballs handed around as a workspace convenience — a state inherited from the predecessor implementation ([ADR-0002](0002-license-and-copyright.md)) and not re-derived here. The infrastructure, on the other hand, is not an obstacle. GitHub Actions runs on this repository: the workflow at `.github/workflows/ci.yml` has been green since 2026-09-19, so the automation described here can be assumed to run rather than hoped for. One thing is already in place: `publishConfig` at `package.json` sets `"access": "public"` and `"provenance": true`, so the package is built to be published with an npm provenance attestation from a CI run — which only makes sense if publishing happens in CI. ## Decision **Semver, starting at 0.x.** The first publication is `0.1.0`. While the major is 0, breaking changes are allowed in a **minor** bump and forbidden in a patch. Every breaking change gets a `Breaking` entry in the CHANGELOG stating the old and the new shape, in English ([ADR-0015](0015-language-policy.md)). The v1 freeze happens when the public surface stops moving: the exports map of [ADR-0011](0011-package-layout-and-adapters.md), the `data-snav-*` attribute names, and the behaviour of the React and vanilla adapters. Until then, callers pin an exact minor. **Conventional commits, enforced by review.** No bot gate at v0: the commit convention is a review item in the pull-request checklist, and `feat` / `fix` / `perf` / `docs` / `refactor` / `test` / `chore` / `ci` are the types in use. A commit message never carries an AI co-author trailer; a pull request that adds one is not merged. Both rules are written in [CONTRIBUTING.md](../../CONTRIBUTING.md). **CHANGELOG generated by release-please**, from the Conventional Commit history of `main`. The tool reads the commits merged since the last release, derives the next version from their types, writes the CHANGELOG entries from their subjects, and keeps a **release pull request** open carrying both. Merging that pull request is the act of releasing: it lands the version bump and the changelog, and the tag it creates is what the published version is built from. It is **not** what triggers the publish — that was the intended shape when this was written and it does not work; the amendment of 2026-09-20 on the wiring says why, and the publish job runs in the same workflow instead. This follows from the commit convention above rather than adding a second one. The convention is already a review item and already the thing a reader greps to find when a behaviour changed; making it the source of the changelog means there is one place a contributor has to get right, not two. It also removes the failure mode where the release note and the commit history disagree, because they stop being two artefacts. The cost is real and accepted: the commit subject *is* the release note, so a badly worded subject ships as a badly worded line, and a change worth one sentence but spread over four commits arrives as four lines. Two answers, and they are the work the wiring has to settle: a squash merge whose subject is written deliberately rather than inherited from the last commit of the branch, and an edit to the generated release pull request before it is merged — release-please leaves that pull request open precisely so it can be edited. Neither is configured today, so neither is a claim about how this repository merges; they are what the rider's closure obliges. **This was unwired when it was decided, and is wired now** — `release-please-config.json`, `.release-please-manifest.json` and `.github/workflows/release.yml`, the third amendment below. Until it has actually run, [CONTRIBUTING.md](../../CONTRIBUTING.md) and [the pull-request template](../../.github/PULL_REQUEST_TEMPLATE.md) say the same thing — the pull-request description carries the one sentence describing the user-facing change, and that sentence becomes the changelog entry. The difference after wiring is that the sentence is taken from the squashed commit subject instead of being copied by hand. **Publication from GitHub Actions only**, in the `release.yml` workflow the third amendment records: lint, `bun run check:docs`, typecheck, build, the build-drift check, then `bun run check:package` — `scripts/check-package.ts` packs the tarball (`scripts/check-package.ts:68`) and runs `publint --strict` (`scripts/check-package.ts:74`) and `attw --profile esm-only` (`scripts/check-package.ts:75`) on it — then `bun run check:size` and the unit and browser suites, then a publish with provenance (`publishConfig.provenance` is already `true`) and the permissions that provenance needs. The shape of that publish is settled, not an open question. `bun publish` emits no provenance attestation at all (`oven-sh/bun#15601`, open since 2024-12-05, last movement 2026-08-28), so the publish step calls the npm CLI. That is the one documented exception, not a silent one: [CONTRIBUTING.md](../../CONTRIBUTING.md) forbids the tool outright — "Never use `npm` or `npx` in this repository", at `CONTRIBUTING.md` — and records the exception in the same breath, as does the release-tooling section of [ROADMAP.md](../../ROADMAP.md). The publish authenticates through **npm trusted publishing**: no token in the workflow, an OIDC token minted by the run under `id-token: write` with `contents: read`, exchanged against a trusted publisher registered on npmjs.com for this repository, the workflow filename `release.yml` and the `npm` environment, with direct `npm publish` allowed — a configuration created after 2026-09-03 allows `npm stage publish` only unless told otherwise. It needs npm >= 11.5.1 and Node >= 22.14.0, and Node 22 ships npm 10, so the job runs on Node 24; the attestation comes with the registry exchange, so the `--provenance` flag went with the token. A trusted publisher is configured against a package that already exists, which is why it could not serve the first publish: `0.1.0` went out with a granular token and `--provenance`, and that token is revoked now that the workflow no longer reads it (sixth amendment). The publishing account has 2FA enabled; publishing is never done from a laptop. **Pre-releases for device validation.** TV runtimes are the risky target and no TV device test has ever been run, for want of the hardware and of an emulator ([ADR-0014](0014-device-and-browser-matrix.md)). Releases meant for device trials are published as `0.x.y-next.N` under the npm dist-tag `next`; `latest` is only moved once the supported-tier browser suite is green. No release note claims a device is supported before a device test exists. ## Consequences - A pull request that changes runtime behaviour states its user-facing effect in one sentence: in the pull-request description today, and in the squashed commit subject release-please reads once a release has run. A docs-only or refactor pull request writes "internal only" instead of skipping it, the phrase the template asks for (`.github/PULL_REQUEST_TEMPLATE.md:29`). - One version number covers core, engines and every adapter ([ADR-0011](0011-package-layout-and-adapters.md)), so the changelog entry must name the affected subpath — "fix(spatial)", not "fix". - Nothing about the infrastructure blocks a release: Actions runs, the CI workflow is green, and the release workflow has run end to end once, on 2026-09-22, which is what put `0.1.0` on npm. - A commit whose type release-please hides — `docs`, `refactor`, `test`, `chore`, `ci` — bumps nothing and publishes nothing, so the README a consumer reads on the package page is the one in the last published tarball until the next `feat`, `fix` or `perf` lands. A change confined to `.md` files is therefore `docs`, never `fix(docs)`: `fix` bumps a patch and publishes a release. - Provenance ties each published version to a public workflow run and a git commit. It also means an emergency publish from a developer machine is not a fallback: it would produce a release without an attestation, visibly different from every other version. - 0.x with breaking minors means consumers must read the CHANGELOG before bumping. The README and the installation snippet therefore pin an exact minor while the major is 0. - release-please adds no file a contributor writes and no dependency to `package.json`: it is a GitHub Action plus two configuration files at the root, and a release pull request that stays open between releases. That open pull request is itself a consequence — it is normal for it to sit there for weeks, and closing it without merging discards the accumulated changelog. - Because the version is derived from the commit history, a breaking change is only a minor bump if the commit says so. A `feat!:` subject or a `BREAKING CHANGE:` footer is what release-please reads; while the major is 0 that footer produces the minor bump the rule above requires, and forgetting it ships a breaking change as a patch. Reviewers check the subject, not only the diff. ## Amendment, 2026-09-20: the changelog tool is settled The rider that had stood since 2026-09-18 is closed. The owner chose **release-please**, and the Decision section above is rewritten to record it as the decision rather than as one of two options; the changesets case moved into `## Alternatives considered` with the reason it lost. Three things about the state of the repository on the day this was settled, so the record does not read as more than it is. `.github/workflows/` contains `ci.yml` only. No `release-please-config.json` and no `.release-please-manifest.json` exist at the repository root. `package.json` declares no changesets dependency and there is no `.changeset/` directory. So release-please is the accepted tool and it runs nowhere: no release pull request exists, nothing has been published, and the release workflow described above is still to be written. There is no `bench` script. It went with the benchmark it ran, because `vitest` 5 exports no `bench` function — [ADR-0018](0018-testing-strategy.md), decision 7. The scripts declared at `package.json` are `dev`, `build`, `typecheck`, `lint`, `lint:fix`, `format`, `test`, `test:unit`, `test:browser`, `test:watch`, `check:size`, `check:package` and `check:docs`. ## Amendment, 2026-09-20: Actions runs, and provenance is settled Two things this record carried as open are closed, and one of them was stated wrongly. GitHub Actions is not blocked and no billing check stands between this repository and a release. The last failing run here was on 2026-09-18 at 20:37 UTC; every run since 2026-09-19 at 15:15 UTC has succeeded — five consecutively, the most recent green across eight checks with no annotations. The Context paragraph that treated the runner as an infrastructure constraint and the Consequences bullet that called the billing check a release blocker are both gone, because both described a condition that no longer holds. Automation described here can be assumed to run. The two provenance points the Decision had left "not verified today" are settled, and the Decision now states the resulting shape rather than the research still to do. `bun publish` emits no provenance attestation (`oven-sh/bun#15601`, open since 2024-12-05, last movement 2026-08-28), which forces the npm CLI into the publish step and makes the exception to the no-`npm` rule of [CONTRIBUTING.md](../../CONTRIBUTING.md) definite rather than conditional. Token-based provenance needs npm >= 9.5.0, a cloud runner, `id-token: write` with `contents: read`, and the `repository` field of `package.json` matching the repository case-sensitively. Trusted publishing needs npm >= 11.5.1 and Node >= 22.14.0 and attests on its own, but is configured against an already-published package, so the first publish cannot use it — `0.1.0` goes out with a granular token and `--provenance`, and the token is revoked once trusted publishing replaces it. Since 2026-09-03 a fresh trusted-publishing configuration also defaults to `npm stage publish` rather than publishing directly, and the workflow filename registered on npmjs.com has to match the one in the repository exactly, which is why the switch is a second step and not a detail of the first. What this amendment does not change: the release workflow is still unwritten, release-please runs nowhere, and nothing is published. ## Amendment, 2026-09-20: the wiring exists, and one sentence of the Decision was wrong `release-please-config.json`, `.release-please-manifest.json` and `.github/workflows/release.yml` are in the repository. Nothing has run: the workflow triggers on a push to `main` and this is still a branch. The configuration is a single root package, `release-type: node`, `bump-minor-pre-major: true` so a breaking change is a minor while the major is 0 — the rule of the Decision above — a manifest at `0.0.0`, and `docs`, `refactor`, `test`, `chore` and `ci` hidden from the changelog so it carries `feat`, `fix` and `perf` only. **What the Decision got wrong.** It said the tag release-please creates would trigger the publish workflow. It would not. GitHub does not trigger workflows on events made with the default `GITHUB_TOKEN` — that is how a workflow is kept from looping on itself — and this record already knew the consequence for the release pull request, which carries no checks for exactly that reason. It is one rule with two consequences, and only one of them had been noticed. A tag-triggered publish would have sat there never running, and the failure mode is silence: a merged release pull request, a tag, a version bumped in `package.json`, and nothing on npm. So the publish job runs in the same workflow, gated on release-please's own `release_created` output. Three jobs: release-please, then a `verify` that checks out the tag and replays every gate CI runs, then `publish`. The verify job is the answer to the other half of the same rule — the commit that lands the version bump and the CHANGELOG comes from a pull request that had no checks, so nothing had ever run against it. A version bump cannot break the browser suite, but it can break `check:package`, because publint reads the manifest, and it can break the build drift gate, because tsdown rewrites the exports map. The browser engines run there anyway: a release is rare, and it is the last gate before bytes reach a registry. One consequence of generating a CHANGELOG had to be settled in the citation gate rather than here. A commit subject may contain a `` `path:line` `` citation, and release-please writes subjects into `CHANGELOG.md`, where `check:docs` would then gate an anchor nobody can fix — the entry is generated, and rewriting it would falsify the release it describes. `CHANGELOG.md` is therefore excluded from that gate, on the grounds that a changelog entry is *supposed* to describe the code as it was at that version (`scripts/check-citations.ts:53-58`). The alternative was a rule that a commit subject may carry no citation, which is the worse trade: the subject is the release note. The `NPM_TOKEN` secret does not exist yet, and neither does 2FA on the publishing account. Both are the owner's, and the workflow fails loudly without the first rather than publishing something unsigned. ## Amendment, 2026-09-22: verify replays seven of CI's eight checks, not eight The amendment above says the `verify` job replays every gate CI runs. It replays every gate *script*: lint, `check:docs`, typecheck, build, the drift check, `check:package`, `check:size`, `test:unit`, and `test:browser` on chromium, firefox and webkit (`.github/workflows/release.yml:48-89`). What it does not replay is the `react-floor` job (`.github/workflows/ci.yml:75-101`), which installs `react@^18.3.1` over the lockfile and re-runs typecheck and the chromium browser project against the declared peer floor. A release commit carries a version bump and a CHANGELOG and cannot move the React peer contract, so the tag is not less verified for it — but the claim was wider than the workflow, and the workflow's own header comment said the same thing until this amendment corrected both. ## Amendment, 2026-09-22 (second): the first run, and the two defaults it exposed The workflow ran for the first time when pull request #1 reached `main` at `4bef558`. release-please read the 105 commits, built the release pull request, pushed its branch, and failed on the last call: "GitHub Actions is not permitted to create or approve pull requests". Nothing was tagged; `verify` and `publish` were skipped. The setting is the organisation's — Settings, Actions, General, Workflow permissions, "Allow GitHub Actions to create and approve pull requests" — and the repository-level equivalent refuses with a conflict until the organisation allows it. That switch is the owner's; the run is replayed once it is on. Two defaults surfaced before the failure, and both contradicted this record. **1. It computed `1.0.0`, not `0.1.0`.** Not from a breaking change — no commit on `main` carries `!` or `BREAKING CHANGE` — but because no release exists yet: the manifest's `0.0.0` matches no tag, so `bump-minor-pre-major` never enters the calculation, and the first version is release-please's `initial-version`, which defaults to `1.0.0`. The Decision says `0.1.0`, and the owner confirmed it on 2026-09-22 when asked whether to take the `1.0.0` instead: a `1.0.0` freezes the attribute names ([ADR-0006](0006-declarative-first.md) makes any rename after it a major) before a single television has run the engine. So `release-please-config.json` gains `initial-version: 0.1.0`, in its own commit after this one. **2. The tag would have been `nav-v1.0.0`.** In a manifest configuration `include-component-in-tag` defaults to true, with the component derived from the package name, which is why the branch the run created is `release-please--branches--main--components--nav` and its commit is "release nav 1.0.0". One package, one tag: `include-component-in-tag: false` in the same config commit, so the tag is `v0.1.0`, the branch `release-please--branches--main`, and the pull request "chore(main): release 0.1.0". The branch under the old name is deleted by hand once the new one exists. Nothing in this repository named a tag format before this amendment, and the trusted-publishing plan above does not depend on one. What stands between the release pull request and npm is unchanged: `NPM_TOKEN` and 2FA. Merging that pull request before both exist creates the tag and a red `publish`, not a release. ## Amendment, 2026-09-22 (third): `0.1.0` is on npm, and the publish moves to trusted publishing **The second run released.** The owner switched the organisation setting on, the repository-level equivalent followed through the API, and the re-run of the workflow opened the release pull request, #3 "chore(main): release 0.1.0". Two things about that pull request the amendment above had not foreseen. Its branch was still `release-please--branches--main--components--nav`: release-please reuses the branch of its open release pull request rather than creating one under the name the new configuration implies, so the old name survived the config change and the deletion of that branch is a hand step once it is merged. And a manual re-run of the workflow triggers `pull_request` on that pull request — the eight CI checks ran on it — where a run under the default `GITHUB_TOKEN` alone would not have; the tag is `v0.1.0` as decided, the only tag on the remote (`git ls-remote --tags origin`, 2026-09-22). The owner merged it at `68bb0c3`, and run `35774862381` on that push went green through all three jobs: release-please from 19:36:25 to 19:36:39 UTC, creating the tag and the GitHub release; `verify` from 19:36:44 to 19:38:33; `publish` from 19:38:37 to 19:38:55. The publish step's own log (`npm publish --provenance --access public`): package size 116.6 kB, unpacked size 369.9 kB, 99 files, shasum `205c388b4a346cc17ef5397717271907cf640c07`, then "Signed provenance statement with source and build information from GitHub Actions". The registry agrees: `curl -s https://registry.npmjs.org/@standarx/nav` on 2026-09-22 answers `dist-tags.latest` `0.1.0`, `time.0.1.0` `2026-09-22T19:38:52.994Z`, `dist.unpackedSize` 369907 and `dist.fileCount` 99. The `NPM_TOKEN` secret and 2FA on the publishing account, which the two amendments above named as what stood between the pull request and npm, were both the owner's doing on 2026-09-22; that 2FA is enabled is the owner's statement of that day, not something this tree can show. **The switch to trusted publishing, decided here and wired in the next commit.** The facts, from the npm documentation (`https://docs.npmjs.com/trusted-publishers`, page dated 2026-09-03, fetched 2026-09-22): it needs npm 11.5.1 or later and Node 22.14.0 or later; a GitHub Actions trusted publisher is registered on npmjs.com against an existing package with the organisation or user, the repository, the workflow filename and an optional environment name; the workflow needs `id-token: write` and `contents: read`; provenance is generated automatically, so the `--provenance` flag is unnecessary; the documentation recommends revoking existing automation tokens once the trusted publisher is verified. "npm stage publish is always allowed. Choose whether this trusted publisher can also publish directly with npm publish", and "Configurations created after Sep 03, 2026 are automatically set to allow npm stage publish, and you can choose whether to also permit direct publishing with npm publish." Node 22 does not qualify: `https://nodejs.org/dist/index.json` fetched 2026-09-22 lists v22.23.2 (2026-07-28) with npm 10.9.8 and v24.21.0 (2026-09-07) with npm 11.19.0. So the `publish` job moves to Node 24, drops `NODE_AUTH_TOKEN` and `--provenance`, and turns the `package-manager-cache` input of `actions/setup-node` off — an input that exists since its v6 and defaults to on (its README, fetched 2026-09-22); bun does the installing here and a release build caches nothing. The `registry-url` input stays: the documentation's own example keeps it next to a token-less publish. **Staged publishing, considered and not taken.** `npm stage publish` uploads the tarball to a queue and "a human maintainer with a 2FA challenge is required to approve a staged package before it is released to the registry" (GitHub changelog, 2026-05-22). That is a second human action after the merge, on npmjs.com, for a repository whose release act is already the owner's merge of a pull request that `verify` replays every gate on; and a staged version nobody approves is the silent failure this record refuses — a tag, a version bump on `main`, and nothing installable. So the trusted publisher is registered **with direct `npm publish` allowed**, which the owner has to tick since the default since 2026-09-03 is staging only, and the workflow keeps calling `npm publish`. If a second gate is ever wanted, the `npm` environment can carry a required reviewer, on GitHub where the run is. Revisit when a second maintainer arrives. **Order of operations, and what is not proven.** Before the commit that drops the token reaches `main`, the owner registers the trusted publisher on npmjs.com: organisation `StandarX-miralabs-tech`, repository `standarnav`, workflow `release.yml`, environment `npm`, direct publish allowed. After the merge, the owner revokes the granular token on npmjs.com and deletes the `NPM_TOKEN` secret: the workflow reads neither from that commit on. Nothing runs through this path until the next release, so as with the third amendment the switch is wired and not proven; the next `feat`, `fix` or `perf` on `main` is what proves it. If the publisher is not registered when that day comes, `publish` fails with the tag already created, and the recovery is to register it and re-run the failed job — the outputs of the two jobs before it stand. **One convention this release taught.** The CHANGELOG of `0.1.0` carries five `fix(docs)` entries under "Bug Fixes" (`CHANGELOG.md`). Each described a document and none a behaviour a consumer could observe, and each on its own would have bumped a patch and published a release. From this amendment on, a change confined to `.md` files is typed `docs`, never `fix(docs)`; the Consequences above carry the rule and [CONTRIBUTING.md](../../CONTRIBUTING.md) states it. Its corollary is visible today: the package page on npm shows the README of the `0.1.0` tarball, which says the package is not published yet, and a `docs` commit correcting the tree does not change that page. The owner decided on 2026-09-22 to let the next feature carry the corrected README rather than cut a `0.1.1` for it. ## Amendment, 2026-09-23: `0.2.0` proves trusted publishing, after two refusals the log could not tell apart **The first release through the trusted publisher.** `0.2.0` was tagged by run `35796937190` on 2026-09-22, at `1d95b94`: release-please and `verify` went green, and `publish` failed at 23:22 UTC with `404 Not Found - PUT https://registry.npmjs.org/@standarx%2fnav`. It was re-run on 2026-09-23 at 18:10 UTC, once the owner had registered the trusted publisher, and failed with the same line. The owner then corrected the configuration on npmjs.com — which field was wrong, this tree cannot show — and the third attempt went green, `publish` from 18:16:17 to 18:16:31 UTC; the tag and the GitHub release of the first attempt stood, as the amendment above said they would. The step's own log: package size 113.7 kB, unpacked size 362.9 kB, 103 files, shasum `6ad1bc99bf2db63448e3c48068c9c64362ee4e68`, then "Your package is being processed and may take a few minutes to become available." and `+ @standarx/nav@0.2.0`. The registry agrees: `curl -s https://registry.npmjs.org/@standarx/nav` on 2026-09-23 answers `dist-tags.latest` `0.2.0`, `time.0.2.0` `2026-09-23T18:18:07.205Z`, `dist.fileCount` 103, `dist.unpackedSize` 362913, a `dist.attestations.provenance` of predicate type `https://slsa.dev/provenance/v1`, and an `_npmUser` named "GitHub Actions" carrying `trustedPublisher.id` `github`. The tarball URL answered 404 until 18:21:52 UTC and 200 from then on, with the shasum above: a version the metadata lists while its tarball is still a 404 is being processed, not lost. **Why the two failures could not be told apart.** All three attempts print "Signed provenance statement with source and build information from GitHub Actions" before their outcome, the two 404s included. `publishConfig` sets `"provenance": true` (Context), so npm signs an attestation whether or not the OIDC exchange with the registry took place: the line proves the run could mint an OIDC token for Sigstore, not that npm accepted one. The Decision's "the attestation comes with the registry exchange" holds for a package without that setting; here it is not the evidence it looks like. In npm 11.19.0 (`https://github.com/npm/cli/blob/v11.19.0/lib/utils/oidc.js`, fetched 2026-09-23) a refused exchange is logged at the `verbose` level only, with the registry's message, and the function returns without a token; `npm publish` carries on with whatever credentials its configuration holds, and at the default level the log shows nothing but the `PUT` that fails after it. Which step refused on 2026-09-22 and at 18:10 on 2026-09-23 is therefore not in the logs. Nor could it be read from here: the configuration is visible to the package's owner only, and `https://registry.npmjs.org/-/package/@standarx%2fnav/trust` answers 401 "Bearer token authorization is required" without a login (2026-09-23). **The publish step logs at `verbose`, from the next commit.** `npm publish --access public --loglevel verbose`, so the next refused exchange prints its `oidc` line with the registry's reason, and an accepted one prints "Successfully retrieved and set token" (same source). The level adds the `http` request lines, one of which is the GitHub ID-token request URL; that URL carries no credential, the bearer being sent as a header, and the exchanged token is never passed to the log (same source). It is not proven until the next release runs, and it could not have helped this one: a re-run of a workflow uses "the same `GITHUB_SHA` (commit SHA) and `GITHUB_REF`" as the original run (`https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs`, fetched 2026-09-23), so it replays the workflow file of the commit it started on — which is why the diagnosis of 2026-09-23 had to go through the owner's view of npmjs.com. **The token is gone.** On 2026-09-23 the owner revoked the granular token that published `0.1.0`, set the package's publishing access to require two-factor authentication and disallow tokens, and deleted the `NPM_TOKEN` secret — the owner's statement of that day. What this tree's tooling can show is the last of the three: `gh secret list` on 2026-09-23 lists no secret for the repository and none for the `npm` environment, and the organisation exposes none to it. From here the trusted publisher is the only way CI publishes; a rename of `release.yml` or of the `npm` environment stops it until npmjs.com is told, and the recovery is there, not in a secret. ## Alternatives considered **Manual publish from the maintainer's laptop**, after a local build. Rejected: no provenance attestation, no proof that what is on npm matches the tagged commit, no guarantee the test suite ran, and a build that depends on one machine's toolchain. It is also the path that makes a compromised developer machine a supply-chain event for everyone installing the package. **Calendar versioning** (`2026.9.0`). Attractive for a project that ships continuously and honest about "time passed, things changed". Rejected: consumers of a navigation library need a machine-readable signal for "this breaks your focus behaviour", and a range like `^0.4.0` carries that signal where a date does not. **Changesets** (`.changeset/*.md`, one file per user-facing change, written by the contributor in the pull request). This was the working choice until 2026-09-20 and the case for it is genuine: a changeset asks for a deliberate sentence rather than reusing a commit subject, it carries a change that spans several commits as one entry, and it forces the contributor to state the bump level — the judgement a maintainer otherwise has to make alone, weeks later, for someone else's patch. Rejected on the count of moving parts. It is a second convention on top of the Conventional Commit one this repository already enforces by review, a dependency and a `.changeset/` directory to maintain, and a file contributors forget — at which point the release note silently loses the change. The commit convention is enforced anyway; the changeset is not. Choosing the artefact that is already checked is what makes the changelog trustworthy. **Hand-written CHANGELOG with manual bumps.** Rejected: it relies on a single author remembering the intent of every change, which stops being true the moment an outside pull request is merged. **A bot enforcing conventional commits** (commitlint in CI). Not rejected on the merits, only deferred: there are no contributors yet, so a review checklist costs nothing while the contributor count is one, and a required check buys nothing a reviewer is not already doing. Revisit when the first outside pull request arrives. ## Evidence - `package.json` in this repository: `"version": "0.2.0"`, written by release-please in the release commit `7d8dab5` (`0.1.0` in `da0b65a` before it), and a `publishConfig` block with `"access": "public"` and `"provenance": true`. The declared scripts are listed in the first amendment above; its `devDependencies` carry `publint` `^0.3.24` and `@arethetypeswrong/cli` `^0.18.5`, the two linters `check:package` runs. - `@standarx/nav@0.1.0` is on the registry: `curl -s https://registry.npmjs.org/@standarx/nav` on 2026-09-22 answers `dist-tags.latest` `0.1.0`, `time.0.1.0` `2026-09-22T19:38:52.994Z`, `dist.unpackedSize` 369907 and `dist.fileCount` 99. The name probes that preceded it are recorded in [ADR-0001](0001-name-scope-and-attribute-prefix.md). - `@standarx/nav@0.2.0` is on the registry, published through the trusted publisher: `curl -s https://registry.npmjs.org/@standarx/nav` on 2026-09-23 answers `dist-tags.latest` `0.2.0` and `time.0.2.0` `2026-09-23T18:18:07.205Z`, with the provenance and publisher fields the seventh amendment lists. - No TV device test has ever been run, for want of the hardware and of an emulator. - Release tooling: `.github/workflows/` holds `ci.yml`, `release.yml` and `pages.yml`; `release-please-config.json` and `.release-please-manifest.json` exist, the manifest at `0.2.0`. There is no `.changeset/` directory, and `package.json` declares neither release-please nor changesets among its `devDependencies` — release-please is a GitHub Action, not a dependency. The workflow has run end to end twice: run `35774862381` on `68bb0c3` for `0.1.0`, three jobs green (sixth amendment), and run `35796937190` on `1d95b94` for `0.2.0`, green on its third attempt (seventh amendment). - `check:package` is wired and green: `scripts/check-package.ts` packs the tarball (`scripts/check-package.ts:68`), runs `publint --strict` on it (`scripts/check-package.ts:74`) and `attw --profile esm-only` (`scripts/check-package.ts:75`), and fails when `package.json` declares any runtime dependency — `scripts/check-package.ts:34-40` reads `manifest.dependencies` and exits non-zero when the list is not empty. In the `build` job of `.github/workflows/ci.yml` it runs after `bun run build` (`:50`) and the `git diff --exit-code` drift gate (`:53-54`), at `:55-56`, with `bun run check:size` behind it at `:57-58`. - Related: [ADR-0003](0003-package-boundaries.md) for what is being released, [ADR-0011](0011-package-layout-and-adapters.md) for the surface a version number covers.