Skip to content

fix(library): dose the broken circuit at climbing pace, and guard the rate - #128

Merged
kilianmc merged 1 commit into
devfrom
feat/f26-broken-circuit-work-seconds
Sep 6, 2026
Merged

kilianmc merged 1 commit into
devfrom
feat/f26-broken-circuit-work-seconds

Conversation

@kilianmc

@kilianmc kilianmc commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes F26 (ruling 50). v8.14.0.

The defect

broken_circuit_redpoint doses work_seconds=25 in STRENGTH and POWER while its own
instructions say "a hard circuit of around twenty-five moves". That is the move count leaked
into a seconds field — 1.00 s/move, the only value below 2.00 anywhere in the library or the
sources. PERFORMANCE's 90 s for the same 25 moves is 3.6 s/move and correct, so the 3.6× "phase
progression" was an artifact of the error rather than a rule.

The 25 s reached a climber in exactly one place: the library-browse line "4 sets · 25s work",
sitting beside instructions saying "around twenty-five moves". protocol.ts::setPhases maps
circuit to an open phase, so it was never a countdown — the fix carries no player risk.

The fix — ruling 50

work_seconds is 90 in all three phases. This deletes the phase step rather than rescaling
it, because §5.4 progresses An Pow by cutting rest, never by lengthening work. PERFORMANCE is
untouched, so only the two wrong cells move.

sets 4/4/3 and rest_between_sets_seconds are unchanged: all four circuit rows carrying
work_seconds dose the whole circuit and use sets for rounds, while the three counting pieces use
reps and carry no work_seconds. No row mixes the idioms, so a set here already means one attempt
at the whole circuit.

⚠️ The sources give NO seconds figure for this circuit — §5.4 doses it by moves and sections
only. So the conversion is declared at the row as the app's own, not dressed as sourced. The
PERFORMANCE comment said "fewer, longer pieces", which one number for the row makes false; it
is reworded.

The guard is the load-bearing half

⚠️ This cell's work_seconds was read by NO guard in the repository. §7's rest:work band guard
excludes power×circuit by scope (ruling 43, correctly — the sources are silent). The progression
arm read 0 pairs from the row's 136 drawn blocks. The player never renders it. Measured: the value
could be 90, 120 or 180 with the whole suite green.

(a) A move-rate band. For every row whose instructions state a move count and whose
prescriptions carry work_seconds, the implied s/move must sit in 1.50–4.17. It reads both
ends
, so "fixing" a breach by editing the prose fails just as loudly. Move counts are held as data
restated from the prose, with an arm proving the restatement still matches — no runtime numeral
parsing. Floor pinned at 4 rows / 16 (row, phase) prescriptions. long_boulder_link_ups
(ruling 45) and explosive_move_intervals are named exclusions carrying their reasons as data.

(b) The progression arm's sampling unit, week PAIR → DRAWN CELL. The pair arm inspected 15
week-pairs against 429 drawn loading-week cells (3.5%) and was silent in strength and
performance entirely
— pooled green, false per phase. Cause: _pool_index rotates the pool by
week, so a row lands in one loading week per block and a pair needs two. Now: every loading week
after the first must have left the authored dose behind, in the direction its rule names, read as
authored-vs-emitted without asking progression.py what the rule is. shorter_rest 15 → 189
cells (12.6×); this row 0 → 99.
All three rules green today — a sweep widening, not a re-dose.
The pair arm is kept beside it.

(c) Per-(rule, phase) coverage floors, so a phase falling silent turns red instead of going
quiet. (shorter_rest, strength) is a declared zero: boulders_on_the_two_minute is drawn 104
times there and every draw is week 1, so there is no later-week dose to read. That is a
selection fact — pinned, not papered over with a synthetic plan.

Captured red — all four shown to fail at the final code state

(a) at 25 s:

AssertionError: broken_circuit_redpoint/strength doses 25 s of work against the 25-25 moves
its OWN instructions state = 1.00-1.00 s/move, outside 1.5-4.17. […] The prose and the dose
are ONE claim here, so editing the move count instead of the dose moves the breach rather
than closing it.
assert (1.5 <= 1.0)

(a) the other end — dose left at 90, prose reworded to "ninety moves":

AssertionError: broken_circuit_redpoint's instructions no longer spell ['five', 'twenty'] next
to a move count — the words found there are []. `MOVE_COUNTS` says 25-25 moves and the row's
own text is what that restates, so one of the two has drifted and the band above is now
checking a number nobody authored.

(b) with this row's progression frozen:

AssertionError: broken_circuit_redpoint in performance week 2 of its block (6a/2x/None/8,
plan week 18) is dosed (3, 90, None, 420) against the library's own (3, 90, None, 420): the
dose did not move at all; its shorter_rest rule names a direction.

⚠️ The decisive half: with the same freeze in place and only the cell arm deselected, the
entire rest of the suite is green (1151 passed)
— including the pair arm. That is the measured
proof the cell arm reads a row nothing else in the repo read.

(c) both directions — a floor raised 16 → 99 goes red, and moving the declared zero to the
wrong phase goes red on both halves at once.

Displacement — 72-plan sweep, sabotage-checked at 9999 s before any figure was believed

Only the power phase moves. The row gains +256.3 min there and ruling 27's session fill
absorbs it (filler −266.7, phase estimated_minutes −79). Pooled −87.1 min / −9 blocks over
72 plans. 25 of 72 profiles move at all; worst case +18.4 min over a 20-week plan ≈ +0.9 min a
week
. Zero guard failures at 90 (240 breaks two, so "a bit more" is not free).

library_digest ad0cbbc9… → cca51ea1… — intended: the library is a third reproducibility
input. Persisted plans keep their stored digest and their stored 25 s, which is correct provenance.
python -m server.contentseed re-run; prescription_template verified at 90 in all three phases.

GENERATOR_VERSION stays 8.0.0. fingerprint.py and PlanOut both scope the library to
library_digest(); contract.py's own comment scopes the version to the planner package, which
this PR does not touch. Checked with --numstat: every bump #120–#126 also changed
generate/climbing/selection/periodisation/progression, and #125 re-authored doses in
exercises.py — including an aspect_key re-file that moves selection — without bumping.

Corrections to the research report, recorded rather than smoothed

  • its "12-week plan" is wrong — _SWEEP builds 20-week plans, so its +1.5 min/week was
    overstated 1.67×; the real figure is +0.9
  • 25 of 72 profiles move, not 23; filler −266.7 not −272.6; this row's power draws fall
    64 → 63 (a longer block stops fitting one session's ceiling)
  • it missed a sixth in-scope row for the band, explosive_move_intervals

Everything load-bearing reproduced exactly: pooled minutes, blocks, the power gain, and every
coverage figure.

Found, not fixed

Two side findings, neither F26's: the library holds two incompatible circuit time models
(work_seconds at ~3 s/move vs SECONDS_PER_REP = 4 pricing a whole 5–7 move boulder at 4 s, so
the counted-pieces rows systematically under-contribute to every minute figure in the repo), and
short_rest_boulder_sets states its work period only in prose — guard (a)'s completeness arm now
turns red if anyone authors work_seconds onto it.

npm run check green: web 1282, server 1322 passed.

🤖 Generated with Claude Code

… rate

`broken_circuit_redpoint`'s `work_seconds` was 25 in STRENGTH and POWER against
instructions saying "around twenty-five moves" — the move count leaked into a
seconds field, at 1.00 s/move. Ruling 50: 90 s in all three phases, which deletes
the 3.6x phase step rather than rescaling it. PERFORMANCE already carried 90;
`sets` 4/4/3 and the rests are untouched, because a set of a work_seconds-carrying
circuit is one attempt at the whole circuit by the library's own filing convention.

No source states a move->seconds rate for An Pow, so the conversion is declared at
the row as the app's own rather than dressed as sourced.

The guard is the load-bearing half: that cell's `work_seconds` was read by NO guard
in the repo, so it could have been 90, 120 or 180 with a green suite.

- a move-rate band over every row stating a move count AND carrying `work_seconds`
  (1.50-4.17 s/move), reading BOTH ends so editing the prose to close a breach
  fails just as loudly
- the progression arm's sampling unit widened from a week PAIR to a DRAWN CELL:
  `shorter_rest` 15 -> 189 cells, and this row 0 -> 99. Pair arm kept beside it
- per-(rule, phase) coverage floors, so a phase falling silent turns red instead
  of going quiet

Displacement, 72-plan sweep sabotage-checked at 9999 s first: only `power` moves,
+256.3 min there, ruling 27's fill absorbs it; pooled -87.1 min / -9 blocks.
`library_digest` ad0cbbc9 -> cca51ea1, which is the intended provenance signal.
`GENERATOR_VERSION` unmoved: the library is a third input covered by the digest,
and every past bump also changed the planner package.

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

vercel Bot commented Sep 6, 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 6, 2026 6:33pm UTC

@kilianmc
kilianmc merged commit 87b7b5e into dev Sep 6, 2026
5 checks passed
@kilianmc
kilianmc deleted the feat/f26-broken-circuit-work-seconds branch September 6, 2026 18:34

This branch was successfully deployed

1 active deployment
Preview — 358e6dea Deployed Sep 6, 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