Tabbycat Adjcore Toolkit

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.

Your Tabbycat the only source read, never written GET only One client core/tabread.py refuses any verb that is not a read Tester tracking derives who has been watched, from the draw The fold rules + allowlist decide what a spectator sees Judge feedback mask → model → gate → redraft → a person Your laptop nothing published A public page one static file One page per judge reachable only by them
Everything comes from one place and goes through one client. The three tools never talk to each other, and none of them writes anything back.

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/neg rather 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 fixThe 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:

Extending it

The things most likely to want changing, and where they are:

To changeEdit
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

Next: what it will not do →