diff --git a/.gitignore b/.gitignore index 6a01fa60..910675fe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,9 @@ # and it is generated from docs/ plus src/ by `cargo docs`. Never commit it. /target/doc -# Cargo.lock is tracked on purpose: atgc is a binary crate, so the lockfile -# pins the exact dependency versions a build is reproduced from. Do not -# ignore it. +# Cargo.lock is tracked on purpose: this workspace builds binaries, not a +# library for others to depend on, so the lockfile pins the exact dependency +# versions a build is reproduced from. Do not ignore it. # Editors and IDEs .vscode/ diff --git a/cliff.toml b/cliff.toml index 44d8ed8a..6bb8261d 100644 --- a/cliff.toml +++ b/cliff.toml @@ -3,38 +3,29 @@ # imply. No changelog is generated, so there is no [changelog] section and no # template; `git cliff --unreleased --bumped-version` prints a version and # exits, and that is the whole of this file's job. cargo-release does the -# writing — see release.toml, and CONTRIBUTING.md for the one command that -# joins them. +# writing — see release.toml. [git] conventional_commits = true -# Drop commits that do not parse rather than failing on them. Only the 36 -# commits from 5f7d819 onwards are conventional; the 46 before it are prose, -# and they are inside the repository forever. Without this, computing a version -# means arguing with history. It costs nothing in practice because the epoch -# tag sits on 6d16818, the last prose commit, so every commit in the range this -# is ever asked about is already conventional. --unreleased narrows it to that -# range and, as a side effect, silences the "46 commits skipped" warning that -# a whole-history run prints. +# Drop commits that do not parse rather than failing on them. Every commit in +# this repository conforms today and the commit-msg hook keeps it that way, so +# this line is currently inert. It stays because the alternative — a version +# computation that fails outright on a single malformed subject — turns a +# cosmetic mistake into a blocked release. filter_unconventional = false [bump] -# The line this file exists for. -# # git-cliff defaults `breaking_always_bump_major` to true, and its -# documentation is explicit that this includes "the transition from version 0 -# to 1". This repo is 0.1.0 and has five `!` commits in the range since the -# epoch tag, so under the default the first bump is 1.0.0 — verified, not -# inferred: with no [bump] section `git cliff --unreleased --bumped-version` -# prints v1.0.0 here, and with this line it prints v0.2.0. +# documentation is explicit that this includes the transition from 0 to 1. That +# would make the first breaking change in this repository 1.0.0. # -# 1.0.0 would be a claim this project does not make. README and CONTRIBUTING -# both say single-user, largely AI generated, no stability guarantees; a major -# version is a promise about compatibility, and the point of 0.x is that no -# such promise has been made. So a breaking change inside 0.x bumps the minor, -# and 1.0.0 stays a decision someone takes on purpose, by passing the version -# by hand, rather than one that falls out of a `!` in a subject line. +# 1.0.0 is a claim this project does not make. The README says single-user, +# largely AI generated, no stability guarantees; a major version is a promise +# about compatibility, and the point of 0.x is that no such promise has been +# made. So a breaking change inside 0.x bumps the minor, and 1.0.0 stays a +# decision someone takes on purpose, by passing the version by hand, rather +# than one that falls out of a `!` in a subject line. breaking_always_bump_major = false # Left at git-cliff's default of true, but set explicitly because the default diff --git a/committed.toml b/committed.toml index fc9651a0..5debe5fa 100644 --- a/committed.toml +++ b/committed.toml @@ -1,37 +1,29 @@ # Rules for `committed` (https://github.com/crate-ci/committed), run as a # commit-msg hook from prek.toml. # -# Nearly every line below overrides a default, which looks like fussing until -# you run the defaults against this repo: with a bare `style = "conventional"` -# and nothing else, all 35 conforming commits since 5f7d819 fail. With the -# settings below, all 35 pass and the pre-5f7d819 prose subjects still fail. -# That is the test this file was written against — a linter that rejects the -# history it is meant to describe is a linter that gets uninstalled. +# Most lines below override a default. The overrides are what make the linter +# describe the style this project actually writes, rather than rejecting it. style = "conventional" # committed capitalizes the subject by default, which for a conventional commit # means requiring `feat(auth): Leave the active account alone`. This project -# writes the description in lower case. This one default is what failed all 35. +# writes the description in lower case. subject_capitalized = false -# The defaults are 50 columns of subject and 72 of any wrapped line. The -# longest subject in the conforming range is 77 — -# fix(http): bound every request, so an unreachable host cannot stall a command -# — and bodies here are wrapped at 80. So 80 is the limit that already exists; -# 50 is a limit that the whole history violates, and one of those is a rule and -# the other is a lie. Note that the conventional `type(scope)!: ` prefix eats -# 10-20 columns before the description starts, which is most of why 50 does not -# fit. +# The defaults are 50 columns of subject and 72 of any wrapped line. Bodies +# here are wrapped at 80, and the conventional `type(scope)!: ` prefix eats ten +# to twenty columns before the description even starts, which is most of why 50 +# does not fit. subject_length = 80 line_length = 80 # The default list is fix, feat, chore, docs, style, refactor, perf, test. Two -# additions are load-bearing rather than speculative: `build` is already in the -# history (build: keep the whitespace fixers off captured fixtures), and `chore` -# is what cargo-release writes for the version commit (see release.toml), so -# without it the bump command cannot commit through its own hook. `ci` and -# `revert` are here to complete the conventional set, and are unused so far. +# additions are load-bearing rather than speculative: `build` covers changes to +# the workspace layout and the hook configuration, and `chore` is what +# cargo-release writes for the version commit (see release.toml), so without it +# the bump command could not commit through its own hook. `ci` and `revert` are +# here to complete the conventional set. allowed_types = [ "feat", "fix", @@ -48,23 +40,20 @@ allowed_types = [ # Left at their defaults, deliberately: # -# allowed_scopes — unset, so any scope passes. Twelve are in use (auth, authlog, -# account, bobbin, http, identity, models, pr, repo, resolve, review, -# sessions) and they track the module layout, which still moves. Pinning the -# list would mean a rename could only land by editing this file first. +# allowed_scopes — unset, so any scope passes. The scopes here track the crate +# layout, which is still moving. Pinning the list would mean a rename could +# only land by editing this file first. # # imperative_subject — true, and worth knowing it is a wordlist heuristic, not # grammar, and an uneven one. It rejects a description opening with an -# article (`fix: a fix for it`, `fix: the thing`) and some inflected verbs -# (`adds`, `added`); it passes `corrects it`, `correcting it` and non-verb -# openings like `how it works`. Kept on because the forms it does catch are -# ones people type, and because every subject in the conforming history -# survives it, but it is a nudge and not a guarantee. If it ever rejects a -# subject that is genuinely imperative, that is the bug, and this is the knob. +# article (`fix: a fix for it`) and some inflected verbs (`adds`, `added`); +# it passes `corrects it`, `correcting it` and non-verb openings like `how it +# works`. Kept on because the forms it does catch are ones people type, but +# it is a nudge and not a guarantee. If it ever rejects a subject that is +# genuinely imperative, that is the bug, and this is the knob. # -# subject_not_punctuated — true. Trailing `.` is rejected; inline punctuation is -# fine, including the asterisks in -# `fix(review): read the record the appview page is *about*`. +# subject_not_punctuated — true. A trailing full stop is rejected; inline +# punctuation is fine. # # no_wip, no_fixup — true. Both are aimed at a branch that is about to be # rebased anyway, and this project rebases. @@ -72,4 +61,4 @@ allowed_types = [ # merge_commit — true, and inert here. In commit-msg mode committed is handed a # message file with no parent information, so it cannot tell a merge from # anything else. The rule only bites when committed is run over a commit -# range, which nothing in this repo does. +# range, which nothing here does. diff --git a/prek.toml b/prek.toml index 32a2b4f0..0facd530 100644 --- a/prek.toml +++ b/prek.toml @@ -1,32 +1,21 @@ # Hooks for prek (https://prek.j178.dev/). See README for setup. -# The excludes on the fixers cover the two directories that hold somebody -# else's bytes. vendor/jacquard-oauth is a near-verbatim copy of an upstream -# crate; tests/fixtures/ is real records and pages captured off live services, -# and a fixture whose whitespace has been tidied is no longer evidence of what -# the service sent. Only the hooks that actually edit files need the exclude; -# the read-only checks below are left to cover both, since a merge marker or a -# broken Cargo.toml in there would be worth hearing about. # `prek install` wires the pre-commit hook and nothing else unless it is told # otherwise, so without this line the commit-msg hook below would sit in the -# file being installed by nobody and firing never. Naming both types here keeps -# the contributor instruction at one command; CONTRIBUTING.md says the same -# thing, and also gives the explicit `--hook-type` form for anyone whose prek -# predates support for this key. +# file, installed by nobody and firing never. default_install_hook_types = ["pre-commit", "commit-msg"] -# `stages` appears on every hook below now that there is more than one stage in -# play. A hook that does not name its stages runs in all of them, so without -# this the file-hygiene and cargo hooks would also fire on commit-msg, where -# the only path they are handed is .git/COMMIT_EDITMSG — eight lines of -# "no files to check" before every commit, and the whitespace fixers quietly -# rewriting the message file. Not harmful, but noise on a path nobody reads. -# The one hook deliberately without a `stages` here is committed, further down, -# which declares `commit-msg` in its own manifest. It has to be spelled out per -# hook rather than once as a top-level `default_stages`, because that key is -# applied to hooks before the manifest is consulted and would override -# committed's own `commit-msg` — turning the new hook off while looking like it -# tidied up. +# `stages` appears on every hook because there is more than one stage in play. +# A hook that does not name its stages runs in all of them, so without this the +# file-hygiene and cargo hooks would also fire on commit-msg, where the only +# path they are handed is .git/COMMIT_EDITMSG — several lines of "no files to +# check" before every commit, and the whitespace fixers quietly rewriting the +# message file. The one hook deliberately without a `stages` here is committed, +# further down, which declares `commit-msg` in its own manifest. It has to be +# spelled out per hook rather than once as a top-level `default_stages`, +# because that key is applied before the manifest is consulted and would +# override committed's own stage — turning the new hook off while looking like +# it tidied up. [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" rev = "v6.0.0" @@ -35,60 +24,52 @@ hooks = [ { id = "check-added-large-files", stages = ["pre-commit"] }, { id = "check-yaml", stages = ["pre-commit"] }, { id = "check-toml", stages = ["pre-commit"] }, - { id = "mixed-line-ending", args = ["--fix=lf"], exclude = '^(vendor|tests/fixtures)/', stages = ["pre-commit"] }, - { id = "end-of-file-fixer", exclude = '^(vendor|tests/fixtures)/', stages = ["pre-commit"] }, - { id = "trailing-whitespace", exclude = '^(vendor|tests/fixtures)/', stages = ["pre-commit"] }, + { id = "check-json", stages = ["pre-commit"] }, + { id = "mixed-line-ending", args = ["--fix=lf"], stages = ["pre-commit"] }, + { id = "end-of-file-fixer", stages = ["pre-commit"] }, + { id = "trailing-whitespace", stages = ["pre-commit"] }, ] -# Run the project's own toolchain rather than a pinned copy of it. -# Both hooks act on the whole workspace, so they take no filenames and run -# at most once per commit; `files` only decides whether they run at all. -# Deliberately no vendor/ exclude here: with pass_filenames = false it would -# do nothing, and cargo already leaves the vendored crate alone. It is a -# [patch.crates-io] dependency, not a workspace member, so `cargo fmt --all` -# never reaches it, and while clippy does compile it, `-D warnings` applies -# only to the primary package, so warnings from it cannot fail the hook. -# Both checked by injecting violations into the vendored crate. +# Run the project's own toolchain rather than a pinned copy of it. Every cargo +# hook acts on the whole workspace, so it takes no filenames and runs at most +# once per commit; `files` only decides whether it runs at all. +# +# There is no vendor/ exclude. If a patched dependency is ever vendored here — +# see D25 in DECISIONS.md, which contemplates exactly that — it belongs in +# [patch.crates-io] rather than as a workspace member, so `cargo fmt --all` +# will not reach it, and clippy's -D warnings applies to workspace members +# only. Add an exclude when there is something to exclude, not before. [[repos]] repo = "local" hooks = [ { id = "cargo-fmt", name = "cargo fmt", entry = "cargo fmt --all -- --check", language = "system", types = ["file"], files = '\.rs$', pass_filenames = false, stages = ["pre-commit"] }, # clippy type-checks as it lints, so there is no separate cargo check hook. - { id = "cargo-clippy", name = "cargo clippy", entry = "cargo clippy --all-targets --all-features -- -D warnings", language = "system", types = ["file"], files = '(\.rs$|^Cargo\.(toml|lock)$)', pass_filenames = false, stages = ["pre-commit"] }, + { id = "cargo-clippy", name = "cargo clippy", entry = "cargo clippy --workspace --all-targets --all-features -- -D warnings", language = "system", types = ["file"], files = '(\.rs$|Cargo\.(toml|lock)$)', pass_filenames = false, stages = ["pre-commit"] }, # The doc build is a check, not just a build: .cargo/config.toml sets - # rustdocflags = -D warnings, so a prose page that links to an item that no - # longer exists fails here. That is the whole reason the narrative docs are - # compiled into rustdoc rather than kept as a separate site. Runs after - # clippy, and mostly off the same - # warm target dir. Triggered by docs/ as well as src/, since the pages are - # include_str!'d into the crate and a change to one is a change to the doc - # build's input. - { id = "cargo-doc", name = "cargo doc", entry = "cargo docs", language = "system", types = ["file"], files = '(\.rs$|^docs/.*\.md$|^Cargo\.(toml|lock)$|^\.cargo/config\.toml$)', pass_filenames = false, stages = ["pre-commit"] }, + # rustdocflags = -D warnings, so a prose page that links at an item which no + # longer exists fails here. That is the whole reason the narrative pages are + # compiled into rustdoc rather than kept as a separate site. Triggered by + # docs/ as well as src/, since the pages are include_str!'d into the facade + # crate and a change to one is a change to the doc build's input. + { id = "cargo-doc", name = "cargo doc", entry = "cargo docs", language = "system", types = ["file"], files = '(\.rs$|^docs/.*\.md$|Cargo\.(toml|lock)$|^\.cargo/config\.toml$)', pass_filenames = false, stages = ["pre-commit"] }, # The two things the doc build cannot see: an untested rust code fence, and # a relative link between pages whose target no longer exists. { id = "doc-lint", name = "doc lint", entry = "scripts/doc-lint.sh", language = "system", types = ["file"], files = '^(docs/.*\.md|scripts/doc-lint\.sh)$', pass_filenames = false, stages = ["pre-commit"] }, + # The lexicon JSON is the protocol's source of truth and is compiled into + # vibescrobble-lexicon, where the tests assert that every document parses, + # that its id matches its path, and that the set agrees with the namespace + # constants. Running them on a lexicon change catches a rename that touched + # the file but not the constant. + { id = "lexicon-tests", name = "lexicon tests", entry = "cargo test -p vibescrobble-lexicon --all-features", language = "system", types = ["file"], files = '^lexicons/.*\.json$', pass_filenames = false, stages = ["pre-commit"] }, ] -# Conventional Commits, checked as the message is written. -# -# This is not a new rule. Every commit from 5f7d819 onwards already conforms — -# 36 of the 82 in the tree, and zero violations in the 35 since — so all this -# does is move a convention that lived in someone's head into a place that can -# fail. It matters more than house style now that the version in Cargo.toml is -# computed from these subjects (see cliff.toml): a subject that does not parse -# is a change that silently does not count towards the next version, and the -# failure is invisible until someone reads a version number and disbelieves it. -# -# The pre-5f7d819 prose subjects are not a problem and need no rewrite. A -# commit-msg hook is handed one path — .git/COMMIT_EDITMSG — and committed is -# invoked here with `--commit-file`, so it reads that file and nothing else; -# history is not an input. Confirmed rather than assumed: `committed -# --commit-file` was run against every subject in the tree, and the old ones -# fail exactly as they would if they were being written today, which is a -# result no hook ever sees. +# Conventional Commits, checked as the message is written. It matters more than +# house style here because the version is computed from these subjects (see +# cliff.toml): a subject that does not parse is a change that silently does not +# count towards the next version, and the failure is invisible until someone +# reads a version number and disbelieves it. # -# The rules are in committed.toml, which has to override most of committed's -# defaults to describe the style already in use here. See the notes there. +# The rules are in committed.toml. [[repos]] repo = "https://github.com/crate-ci/committed" rev = "v1.1.11" diff --git a/release.toml b/release.toml index e80b2443..83f594c8 100644 --- a/release.toml +++ b/release.toml @@ -1,79 +1,50 @@ # cargo-release (https://github.com/crate-ci/cargo-release). git-cliff decides # what the next version is (cliff.toml); this decides what happens to the tree -# when it is applied. Run from `main`, by a person, as described in -# CONTRIBUTING.md — never from a feature branch, and never inside a PR. +# when it is applied. Run from `main`, by a person — never from a feature +# branch, and never inside a pull request. # # Everything that leaves the machine is off. This project publishes nothing: # no crates.io release, no artifacts, no prebuilt binaries. What is wanted is -# narrow — the version in Cargo.toml moves, Cargo.lock follows it, and a tag -# marks where the next bump should start counting from. +# narrow — the workspace version moves, Cargo.lock follows it, and a tag marks +# where the next bump starts counting from. -# Not `cargo publish`, not now and not by accident. Checked by flipping this to -# true in a scratch clone: with `publish = true` the run reaches "Updating -# crates.io index" and dies on a missing token, and with it false it does not -# touch the network at all. The "Publishing" line that still prints in -# both cases is a step banner, not the step. +# Not `cargo publish`, not now and not by accident. Every crate also carries +# publish = false in its manifest, so this is the second of two locks. publish = false -# No `git push`. The bump is a local act that is reviewed like any other commit -# before it goes anywhere, and pushing a tag to the shared repo is deliberately -# a separate, deliberate command. +# No `git push`. The bump is a local act, reviewed like any other commit before +# it goes anywhere, and pushing a tag is deliberately a separate command. push = false +# One version across every crate in the workspace. These crates are one product +# released together, not independently useful libraries, and a reader who sees +# vibescrobble-hook 0.4.2 next to vibescrobble-pds 0.1.9 has learned nothing +# except that the numbers are meaningless. It is also what makes a single tag +# and a single README line correct below. +shared-version = true + +# One commit for the whole bump rather than one per crate. With shared-version +# set, {{version}} still renders in the message; without it, cargo-release +# leaves the placeholder as literal text. +consolidate-commits = true + +# The version commit has to satisfy the commit-msg hook (prek.toml), because +# cargo-release shells out to `git commit` and the hook fires. Hence the +# `chore(release):` prefix and the lower-case description; `chore` is in +# committed.toml's allowed_types for exactly this. +pre-release-commit-message = "chore(release): vibescrobble v{{version}}" + # A tag is the one piece that is not optional. `git cliff --bumped-version` -# counts commits since the most recent tag, so a bump that does not leave a tag -# behind cannot be run twice: the second run would recompute from the epoch tag -# and hand back the same answer. The tag is what makes this repeatable. +# counts commits since the most recent tag, so a bump that leaves no tag cannot +# be run twice: the second run would recompute from the same starting point and +# hand back the same answer. +# +# The name carries no crate name, which is what keeps a workspace bump to a +# single tag rather than one per member. tag = true tag-name = "v{{version}}" # `tag-message` being set is what makes the tag annotated rather than -# lightweight. That distinction matters beyond git's own reachability rules: -# an annotated tag is an object with its own hash, and the wider Tangled -# ecosystem keys off that object. Verified in a scratch clone — -# `git cat-file -t v0.2.0` returns `tag`, not `commit`. +# lightweight, and that distinction reaches past git's own rules: an annotated +# tag is an object with its own hash, and Tangled keys off that object. tag-message = "vibescrobble {{tag_name}}" - -# The version commit has to satisfy the commit-msg hook this repo now installs -# (prek.toml), because cargo-release shells out to `git commit` and the hook -# fires. Hence `chore(release):` and a lower-case description; `chore` was added -# to committed.toml's allowed_types for exactly this. Note that {{version}} -# only renders here with consolidate-commits off — with it on, the commit spans -# packages and the placeholder is left as literal text with a warning. This is -# a single-crate repo, so off is also simply the truthful setting. -pre-release-commit-message = "chore(release): vibescrobble v{{version}}" -consolidate-commits = false - -# The README says which release is current, and this is what writes it. The -# line is rewritten before the version commit is made, so it lands in that -# commit alongside Cargo.toml and Cargo.lock rather than trailing a release -# by however long it takes someone to notice. -# -# The line is found by its opening prose and not by hidden `` markers -# around it. Markers would let the wording drift freely, but they only stay -# invisible if the renderer drops comments, and this README is read through -# Tangled's sanitizer as well as any other — a renderer that escaped them -# instead would print the scaffolding at the top of the front page. The -# tradeoff bought by not finding out: "*Current release: " is now load-bearing -# text. Reword it here and in tests/release_metadata.rs together. -# -# `(?m)` so `^` and `$` bind to the line rather than the file. The match stops -# at the newline, which is why the replacement is a single line — it is one -# line by design, and widening it later (a built binary alongside the tag) is -# an edit to this string, not to the shape of the rule. -# -# `exactly = 1` is the check that matters. Without it a reword that got out of -# step would leave the README pinned at whatever version it last said, and a -# release that quietly does not update the thing it exists to update is worse -# than one that fails; with it, cargo-release stops with "N replacements -# expected, found M" before anything is committed. -# -# The repository URL is spelled out rather than taken from {{repository}}. -# Only {{version}} is documented for this step, and an unrecognized -# placeholder is not an error — it is copied through as literal text, which -# would put a dead link in the README and tag it as current. The test in -# tests/release_metadata.rs is the backstop for that whole class of failure: -# it reads this line back and fails if it disagrees with Cargo.toml. -pre-release-replacements = [ - { file = "README.md", exactly = 1, search = "(?m)^\\*Current release: .*$", replace = "*Current release: [v{{version}}](https://tangled.org/permadeath.com/vibescrobble/tags/v{{version}})*" }, -]