Skip to content

docs: describe the safety guardrails structurally, and gate published constants - #34

Merged
DrAlexHarrison merged 3 commits into
mainfrom
safetygen
Aug 12, 2026
Merged

docs: describe the safety guardrails structurally, and gate published constants#34
DrAlexHarrison merged 3 commits into
mainfrom
safetygen

Conversation

@DrAlexHarrison

@DrAlexHarrison DrAlexHarrison commented Aug 12, 2026

Copy link
Copy Markdown
Member

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 section

The 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 by calculator-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 Constants gate

The 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.

Rule What it sees
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, units/hr
nutrient-ceiling a nutrient, a cap word and a number on one line. The nutrient word is what keeps rate-limiting.mdx out of it: a daily call ceiling is not a fueling ceiling
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. 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 rule api-drift-baseline.txt already sets, 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, because a checker that silently stops matching reports the same clean run as a clean tree. Its fixture numbers are invented.

Verification

$ npx mint@latest broken-links
success no broken links found

$ python3 scripts/check-engine-constants.py --docs .
Clean: 19 allowed match(es), no engine constants.

The gate's own regression cases, run locally:

Case Result
Current tree exit 0, 19 allowed matches
The page as it read before this branch exit 1, 20 findings
A coefficient restated in kilograms caught by body-weight-scaling
A ceiling with no unit and no cap word caught by nutrient-ceiling
A deliberately stale allowlist entry exit 1
The self-test step, extracted and run verbatim all four rules fired

Rendered from mint dev and checked at both widths required by .claude/CLAUDE.md:

[desktop 1440px] horizontal page overflow: false
[mobile 375px]   horizontal page overflow: false

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.mdx links 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.json is 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's llms-*.md files served on saturday.fit, the SDK repos, and saturday-skills each need their own.

…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 DrAlexHarrison changed the title safety: describe the guardrails structurally, and add the low-number path docs: describe the safety guardrails structurally, and gate published constants Aug 12, 2026
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
@DrAlexHarrison
DrAlexHarrison merged commit 0c736b1 into main Aug 12, 2026
3 of 5 checks passed
@DrAlexHarrison
DrAlexHarrison deleted the safetygen branch August 12, 2026 19:42
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