How it all works
The architecture, the two guarantees worth checking rather than believing, and exactly which parts of your tournament are read from the tab rather than configured.
Why the three tools do not know about each other
They share a Tabbycat client and nothing else. No shared database, no shared state, no tool reading another's output. That separation is doing real work.
The clearest example: the fold's public page is built directly from the tab and never reads any private draft of a break-round draw. So a room that has not been drawn yet is genuinely empty on the public page, rather than merely hidden — there is nothing there to leak. Wiring the tools together would make that a matter of care instead of a matter of fact.
The read-only guarantee
One file talks to Tabbycat. It subclasses the HTTP session class and overrides the single method every request funnels through, raising on anything that is not a read, before a packet leaves the machine.
SAFE = {"GET", "HEAD", "OPTIONS"}
class _GetOnlySession(requests.Session):
def request(self, method, url, *a, **kw):
if str(method).upper() not in SAFE:
raise ReadOnlyViolation(...)
return super().request(method, url, *a, **kw)
There is no write function in the toolkit to call and no setting that enables one. The single POST anywhere is the sign-in form, and it runs on a separate, throwaway session that is closed immediately; only its cookies are carried into the read-only client.
You do not have to take that on faith, and if you are asking a tab director for an account you should not:
python3 tests/test_release.py
Technical That suite parses every Python file
with ast and asserts no call to
.post/.patch/.put/.delete exists outside the sign-in form —
parsing rather than grepping, specifically so the docstring that explains the
guarantee does not itself trip the check. It also instantiates the session and
asserts each write verb raises, and asserts the class has no
write attribute at all.
This was a real change made for release. The feedback tool used to use a second client that did have a write method — publishing a “read-only by construction” toolkit with a writable client inside it would have been a lie, so the two were collapsed into one and the write path deleted.
What is read from your tab, and what you configure
You configure four things. Everything else is read, every run.
You configure
- your tab's web address
- your tab slug
- what to call any published site
- your token (in
.env, separately)
Read from the tab
- the tournament's name
- how many rounds, and which are break rounds
- break categories: names, sizes, how many
- teams per debate, speakers per team, side names
- panel sizes, per round
- the feedback scale, and which question is written
- every public / silent / released switch
- motions, institutions, regions
- who is on the adjudication core
This is the claim most worth being suspicious of, because almost every tool like this is quietly bound to the tournament it was written at. So the repository ships the means to check it rather than an assurance:
$ python3 demo/verify.py
Riverbend Open 2027 4 teams a debate · 9 prelims · 5 elim rounds · 2 categories
Ashfield Invitational 2027 2 teams a debate · 5 prelims · 3 elim rounds · 1 category
Kestrel Bay Novice Cup 4 teams a debate · 4 prelims · break not announced
Every tool ran against every tournament with no code changed.
The three shapes are chosen to break things Technical
- A sub-category break line that is not the top N. Teams eligible for the second category break the general one instead, so the second category's line falls below its own top eight.
- A two-team format. Points are 1/0 rather than 3/2/1/0, the side
codes are
aff/negrather than the four benches, and British Parliamentary craft vocabulary would read as nonsense. - A break that has not been announced. Every bracket and simulator view has to render an empty state rather than an error — and the test suite has to report those checks as not applicable rather than failed, or it cries wolf at anyone who runs it before their break.
- Panels that change size mid-tournament, because that is normal and a configured panel size would be wrong from round two.
- A half-decided break round, where some rooms have ballots in and some do not.
Building this found real bugs, which is the argument for it. Among them: a guard that compared the current break against the previous pull without checking it was the same tournament, so pointing the tool at a second tab refused to publish; six test assertions hardcoded to one tournament's team count, break size and round names; and a break count that reported the whole eligible field as having broken.
The pattern that shows up twice: two mechanisms, not one
Both the fold and the feedback tool protect something, and both do it the same way — because one mechanism is never enough to trust.
| The cheap fix | The proof | |
|---|---|---|
| The fold | Rules read from your tab's own switches decide what may be shown | An allowlist declares every permitted field, and the build refuses anything undeclared |
| Judge feedback | Names are masked out before the model sees a comment | A gate checks every draft and redrafts; and runs again at build time, because a hand-edit leaks just as easily |
In both cases the second mechanism is a deploy gate, not a warning. The refresh script will not publish a build that fails it. That distinction is the difference between a check and a habit.
Failures are made loud on purpose
The most dangerous bug in a tool like this is not a crash. It is a soft failure that produces a plausible page.
A transient error on one endpoint was once being swallowed and treated as an empty result — so the public site announced that a break had not happened, over a break that had been out for a day. No error, no log line. Both of the things that fixed it are now general principles here:
- An empty result that is also a real state must never be inferred from a failure. “No break yet” is a true state before the break, which is exactly why it may not be guessed at.
- Compare against the last known good. A successful response with an empty body is indistinguishable from “not announced”, so retrying cannot catch it; only noticing that something used to be there can.
Extending it
The things most likely to want changing, and where they are:
| To change | Edit |
|---|---|
| What the feedback summaries say and sound like | feedback/prompt.md — the whole instruction set, meant
to be edited |
| What the feedback gate allows | feedback/gate.py, and the common-word list in
core/common_english.py if an ordinary word is being
banned |
| What the public page may show | fold/gate.py — add the field to the allowlist
deliberately, which is the point of it |
| Who counts as a tester | the dashboard itself; the default is Tabbycat's adjudication-core flag |
| A country missing a flag | core/countries.py, then re-run
core/gen_flags.py — one source feeds both the flags and the
feedback ban list |
| Anything at all | add a shape to demo/shapes/ that exercises it, and
demo/verify.py will keep you honest |