For most of Shindo’s life the rule was simple and absolute: the engines compute, the model only phrases. Readiness, training load, form, nutrition targets — all computed by deterministic code on the iPhone before any text is generated. The language model receives finished numbers and writes sentences about them. If the generated text and the computed value disagree, the computed value wins, silently, every time.
That rule is still in force everywhere except one place. In one place we deliberately inverted it, and this is the story of what that cost.
The thing a deterministic planner cannot do#
A periodization engine is very good at arithmetic and very bad at judgement.
Ours knows the phase you are in, the load you can absorb, the eight-percent weekly ramp ceiling, the eighty-percent easy split, your availability windows and your form. From those it will build a mathematically defensible week, every time, forever. What it will not do is notice that you have three consecutive evenings free and a long ride would fit better on Thursday than Saturday this once, or that your last four weeks have been monotonous in a way the numbers permit but a coach would fix.
Those are compositional judgements. They are exactly what a language model is good at, and exactly what a rule-based generator is not. So we let it try.
The inversion#
For the weekly plan, and only there, the model is asked for a whole seven-day week. Not text about a week — the week itself: each session’s day, sport, intensity band, phase, target load, duration, scheduled hour, whether it is a key session, and the full list of structured steps inside it.
That is the only place in the app where a number produced by a language model can become a persisted training value. Everywhere else the guardrail overwrites it.
So the interesting engineering question was never “can the model write a good week”. It usually can. The question was: what stops a bad one?
The validator invents nothing#
The answer is a single file that takes a proposed week and returns either accepted or rejected, and here are the named reasons.
The design constraint that mattered most is the one that is easiest to get wrong: the validator has no training science of its own. Not one threshold in it is a number somebody typed into the validator. Every limit is read back at runtime from the engine that already owns it —
- the weekly volume ceiling from the plan generator, asked for its own total over the same rolling window;
- the ramp cap from the periodization model;
- the eighty-percent easy share from the intensity model;
- monotony and acute-to-chronic workload ratio from the risk model;
- your availability windows, permitted sports, phase schedule and today’s readiness gate from your own profile and today’s data.
This matters more than it sounds. A validator that carried its own copy of “the ramp cap is eight percent” would be a second source of truth, and the day somebody changed the ramp cap in the engine, the validator would start enforcing a limit the app no longer believed in. Every threshold being a question asked of the owning engine is what makes the boundary maintainable rather than a slowly rotting duplicate.
Nineteen rules, and what they are for#
There are nineteen, each with a stable machine name, because a rejection that cannot say why is useless to what happens next.
Fifteen are fatal — a week carrying one of them is rejected outright:
volume above the engine’s own target · weekly ramp exceeded · a session outside the seven-day horizon · a session on a day you said you were not available · a session longer than that day’s window · a sport you have no equipment for · a phase that disagrees with the phase schedule · the easy share below the polarization threshold · two sessions crammed onto one calendar day · steps whose load does not reconcile with the session · steps whose durations do not reconcile · a step whose target cannot be resolved into a concrete instruction · a settled session’s day changed underneath you · a settled session’s sport changed · today’s readiness gate ignored.
Four are warnings — reported, never fatal:
a sport outside your declared goal focus · ramping while form is in a hole · projected monotony high · projected workload ratio out of range.
The split is not arbitrary. A rule is fatal when a rewrite could actually fix it. The warnings are the cases where rejecting would just loop: your form is already in a hole because of training you have already done, and no amount of rewriting next week changes last week. Rejecting on something the model cannot change is not strictness, it is an infinite retry.
There is one more distinction worth stating, because it is where the design nearly went wrong. A step set that does not reconcile against its own envelope is fatal at the week level but not fatal to the session — the session survives and its steps are replaced by the engine’s own decomposition of the same envelope. Rejecting a whole week because one interval block’s arithmetic drifted by ninety seconds would throw away good judgement to punish bad arithmetic. Normalize the arithmetic; keep the judgement.
Propose, validate, one retry, then the engine#
The loop is deliberately short.
The model proposes. The validator judges. A rejection is handed back verbatim — the named violations, in plain language — as the instruction for exactly one rewrite. If the rewrite is also rejected, we stop asking and publish the engine’s own week.
One retry, not three. A model that could not satisfy the boundary when told precisely which rules it broke is not going to find it on the third attempt; it is going to spend another API call and another few seconds of somebody’s morning.
The fallback is mandatory and invisible. The athlete is never shown a failure. They open the app and there is a coherent week, and nothing on the screen says whether it came from the model or the engine. That is not us hiding a problem — it is the correct behaviour. “The AI could not produce a valid plan, please try again” is not information a runner can act on at 6am.
The test that had to pass first#
Here is the part I would tell anyone building a validator around a generative system, because it is the step that is easy to skip and expensive to skip.
Before we could switch authorship on, one question had to be answered:
Does the engine’s own week pass this validator?
If it did not, then the fallback — “a rejected proposal leaves the engine’s plan standing” — would be a fallback onto something the app itself calls invalid. The safety net would have a hole in exactly the shape of the thing it was catching.
So there is a preflight test that generates the engine’s own weeks across phases, day counts, availability caps and adaptation corrections, and asserts every one of them is accepted.
It failed. Twice, on two different rules. And the instruction we gave ourselves was: fix the rules, never the engine.
The first failure was a window mismatch. The volume rule compared a rolling seven-day total against the calendar week’s ceiling. Those are not the same window: a rolling week starting on a Friday carries two days of a lighter week plus five of the next, heavier one. The engine’s own sessions summed to about 1.06× “its own” ceiling — the validator was rejecting the engine for exceeding a limit the engine had never been given. The fix was to ask the generator for its own total over the same window, which is an apples-to-apples number and still entirely engine-owned.
The second failure was a tolerance that assumed too much. The step rules compared a step set against its target load and duration with a fixed tolerance. But the engine’s own step generator misses those envelopes legitimately, in two ways: its interval arithmetic can land a couple of minutes off after rounding, and a neuromuscular session carrying a short high-intensity stride block costs more load than the same minutes ridden easy — up to about seven percent above the envelope. Both bounds now come from the engine’s own decompositions of that same envelope, so an engine-generated step set passes by construction.
Both failures were the same mistake in different clothes: the validator had quietly invented a threshold instead of asking. The preflight test is what surfaced it, and it is the reason I would now say a boundary around a generative system is not finished until you have proved the fallback passes it.
The lock, and why it expires#
An accepted week has one more problem, and it is not an AI problem at all.
The plan regenerates. Data arrives, readiness recomputes, the engine re-mints the upcoming days — and it would cheerfully overwrite the week the model just proposed and the validator just ratified, within hours, for no reason the athlete could see.
So an accepted week takes out a lock: those days are detached from regeneration and survive it intact. Accepted rows are written as coach-authored; fallback rows as engine-authored, so we can always tell which is which afterwards.
The important property of that lock is that it always expires, in at most seven days. Not “is cleaned up by a sweep”, not “is released when something else happens” — it expires by construction, and the engine silently regains the whole plan. A lock that depends on a cleanup path is a lock that will one day be held forever by an athlete whose app crashed at the wrong moment.
Two switches, no release needed#
Both are remote, and they do different things on purpose.
One returns authorship to the engine on the next generation: no proposal is requested, nothing is validated, no lock is armed. A lock already standing is deliberately not revoked — it expires normally. Yanking a plan out from under someone mid-week to make a kill switch tidier is a worse outcome than letting a validated week finish.
The other stops every AI call in the app.
And the free tier never reaches any of this. Plan authoring is a paid feature, and the check that enforces it sits below the UI, in the client itself — a free profile cannot physically make the request. A free athlete’s week is the engine’s, deterministic end to end, always.
What I would take from this#
The claim we can make is not that the model behaves. We have no way to guarantee that, and neither does anyone else.
The claim is narrower and actually checkable: what reaches you is either a number the engine computed, or one the engine explicitly ratified. The trustworthiness lives in the boundary, not in the model’s restraint.
Building it left me with three things I would apply again anywhere a generative system is allowed to produce something that matters:
- The boundary must own no facts. Every threshold it enforces should be read from whatever already owns that threshold. A validator with its own constants is a second source of truth waiting to drift.
- Prove the fallback passes the boundary. If the safe path cannot satisfy your own checks, you do not have a safe path. This is the step that found both of our bugs.
- A rejection must be a sentence, not a boolean. Named violations are what make one retry worth attempting — and they are also, unexpectedly, the best documentation of what the system actually believes.
The plan validator’s rules, thresholds and their owning engines are documented on the methodology page, along with every other number Shindo computes. The rule this post is the exception to — that generated text may never contain a figure the engine did not produce — is its own post.
Read next