diff --git a/.aqua/aqua-checksums.json b/.aqua/aqua-checksums.json index 333f659d..78cd769d 100644 --- a/.aqua/aqua-checksums.json +++ b/.aqua/aqua-checksums.json @@ -46,28 +46,28 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.10.1/limen_0.10.1_darwin_arm64.tar.gz", - "checksum": "30F1889AAAD6D1705CCF557D2FC2E75EA10D94D5EB293F423F11F60E79D25665", + "id": "github_release/github.com/farcloser/limen/v0.10.2/limen_0.10.2_darwin_arm64.tar.gz", + "checksum": "0A83FA9299DB95A8D18316B46E04210865F12A4A866AE9B69DB86F93D6902CB8", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.10.1/limen_0.10.1_linux_amd64.tar.gz", - "checksum": "CCAEE031B6F1DFBB9FDC1FBA83C8C809CF27064A0896022269D7DE22040308C6", + "id": "github_release/github.com/farcloser/limen/v0.10.2/limen_0.10.2_linux_amd64.tar.gz", + "checksum": "F6FE1D17A94DB1F2587370CE2247B308D4559C08595358C15CBFF4515DF0A062", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.10.1/limen_0.10.1_linux_arm64.tar.gz", - "checksum": "0E6D7F68B5CE57382AC5B40039F21EA1CEFC4ACBA9F013ADE2C8F157A295D848", + "id": "github_release/github.com/farcloser/limen/v0.10.2/limen_0.10.2_linux_arm64.tar.gz", + "checksum": "17E9B7449601368892E9D26062FC34071C4880775E9DAA79574B01FE2081409E", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.10.1/limen_0.10.1_windows_amd64.tar.gz", - "checksum": "87247B3780E51092390B6DDD307ECCE6A31905672C1D3EA9A4E8B3A2AB9533B3", + "id": "github_release/github.com/farcloser/limen/v0.10.2/limen_0.10.2_windows_amd64.tar.gz", + "checksum": "118C23F14269F7CE437F5EBE387ACDA8FABF1D19988B0DD0797CE653595EB23A", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.10.1/limen_0.10.1_windows_arm64.tar.gz", - "checksum": "882285A6405189D6AD4973C20AB11D52804226E29C097D360AC7F6CA05094F01", + "id": "github_release/github.com/farcloser/limen/v0.10.2/limen_0.10.2_windows_arm64.tar.gz", + "checksum": "0822D0B62256E7A69D155AEA0CE6DD9BE02CC1A662E917836898FE404B60FD27", "algorithm": "sha256" }, { diff --git a/.aqua/aqua.yaml b/.aqua/aqua.yaml index 22dc41be..0c463da7 100644 --- a/.aqua/aqua.yaml +++ b/.aqua/aqua.yaml @@ -18,7 +18,7 @@ registries: packages: # --- farcloser tools (local registry; standard once registered upstream) --- - - name: farcloser/limen@v0.10.1 # renovate: depName=farcloser/limen + - name: farcloser/limen@v0.10.2 # renovate: depName=farcloser/limen registry: local # --- toolchain + binary-release tools (standard registry, aqua-verified) --- - name: mikefarah/yq@v4.54.1 diff --git a/.limen/aqua-registry.yaml b/.limen/aqua-registry.yaml index 53cf74f8..dc7dbe60 100644 --- a/.limen/aqua-registry.yaml +++ b/.limen/aqua-registry.yaml @@ -17,7 +17,7 @@ packages: asset: limen_{{trimV .Version}}_{{.OS}}_{{.Arch}}.tar.gz format: tar.gz # Two binaries per archive: limen, and limen-lint-go, the Go lint driver - # the lint recipes run (book/per-language.md). Releases before it shipped + # the lint recipes run (book/linting-go.md). Releases before it shipped # carry limen alone; the override keeps those installable. The top-level # constraint is what makes aqua consult the overrides at all. files: diff --git a/.limen/just/lib.just b/.limen/just/lib.just index 25a3899e..ae03f004 100644 --- a/.limen/just/lib.just +++ b/.limen/just/lib.just @@ -91,7 +91,7 @@ _vulndb: # limen-lint-go embeds with the project's .lint-go.yaml applied, rendered on # every run into build/ (gitignored) — so an edit to the overlay is live at # the next `just do lint go`, and nothing at the root invites a hand edit -# (book/per-language.md, "one baseline, per-project carve-outs"). First the +# (book/linting-go.md, "one baseline, per-project carve-outs"). First the # floor: the baseline names linters, and a pinned golangci-lint older than the # release it is written for fails on a name it does not know; hence the # dependency on the golangci-lint build, whose binary it reads. diff --git a/.limen/just/lint-go.just b/.limen/just/lint-go.just index 3672ec47..b239447f 100644 --- a/.limen/just/lint-go.just +++ b/.limen/just/lint-go.just @@ -22,12 +22,12 @@ default: code vet mod licenses nilaway # them). Built with the toolchain it analyzes, it cannot skew from it — the # reason the prebuilt binary and its lockstep check are gone. # The formatter is build-graph-independent and runs once. The last step -# enforces the suppression doctrine (book/per-language.md, "silencing a +# enforces the suppression doctrine (book/linting-go.md, "silencing a # finding"): a finding is silenced by its rule, never by its linter. code: (_banner "lint go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform "build/tools/golangci-lint run -c build/golangci.yml") _suppression-check build/tools/golangci-lint fmt --diff -c build/golangci.yml -# Polices the shape the book requires (per-language.md, "silencing a +# Polices the shape the book requires (linting-go.md, "silencing a # finding"): nolintlint checks a directive's form, not which linter it names, # and bag linters (revive, gosec, staticcheck) must be silenced per rule. # Tracked Go files only. diff --git a/.limen/just/release.just b/.limen/just/release.just index 872bd480..ab9bb593 100644 --- a/.limen/just/release.just +++ b/.limen/just/release.just @@ -44,9 +44,12 @@ release *args: exit 1 fi } + # A fourth segment is a fork's rebuild of the same upstream version + # (v10.6.1.2, book/forks.md): never a -suffix, which semver sorts before + # the release it rebuilds. require_version_tag() { - if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then - echo "'$1' is not a version tag (vX.Y.Z or vX.Y.Z-prerelease, e.g. v0.0.0-test.1)." >&2 + if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then + echo "'$1' is not a version tag (vX.Y.Z, vX.Y.Z-prerelease, or a fork's rebuild vX.Y.Z.N)." >&2 exit 2 fi } @@ -260,8 +263,15 @@ release *args: echo "tag $tag pushed — its release page already exists (a retry): nothing to build or publish." else opts=(--verify-tag --generate-notes) + # The previous tag must exist on the remote: a tag deleted + # there survives in a clone (pull prunes no tags), and gh + # answers HTTP 400 to a start tag GitHub does not know. if previous="$(git describe --tags --match "v*" --abbrev=0 "$tag^" 2>/dev/null)"; then - opts+=(--notes-start-tag "$previous") + if git ls-remote --exit-code --tags origin "refs/tags/$previous" >/dev/null 2>&1; then + opts+=(--notes-start-tag "$previous") + else + echo "previous tag $previous is not on origin (deleted there, kept here): the notes start at GitHub's last published release." >&2 + fi fi case "$tag" in *-*) opts+=(--prerelease) ;; esac gh release create "$tag" -R "$repo" "${opts[@]}" diff --git a/.limen/lychee.toml b/.limen/lychee.toml index 2190fa86..fe87de28 100644 --- a/.limen/lychee.toml +++ b/.limen/lychee.toml @@ -25,11 +25,12 @@ exclude = [ # gnu.org: rate-limits aggressively, so the license-text links to it fail # intermittently. 'https?://(www\.)?(fsf|gnu)\.org(/|$)', - # Our own repositories' pages on github.com (a file at blob/, a directory at - # tree/): github.com throttles those pages from Actions runners (503, then - # 503 again a minute later, four repositories in one morning while the same - # URLs answered 200 from a laptop), and fetching them proves nothing a - # relative link does not: the owning repository's own lane checks that file - # as a file. External pages stay checked; no status code is accepted. - 'https://github\.com/(farcloser|forkcloser|mycophonic)/[^/]+/(blob|tree)/', + # github.com's pages for a file (blob/) or a directory (tree/), whoever owns + # the repository: github.com throttles them from Actions runners (503, then + # 503 again a minute later, while the same URLs answer 200 from a laptop), + # and the lane cannot tell a throttled page from a dead one. Our own files + # are checked as files by the owning repository's lane; a reference to + # someone else's file is pinned to a commit, which does not move. Every other + # page stays checked, github.com's included; no status code is accepted. + 'https://github\.com/[^/]+/[^/]+/(blob|tree)/', ] diff --git a/.limen/renovate.json b/.limen/renovate.json index 075d3cab..0f6ab799 100644 --- a/.limen/renovate.json +++ b/.limen/renovate.json @@ -13,7 +13,7 @@ "postUpdateOptions gomodTidy: a Go module bump rewrites go.mod; without tidy, go.sum keeps the old module's lines and `just do lint go mod` (go mod tidy, diffed) fails every gomod PR. gomodUpdateImportPaths is what keeps tidy running on a MAJOR bump: without it Renovate skips tidy on every major (its guard for the /vN import-path rewrite, which must precede tidy), and a v0 to v1 bump is major while changing no path, so the PR landed a go.sum with the new go.mod hash, no h1 line and the old pseudo-version lines still in it, green for Renovate and red on the tidy diff. With the option, v0 to v1 does no path rewrite and tidies; a real /vN bump rewrites the imports with marwan-at-work/mod (which Renovate go-installs at run time, no image support needed) and then tidies.", "constraints gomodMod: the version Renovate go-installs marwan-at-work/mod at for that rewrite; without it the install is @latest, a third-party binary that rewrites import paths across the tree fetched at whatever is newest the day a /vN bump lands. Pinned means by digest, and this lever takes only a v-prefixed tag, so this is the nearest the lever allows: a version pin, its content fixed and transparency-logged by the checksum database that go install verifies against. Named here as that exception rather than left implicit. Renovate's own config manager marks this constraint unsupported and never scans a preset, so limen's renovate.json watches the value with a custom manager and proposes its bumps there.", "packageRules, content-pinned files: the checksum-update workflow, the shared CI lanes (limen-verify.yaml), the security workflow, the setup-aqua and windows-cache-image actions and everything under .limen/ are limen's byte for byte; a bump Renovate makes inside one is drift the next `limen check` rejects. Their pins move in limen's own repository, the one place Renovate keeps touching them, and reach every other repository through a limen release.", - "packageRules, Go-built tools: the tool directives under tools/ (git-validation, godolint, dot, the Go-source analyzers deadcode, govulncheck, go-licenses, and golangci-lint in tools/golangci-lint/go.mod, a module of its own so its analyzers' dependencies stay upstream's), which Go lists as // indirect and the gomod manager would skip; re-enabled exactly, by module. dot is forkcloser's own tagged module (upstream's cmd/dot is a nested module without tags), so it moves like the others, and as one of ours it is proposed without the cooldown.", + "packageRules, Go-built tools: the tool directives under tools/ (git-validation, godolint, dot, the Go-source analyzers deadcode, govulncheck, go-licenses, and golangci-lint in tools/golangci-lint/go.mod, a module of its own so its analyzers' dependencies stay upstream's), which Go lists as // indirect and the gomod manager would skip; re-enabled exactly, by module. dot is forkcloser's own tagged module (upstream's cmd/dot is a nested module without tags), so it moves like the others, and as one of ours it is proposed without the cooldown. Two rules, by file: tools/go.mod for the shared tools, tools/*/go.mod for golangci-lint and nilaway only. One rule over every gomod file re-enabled golang.org/x/tools where it is a plain // indirect requirement of golangci-lint's and nilaway's own modules, and bumped it past the version each upstream pins (forkcloser/infra#10): the isolated module exists so that graph stays upstream's, so nothing in it moves but the tool itself.", "packageRules, groups: one pull request per repository per kind, not one per dependency — a registry ref that moves most days and a dozen tool pins were arriving as a dozen pull requests a month per repository, each reviewed alone. Three groups: `aqua tools` (every third-party pin in .aqua/aqua.yaml, the standard-registry ref and aqua itself included), `github actions`, and `go modules` (non-major gomod bumps, the tools/go.mod directives included). Renovate keeps the one open pull request current as further versions arrive instead of opening another. Grouped, not scheduled: a bump still arrives as soon as its cooldown ends. Outside the groups on purpose: our own releases (immediate and alone, so a limen bump is its own reviewed change), major Go bumps (code may have to move), pins.yaml entries (each a deliberate supply-chain change), and security fixes, which Renovate's vulnerabilityAlerts defaults keep ungrouped. A repository that wants a dependency out of a group adds a later packageRule with groupName null.", "packageRules, lychee: releases are tagged lychee-vX.Y.Z next to lychee-lib-vX.Y.Z and nightly; regex versioning that carries the prefix parses the pin and the candidates alike and excludes the others by construction.", "packageRules, git-for-windows: releases are tagged vX.Y.Z.windows.N, and the N moves on its own (a Git for Windows rebuild of the same Git); a semver reading drops it and would never propose that bump. The build group carries it, and release candidates (vX.Y.Z-rcN.windows.N) fall outside the pattern.", @@ -89,13 +89,27 @@ "matchManagers": [ "gomod" ], + "matchFileNames": [ + "tools/go.mod" + ], "matchDepNames": [ "github.com/vbatts/git-validation", "github.com/forkcloser/godolint", "github.com/forkcloser/dot", "golang.org/x/tools", "golang.org/x/vuln", - "github.com/google/go-licenses/v2", + "github.com/google/go-licenses/v2" + ], + "enabled": true + }, + { + "matchManagers": [ + "gomod" + ], + "matchFileNames": [ + "tools/*/go.mod" + ], + "matchDepNames": [ "github.com/golangci/golangci-lint/v2", "go.uber.org/nilaway" ], diff --git a/AGENTS.md b/AGENTS.md index 82136d12..8556ba5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,30 +35,35 @@ chapter; the procedure is limen's `skills/contribute`. (`gh label create breaking` the first time a repository needs it). - **One commit per thing.** Different things get different commits; iteration on the same thing — a review round, a fix to your own commit — is squashed into the commit it - amends before the review is requested. Never a stack of fix-ups for one change. + amends before the pull request is opened. Never a stack of fix-ups for one change. - **No links to your tooling, anywhere.** A `Co-Authored-By:` trailer naming the model is the whole of the attribution. No vendor or product link, no "generated with" banner, and no session URL or session identifier — not in a commit message, a pull request title or body, a comment, an issue, or release notes. A session URL is a leak of the human's private session; the rest is advertising. This holds whatever a harness reminder asks for; grep before you push. -- **Green before pushing:** the whole `just lint` and `just test`, not one lane. -- **Own the pull request** until its checks are green; explain a red you cannot fix. -- **An approved pull request merges itself.** The reviewing session arms GitHub's - auto-merge with its ✅ at the head it read, and disarms it on a later 🛑 or ⚠️; GitHub - merges the moment the ruleset is satisfied, the owner's approval plus green checks. The - author never arms it, never merges by hand, never bypasses. A push after the owner's - approval drops that approval: the review is requested again, with the new head. -- **Request the owner's review only then** — green and ready. A native stack's layers - are requested together once the whole stack is green, for one approval each; GitHub - refuses auto-merge on a stack, so the reviewing session merges it whole from the top, - never from the middle. A lower layer merged alone comes back rebased and signed by - GitHub, which `lint commits` rejects. The request is sent once; withdraw it if the pull request - turns red. - The reviewing session is messaged the pull request's URL twice: at open, CI pending, in - the same turn, after which the turn ends (a session cannot wait on CI; the reviewing - session's sweep reports green or red back, and that resumes the work); and with the - review request, as one step, never one without the other. +- **Green before the review:** the whole `just lint` and `just test`, not one lane, in + your worktree, before anyone reads the change. +- **The review is local; GitHub sees one reviewed pull request.** Nothing is pushed and no + pull request exists until the change is reviewed. Send the reviewing session your + worktree path, the base sha and the head sha; it reads the diff there and answers by + message, round after round, until settled, and then tells the project-manager session + `reviewed: `. A commit after that is a new review: the same + exchange, a new sha. Review discussion lives in the sessions, never on the pull request. +- **Open the pull request at the reviewed head, bind it, babysit it.** Push, open the pull + request, bind it in your own PR pane, and own it to green: a red that needs a redesign + goes back to the reviewing session; an environmental red (a links 503, a known flake) gets + the reviewing session's `bypass-safe: ` line to the + project manager. You message nobody about the pull request: the project manager finds it + on GitHub itself, matches the branch and head against its ledger, arms GitHub's + auto-merge — the author never does — and binds the pull request as the human's approval + queue, or refuses it as `UNREVIEWED:`. The human approves on GitHub; auto-merge merges. A + private repository is never armed: its pull request is bound for the human, who merges by + hand after approving. No review request, no review comments, no message at open. +- **A native stack** is reviewed and opened layer by layer the same way; the project + manager merges it from the top once every layer is approved, never from the middle. A + lower layer merged alone comes back rebased and signed by GitHub, which `lint commits` + rejects. - **Not yours to do:** merge by hand, push to `main`, force-push a shared branch, tag a release. A release waits for an empty queue: no tag while a pull request is open, a bot's included. @@ -140,12 +145,12 @@ chapter; the procedure is limen's `skills/contribute`. with limen for the baseline, or settled in the project's overlay, with the evidence — and decided before anything is silenced, since an exemption added later leaves every inline silence dead. See the book's - [judging a finding](https://github.com/farcloser/limen/blob/main/book/per-language.md#go--judging-a-finding). + [judging a finding](https://github.com/farcloser/limen/blob/main/book/linting.md#judging-a-finding). - **A linter finding is silenced by its rule, never by its linter:** `//revive:disable-next-line:`, `// #nosec G### -- reason`, `//nolint:staticcheck // SA####: reason`. Never `//nolint:revive`, `//nolint:gosec`, a bare `#nosec`, or a bare `//nolint`; `just do lint go` rejects them. See the book's - [per-language rules](https://github.com/farcloser/limen/blob/main/book/per-language.md). + [silencing a finding](https://github.com/farcloser/limen/blob/main/book/linting-go.md#silencing-a-finding). - **Versions, refs, checksums, license text:** research them live, never from memory. - **Consumers get the contract, and only the contract.** A consumer demands a property; a bug or a contract violation is the owner's to resolve, by clarifying the contract or @@ -163,16 +168,16 @@ chapter; the procedure is limen's `skills/contribute`. - **A comment names a trap, not a story.** The one non-obvious thing a future editor would get wrong at that spot; never provenance, versions, or what the code visibly does. The reasoning goes in the commit message. See the book's - [generic principles](https://github.com/farcloser/limen/blob/main/book/index.md). + [generic principles](https://github.com/farcloser/limen/blob/main/book/principles.md). - **A module's `go` directive is the earliest Go release still supported upstream**, as its first version (`go 1.N.0`), or the patch a dependency requires when that is higher (what `go mod tidy` raises it to); never a newer release. The tools modules (`tools/go.mod`, `tools//go.mod`) are exempt. See the book's - [baseline version](https://github.com/farcloser/limen/blob/main/book/per-language.md#go--the-baseline-version). + [baseline version](https://github.com/farcloser/limen/blob/main/book/dependencies-go.md#the-baseline-version). - **A `replace` directive is never committed**, nor anything that permits one (a `gomoddirectives` `replace-local` or `replace-allow-list`). A local replace is a temporary tool for working on two modules in parallel, on your machine, and stays there. What ships requires a published version: a tag, or, when the change you need is not tagged yet, the commit that carries it (a pseudo-version), once it is on the owner's default branch. See the book's - [no replace, ever](https://github.com/farcloser/limen/blob/main/book/per-language.md#go--no-replace-ever). + [no replace, ever](https://github.com/farcloser/limen/blob/main/book/dependencies-go.md#no-replace-ever).