Skip to content

release: v11.2.0 — per-aspect volume, the monoculture guard, and Dependabot for security only - #155

Merged
kilianmc merged 10 commits into
mainfrom
release/11.2.0
Sep 10, 2026
Merged

kilianmc merged 10 commits into
mainfrom
release/11.2.0

Conversation

@kilianmc

Copy link
Copy Markdown
Owner

Promotes dev to production. v11.0.0 → v11.2.0, a minor: one user-visible feature plus housekeeping, which is what the bump is meant to track rather than defaulting to major.

What ships

Dashboard: logged sets per climbing aspect (#151) — the per-aspect volume view.

Issue #89 closes as a non-defect, and ruling 55 pins what PR #120 won (#152). "~36 exercises carry ~80% of every plan" turned out not to be a defect: measured over 144 plans there is no concentrated head (80% of blocks sit on 55% of the rows a plan uses, where an even spread reads 80%), and the head count is mostly set by the eligible pool, which the climber's equipment decides. What was unguarded is that #120's monoculture fix could regress silently — so no single exercise may now exceed 17% of a plan's blocks or 23% of its minutes, shown to fail by narrowing the candidate pool.

Router pinned at the last version the federated mount survives (#153). From @tanstack/react-router 1.170.34 the router treats a scheme-carrying href as external and drops its onClick; createRemoteHistory produces exactly such hrefs by construction (issue #16), so the mount would leave kilianmc.com on every left-click. Pinned exactly at 1.170.33 — a caret permits the breaking version. The same release retyped boundary errors as unknown, which surfaced a real bug: the error component crashed on a null throw and rendered blank for anything that was not an Error.

Dependabot stops opening scheduled version-update PRs (#154). Alerts and security updates are a repo-level setting and are untouched — advisories still arrive automatically. This promotion is what makes it live, since the config is read from the default branch.

Verification

CI green on each PR into dev individually, and green on this branch. No migration owed — DB 0008 is unchanged. GENERATOR_VERSION unchanged at 9.0.0: nothing here alters plan generation, and the monoculture arm only measures it.

Known limits, carried deliberately

🤖 Generated with Claude Code

kilianmc and others added 10 commits September 10, 2026 16:37
chore: back-merge v11.0.0 into dev
Closes the surviving half of planned PR #17. The send pyramid died with
ascent logging and the sRPE trend dies with the pending RPE removal, so
per-aspect volume is the one progress view left.

`GET /api/sessions/volume` counts `logged_set` rows per (training day x
aspect) in one statement and folds them server-side to ten totals. The
per-day rows never reach the wire: a series with no reader is the
orphaned-wire-field shape the register already tracks.

Two things it deliberately does not do:

- It reports SETS, not minutes. `activity.duration_minutes` is
  session-level over a session that mixes aspects, `session_block`
  snapshots no aspect, and `logged_set.actual_work_seconds` is nullable,
  so summing seconds would undercount every rep-based exercise while
  looking like a total.
- Bars are shares of the busiest aspect, never of a target. Nothing
  prescribes an aspect's volume, so reusing `ProfileProgress` and its
  `aria-valuemax` would assert a goal the generator never made.

The read adopts `server/journal`'s settled cap pattern — `LIMIT cap + 1`
and a strict `>` — so `truncated` is exact rather than a false positive
on a window holding exactly the cap. It goes one step further than
`_fold_sessions`: a day the cap split is dropped WHOLE, because
`truncated` promises "older training is missing" and cannot say "one of
these totals is short".

Ten aspects is past where a colour per series stays legible, so they are
table rows with one hue and the count as text in every row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(progress): count logged sets per climbing aspect on the dashboard
…lose #89

Issue #89 tracked "~36 exercises carry ~80% of every plan" as a library
concentration defect. Measured over 144 plans, that metric is not one: no
exercise exceeds 15% of a plan's blocks or 14% of its minutes, 80% of the
blocks sit on 55% of the rows a plan uses where an even spread reads 80%, and
the head count is mostly set by the eligible pool — 94 rows with full gear
against 38 on a bare bouldering wall — so it moves on a purchase rather than
on a defect. Ruling 47 had already priced the one route to moving it.

Closed as a non-defect by ruling 55, which keeps the part that IS owed: PR
#120 fixed a real monoculture at its cause and nothing pinned the result, so a
library or ranking edit could have walked it back silently.

The guard asserts a per-plan ceiling on the single largest exercise over the
existing twelve-row sweep at six session counts and three weaknesses, 216
plans: 17% of a plan's blocks and 23% of its seconds. Both halves, because
#120 stated its result in minutes while #89's metric was blocks, and a ceiling
on one leaves the other free to regress. `OPEN_CLIMBING_KEYS` is out of both
numerator and denominator — those blocks arrive as ruling 27's length fill by
ruling 29's decision, so counting them would measure a ruling, not a defect.

Shown to fail, not assumed to: narrowing `prescribable()` to one row per cell
— the generalised pre-#120 condition — puts `limit_boulders` at 17.50% of
blocks and 30.78% of minutes, over both ceilings, on all twelve rows. Freezing
the rotation instead stays green, which is the useful negative: pool narrowness
makes a monoculture, rotation order does not. The worst case in both eras is
the narrow-equipment column, so a full-vocabulary-only sweep would have been
green for the wrong reason.

Tests only — no product code changes, and no new sweep, plan builder or minutes
helper: both arms read `generate()` output through the file's own `_input` and
`_block_seconds`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(test): guard a plan against a single-exercise monoculture, and close #89
…t version the mount survives

#144's bump is a version update with no advisory behind it, and its `web` job
fails: `@tanstack/react-router` 1.170.33 retypes a boundary's `error` as
`unknown` (TanStack #8209), which `RouteError` declared as `Error`.

Two things came out of taking it seriously.

The error component was lying about what it receives. The router preserves
falsy throws, so `0`, `false`, `''`, `null` and a plain object all reach the
boundary; the old `{error.message}` crashed outright on `null` and rendered an
empty line for the rest. `errorText()` narrows for real — no cast, no `any` —
and falls back to a sentence a user can read. Nine cases in
`web/src/ui/routeError.test.tsx` go through the render, not the helper: eight
of them fail against the previous `(error as Error).message`.

The pin is the other half, and it is NOT what dependabot proposed. Latest
stable is 1.170.35 / 1.168.37, verified against the registry, and the pair
cannot be split: router-plugin 1.168.37 peers `^1.170.34`. From 1.170.34
(TanStack #8308) the router classifies any href carrying a scheme as an
external link and returns a bare anchor with no `onClick` — and
`createRemoteHistory` sets `createHref` to an absolute standalone URL by
construction (issue #16), so every left-click in the federated mount would
leave kilianmc.com instead of navigating in place. `remote.guard.test.tsx`
catches it: 1.170.33 green, 1.170.34 red.

So both packages are pinned EXACTLY rather than with a caret, because
`^1.170.33` permits the version that breaks the mount. The prohibition sits on
the `createHref` assignment that causes it, the guard test points at that
line, and the docstring's claim that left-clicks are unaffected is now bounded
by the version it stops being true at.

Reworking issue #16's absolute-href mechanism is what would unblock 1.170.35.
That is not this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(deps): supersede dependabot #144, pinning the router at the last version the mount survives
…y only

Kilian believed every Dependabot PR was a security alert. They were not: this
config scheduled weekly VERSION updates across three ecosystems, and that is
where #137, #138 and #144 came from. #144 carried no advisory and broke the
web build. Alerts and security PRs are a repo-level setting, already enabled
and untouched by this change, so advisories still arrive automatically.

There is no version-updates opt-out short of removing the config, so the file
goes. It is read from the default branch only, so this takes effect when `dev`
is next promoted.

Deleting it would have left two lines in `CLAUDE.md` false:

- one cited the config as a path, which the path arm of
  `test_claude_md_claims.py` would have caught. Its subject — a class of file
  read only from `main` — is still true, and `workflow_dispatch` registration
  is another member, so the line keeps the doctrine and drops the example.
- "SOME pinned action SHAs Dependabot can never bump" implied the rest were
  handled. Nothing is now bumped on a schedule, and only an advisory can move
  an action pin, so the line says every pin is checked by hand.

The config's two `ignore` rules were holds, and a prohibition may not be
deleted with the file that carried it. The `typescript` hold is already stated
at `CLAUDE.md:64` and needed nothing. The `@types/node` hold had no home in
the tree — only PR #6's commit body and an archived reason — so it becomes
`tests/test_node_types_pin.py`, which asserts the `@types/node` major equals
the `.nvmrc` runtime major. A guard rather than a tripwire line, because the
claim is executable: staleness is the correct state here, nothing else in the
repo objects to a newer major, and a hold nothing enforces is one the next
reader "fixes". Shown to fail at `^26.0.0`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: stop scheduled version updates, keeping Dependabot for security only
…ndabot for security only

A minor rather than a major: one user-visible feature and two pieces of
housekeeping, which is what the bump is meant to track.

- the dashboard counts logged sets per climbing aspect (#151)
- issue #89 closes as a non-defect, and ruling 55 pins what PR #120 actually
  won: no single exercise may take more than 17% of a plan's blocks or 23% of
  its minutes (#152)
- `@tanstack/react-router` is pinned exactly at 1.170.33, the last version
  where the federated mount still navigates in place, and the error boundary
  now handles a non-`Error` throw instead of crashing on it (#153)
- Dependabot no longer opens scheduled version-update PRs; alerts and security
  updates are a repo setting and are unaffected. This is the promotion that
  makes that live, since the config is read from `main` (#154)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
climb-trainer Ready Ready Preview Sep 10, 2026 8:46pm UTC

@kilianmc
kilianmc merged commit 576656d into main Sep 10, 2026
5 checks passed
@kilianmc
kilianmc deleted the release/11.2.0 branch September 10, 2026 20:49

This branch was successfully deployed

1 active deployment
Preview — 41ea53cd Deployed Sep 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant