docs: describe the safety guardrails structurally, and gate published constants - #34
Merged
Conversation
…path The Engine limits table now says what bounds each nutrient and what narrows it, which is what a partner acts on. The sodium row carries the property that matters most: the ceiling scales with the athlete rather than sitting flat. The paragraph below it says why the limits are not partner-overridable. A caller able to raise a ceiling could raise it past what is safe for that athlete, and the athlete would never know. New subsection covers the support question this page did not answer: why a prescription can read lower than the athlete expects. Six causes, each one checkable from the request and the response, so an integrator can tell a guardrail from a gap in the profile without opening a ticket. Teaser note keeps both clamping promises and drops the worked upper-bound example, which invited readers to treat a bucket edge as a target. Verified: mint broken-links clean; page renders at 1440px and 375px with no horizontal overflow; the new heading registers in the on-this-page nav. Ci-From: linux Ci-Session: 87067284-6412-4360-a52c-38cf35d2bfbf
The safety page states what the engine will and will not prescribe. The
promises belong in public; the values it clamps to do not, and this repo is
public and permanent, so putting one back is a one-way door.
Care is not the control. A value lands on a page as a side effect of a PR
about something else, and every reviewer reads past it because it is not what
the PR is for. A check that runs on every PR catches what care does not.
Four shape rules rather than a list of values, because a gate naming the
constants would print them in a public repository and would only ever catch
the ones somebody wrote down:
imperial-mass lb or pounds on a page whose API is metric throughout
rate-adjacent a number touching g/hr, mg/hr, mL/hr, g/L, mg/L
nutrient-ceiling a nutrient, a cap word and a number on one line
body-weight-scaling body weight and a number, which is the metric
restatement the first three would miss
engine-constants-allowlist.txt holds the 19 matches that are meant to be
there, each with the argument for it. It may grow: publishing a new response
field is legitimate, and the cost is writing the reason where a reviewer sees
it. An entry that stops matching fails the gate, the same rule the drift
baseline gives, since a suppression that suppresses nothing would absorb the
next real one.
The self-test job proves the gate still rejects what it was built for. Its
fixture numbers are invented; a fixture built from the real values would
publish them in a public file.
Verified locally: clean tree exits 0 with 19 allowed matches. The page as it
read before this branch produces 20 findings and exits 1. A coefficient
restated in kilograms is caught by body-weight-scaling. A ceiling written with
no unit and no cap word is caught by nutrient-ceiling. A deliberately stale
allowlist entry fails. The self-test step, extracted from the workflow and run
verbatim, reports all four rules firing.
Does not reach git history, and does not reach sweb, the SDK repos or
saturday-skills. Those need their own guards.
Ci-From: linux
Ci-Session: 87067284-6412-4360-a52c-38cf35d2bfbf
DrAlexHarrison
force-pushed
the
safetygen
branch
from
August 12, 2026 19:06
93554dc to
9469fd1
Compare
This repository is public, and the organization's self-hosted runner group sets allows_public_repositories: false. A job here asking for self-hosted is never picked up. It does not fail; it queues until something cancels it and reports pending forever, which is the one CI state that looks like patience and means nothing is watching. That setting is correct and should stay. A public repository accepts pull requests from strangers, and a self-hosted runner would run their code on the machine hosting it. So: ubuntu-latest, which is free for public repositories and is what the other public repos in this organization already use. This job needs nothing from the host. Standard library python, no clone, no network, no secrets. The header now says all of that, because the natural instinct on reading a GitHub-hosted runner in a repo whose sibling workflow says self-hosted is to make them match. Ci-From: linux Ci-Session: 87067284-6412-4360-a52c-38cf35d2bfbf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits. The first rewrites a section of
guides/safety.mdx; the second adds the check that keeps it that way.1.
guides/safety.mdx, the Engine limits sectionThe table describes what bounds each nutrient and what narrows it. Two columns instead of three. The sodium row now carries the property a partner should know about: the ceiling scales with the athlete rather than sitting flat, so a smaller athlete is held tighter than a larger one on the same session. The carbohydrate row lists the three things that narrow it, since a caller can check all three from the athlete's own settings.
The paragraph under the table said the limits were not partner-overridable and left it there, which reads as an API restriction. It is a safety property: a caller able to raise a ceiling could raise it past what is safe for that athlete, and the athlete would have no way to know it happened.
It also said sodium was the only limit that scales with the athlete. That is not true. The carbohydrate ceiling is bounded by the athlete's own body weight and age as well, in both engines (
fuel-backend/pkg/chat/engine/carb.go,saturday/lib/core/carb.dart, held at parity bycalculator-crossval.yml). It now says several scale, which is both accurate and the more reassuring version.New subsection, "When a prescription reads lower than expected". This is the support question the page did not answer. Six causes, each checkable from the request and the response: a self-imposed
carb_upper_limit_override, low stated carb experience,performance: false, cool conditions, long duration, and an incomplete profile. It closes with the fastest way to tell a guardrail from missing data, which is to resend with a fuller profile and see whether the number moves.Teaser note keeps both clamping promises, upper bound and lower bound, and drops the worked upper-bound example. That example invited readers to treat a bucket edge as a target, which the very next sentence tells them not to do.
2.
Engine ConstantsgateThe page describes what the engine will and will not prescribe. The promises belong in public. The values it clamps to are a different kind of thing: no caller sets one, no response carries one, and this repository is public and permanent, so putting one on a page is a one-way door.
Four shape rules rather than a list of values, in
scripts/check-engine-constants.py. A gate that named the values would print them in a public repository, and it would only ever catch the ones somebody thought to write down.imperial-masslborpoundson a page whose API is metric throughoutrate-adjacentg/hr,mg/hr,mL/hr,g/L,mg/L,units/hrnutrient-ceilingrate-limiting.mdxout of it: a daily call ceiling is not a fueling ceilingbody-weight-scalingengine-constants-allowlist.txtholds the 19 matches that are meant to be there, each with the argument for it. Three kinds qualify and each entry says which it is: a value the API returns on every response, a bound a caller must respect to avoid a 400, and illustrative sample copy. It may grow, because publishing a genuinely new response field is legitimate; the cost is writing the reason into a public file that a reviewer reads. An entry that stops matching fails the gate, the ruleapi-drift-baseline.txtalready sets, since a suppression that suppresses nothing would absorb the next real one.The
self-testjob proves the gate still rejects what it was built for, because a checker that silently stops matching reports the same clean run as a clean tree. Its fixture numbers are invented.Verification
The gate's own regression cases, run locally:
body-weight-scalingnutrient-ceilingself-teststep, extracted and run verbatimRendered from
mint devand checked at both widths required by.claude/CLAUDE.md:The two-column table wraps better on mobile than the three-column one did. The new
###heading registers in the on-this-page nav under Engine limits. No anchor changed:activities.mdxlinks to/guides/safety#where-each-safety-field-is-populated, which this does not touch, and no other page links to an anchor on this one.docs.jsonis untouched.What this does not cover
The gate reads the working tree, so it cannot reach git history. It also only reads this repository:
sweb'sllms-*.mdfiles served onsaturday.fit, the SDK repos, andsaturday-skillseach need their own.