Skip to content

1.0.P6 Pattern: Preconditions

Julio Toboso edited this page Oct 26, 2026 · 1 revision

Pattern 5 · Gatekeeping

When you want a Tag that not everyone may take. Think of a door with a bouncer. Before a Tag is applied, the Tag looks at the Agent and decides: in, or not. That look is a Precondition, written with @Pre. It is a function that receives the Agent and answers True (come in) or False (stay out).

class Wizard(Tag):

    @Pre
    def Can_Study(agent):
        return agent.level >= 1


class War_Caster(Tag):

    @Pre
    def Is_A_Caster(agent):             # synergy: needs another Tag
        return agent in Wizard


bruk = Character("Bruk", level=2)

try:
    War_Caster(bruk)
except Precondition.Is_A_Caster:
    pass                                # Bruk is no caster: nothing changed

assert bruk not in War_Caster

Wizard(bruk)
War_Caster(bruk)                        # now it takes

Three things to notice.

  • A refused Agent is untouched. When a Precondition says no, the tagging stops before anything is written. Bruk is exactly as he was.
  • The error carries the name you wrote. except Precondition.Is_A_Caster catches that one refusal and nothing else. except TagPreconditionError catches every refusal. Both work; the named one reads better. A name you never declared, such as a typo, is an error at the except line, so a silent handler that never fires cannot happen.
  • A gate can ask about other Tags. agent in Wizard is an ordinary check, so a Tag can require another Tag. That is how synergies are written: the second Tag needs the first.

Inputs travel with the tagging. Sometimes the gate needs information that is not on the Agent yet, like a code number. You give it at the call: MI6(bond, code="007"). TopKit then hands code, by name, to every Precondition, Record builder and Imprint of that Tag that has a parameter called code. The usual use is a Record that simply keeps the input:

class MI6(Tag):

    @Pre
    def Has_A_Code(agent, code):
        return code is not None

    @Record
    def code(agent, *, code):           # keyword-only: "this comes from the call"
        return code


bond = Character("Bond")
MI6(bond, code="007")

assert bond.code == "007"

Read the * as a blank space. A Record's function has three places, in order: the Agent, what was already stored under that name (pattern 3), and what the call brings. Here nothing was stored, so the star holds that seat empty: def code(agent, *, code) says "agent, nothing stored, then code from the call". If you wrote def code(agent, code), Python would put code in the stored seat, and TopKit refuses that at tagging time and prints the spelling above. Preconditions and Imprints have no stored seat, so there def Has_A_Code(agent, code) is enough.

Watch out. A gate must answer True, False, or nothing at all. Do not return a number and hope: a count of 0 is not False to TopKit, and it will refuse the raw value. Write the comparison you mean, such as return agent.slots > 0. This keeps a real zero from being mistaken for a refusal.

Clone this wiki locally