From f6f6c899c17b656a1b03f41a48de9b40fb9c9f5f Mon Sep 17 00:00:00 2001 From: Ashlen Date: Tue, 4 Aug 2026 13:00:35 -0600 Subject: [PATCH] docs(specs): correct the renovate design after adversarial review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An adversarial re-verification pass over the Renovate deliverable (four claim reviews, each independently re-derived then refuted, plus a completeness critique) found two defects that would have shipped: - The packageRules entry was under-scoped. matchDepNames matches depName only; the mise manager surfaces mise.toml's pin as depName `uv` while the github-actions manager extracts setup-uv's `version:` input as `astral-sh/uv`. The rule would have silenced the mise side and left all five workflow inputs free to bump, causing the desync it was written to prevent. Corrected to matchPackageNames: ["astral-sh/uv"], which covers both without touching the action's own digest pin. - Renovate has no manager named `uv`; pep621 reads pyproject.toml and uv.lock and must stay enabled. A top-level "uv" key is an invalid option that aborts the repository run rather than disabling anything. The spec also predates gaps #1-#5 and #8, so several premises had gone stale: the harness now has a pre-commit config, a `just ci`, a `just audit` with a pinned pip-audit, and five uvx pins rather than one. Every test_generation.py citation was stale. Records three findings no prior document had: lockFileMaintenance is the load-bearing line (all dev deps are capped ranges, so pep621's `replace` strategy opens no PRs for in-range releases, including the spec's own motivating examples); no Renovate manager can see the template layer because every file there is .jinja, so a pin-parity policy test is a precondition rather than a follow-up; and the template ships "pre-commit": {enabled: true} alongside bare-SHA revs, which now needs an explicit decision. Resequences accordingly — tag, parity test, enable the app for its reversible onboarding dry run, branch protection, then the config. The app was confirmed never enabled and main carries no branch protection, both of which the sequence previously assumed away. --- .../2026-06-25-dependency-inventory-design.md | 288 ++++++++++++++---- 1 file changed, 223 insertions(+), 65 deletions(-) diff --git a/docs/superpowers/specs/2026-06-25-dependency-inventory-design.md b/docs/superpowers/specs/2026-06-25-dependency-inventory-design.md index 3b9ae27..ea80493 100644 --- a/docs/superpowers/specs/2026-06-25-dependency-inventory-design.md +++ b/docs/superpowers/specs/2026-06-25-dependency-inventory-design.md @@ -1,7 +1,9 @@ # Design: comprehensive, trustworthy dependency-surface visibility **Date:** 2026-06-25 -**Status:** proposed (awaiting review) — revised after a 5-dimension adversarial spec review +**Status:** proposed (awaiting review) — revised after a 5-dimension adversarial spec review, +then amended 2026-08-04 after an adversarial re-verification of the Renovate design (see +"Amendment note") **Scope:** Make every dependency surface — across **both layers** (the maintainer harness *and* the generated project) — discoverable and trustworthy. The **inventory of record is in-repo**: a hand-maintained surface-map in `AGENTS.md` plus `just deps`, both @@ -27,6 +29,33 @@ leg rests on a single `[medium; 2-1]` claim — see Evidence). > model is now inverted (in-repo record; Renovate as freshness), and the maintainer > `renovate.json` is scoped to avoid both desyncs. +> **Amendment note (2026-08-04).** Only deliverable ③ (`vcs_ref="HEAD"` in the `_render` +> fixture) has landed, via PR #4. Everything else here is still unbuilt, and in the interim +> gaps #1–#5 and #8 of the dogfood-gap audit landed — invalidating several of this spec's +> factual premises (the harness *does* now have a pre-commit config, a `just ci` aggregate, +> a `just audit` with a pinned `pip-audit`, and five `uvx` pins rather than one). An +> adversarial re-verification pass (2026-08-04; four claim reviews, each independently +> re-derived and then refuted, plus a completeness critique) also found **two defects in the +> Renovate design that would have shipped**: +> +> 1. **The `packageRules` entry was under-scoped and would have caused the very desync it +> was written to prevent.** `matchDepNames: ["uv"]` matches depName only. Renovate's +> `mise` manager surfaces `mise.toml`'s `uv` pin as depName `uv`, but its `github-actions` +> manager natively extracts `astral-sh/setup-uv`'s `with: version:` input as depName +> `astral-sh/uv`. The original rule would therefore have silenced the `mise.toml` side +> while leaving all five workflow inputs free to bump. Corrected to +> `matchPackageNames: ["astral-sh/uv"]`, which matches both (both carry that packageName) +> and does not touch the action's own digest pin (packageName `astral-sh/setup-uv`). +> 2. **There is no Renovate manager named `uv`.** The manager reading `pyproject.toml` + +> `uv.lock` is `pep621`. A top-level `"uv": {…}` key is an *invalid configuration option*, +> which aborts the whole repository run — not a silent no-op. This spec never wrote that +> key, but the gap-audit's restatement of this bullet ("Disable the `uv` manager") reads +> as an instruction to, so the wording is corrected here and in that plan. +> +> The pass also established, by direct query, that **the Mend Renovate app has never been +> enabled on this repo** and that **`main` carries no branch protection and no rulesets** — +> both of which change the sequencing (see "Operator preconditions"). + ## Problem `python-kickstarter` pins dependencies across many heterogeneous surfaces, and only one of @@ -36,9 +65,9 @@ them (`uv.lock`) is inspectable with a single command today. |---|---|---| | uv / PEP 621 Python deps | `pyproject.toml` + `uv.lock` | `template/pyproject.toml.jinja` (+ generated `uv.lock`) | | mise tool pins | `mise.toml` `[tools]` | `template/mise.toml.jinja` (+ `gitleaks`) | -| pre-commit hooks | — *(none of its own)* | `template/.pre-commit-config.yaml.jinja` (`rev:` SHAs) | +| pre-commit hooks | `.pre-commit-config.yaml` (`rev:` SHA) *(landed since — gap #1)* | `template/.pre-commit-config.yaml.jinja` (`rev:` SHAs) | | GitHub Actions | `.github/workflows/*.yml` (`uses:` SHAs) | `template/.github/workflows/*.jinja` | -| uvx / mise pins in run-steps | `uvx zizmor@…` | `uvx zizmor@/semgrep@/pip-audit@…`, `mise … gitleaks` | +| uvx / mise pins in run-steps | `uvx zizmor@/semgrep@/pip-audit@…` (5 sites across `justfile` + `test-template.yml`), `mise … gitleaks` *(semgrep/pip-audit/gitleaks landed since — gaps #2, #3)* | `uvx zizmor@/semgrep@/pip-audit@…`, `mise … gitleaks` | Two concrete gaps: @@ -135,16 +164,20 @@ test is a deliberate non-goal. documented in `AGENTS.md` and run ad hoc; `just deps` stays graph-only. The template already ships `just audit` (gated on `enable_dependency_audit`). - **Changing the *template's* Renovate `uv` handling.** The maintainer config (added here) - disables `uv` to avoid desyncing its own multi-site `uv` pins (`mise.toml` + the `setup-uv` - inputs), which no maintainer-side test guards; the *template's* shipped config has the same - multi-site `uv` pin (plus the `uv_build` floor) but its *rendered* pins **are** checked, by - `test_generation.py:802`. Reconciling the template's Renovate-vs-uv-pins is a known follow-up, - out of scope here (this spec adds no new downstream drift — it only adds a maintainer-side - config). + disables the `uv` *tool* to avoid desyncing its own multi-site `uv` pins (`mise.toml` + the + `setup-uv` inputs), which no maintainer-side test guards; the *template's* shipped config has + the same multi-site `uv` pin (plus the `uv_build` floor) but its *rendered* pins **are** + checked, by `test_generation.py:836-839`. Reconciling the template's Renovate-vs-uv-pins is a + known follow-up, out of scope here (this spec adds no new downstream drift — it only adds a + maintainer-side config). **Amended:** the template's `"pre-commit": { "enabled": true }` is a + *separate* question that this spec's findings now put in scope for a decision — see ⑧. - **OpenSSF Scorecard / pinact workflows.** zizmor covers Actions pin-trust; Scorecard is a possible future layer. -- **A maintainer `just ci` aggregate.** The repo has none; `deps`/`deps-template` are - standalone, on-demand inspection — not blocking gates. +- **Adding `deps`/`deps-template` to a gate.** They are standalone, on-demand inspection — not + blocking gates. *(Amended: this bullet originally read "a maintainer `just ci` aggregate — the + repo has none." A `just ci` aggregate landed with gap #5; the non-goal is now specifically + that `deps`/`deps-template` stay **off** it, matching the template, which ships no `deps` + gate either.)* ## Design @@ -171,51 +204,102 @@ Three roles, ordered by trustworthiness: ### Maintainer harness deliverables **① `renovate.json` (NEW), scoped to avoid the `zizmor` parity desync and the multi-site `uv` -desync.** Verified against the suite: `test_generation.py:797` asserts the maintainer -`uvx zizmor@…` pin equals the rendered `scan.yml` pin (single shared version). Therefore the -maintainer config **omits a `customManager`** — its only `uvx` pin is `zizmor`, which must move -in lockstep with the template, not bumped independently (and a markdown-embedded pin would be -untrackable regardless; see the unpinned advisory in ④). It also **disables `uv` updates**, but not to keep a parity -*test* green: that test (`test_generation.py:802`) reads the *rendered* template, not the +desync.** + +**No `customManager` for `uvx @` pins.** The maintainer has **five** such pins +across three tools — `justfile:60` (`semgrep`), `justfile:70` (`pip-audit`), +`test-template.yml:54` (`zizmor`), `:106` (`pip-audit`), `:108` (`semgrep`). Only `zizmor` is +parity-locked: `test_generation.py:834` asserts the maintainer's `uvx zizmor@…` pin equals the +rendered `scan.yml` pin, so a Renovate bump of it fails the `test` matrix **loudly**. The other +two are worse, not better — `test_generation.py:825` compares two *rendered* files (the +maintainer's `justfile`/`test-template.yml` are never read) and `pip-audit` is deliberately +excluded from the drift test entirely (`:831-832`, because `scan.yml` pins an exact version +while the template's `pyproject` floors `>=X.Y`). The template layer is invisible to every +Renovate manager (every file under `template/` ends in `.jinja`), so a `semgrep` or `pip-audit` +PR would bump the maintainer sites while `template/justfile.jinja` and the template `scan.yml` +stay behind — **all gates green**, downstreams shipping a stale pin indefinitely. That is +exactly the drift `AGENTS.md`'s "bump every literal site by hand, against the template" +obligation exists to prevent. A markdown-embedded pin is untrackable regardless (see ④). + +**Disable the `uv` *tool* — via `matchPackageNames`, not `matchDepNames`.** This is not to keep +a parity *test* green: `test_generation.py:836-839` reads the *rendered* project, not the maintainer's own files, so a maintainer `uv` bump cannot touch it. The real reason is that the -maintainer's `uv` is genuinely multi-site — `mise.toml` plus every `setup-uv version:` input in -`test-template.yml`, with **no** maintainer-side test asserting they agree — so Renovate's -`mise` manager would bump only `mise.toml` and silently desync the workflow inputs. `python` and -`just` are single-site in `mise.toml` and bump freely (Renovate's `mise` manager tracks both); -`copier` is pinned in `mise.toml` (exact, uncapped) **and** declared as a `uv` dev dep -(`>= 9.6, < 10`) — the two agree today and both bump within 9.x, but a `copier` 10.x release -would push the `mise.toml` pin past the dev-dep cap, with no maintainer test asserting they -stay in sync (a known multi-site gap, like `uv`). The -`pre-commit` manager is omitted (the harness has no pre-commit config). `uv` is bumped manually -across all sites, as today. - -> **Residual risk (eyes open).** Disabling `uv` leaves the most drift-prone, multi-site pin with -> no freshness and no drift detection. The freshness-preserving alternative is a `customManager` -> matching the `setup-uv version:` inputs (datasource `pypi`, depName `uv`) grouped with the -> `mise` manager so both move together — deferred here for the simpler manual bump, but recorded -> as the better long-term option. +maintainer's `uv` is genuinely multi-site — `mise.toml:3` plus all five `setup-uv version:` +inputs in `test-template.yml` (`:31`, `:49`, `:65`, `:81`, `:101`) — with **no** maintainer-side +test asserting they agree. Renovate sees both surfaces, but under *different depNames*: the +`mise` manager extracts depName `uv`, while the `github-actions` manager natively extracts +`astral-sh/setup-uv`'s `version:` input as depName `astral-sh/uv`. They share the packageName +`astral-sh/uv`. **`matchDepNames: ["uv"]` therefore matches only the `mise.toml` side** and +would leave the five workflow inputs free to bump — reproducing the desync in the opposite +direction. `matchPackageNames: ["astral-sh/uv"]` matches both, and does not touch the action's +own digest pin (packageName `astral-sh/setup-uv`). + +**Naming discipline:** there is no Renovate manager called `uv`. `pep621` is the manager that +reads `pyproject.toml` + `uv.lock` (and it is the one that must stay **enabled**). A top-level +`"uv": {…}` key is an invalid configuration option, which aborts the repository run and opens an +"Action Required" issue — a config that looks like it closes this gap while producing zero PRs +on every surface. + +**Other mise pins.** `python` and `just` are single-site in `mise.toml` and bump freely +(Renovate's `mise` manager tracks both) — though a `python` bump to 3.14 would desync from the +CI matrix (`test-template.yml:21`) and `copier.yml`'s `python_version` choice list, neither of +which any manager reads. `copier` is pinned in `mise.toml` (exact, uncapped) **and** declared as +a `uv` dev dep (`>=9.6,<10`) — the two agree today and both bump within 9.x, but a `copier` 10.x +release would push the `mise.toml` pin past the dev-dep cap, with no maintainer test asserting +they stay in sync (a known multi-site gap, like `uv`). `gitleaks` is single-site in `mise.toml` +for *execution* (`justfile` and CI both resolve from it, so local and CI cannot diverge) but its +version literal is repeated in `AGENTS.md` prose, which a Renovate bump leaves stale. + +**The `pre-commit` manager stays off** — but **not** for the reason originally given here. The +harness *does* now have a `.pre-commit-config.yaml` (gap #1 landed), so "the harness has no +pre-commit config" is obsolete. The current reasons are: (a) its one non-`local` entry is +pinned to a bare 40-hex SHA, a form Renovate handles badly — a static trace of the manager +indicates the SHA is semver-coerced rather than skipped, so Renovate would propose *replacing* +it with a tag, unpinning the SHA and potentially downgrading it, while the `# v6.0.0` comment +silently goes stale; and (b) `AGENTS.md` requires that `rev:` and the template's copy be bumped +**together**, and the template copy is a `.jinja` file no manager can see. *(a) is a source +trace, not an executed run — the onboarding PR (see "Operator preconditions") settles it +empirically. (b) holds regardless, and is sufficient on its own.* Note the manager is off by +default, so the actionable form is an explicit `"pre-commit": { "enabled": false }` carrying +that rationale, rather than silence. + +> **Residual risk (eyes open).** Disabling the `uv` tool leaves the most drift-prone, multi-site +> pin with no freshness and no drift detection. The freshness-preserving alternative is a +> `groupName` rule keeping the `mise` and `github-actions` deps in a single PR so both move +> together — now cheaper than when this spec was first written, since Renovate's native +> `setup-uv` support means no `customManager` is needed. Deferred here for the simpler manual +> bump, but recorded as the better long-term option. ```json { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended", "helpers:pinGitHubActionDigests"], "lockFileMaintenance": { "enabled": true, "schedule": ["before 4am on monday"] }, + "pre-commit": { "enabled": false }, "packageRules": [ { - "description": "uv is pinned in mise.toml AND every setup-uv version: input in test-template.yml, with no maintainer-side test asserting they agree. Renovate's mise manager would bump only mise.toml and silently desync the workflow inputs — bump uv manually across all sites instead.", - "matchDepNames": ["uv"], + "description": "uv is pinned in mise.toml AND every setup-uv version: input in test-template.yml, with no maintainer-side test asserting they agree. Renovate sees both (mise depName 'uv'; github-actions depName 'astral-sh/uv') but would bump them in separate PRs. Match on the shared packageName so BOTH are disabled — matchDepNames: ['uv'] would silence only the mise side. Bump uv manually across all six sites instead.", + "matchPackageNames": ["astral-sh/uv"], "enabled": false } ] } ``` -(The Problem-② in-range drift is caught by `lockFileMaintenance` — the weekly `uv.lock` -refresh surfaces `ruff`/`pydantic-core` (and `copier`'s lock entry) even though they stay -within their `pyproject` ranges; the built-in `mise` manager catches `copier`'s exact -`mise.toml` pin and bumps `just`. `config:recommended`'s `pep621` manager only opens PRs for -constraint-*violating* upgrades. `github-actions` + `helpers:pinGitHubActionDigests` keep the -workflow `uses:` pins fresh.) +**`lockFileMaintenance` is the load-bearing line, not `config:recommended`.** Every dev dep in +`pyproject.toml` is a *capped* range (`copier>=9.6,<10`, `ruff>=0.15,<0.16`, +`basedpyright>=1.39,<1.40`, …), and `pep621` exports no `getRangeStrategy`, so the default +`rangeStrategy: "auto"` resolves to `replace` — which rewrites a range only when the new version +falls **outside** it. The `pep621` manager will therefore open **zero** PRs for in-range +releases, including all three of Problem ②'s motivating examples (`copier 9.15.2→9.16.0`, +`ruff 0.15.19→0.15.20`, `pydantic-core 2.46.4→2.47.0`); it fires only on the seven rare +cap-crossing events. Those three are surfaced by the weekly `lockFileMaintenance` `uv.lock` +refresh and by nothing else — if that line is ever dropped or silently fails, the config looks +correct and does nothing on the Python surface. The built-in `mise` manager catches `copier`'s +exact `mise.toml` pin and bumps `just`/`python`/`gitleaks`; `github-actions` + +`helpers:pinGitHubActionDigests` keep the workflow `uses:` pins fresh (the latter is close to a +no-op here — every `uses:` in `test-template.yml` is already SHA-pinned with a tag comment, as +`tests/policy/test_gates.py::test_actions_are_sha_pinned` enforces). **② `justfile` — add `deps` and `deps-template`.** `deps-template` must be a shebang recipe (the file's `set shell := [bash -eu -o pipefail -c]` runs each plain line as a separate @@ -286,17 +370,15 @@ block). The maintainer map is unconditional (no toggles in this repo): > | Surface | Pinned in | Read it with | > |---|---|---| > | uv / Python deps | `pyproject.toml`, `uv.lock` | `just deps` (`uv tree --frozen`); freshness `uv tree --outdated`; advisories — see below | -> | mise tools | `mise.toml` `[tools]` | read the file *(Renovate `mise` manager tracks `python`/`just`/`copier`; `uv` is bump-manually)* | -> | GitHub Actions | `.github/workflows/*.yml` `uses:` (SHA + tag comment) | `grep -rn 'uses:' .github/workflows`; **trust:** the `zizmor` job enforces SHA pinning | -> | uvx tool pins | run-steps (`uvx @`) | `grep -rn 'uvx .*@' .github/workflows` *(`zizmor` pin is parity-locked to the template; bump both together)* | +> | mise tools | `mise.toml` `[tools]` | read the file *(Renovate `mise` manager tracks `python`/`just`/`copier`/`gitleaks`; `uv` is bump-manually)* | +> | pre-commit hooks | `.pre-commit-config.yaml` (`rev:` SHA + tag comment) | read the file *(Renovate's `pre-commit` manager is **disabled** — see ①; bump the `rev:` and the template's copy together, by hand)* | +> | GitHub Actions | `.github/workflows/*.yml` `uses:` (SHA + tag comment) | `grep -rn 'uses:' .github/workflows`; **trust:** the `zizmor` job enforces SHA pinning, and `tests/policy/test_gates.py` pins the `@ # v` form | +> | uvx tool pins | run-steps (`uvx @`) — 5 sites across `justfile` **and** `.github/workflows` | `grep -rn 'uvx .*@' justfile .github/workflows` *(no Renovate manager tracks these by design; the `zizmor` pin is parity-locked to the template by a test, `semgrep`/`pip-audit` are not — bump all of them by hand, against the template)* | > | generated project's graph | rendered template | `just deps-template` | > -> *(No pre-commit surface here — the maintainer harness has none of its own; downstream projects -> get one, mapped in the template's `## Dependencies`.)* -> -> Advisories (the harness's deps are all dev, so no `--no-dev`; `pip-audit` is left unpinned — -> a version baked into this markdown would be a tool pin no Renovate manager tracks): -> `uv export --frozen --no-emit-project --no-hashes -o requirements-audit.txt && uvx pip-audit -r requirements-audit.txt && rm -f requirements-audit.txt` +> Advisories: `just audit` (`pip-audit` over the full locked graph — the harness's deps are all +> dev, so **no** `--no-dev`; see the "Dependency audit" section for why restoring it would make +> the gate pass vacuously). **⑤ `CHANGELOG.md` — `[Unreleased] / ### Added`** (additive → next minor): only the **template-side** additions downstreams receive — the `deps` recipe and the `## Dependencies` @@ -314,7 +396,7 @@ surface-map. **Correct toggle gating** (verified against the actual toggles): | Row / note | Gate | |---|---| | uv/Python deps row, `just deps` | unconditional | -| `uv_build` build-system floor (`[build-system].requires`) named as a uv-pin site **not** covered by the rendered-pin drift test (`test_generation.py:802`) | `project_type == "library"` (applications render `[tool.uv] package = false`, no floor) | +| `uv_build` build-system floor (`[build-system].requires`) named as a uv-pin site **not** covered by the rendered-pin drift test (`test_generation.py:836-839`) | `project_type == "library"` (applications render `[tool.uv] package = false`, no floor) | | mise tools row (`[tools]` always ships) | unconditional | | pre-commit `rev:` row (`.pre-commit-config.yaml` ships unconditionally — there is **no** `enable_precommit` toggle) | unconditional | | GitHub Actions row | unconditional; **zizmor trust note** → `enable_sha_pin_policy` | @@ -325,10 +407,25 @@ surface-map. **Correct toggle gating** (verified against the actual toggles): **⑧ Verify `template/{% if enable_renovate %}renovate.json{% endif %}.jinja`** (the conditional-name idiom — there is no literal `template/renovate.json.jinja`). It already carries `config:recommended`, `helpers:pinGitHubActionDigests`, `"pre-commit": {enabled:true}`, -and a `customManager` matching `uvx (semgrep|zizmor|pip-audit)@…`; `gitleaks` is *expected* to be -covered by the mise manager (verify its mise-registry backend resolves to a Renovate-supported -datasource — likely `github-releases` via aqua). No change expected; change only if -verification finds a gap. +and a `customManager` matching `uvx (semgrep|zizmor|pip-audit)@…`; `gitleaks` **is** covered by +the mise manager (`mise registry` resolves it to `aqua:gitleaks/gitleaks`, and `aqua` is a +Renovate-supported backend). + +**Amended — "no change expected" no longer holds; one decision is now required.** The template +ships `"pre-commit": { "enabled": true }` (pinned by `tests/test_generation.py:483`) *alongside* +bare-SHA `rev:` values in `template/.pre-commit-config.yaml.jinja`. That is the same +SHA-plus-enabled-manager combination the maintainer config deliberately avoids in ①, and it is +the higher-blast-radius artifact because it ships to every downstream. Decide explicitly, in the +same body of work, between: +- flipping it to `false` (matching the maintainer config), or +- converting the template's `rev:` lines to the `# frozen: vX.Y.Z` form that + `pre-commit autoupdate --freeze` writes, which is the one shape Renovate updates while + *keeping* the SHA pinned. + +Either choice must update the `:483` assertion in the same commit. Because this is a `template/` +change it is downstream-visible and must ride a release tag (see Migration). Resolving it is +cheap to defer until the onboarding PR confirms the underlying Renovate behaviour empirically — +but it should not be dropped. **⑨ `tests/test_generation.py` — assertions** (no new file under `template/`, so the NEVER rule's file-addition clause is not triggered, but the new behavior is locked per the house @@ -364,9 +461,30 @@ rule's file-addition clause is not triggered, but the new behavior is locked per absent — only the *freshness PRs and the auto-detected cross-check* lapse. But those are exactly what Goal 1 promises, and **committing `renovate.json` produces zero PRs, zero dashboard, and zero cross-check on its own**: the Mend Renovate GitHub App (or a self-hosted - runner) must be enabled on `maybebyte/python-kickstarter`. Confirm whether it is already - installed; if not, enabling it is a required step to close Goal 1 (tracked in the Sequence), - not just a recommendation. + runner) must be enabled on `maybebyte/python-kickstarter`. Enabling it is a required step to + close Goal 1 (tracked in the Sequence), not just a recommendation. +- **Confirmed 2026-08-04: the app has never been enabled.** Established by the absence of every + artifact it would create — zero `renovate`-authored PRs (`gh pr list --state all`: 10 PRs, all + human-authored), zero issues *ever* opened on the repo (so no Dependency Dashboard, which + `config:recommended` would create via `:dependencyDashboard`), and no `renovate/*` remote + branch. Direct interrogation is blocked by token scope (`/user/installations` → 403), so this + is an argument from absence — but a conclusive one for a public repo with 75 commits since + creation. Issues are enabled on the repo, so this is not a disabled-issues false negative. +- **Install the app BEFORE committing `renovate.json`, not after.** With no config on the + default branch, Renovate raises a *reversible* onboarding PR on `renovate/configure` and makes + no other change until it is merged; closing it undoes everything. That PR's body carries + Renovate's own auto-detected dependency inventory — which manager sees which file, and what + depName/packageName each pin resolves to. That is the empirical ground truth this design has + so far been reconstructing by reading Renovate's source, and it is precisely the "independent + auto-detected cross-check" the Trust model already assigns to Renovate. Committing the config + first skips onboarding entirely and throws that free dry run away. Write the scoped config + *into* the onboarding branch (the PR updates in place) and merge it. +- **`main` has no branch protection and no rulesets** (confirmed 2026-08-04: + `/branches/main/protection` → 404, `/rulesets` → `[]`). This matters because the safety + argument for the whole `uvx`/parity design is "the test would go red" — with nothing blocking + merge, red is advisory, and a Renovate PR can be merged past a failing gate. Adding required + status checks on `main` is the cheapest single change that makes the rest of this design + enforceable rather than advisory. Operator step; not something the implementation can do. ## Migration / release impact @@ -380,7 +498,7 @@ rule's file-addition clause is not triggered, but the new behavior is locked per | piece | how it's verified | |---|---| | maintainer `renovate.json` | `npx --yes --package renovate@ -- renovate-config-validator` passes (it auto-detects `renovate.json`; there is **no** standalone `renovate-config-validator` npm package — it ships inside `renovate`). Pin the version rather than `renovate@latest`, and note it needs a Node runtime — a dev-only tool in no tracked surface (no `package.json`; `mise.toml` pins no node). This validation is **manual/one-time, not a CI gate** — like the template's own shipped `renovate.json`, it is unguarded in CI, so a later invalid edit would not fail a build (accepted tradeoff; a shared validator job could cover both) | -| no parity desync | rendered-template parity tests `test_generation.py:788`/`:797`/`:802` are unaffected (they read the *rendered* template, not the maintainer config); the maintainer's own `uvx zizmor` pin stays single-version because the config omits a `customManager`, and its multi-site `uv` pins stay in sync because the config disables `uv` | +| no parity desync | `test_tool_version_pins_have_no_drift` (`test_generation.py:800-839`) is unaffected — its `semgrep` and `uv` assertions read the *rendered* project, and its one maintainer-facing assertion (`:834`, `zizmor`) stays single-version because the config omits a `customManager`. The multi-site `uv` pins stay in sync because the config disables packageName `astral-sh/uv` (**not** depName `uv`, which would cover only the `mise.toml` site). **Caveat:** nothing guards `semgrep` or `pip-audit` parity — see the new maintainer↔template parity risk below | | `just deps` (both layers) | prints `uv tree --frozen` output; rendered-project run asserted in `test_generation.py` | | `just deps-template` | renders HEAD/worktree all-guardrails-on with `--skip-tasks` (every copy-time `_task` — uv sync, hook install — skipped), locks, prints the tree, cleans up; exit 0 | | template `deps` recipe + AGENTS surface-map | new present-when-on / absent-when-off `test_generation.py` assertions with named anchors | @@ -400,6 +518,28 @@ rule's file-addition clause is not triggered, but the new behavior is locked per `failOnWarnings=true`, a ruff/basedpyright bump can introduce a new finding that fails `just lint`/`typecheck` with zero code change — each such Renovate PR needs a manual fix. This is the cost of the freshness layer; consider scheduling/grouping to bound the cadence. + Note this is now *recurring* rather than occasional: `lockFileMaintenance` runs weekly and + moves `ruff` within `>=0.15,<0.16`, so the mechanism fires on a schedule, against a 6-cell + OS×Python matrix, with the non-hermetic `just audit` in the same run. +- **Renovate can only ever bump ONE of the two layers.** Every file under `template/` ends in + `.jinja` (except `.editorconfig`, `LICENSE-APACHE.txt`, and `py.typed`), so **no** Renovate + manager can match the template layer — not `github-actions` (needs `\.ya?ml$`), not `mise` + (needs a `.toml` terminal), not `pre-commit`. Every Renovate PR that touches a pin duplicated + across the two layers will therefore desync them, every time. Today the *only* cross-layer + guard is the `zizmor` assertion at `test_generation.py:834`; the four Action SHAs, `semgrep`, + `pip-audit`, `gitleaks`, and the `pre-commit` `rev:` are all unguarded, and `AGENTS.md`'s + hand-sync obligations are prose that cannot fail a build. **This makes a maintainer pin-parity + policy test a precondition for landing `renovate.json`, not a follow-up** — the first Renovate + PR merges before anyone would otherwise write it. Extend `tests/policy/` with assertions that + compare literals *without* hardcoding versions, the pattern + `test_tool_version_pins_have_no_drift` already uses downstream. This widens the policy suite's + declared scope (`AGENTS.md` currently says it pins config literals, not recipe bodies), so say + so in the same commit. +- **Prose version literals are collateral.** `AGENTS.md` carries version literals for `gitleaks`, + `semgrep`, `pip-audit`, and — the one a *weekly* automated PR will invalidate — `ruff` + ("locked 0.15.19", derived from `uv.lock`). Nothing tests any of them. Landing Renovate also + falsifies `AGENTS.md`'s three standing "**the maintainer has no Renovate**, so the pins are + static" claims, which must be swept in the same commit that adds `renovate.json`. - **Two Renovate configs are intentionally different, not parity-bound.** The maintainer config omits the `customManager` and `pre-commit` manager and disables `uv` (all for the reasons above); the template's keeps them. There is no parity invariant to enforce — the @@ -420,9 +560,27 @@ rule's file-addition clause is not triggered, but the new behavior is locked per ## Sequence -Maintainer `renovate.json` → **enable the Renovate app (or confirm it is installed) so the -freshness PRs Goal 1 promises actually appear** → `_render` fixture `vcs_ref="HEAD"` → `just -deps` + `deps-template` recipes → maintainer `AGENTS.md` surface-map → `CHANGELOG` → template -`justfile.jinja` `deps` → template `AGENTS.md.jinja` surface-map (toggle-correct) → -`test_generation.py` present/absent assertions → verify the template Renovate config. The -detailed, commit-by-commit plan is produced next by `writing-plans`. +**Amended 2026-08-04 — the original order put `renovate.json` first and the app second. That is +backwards:** it forfeits the onboarding dry run, and it lets the first Renovate PR land before +any cross-layer parity guard exists. `_render` fixture `vcs_ref="HEAD"` has since landed (PR #4) +and drops out of the sequence. + +1. **Cut a release tag.** `main` is 75 commits past `v0.1.0`, which already violates the Release + rule in `AGENTS.md`. Doing it now gives a clean pre-Renovate baseline (so "did a dep bump + break the template contract" stays bisectable against a release), keeps bot-authored commits + out of the tree being tagged, and unblocks the ⑧ template-side fix, which is invisible to + downstreams until a tag exists. +2. **Maintainer pin-parity policy test** — before any Renovate config merges (see Risks). +3. **Operator: enable the Mend Renovate app** with no config committed; read the onboarding PR's + detected-dependency inventory and reconcile it against this spec's Problem table. This is the + empirical check that settles the `pre-commit`-manager and `rangeStrategy` questions. +4. **Operator: add branch protection / required status checks on `main`**, so the parity test + from step 2 actually blocks a merge. +5. **Write the scoped `renovate.json` into the onboarding branch and merge it**, sweeping + `AGENTS.md`'s "no Renovate" claims in the same commit. +6. Then the inventory deliverables, unchanged in order: `just deps` + `deps-template` recipes → + maintainer `AGENTS.md` surface-map → `CHANGELOG` → template `justfile.jinja` `deps` → + template `AGENTS.md.jinja` surface-map (toggle-correct) → `test_generation.py` present/absent + assertions → resolve the ⑧ template Renovate `pre-commit` decision. + +The detailed, commit-by-commit plan is produced next by `writing-plans`.