Skip to content

release: v10.0.0 — the training diary, ascent logging cut, and the dependency pass - #145

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

kilianmc merged 10 commits into
mainfrom
release/10.0.0

Conversation

@kilianmc

Copy link
Copy Markdown
Owner

Promotes dev to main. 9.0.0 → 10.0.0. Four feature PRs plus the dependency pass, none of which has been in production.

What goes live

PR
#140 feat(journal) — write how a session felt, from the summary or mid-run
#141 feat(diary) — read entries back as a per-plan chart
#136 docs — cut ascent logging from the product (#79)
#142 chore(docs) — delete the findings inbox, audit every tripwire
#143 chore(deps) — supersede dependabot #137/#138 at latest stable

The user-visible headline is the training diary: sessions can now be journalled and read back as a chart. Everything before this promotion has been invisible to production since 9.0.0.

No migration is owed

DB stays at 0008_plan_persist. journal_entry was pre-provisioned in 0004_domain_schema, so the diary feature needed no schema change and this promotion dispatches nothing.

GENERATOR_VERSION deliberately stays at 9.0.0

It describes the algorithm and bumps only on a behaviour change. Nothing in this promotion touches plan generation, so bumping it would falsely invalidate existing plans — Plan promises that version + input reproduces the tree.

Dependency pass

Ten packages from dependabot #137/#138, pinned to latest stable verified against the registry — three above what those PRs proposed. vitest 4 → 5 needed no config change, and was proven non-vacuous by running vitest 5 against the pre-upgrade dist/ (still 1502, so the runner contributes zero tests).

@types/node and typescript keep their held-back majors per dependabot.yml.

Four fixes to prose and config that no CI job checks: a prohibition vitest 5 made void, three stale measured numbers (deleted rather than refreshed — a measurement in prose just rots again), a silently-dropped **/.git/** exclude default, and an engines.node range that admitted a Node version vitest 5 refuses.

Verification

  • server 1430 passed, web 1505 passed across 62 files, no skips
  • full CI green on dev at f0575ab
  • PR previews checked by Kilian before merge

Note on Dependabot

The one open alert (adm-zip) will not close on this promotion — it has no patched version, and MF 1.21.5 still pulls adm-zip@0.6.0 via dts-plugin@2.9.0. vite.config.ts sets dts: false, so that codepath never loads. Dependabot PRs #137/#138 should auto-close once it re-evaluates dev.

🤖 Generated with Claude Code

kilianmc and others added 10 commits September 7, 2026 12:33
chore: back-merge v9.0.0 into dev
Ascents are cut, not deferred. Nothing in the app trains on one: the plan
generator takes no new inputs and `current_grade_id` is stored rather than
derived, so an ascent log would have fed only two unbuilt display views.

The feature rested on `Ascent`'s docstring opening "the emotional payload of
the whole app" — a sentence both `server/sessions/__init__.py` and #79 were
quoting, and which the register mis-attributed to CLAUDE.md, where the word
"ascent" has never appeared.

The three `ascent*` tables stay as inert schema; dropping them is the contract
half of expand -> deploy -> contract and is scheduled separately. The session
package's note is rewritten from a promise into the prohibition it needs to be,
so nobody wires them up.

`server/sessions/routes.py`'s "Ascents are not loggable here." stays: it is a
route docstring, so FastAPI ships it as the OpenAPI description and it is
frozen under the generated types' `openapi-sha256`. Deleting it fails
`test_the_generated_types_were_built_from_this_api` (b33338e3 != 02aac3d6);
verified red, then restored. It reads as a flat statement of the contract.

The grade-floor filter in `web/src/profile/grades.ts` is kept — the ascent log
justified one bullet about its *placement*, never the filter, and two of the
three placement reasons stand alone.

README item 4 is rewritten: the diary is rescoped from replaying what you did,
which the plan screen already shows, to recording how it felt.

Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…140)

`journal_entry` has carried free text, three 1-5 scales and a weigh-in since
the schema was written, with nothing able to fill it: no endpoint, no UI, no
rows. This adds both ends.

`PUT /api/journal/{client_uuid}` upserts on `(user_id, client_uuid)` with the
user taken from the token, so replay-safety and authorisation are one
mechanism rather than two. An unowned `logged_session_id` gets the same 404 as
one that does not exist. No free text is echoed back, so nothing in the
response needs escaping downstream. The `not_empty` CHECK is enforced at the
edge as a 422 rather than reaching Postgres as a 500.

The write form renders in two places against one draft: inline on the session
summary, and in an overlay over the running player, because a note is often
worth taking mid-session rather than only after Finish. Both reuse the run's
uuid, so a second submit edits the row instead of adding one. The draft lives
on the persisted `RunRecord` and is written on every keystroke, so a failed
write cannot lose what was typed. The save control states what the press would
do — absent when there is nothing to send, Save when nothing has landed
(including after a 4xx), Update only when the server holds a row.

Opening the overlay stops the countdown through the player's existing pause,
and gives back only the pause it took: a run the climber had already paused
deliberately stays paused on close.

This is the app's first overlay, so it owns the machinery every later one will
copy — focus in and back to its opener, a Tab trap, Escape, and a visible
Close as the primary way out. Native `<dialog>` was rejected because jsdom
implements neither `showModal` nor `close`, which would have left every focus
and trap claim unprovable in the gate.

The weigh-in input is gated on `show_body_metrics`, and an unread profile
reads as off, so nothing prompts for a weight it was told not to ask for. No
copy anywhere frames weight as something to reduce.

No migration: DB stays at `0008` and `GENERATOR_VERSION` is untouched. The
per-set body-weight snapshot stays null until #135, its only consumer.

Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eet prose (#141)

Completes #16b. The write half shipped in #140; this is the reading half.

The diary is now one section per plan, each the same shape: the plan's name
(renameable), then a card holding its chart, those readings as a table, and its
own entries. feel/sleep/skin are RAW readings over one span told apart by stroke
style, never colour alone. Entries open in a sheet and edit in place, reusing
each entry's own client_uuid so an edit can never mint a second row for one
session — which is why this no longer waits on #80.

The smoothed weight trend was cut rather than built (Kilian), so the Body weight
section is absent and the weigh-in is a column of the readings table.
JournalResponse.trends is left on the wire with no consumer, recorded in the
findings inbox rather than removed here.

Server: GET /api/journal in two flavours, scoped to a plan and unscoped, each its
own cache entry so opening the whole history and going back costs no read. The
read fetches cap + 1 to tell a real truncation from a count that merely lands on
the cap. PUT /api/plans/{id}/name renames a plan, refused for a demo principal.

Fixes found while building:
- the scope switch changed the query key, so the gate met the empty key with its
  loading page, the document collapsed and the browser clamped the reader to the
  top. keepPreviousData holds the previous scope until the new read lands, with a
  test that holds the read open to observe the in-flight moment.
- .ct-app__chartbox rendered 300x123.75 inside a 1312px card at every screen
  size. Cross-axis auto margins suppress the flex stretch, so the wrapper
  shrink-wrapped to the svg's 300x150 default. It now carries inline-size: 100%,
  and its size steps 320x132 / 606x250 / 904x373 at the named screen sizes.

Stylesheets now carry NO comments at all (Kilian): 1,530 comment lines removed
from the 15 already-committed stylesheets, with _diary.scss arriving new and
already free of them — 1,659 lines over 16 files as the sweep actually ran. The compiled CSS is byte-identical — both asset hashes match the
pre-sweep build — so this removed prose and nothing else. .scss is brought under
tests/test_comment_budget.py at a cap of zero, with a third detector for a
trailing comment after code and no allowlist exemption, since a length argument
collapses at zero. The rules that prose carried and nothing else stated are now
tripwire lines in CLAUDE.md; the reasons are gone deliberately.

CLAUDE.md's landing-page tripwire was stale and is corrected: text over a
photograph is legal behind the --ct-scrim overlay, which the code has shipped
since it was restored, and the lightest stop under copy is the contrast floor.

Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p stale allowlist headroom (#142)

The `## Findings inbox` section is gone (Kilian). It staged a finding until the next
promotion to `main`, which ties triage to an event weeks away while nothing re-checks a
staged line — so it accumulates claims that have stopped being true. One of the five lines
it held described a gap that the very PR which surfaced it had already closed: #141 stripped
1,657 comment lines from the stylesheets *and* shipped the `.scss` budget guard in the same
commit. The rule is now one line in the header: a finding is triaged the moment it is found.

The five staged findings, each routed:
- the `/api/sessions/completion` off-by-one at the row cap -> filed as a register issue
- the blanked-dev-server recovery -> folded into the existing tripwire as a clause
- `.scss` outside the prose budget -> DELETED, already guarded by #141
- `JournalResponse.trends` having no reader -> added to the unused-columns sweep
- the web suite's `settle()` race -> filed as a register issue

Tripwires audited, 104 -> 103 bullets. One deletion: the gitleaks bullet, whose digest
clause duplicated the stronger `openapi-sha256` phrasing and whose `useDefault` clause is
asserted by `tests/test_gitleaks_config.py`. Three further clauses trimmed because a test
already enforces them, and in each case the test was rewritten to own the rule rather than
cite the prose being deleted — `pwaContract.test.ts` opened "Four PWA properties CLAUDE.md
records", which the trim would have made false, so its four reasons moved into the `it()`
names and `ABSENT_PATHS` now carries its own.

Four false claims repaired in lines that stayed. `set_index` was described as the whole
logged session's ordinal, which is true of `logged_set` and NOT of `prescribed_set`, where
it is block-scoped by `UniqueConstraint("session_block_id", "set_index")`. A diagnostics
header allowlist was asserted in the present tense and does not exist. `_sizes.scss` was
cited for arithmetic that c95e03a stripped. One archive pointer did not grep-resolve.

The allowlist had no deletable row, and that is provable rather than a judgement: the arm at
`test_comment_budget.py:511` fails on any entry covering a comment now within its cap, and
it is green, so all rows are load-bearing. What was stale is the `limit` field, which no arm
checks — 41 rows carried headroom above their measured span, licensing silent regrowth
(three `useSessionRun.ts` blocks of 3, 5 and 9 lines were all pinned at 26). Lowered to
span. One further row was orphaned by the PWA trim and deleted, so 1,279 -> 1,278 entries.
BASELINE stays 972 and `BASELINE_RATCHET` needs no move: no backlog row was removed.

`npm run check` green: 1430 server, 1502 web across 62 files.

Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Takes all ten packages from the two open dependabot PRs, pinned to the latest
stable verified against the registry rather than to what those PRs proposed.
Three land above the proposal: typescript-eslint 8.70.0, @module-federation/vite
1.21.5, @types/react-dom 19.3.0.

python:  alembic 1.19.2, psycopg[binary] 3.3.5, ruff 0.16.6
web:     vitest 5.0.0, eslint 10.10.0, globals 17.12.0, sass 1.104.0

@types/node and typescript keep their held-back majors per dependabot.yml.

vitest 4 -> 5 needed no config or test changes. Verified it is not a vacuous
pass: running vitest 5 against the pre-upgrade dist/ still reports 1502, so the
runner contributes zero tests. The web count moves 1502 -> 1505 because
@module-federation/vite 1.21.5 emits three further chunks reachable from
remoteEntry.js, and distContract.test.ts derives one case per reachable
artifact. Server 1430 passed, web 1505 passed across 62 files, no skips.

Four fixes to prose and config that no CI job checks:

- vitest.config.ts told readers to avoid environmentOptions.jsdom.userAgent and
  use jsdom's `resources: { userAgent }`, because jsdom >=28 dropped the
  ResourceLoader export vitest imported. Vitest 5 fixes exactly that, so the
  prohibition is void and is deleted. Confirmed at both ends: v4.1.11 has the
  unguarded `new ResourceLoader(...)`, v5.0.0 adds getResourceOptions() with a
  fallback.
- distContract.test.ts carried three stale "measured today" numbers: 43 -> 51
  precache entries, 22 -> 21 boot hrefs (its stated breakdown did not sum to its
  own total), and a byte figure now cited as the KiB workbox actually reports.
- Naming `exclude` REPLACES vitest's defaults rather than extending them, so
  **/.git/** had been silently dropped. Restored, with the reason stated.
- engines.node was `>=24.15.0`, which admits Node 25 — a version vitest 5
  refuses outright. Narrowed to `^24.15.0 || >=26.0.0`.

Does not close the adm-zip advisory: it has no patched version, and
@module-federation/vite 1.21.5 still pulls adm-zip@0.6.0 via dts-plugin@2.9.0.
vite.config.ts sets `dts: false`, so that codepath does not load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The replacement comment was 3 lines against a plain-tier cap of 2, so
test_comment_budget failed in CI. Trimmed rather than allowlisted: it carries no
justification an [[exception]] entry could honestly state, it was simply wordy.

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

Kilian's call: fit the cap, or write no comment at all.

The three "N today" annotations in distContract.test.ts were measurements living
in prose. One had already rotted — it read 43 against a true 51 — and refreshing
the number just resets the clock, because the figures track the MF chunk graph
and move on any @module-federation/vite bump. The executable claims are the
floors (30, 15) and PRECACHE_BYTE_CEILING; those stay and are what actually
guard anything.

The vitest.config.ts exclude comment goes back to one line. It states a
non-obvious mechanism rather than a measurement — naming `exclude` replaces
vitest's defaults instead of extending them, which is why .git has to be
restated — so it earns its line, but not three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(deps): v9.4.0 — supersede dependabot #137/#138 at latest stable
@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 2:11pm UTC

@kilianmc
kilianmc merged commit 8c1b1dd into main Sep 10, 2026
5 checks passed
@kilianmc
kilianmc deleted the release/10.0.0 branch September 10, 2026 14:13

This branch was successfully deployed

1 active deployment
Preview — 6c96581b 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