Independent Build Open Source

The Taper Editor

An editor for AI-generated endurance training plans. It doesn't rewrite your plan and it doesn't coach your body. It reads what an AI wrote for you, checks it against eight structural rules, and hands it back with findings and a question.

github.com/jmarielee/taper-editor arrow_outward

The Plan Sounded So Sure of Itself That I Followed It Over My Own Judgment

In July 2025 I asked ChatGPT for a taper plan for a local sprint triathlon. I followed it. I felt fine at the start line, and found out how fried I was in the middle of the race, when there was nothing left to find. I finished in 1:44.

The year before, my bike shoe broke at the start of the bike leg. Any cyclist can tell you what that costs: the pull-up half of every pedal stroke, gone for the entire ride. That race, on broken equipment with half a pedal stroke, was four minutes faster than the race where I followed the plan.

The plan had also labeled race day "Fri, July 12." July 12, 2025 was a Saturday. I didn't catch it, and the plan didn't catch itself. That mistake didn't cost me the race, but it told me what I was dealing with: a plan that can't count days shouldn't be trusted to count load unchecked.

The worst part is that I knew better. I've raced enough to know what a taper looks like, and that final week didn't look like one. But the plan sounded so informed that I followed it over myself. That's the failure this editor exists for. A newer athlete doesn't know which claims to question. An experienced one can still be talked out of what she knows. Either way the plan's confidence goes unchecked, so this tool checks it.

What this editor will never do

Rewrite, redraft, or produce a corrected plan. Prescribe volume, intensity, weight, or pace. Comment on your body. Assess whether an injury accommodation is medically adequate — that's a clinician's call, and it says so when it declines. Perform arithmetic by guesswork when the checker can run instead. Praise.

One Rule Blocks. Seven Flag.

What the math checks

The race-week rule and all calendar and total arithmetic run in checker.py, a deterministic engine — never by model diligence. A second gate, check_contract.py, verifies the shape of the report itself.

What the model judges

Editorial review layered on top of the machine state: which findings matter most, how to say what a violation costs, and what question to hand back.

What it refuses to say

Anything about the athlete's body. It reports plan structure and never predicts injury. A BLOCK means the math didn't clear, not that the plan is unsafe.

The scope precondition runs before any rule

Two questions first: is this AI-generated, and is it an endurance plan? If either is false the editor declines and stops. Most tools evaluate and then caveat. This one decides whether it's entitled to an opinion before it forms one, because a partial answer on a training plan reads as a review.

The eight rules

RuleSeverityWhat it checks
1 · Race-week loadBlocksFor a declared goal race, the final seven days must show declining load against the preceding trend, by machine count.
2 · Missing ceilingFlagEvery stated limit must define what happens when it's broken. A limit with no response is decoration.
3 · Calendar & arithmeticFlagEvery weekday label must match a real calendar and every total must recompute, by script.
4 · Phantom athleteFlagEvery number about the athlete must trace to something the athlete actually stated.
5 · No bad-day ruleFlagThe plan must define behavior for both the missed session and the added one.
6 · Self-certificationFlagA plan's claims about its own safety are claims, not facts, and are treated that way.
7 · Undeclared race roleFlagEvery race must state what it's for: the goal, or training for a named larger goal.
8 · The solo planFlagThe plan must account for the athlete's total load, including standing activities outside it, or ask.

Only the check closest to the body blocks. Everything else respects the athlete's authority to accept a flag and go anyway.

A fixed four-part shape, and the last part is always a question

Every finding returns the same four parts in the same order, ranked by consequence and capped at five per report.

Your plan says

The exact line, quoted.

The problem

The rule it breaks, in plain English.

What it costs you

Stated plainly, no hedging.

Your call

A question. Never a rewritten line.

Two Deterministic Gates: One on the Math, One on the Manners

checker.py proves the arithmetic. check_contract.py proves the shape every report promises — it blocks any report where a finding drops one of its four parts, where "Your call" hands back anything but a question, or where a quote can't be found as a contiguous run of words in that specimen's committed evidence. The no-rewrite rule is a constraint the repo enforces on itself, not a promise in prose.

The rule engine: 8/8

Six confirm a rule fires when it should. One is a control case that must not fire — without it, a checker that fires on everything scores identically to a checker that works. The eighth pins a known limitation in place, which is its own story further down.

$ python3 checker.py --selftest PASS weekday_mismatch (expect FINDINGS + [WEEKDAY]) (exit 1, wanted 1) PASS total_mismatch (expect FINDINGS + [TOTAL]) (exit 1, wanted 1) PASS goal_race_rising_load (expect BLOCK) (exit 2, wanted 2) PASS undeclared_race (expect HOLD + one question) (exit 3, wanted 3) PASS training_race_stand_down (expect no gate) (exit 0, wanted 0) PASS goal_race_unverifiable_race_week (expect HOLD) (exit 3, wanted 3) PASS mixed_unit_baseline_false_block (KNOWN LIMITATION pinned) (exit 2, wanted 2) PASS control_clean_taper (expect CLEAN — this must NOT fire) (exit 0, wanted 0) selftest: 8/8 passed

Receipt: runs/2026-07-21-selftest-run.txt. The suite grew 6/6 → 7/7 → 8/8 as cases were found; the earlier receipts are committed too.

The output contract: 8/8

Run against the repo it clears all five reports and verifies 25 quotes against source. The NYC report ships no source text by the anonymous witness's terms, so it is reported SKIPPED — not silently passed.

$ python3 check_contract.py --selftest PASS clean report clears the gate PASS question + parenthetical still clears PASS flattened table row still clears (emoji/dashes/tabs ignored) PASS handoff that isn't a question is blocked PASS handoff that ends in a fix is blocked PASS fabricated quote is blocked PASS reordered words are blocked (not a verbatim run) PASS missing a required part is blocked contract selftest: 8/8 passed

The planted trap, and its machine receipt

Specimen 2 is a plan I built to violate the one hard rule, committed in full as trap-plan.md so anyone can re-spring it: python3 checker.py trap-plan.ledger. The biggest training day of the entire plan sits inside race week.

[RAMP] 2025-07-06 Sun 60 (race week) [RAMP] 2025-07-07 Mon 90 (race week) [RAMP] 2025-07-08 Tue 45 (race week) [RAMP] 2025-07-09 Wed 45 (race week) [RAMP] 2025-07-10 Thu 60 (race week) [RAMP] 2025-07-11 Fri 30 (race week) [RAMP] 2025-07-12 Sat 0 (race week) [RAMP] preceding daily avg = 45.7 min; race-week daily avg = 47.1 min [RAMP] preceding max day = 75 min; race-week max training day = 90 min [GATE] BLOCK — race-week daily average (47.1) is not below the preceding average (45.7) [GATE] BLOCK — a race-week training day (90 min) exceeds the largest preceding day (75 min) ------------------------------------------------------------------------ VERDICT: BLOCK No overall 'clear to train' may appear anywhere in the editor's report while this block is live (rules.md, Rule 1).

The decline

Specimen 5 is a real strength program submitted as the deliberate out-of-scope test. The editor declined on both grounds of the scope precondition — wrong sport, and not AI-generated — after verifying against all eight rules individually. Almost nobody ships a demo of their own system declining to answer.

"This is a strength program — squats, presses, hip thrusts, rows, on a Monday/Wednesday/Friday split. I review AI-generated endurance training plans… There's no race here, no taper, no endurance load for me to check. I'd be inventing problems to have something to say, and I won't do that."

"One more thing, separate from scope: this plan wasn't generated by an AI. It reads like real programming from a coach… A human coach's program isn't what I'm calibrated against, on top of not being the right sport."

Six specimens, every verdict the editor can return

#WhatEditor verdictMachine receipt
1The original 2025 plan that started thisHOLDHOLD
2A constructed trap, built to violate the hard ruleBLOCKBLOCK
3A real Detroit half marathon planPASS, 5 flagsFINDINGS
4A real NYC Marathon plan — external witnessPASS, 3 flagsCLEAN
5A real strength program — the scope declineDECLINE— model-only
6A real triathlon plan — second external witnessBLOCK, disputedBLOCK

Two columns on purpose. The machine receipt is the exact word checker.py prints. The editor verdict is the model-reviewed layer on top of it. A machine FINDINGS can carry non-blocking flags the athlete may accept; only a machine BLOCK forces an editor BLOCK. The words differ by design, and the README says so, so nobody has to discover it.

I Said My Tool Was Wrong. The Athlete Said It Was Right.

Specimen 6 is a real AI-generated triathlon plan from the second external witness. It states almost every session in yards and miles — swim sets in yards, long rides and runs in miles — with only race week itself stated in minutes. Run as written, the checker returned BLOCK: race week's daily average of 13.6 minutes came back higher than the preceding eleven weeks' 9.1.

That is the correct output for the input it was given, and it is also wrong in the way that matters. Those eleven weeks are full of real 60-to-90-minute sessions the checker had no way to convert, so it counted them as zero. Race week only looked heavy because it was the one stretch stated in a unit the checker could read.

I proved it rather than asserting it. Same ledger, same plan, same rules, one change: every distance session converted using the athlete's own stated race-day paces. The preceding average moved from 9.1 to 24.5 minutes a day. The verdict flipped from BLOCK to FINDINGS. Both runs are committed.

Then the athlete read the same evidence

"Now that it's been pointed out to me I can't unsee that the original plan was written with both distances and times. That IS a huge red flag, especially if your true intention is to taper before race day."

"Overall, I think your math rules worked exactly as intended. That plan SHOULD have failed; it looks good at a cursory glance but a deeper dive shows the inconsistencies could absolutely lead to overtraining."

My own LIMITATIONS.md presents that block as a false positive. She read the same evidence and disagreed with me — she says the block was correct, because a plan that mixes distances and times that inconsistently is itself the hazard, not just a unit my tool can't count.

Tool says data gap. User says real flag. That disagreement is kept unresolved, on the record.

I didn't patch it, and the reason is in the repo: a proper fix needs athlete-stated pace directives, because assuming a default pace would be exactly the phantom-athlete failure Rule 4 exists to catch. That's real feature work. Two days before a deadline, with five other specimens' credibility riding on the checker's current tested behavior, that trade wasn't worth making. A claim a stranger can rerun is worth more than a fix nobody had time to verify.

What I did instead

Instead of only writing the mistake down, I turned it into a check the tool runs on itself — the eighth one in the selftest. It re-tests this exact case on every run and fails loudly if the behavior ever drifts. The test asserts the limitation, not its correctness. The limitation is locked into the code rather than noted in a file, so no future change to it can happen quietly, including by me.

The summary that was cleaner than the truth

While assembling evidence I found that an early AI-written summary of the 2025 plan had quietly corrected the weekday error the verbatim source actually contained. The summary was tidier than what happened. I logged the discrepancy and kept the flawed source — the same rule the tool enforces, applied to my own documentation. It's committed as evidence-recovered-summaries.md. A record that silently improves on the past is not a record.

The gap an outside reviewer found that the rules don't cover

An independent trainer reviewed Specimen 4's plan and report without seeing rules.md first. Two of her points converged with what the tool already found. One did not: a plan's intensity vocabulary — "easy," "hard," "medium" — can go entirely unanchored to any measurable target, and no rule here evaluates that. Rule 3 catches rows whose amounts can't be computed. It cannot catch a plan that computes perfectly and still never defines what "easy" means for the athlete holding it. Logged as a limitation rather than patched, because a ninth rule the day before submission would be untested against six specimens whose credibility rides on the current, tested eight.

Known gaps, stated up front

Four of six specimens ship their full source plan text, so every quote in those reports can be checked by hand against a committed file. Specimens 4 and 5 do not — both for witness privacy. Quotes in those two are verifiable by the athletes who own the plans, not by a stranger with only this repo.

One evidence file carries a deliberate redaction: a single word replaced with the visible marker [joint]. Race name and injury history are kept as originally written. A documented choice, not an omission.

Validation status

A tested prototype with committed, reproducible receipts — not a clinically or scientifically validated training assessment tool. A BLOCK from this editor means the math didn't clear, not that the plan is unsafe, and the report says so every time rather than letting a technical verdict masquerade as a training judgment.

The Taper Editor is one of three builds that split the same way: the model labels, the math decides, the human makes the call.

Read the method arrow_forward