Specifications - JulTob/Tag_Oriented_Programming GitHub Wiki

🏷️ Tag-Oriented Programming: The Specification 🔖

OOP owns inherent structure; TOP owns semantic context.

Tag-Oriented Programming (TOP) provides utility without changing identity.

A hero is one character. Over a game she becomes a Human, a Wizard, a Sage, a Harper, and stays the same hero. TOP adds each meaning as a Tag; the object keeps its identity the whole way. Traditional programming asks "what is this object?" TOP asks "what is this object for, here?", and lets the answer grow.

TOP was born from tabletop character creation: species, class, background and feats are independent choices, and the character sheet is what they compose. A spell list is written by all of them. TOP gives that idea a precise algebra: one identity, many layers, clear rules for how the layers meet.

This document is the source of truth. It is written in rings, from the kernel outward. Everything in an inner ring holds without the outer rings. A conforming implementation must preserve the observable semantics of every ring it claims. Examples are in Python; the laws are language-neutral.

Ring Contents
0 · Kernel identity, membership, Geometry, the tagging sequence, Rip
1 · Contributions Actions, Records, Operations, Reports, Overlay and Underlay, publication, access
2 · Contracts Preconditions, Imprints, Postconditions, defective Agents
3 · Lifecycle teardown protocols, Scope, deletion
4 · Edges what TOP does not promise, and why

The last sections give the failure model, the conformance obligations, and where TOP stands among paradigms.


Ring 0 · The Kernel

0.1 Identity

A Target is any object a program already has. Tagging a Target never replaces it: before and after, it is the same object. Precisely:

  • object identity is preserved (tagged is original, same id, same hash, same equality);
  • every attribute and method the Target had keeps working the way it did, unless a Tag deliberately contributes a member of that name;
  • the Target's own special methods (__contains__, __len__, __bool__, operators, __getattr__) keep working, with one deliberate exception: bool(agent) gains contract meaning once a Postcondition is visible (§2.5);
  • the name of the object's type is unchanged.

What TOP does not promise is nominal type identity: a Python implementation may swap the object's class for a runtime subclass of it. isinstance(agent, Host) stays true; type(agent) is Host may not. See Ring 4.

0.2 Vocabulary

Term Meaning
Tag A semantic category that can contribute meaning, behaviour, values and constraints. A noun: something you are (ser).
Target An object before or during tagging.
Tagging Applying a Tag to a Target: Human(charlie).
Agent A Target that belongs to at least one Tag.
Field The population of Agents currently carrying a Tag.
Base A broader Tag that a Shape specializes.
Shape A more specific Tag built over one or more Bases.
Form The ordered, duplicate-free, Base-first closure of one Tag, ending with the Tag itself.
Geometry The whole relationship structure of Tags, Bases and Shapes.
Layer One Tag's position in an Agent's composition.
Overlay The currently visible result of all active Layers.
Underlay The prior visible contribution of a name, captured for a later Layer to extend.
Pin A Tag marked @Pin, whose Targets are Tags (§1.9).
Pinning Applying a Pin to a Tag: Rare(Wizard). The pinned Tag is the Pin's Agent.

TOP uses Base and Shape, never parent and child: the words describe a different model. A Tag may be both: Person is a Shape of Being and a Base of Wizard.

0.3 Membership

The primary result of a Tagging is membership. An otherwise empty Tag is complete: it still defines a category and a Field.

class Wizard(Tag):
    pass

Wizard(charlie)

assert charlie in Wizard            # active membership
for wizard in Wizard:               # the Field (its sound members, §2.5)
    Observe(wizard)

Membership is closed upward through Bases: an Agent of a Shape is an Agent of every Base in the Shape's Form, and appears in each of those Fields.

class Mortal(Tag): pass
class Human(Mortal): pass

Human(charlie)
assert charlie in Human
assert charlie in Mortal

Membership is monotonic by design. Only Rip (§0.7) ends it, and Rip does not erase history: an Agent that has ever been a member of a Tag remains an instance of that Tag. In Python, isinstance(agent, Tag) is that has-been check and stays true after Rip. agent in Tag is the is-now check.

A Field never keeps an Agent alive. When an Agent ceases to exist it vanishes from every Field. Fields are indexed by identity: two equal but distinct Agents are two members.

0.4 Geometry

Bases and Shapes form a directed graph, the Geometry. Each Tag has one Form: its Bases, deepest first, in declaration order, each once, then the Tag.

class Spellcaster(Tag): pass
class Duelist(Tag): pass

class Arcane_Duelist(Spellcaster, Duelist):
    pass

assert Form(Arcane_Duelist) == (Spellcaster, Duelist, Arcane_Duelist)
assert f"{Arcane_Duelist:form}" == "Spellcaster → Duelist → Arcane_Duelist"

A Base reachable through several paths appears once. Forming follows the Form downward, general to specific. Deforming (Rip) goes the other way: a Shape leaves before the Bases that support it.

0.5 Applying a Tag

The canonical act is:

Tag(target, **inputs)

It applies every Tag in the Form that is not yet active, Bases first, then returns the same Target. Keyword inputs are handed by name to the Preconditions, Record builders and Imprints run during that call (§1.3, §2.2, §2.3).

MI6(bond, code="007")      # every protocol that names `code` receives it

Reapplying an active Tag does nothing. It does not duplicate membership, does not rerun Imprints, does not reset Records. Resetting is a deliberate act: Rip, then apply again.

0.6 The tagging sequence

Think of a factory line. Once, for the whole call:

  1. Gate. The Preconditions visible in the Form this call will produce inspect the incoming Agent and the inputs. A Shape's Precondition overrides its Base's, so a Shape can relax the gate. A failed gate stops the call before anything changes.

Then for each Tag in the Form, in order:

  1. Parts. That Tag's Records are built, each allowed to read the value already stored under its name.
  2. Commit. The Agent enters the Tag's Field; the Overlay and the Agent-bound view are set.
  3. Write. That Tag's Imprints run, in declaration order.

Then, once for the whole call:

  1. Quality check. Every visible Postcondition inspects the finished Agent.

The call boundary decides what a failure means:

  • A failure in steps 1 or 2, for any Tag in the Form, rolls the whole call back. The Agent is exactly as it was at the call, including Bases pulled in by this call. Tags committed by earlier calls are untouched. Nothing partial is ever published.
  • A failure in steps 4 or 5 raises, but the Tags stay. The product left the line. A defective product is not melted back to materials: it is flagged, repaired, or Ripped (§2.5).
Citadel(ari)                      Form: Territory, Citadel
    gate       Has_Charter FAILS
→ TagPreconditionError; ari in Territory == False, ari in Citadel == False

Broken_Wizard(ari)                Form: Person, Broken_Wizard
    gate       ok
    Person     parts, commit, write
    Wizard     parts, commit, write
    check      Has_Spellbook FAILS
→ TagPostconditionError; ari in Person and ari in Wizard; bool(ari) == False

Tagging is otherwise side-effect free: TOP-managed state is restored on rollback. An Imprint's in-place mutation of a pre-existing mutable value (events.append(...)) is outside what TOP can undo; keep such effects for step 4, where they are never rolled back, or hold them in Records.

0.7 Rip

Tags can be seen like a pass, a sticker, a badge. Ripping one is an exceptional, violent act of Field expulsion: "Give me your badge and weapon. You can no longer go into the evidence room."

del MI6[bond]
assert bond not in MI6
assert isinstance(bond, MI6)      # once an agent, always an agent

Rip is the only exit from a Field, and it obeys three laws:

  • Contributions are sticky. Actions and Records stay on the Agent after Rip unless a @Rip protocol (§3.1) changes them. The result is a Rogue Agent: it has what it learned, but no active access to the Tag's Field, Operations, Reports or Agent-bound view. Rogue Agents are dangerous… but useful.
  • Rip is refused while a Shape needs the Base. del Beast[wolf] fails while Wolf is active. Deform the Shape first. Rip never cascades: TOP does not run other Tags' protocols behind your back.
  • Conditions are sticky too (STEP-SPEC-12). Rip does not touch a Tag's Preconditions or Postconditions: a promise that outlives its Tag fails loud, never silently. Rip does one thing, and it is the one act nothing can roll back, so it is not the place for a second rule. The author ends a condition, in one of two visible ways: a guard in the condition (if agent not in Wizard: return True), which can follow any membership at all, another Tag's, a keyword on a Tag, the Tag under an Underlay; or an explicit deletion from the Tag's own @Rip protocol, Contract.Delete(agent, "Has_Book"), one deliberate name at a time. A name that is not a condition on the Agent is a Resolution Failure.
  • Reapplying a Ripped Tag is a fresh Tagging. Imprints run again; Records are rebuilt.
  • A Scope Rips what it applied, and only that. A Tag the Agent already carried at entry is left as it was on exit; a Tag that applied and reported a broken promise at the Scope's door did apply, and is Ripped on the way out with the rest.

0.8 Spellings

TOP borrows the language's own syntax for every Tag-level act and leaves the Tag's dotted namespace, Wizard.something, to the program. A Report called Field, Form or Rip must be possible. Structure without stepping on the programmer's choices: TOP should feel like part of the language, not a library's naming.

Act Python spelling
apply Wizard(agent, **inputs)
active member? agent in Wizard
carries a keyword? (Flags, §1.8) "Undead" in ghoul, Undead in ghoul, Keyword(ghoul, "Undead"); a Flag's words the same, "Wolf" in howler
ever a member? isinstance(agent, Wizard)
the sound population for w in Wizard, len(Wizard), if Wizard:
the defective population for w in ~Wizard, if ~Wizard:
everyone in the Field Wizard[:], if Wizard[:]:
populations combined (§2.5) Wizard | Fighter, Wizard & Fighter, Wizard - Sworn; the same on Wizard[:] and ~Wizard
one condition, read on the Agent (§2.5) agent.Has_Book
the Agent-bound view Wizard[agent]
leave the Field (Rip) del Wizard[agent]
the Form, as Tags Form(Wizard)
the Form, as text f"{Wizard:form}"
an Agent's Tags, Outline, contract, as text f"{agent:tags}", f"{agent:outline}", f"{agent:contract}"
catch one check's failure (§2.6) except Precondition.Is_A_Caster:, except Postcondition.Has_Book:, except Imprint.Arm:
catch a Rogue Agent's access (§1.5) except TagRogueAccessError:
end a condition explicitly (§0.7), from a @Rip protocol Contract.Delete(agent, "Has_Book")
necessary to enter and to stay (§2.7) @Pre + @Post, or @Requirement
pin a Tag (§1.9), and every act above with a Tag in the Agent's seat Rare(Wizard), Wizard in Rare, for tag in Rare, Rare[Wizard], del Rare[Wizard]
a Tag carries a keyword? (Flag Pins, §1.9) "Deprecated" in Wizard, Keyword(Wizard, "Deprecated")
a Tag's Pins and contract, as text f"{Wizard:pins}", f"{Wizard:contract}"

Truth on a Tag is truth on a collection: if Wizard: asks whether anyone sound is a Wizard right now, while Enemy: fights while an enemy stands, if not Boss: spawns one, if ~Wizard: asks whether anyone waits for repair.

The Agent's own in is reserved for keywords: Tags marked @Flag (§1.8). Ordinary Tags are asked from the Tag's side, agent in Wizard.

Format specs are the display door: a language's own string formatting renders what a Tag or Agent is, without a method on either. On an Agent the door follows the empty-seat rule: a host with its own formatting keeps it.

Queries that need a name are functions (Form, Tags, Keyword, Apply, Outline, Contract, Scope), never members of the Tag or of the Agent. Another language profile chooses its own native spellings; the acts and their distinctions are what must survive.


Ring 1 · Contributions

1.1 Two scopes

Every contribution has a receiver, and the receiver decides its scope.

Scope Contributions Receiver Meaning
Agent Action, Record one Agent what this Agent does and holds
Tag Operation, Report the Tag what the whole Field shares

Where a contribution is declared does not change its scope. An Action written inside a Tag class is still an Agent contribution: the Tag stores it once, every Agent is its receiver.

The receiver may itself be a Tag. When a Tag is applied to another Tag (pinning, §1.9), the pinned Tag is the receiver, so the Pin's Records land as Reports and its Actions as Operations of that Tag: Tag scope, because a Tag is what receives them.

In Ada's terms, the Tag is the package specification and the Agent is the body. The specification declares what is shared; the body is what acts.

A name identifies one slot per scope: (scope, name). In Agent scope that slot holds an Action or a Record, never both at once:

  • Independent Tags (neither in the other's Form) cannot place an Action and a Record at the same Agent name. The Tagging fails at step 1, atomic.
  • Within one Form, a Shape may change the kind of a Base slot: fix a Base Action as a Record, or compute a Base Record with an Action. The Base's view (§1.7) keeps the prior kind.
  • Across scopes, equal names do not collide. Fire.colour (Tag) and ember.colour (Agent) are two slots with two histories.

1.2 Actions

An Action is Agent behaviour. Declared as a plain method on the Tag or with @Action; the first parameter is the Agent, by discipline, under any name.

class Person(Tag):
    def Attack(agent) -> str:
        return "Attack!"

The latest applied Layer is the visible Overlay for a name. An Action declared without an Underlay replaces; with @Underlay it extends:

class Elf(Person):
    @Action
    @Underlay
    def Attack(agent, underlay) -> str:
        return "With elven grace " + underlay()

The Underlay is captured when the Tag applies: it is the complete Action visible immediately before this Layer. It forms a backward chain of callables and never resolves again later. Calling it with no arguments forwards the current call's arguments; calling it with arguments passes those instead. If no prior Action exists, a Tag asking for an Underlay cannot apply (Tag Resolution Failure).

An Action that calls another Action through the Agent uses the current Overlay at play time:

class Combatant(Tag):
    def Combat(agent) -> str:
        return agent.Attack()          # whatever Attack is visible now

Replacing an Action of an independent Tag without an Underlay is allowed but diagnosed (a warning): something was overwritten that another meaning still relies on.

The same rules bind special methods: a Tag may contribute __add__, __eq__ and the like, and they actualize the Agent.

1.3 Records

A Record is Agent state contributed by a Tag. A Tag is something you are; a Record is something you are currently (estar): an adjective.

Tag, you became this Record, this can change
Human, Elf: species asleep, poisoned: conditions
Wizard, Paladin: class hit points, gold: resources
Sage, Soldier: background location, morals
Harpers: affiliation affinity, standing: current status
Spellcaster: learned capability spell slots left
Access Pass: recognized by the system clearance: the specific level

A Record is declared as a builder. The builder runs at step 2 of the tagging sequence; its value is stored on the Agent, fresh for each Agent:

class Inventory(Tag):
    @Record
    def items(agent) -> list[str]:
        return []

A builder may declare a second parameter: it receives the value already stored under that name, or None when there is none. This is how independent Tags pile up on one Record, in application order, and how a Tag extends an ordinary attribute the host already had:

class Elf(Species):
    @Record
    def spells(agent, stored) -> list[str]:
        return (stored or []) + ["Light"]

class Wizard(Class):
    @Record
    def spells(agent, stored) -> list[str]:
        return (stored or []) + ["Magic Missile", "Shield"]

class Sage(Tag):
    @Record
    def spells(agent, stored) -> list[str]:
        return (stored or []) + ["Identify"]

Elf(ari); Wizard(ari); Sage(ari)
assert ari.spells == ["Light", "Magic Missile", "Shield", "Identify"]

The author writes the merge. stored + new, max(stored, new), stored | new: whatever the domain means. A builder without the second parameter replaces; replacing the Record of an independent Tag is diagnosed, like an Action.

A Record signature has three seats: the Agent, the stored value, and the application inputs, which bind by name. The second positional parameter is always the stored seat; when there is nothing to read, a * holds the seat empty and the inputs follow it by name:

class MI6(Tag):

    @Record
    def code(agent, *, code):        # agent, nothing stored, code from the call
        return code

MI6(bond, code="007")
bond.code                            # "007"

A builder whose second positional parameter is named like a supplied input is refused at tagging with a Declaration Failure that shows the spelling above: the stored value must never be mistaken for the input in silence. An input the caller did not supply keeps the parameter's default, or is None when it has none.

After tagging, a Record is an ordinary attribute: read it, assign it, delete it with the language's own del agent.record. Deleting is allowed but rarely good design; frequent deletion means unclear state ownership.

Mutable Record values must be fresh per Agent unless sharing is the explicit intent. Shared values belong in a Report.

1.4 Reports and Operations

A Report is shared data belonging to a Tag; an Operation is shared behaviour. Both take the Tag as their first input, the way Agent members take the Agent. The four kinds are one pattern, two scopes:

behaviour data
Agent @Action def f(agent, ...) @Record def r(agent, stored)
Tag @Operation def f(tag, ...) @Report def r(tag, inherited)
class Community(Tag):

    @Report
    def colour(tag) -> str:
        return "green"

    @Operation
    def Greet(tag, name) -> str:
        return f"{tag.__name__}:{name}"

Community.colour            # "green"
Community.Greet("Ari")      # "Community:Ari"

A Report builder runs once per Tag, on first read, and its value is held on the Tag: one copy for the whole Field. Like a Record, it may declare a second parameter, which receives the value the Tag's Bases give that name, or None, so a Shape can extend a Base's Report rather than replace it.

Reports and Operations are not visible on the Agent. ari.colour does not exist after Community(ari), and neither does ari.Greet. Projecting Tag members onto Agents would invent an Agent receiver for Tag behaviour, silently shadow host methods, and confuse the two histories. The Agent reaches them through its Tag-bound view (§1.7) or through publication.

A shared counter is a Report; the Imprint that changes it acts on the Tag:

class Secret_Agent(Tag):

    @Report
    def active(tag) -> int:
        return 0

    @Imprint
    def Activate(agent):
        Secret_Agent.active += 1

1.5 Publication

Scope says who owns a member. Publication says who may reach it from outside. The defaults follow the Agency/Agent picture: what the Agent does is public; what the Agency keeps is internal.

Contribution Default Mark
Action, Record external @Secret makes it internal
Operation, Report internal @Public publishes it on the Agent

Composition is the door. Inside it, everything of the relevant scopes resolves; outside it, only external members and the control plane (apply, Rip, membership, Fields, views of carried meaning). Inside composition means: while an Action, Imprint, Record builder, condition, or Rip protocol bound to this Agent is running.

@Secret on an Action or Record:

class Fire(Tag):
    @Secret
    @Record
    def ember_heat(agent) -> int:
        return 3

    @Secret
    @Action
    def ignite(agent) -> int:
        return agent.ember_heat * 2

    @Action
    def strike(agent) -> int:
        return agent.ignite() + 1     # inside composition: resolves

ember.strike()        # 7
ember.ignite()        # AttributeError: secret member
ember.ember_heat      # AttributeError: secret member

A secret handle captured inside and called outside fails closed. A secret still occupies its (Agent, name) slot; it is not a third kind.

@Public on a Report or Operation constructs the publishing member for you, so field economy (one value, one body on the Tag) needs no hand-written adapter:

  • A published Report appears on the Agent as a read-only name that reads the Tag's current value. One copy lives on the Tag; the Agent does not carry it.
  • A published Operation appears on the Agent as an Action that forwards to the Operation with the Agent as its second input, after the Tag.
class Agency(Tag):

    @Public
    @Report
    def colour(tag) -> str:
        return "navy"

    @Public
    @Operation
    def dispatch(agency, sender, message):
        return network.broadcast(sender, message)   # a Rogue sender never gets here

Agency(agent)
agent.colour                     # "navy", read-only
agent.dispatch(message)          # Agency.dispatch(Agency, agent, message)

The published Action is a normal Action: it overlays and underlays at (Agent, name) and stays on the Agent after Rip. Its use does not: a published member answers members only, and only sound ones (STEP-SPEC-10). At every use, an Operation's call or a Report's read, TOP checks that the Agent still belongs to the publishing Tag and that every promise on the Agent holds, whichever Tag made it: the same soundness if agent: and the loop ask about. A Rogue Agent (it left) gets a Rogue Access Failure, a Resolution Failure; a stale send = agent.dispatch captured before Rip fails the same way. A defective Agent (a promise broke) gets the broken promise by name, except Postcondition.Has_Homeland, so it can repair what the failure names and retry. Membership or repair opens the member again; nothing is re-declared. The Agent's own Actions and Records are untouched: what it became stays. A defective Agent reads the Tag's shared values through the Tag directly, which asks nothing about the Agent. TOP is not a security system; a long-running task may still re-check authority before an irreversible step.

A Rogue Access Failure is a TOP failure and nothing else. It is not dressed as a failure of the host language's attribute lookup: the question is this Agent a member? belongs to TOP, and answering it in the lower layer's words would let a lower-layer question (hasattr) swallow it and report no such name for a name that plainly exists.

@Secret and @Public are modifiers: they stack with @Action, @Record, @Operation and @Report in either order. A modifier that restates the default (@Public @Record, @Secret @Report) is accepted; both on one member is a contradiction and is rejected at declaration.

1.6 Delete

@Delete on a name removes the visible contribution, or the host member, of that name. It frees the slot: the next contribution may occupy it with either Agent kind, and an Underlay-seeking contribution finds nothing.

class Pacifist(Tag):
    @Delete
    def Attack(agent): ...

1.7 Access model

Three forms, three meanings. An implementation may spell them differently but must keep them distinct.

Current Agent access: the visible Overlay now.

agent.Attack()
agent.weapon

Agent-bound Tag access: the Overlay as it was immediately after that Tag applied to this Agent, including prior independent Tags. It does not change when a later Tag actualizes the same name. It is a snapshot: read only. Two spellings:

agent.Paladin.Attack()       # by name: the last active Tag called Paladin
Paladin[agent].Attack()      # by class: exact, even when names collide

Agent-bound access requires active membership; a Rogue Agent keeps its sticky members but loses the view. Secret members obey the same door in a view.

Direct Tag access: the Tag itself.

Paladin.HONOUR_CODE
Paladin.Operation(...)
Paladin[:]                    # its Field

Worked example:

class Character:
    def Attack(agent) -> str:
        return "Faulty OOP attack."

class Person(Tag):
    def Attack(agent) -> str:
        return "Attack!"

class Elf(Person):
    @Action
    @Underlay
    def Attack(agent, underlay) -> str:
        return "With elven grace " + underlay()

class Paladin(Person):
    @Action
    @Underlay
    def Attack(agent, underlay) -> str:
        return underlay() + " For your holy oath!"

ari = Character()
Paladin(ari)
Elf(ari)

ari.Attack()             # "With elven grace Attack! For your holy oath!"
ari.Paladin.Attack()     # "Attack! For your holy oath!"

Person applies before Paladin and stays active when Elf arrives. Elf captures the complete Attack Overlay (Person, then Paladin). The later Tag replaces ari.Attack() without changing what ari.Paladin.Attack() means.

1.8 Flags: Tags as keywords

Tabletop rules have keywords: Undead, Flying, Fey. A rule does not import the creature's species; it asks whether the word is on the sheet. A Tag marked @Flag is such a word. Its name becomes searchable from the Agent's side, by name or by class:

@Flag
class Undead(Tag):
    pass

Undead(ghoul)

assert "Undead" in ghoul             # by name
assert Undead in ghoul               # by class
assert Keyword(ghoul, "Undead")      # the function form, works on any object
assert ghoul in Undead               # membership, as for every Tag

This is what lets rules live as data. A table entry "Undead-Flying" splits into words, and Keyword(agent, *words) decides; the rule ports to any program whose Tags carry those names.

Flags are opt-in on purpose. Matching by name is global: two Tags that share a name are one word to a string. Only Tags that ask to be words carry that risk, and an ordinary Tag is never found by name. Names match exactly.

Words. A keyword has variants and neighbours: to the rules that name it so, a werewolf is also a Wolf and a Lycanthrope. @Flag takes the words a Tag also answers to:

@Flag("Wolf", "Lycanthrope")
class Werewolf(Tag):
    pass

Werewolf(howler)

assert "Wolf" in howler              # a word
assert "Werewolf" in howler          # the name always flags
assert Werewolf in howler            # the class form is unchanged
assert Keyword(howler, "Lycanthrope")

A word is only a word. It answers in and Keyword, never membership: an Agent may carry the word Beast without being a Beast, the way a secret identity carries a public name. A word that names another Tag is accepted, and says nothing about that Tag's Field. Many Tags may share a word; it answers while any of them is active. The words are the Tag's own: a Shape answers its Base's words because the Base is active (§0.3), never by inheriting them. Bare @Flag is the name alone. Words are non-empty strings and match exactly, like names.

A Flag needs the Agent's in, and one seat holds one meaning. Something else may already answer it: the host, through its own __contains__ or __iter__ (a container, a party that iterates its members), or a Tag's Action of either name, a published Operation included. A Flag and any of them collide, in either order, and within one Form before any of it applies. The later one fails with a Composition Failure naming both sides, like a Record over a host property, and nothing changes: a seat never changes meaning in silence. A Flag that declares such an Action itself is a Declaration Failure.

The host's seat is read as the language reads in. In Python, for __contains__ and then __iter__, the first class in the MRO that defines the name decides. The value None, Python's "in is unavailable", leaves the seat free. __getitem__ alone is not a seat, and a Flag takes it: on a keyed host its in fails or never ends; on an index-style host (the old sequence protocol) its in worked, and stops meaning membership once a Flag lands. Keyword(...) answers for every object, tagged or not.

1.9 Pins: Tags as Targets

Categories have categories. A game marks Tags as Rare or Homebrew; a rules engine keeps Tags by School; a tool walks every Tag that is Deprecated. A Tag marked @Pin applies to Tags, and the pinned Tag is its Agent in every sense of Ring 0: membership, a Field to walk, a gate, a promise, a view, a Rip.

@Pin
class Rare(Tag):

    @Record
    def rarity(tag):                     # lands as Wizard.rarity: a Report
        return "rare"

    @Action
    def Describe(tag):                   # lands as Wizard.Describe: an Operation
        return f"{tag.__name__} is {tag.rarity}"


Rare(Wizard)

assert Wizard in Rare                    # active membership, from the Pin's side
assert list(Rare) == [Wizard]            # a Field of Tags
assert Wizard.rarity == "rare"           # one value, held on the Tag
assert Wizard.Describe() == "Wizard is rare"
assert not hasattr(charlie, "rarity")    # never on the Tag's Agents

Who may be a Target. A Pin applies to Tags only; on an object it fails with a Composition Failure. An ordinary Tag applies to objects only; on a class it is refused, as before. A Pin may not pin itself or any Tag of its own Form. A Shape of a Pin is a Pin, and one Form is all Pins or no Pins: mixing them is a Declaration Failure. Fields therefore never mix Agents and Tags.

The receiver rule (§1.1). The first parameter of a Pin's Agent-scope member is the pinned Tag; write it tag. A Pin's Record lands as a Report of the pinned Tag: one value, held on the Tag, inherited by its Shapes, and readable through the inherited seat of a Shape's own Report (§1.4). A Pin's Action lands as an Operation: read from the pinned Tag or any of its Shapes, it receives the Tag it was read from, as an Operation does. Membership does not inherit: War_Caster in Rare is False until Rare(War_Caster). Nothing a Pin lands is ever projected onto the Tag's Agents. A Pin's own Operations and Reports stay on the Pin.

Patching, with collision control. The pinned Tag's own Tag-scope declarations are host members to a Pin, as a host method is to an Action. A Pin Action over the Tag's own Operation replaces it silently with the Operation as its Underlay, so def Control(tag, underlay, ...) can call the engine it patches; every Agent that reaches the Operation through the Tag is actualized at once, because an Operation is looked up on the Tag at call time. A Pin Record over the Tag's own Report receives the Report's value in the stored seat. A failed pinning rolls the declaration back; a Ripped Pin leaves the patch, sticky, and a Pin's @Rip teardown that declares a second seat receives the Tag's original declarations under the patched names, so un-patching is one line: def Unpatch(tag, original): tag.Control = original.Control. A Pin member named like something the Tag declares in Agent scope or as a protocol, itself or in a Base, is refused at the gate: on a class the scopes share one dictionary, and the write would silently remove the declaration from every future Agent's contract. So is a name every Tag answers through its metaclass. Names another Pin landed follow the Overlay laws of §1.2 and §1.3.

Publication on a Tag. @Secret on a Pin's Record or Action makes it Pin-private state on the Tag: held in the Tag's state, resolved only while the Tag's own protocols or pinned Operations run, an Attribute failure from main, not inherited by Shapes. @Public publishes it onto the pinned Tag's Field: every present Agent receives it at pinning and every future Agent through the Tag, as the Tag's own published Report or Operation (def Control(tag, agent, ...), the Agent second), under the membership rule of §1.5. The Field is checked on copies first; a collision on any one Agent refuses the pinning and touches none. A Pin's own Reports and Operations are not published, and @Delete and special-method Actions on a Pin are Declaration Failures.

Keywords on a Tag. A Pin may be a Flag. A string can never be a member, so on a Tag a string in the in seat asks for a keyword: "Deprecated" in Wizard is True while the Flag Pin Deprecated is active on it, and so is each word it lists; objects and classes in that seat ask membership. Keyword(Wizard, "Deprecated") and Keyword(Wizard, Deprecated) answer the same. A word like Deprecated is a Pin and not a Report for the reason Undead is a Tag and not asleep = True: a Report is a value, and a value implies it may be flipped back; a Pin is membership, with a Field, a gate, a promise, a Rip that is an act, and a history isinstance remembers. A Tag says what something is; a Record says what it has right now.

Seats already taken. bool(Wizard) remains "is anyone a sound Wizard" (§0.8); a pinned Tag's own promises are read from the Pin's side, Wizard in ~Rare, Contract.Display(Wizard), f"{Wizard:contract}". f"{Wizard:pins}" names its Pins. Wizard.Rare is the Pin-bound view by name, on the same miss-path rule as charlie.Wizard.

Contracts, Imprints, Rip. A Pin's Preconditions gate the pinning and receive the Tag; a failed gate leaves the Tag exactly as it was, its metaclass included. Postconditions are checked once per pinning and re-checked at later pinning boundaries of that Tag; a broken promise leaves the Tag pinned and defective (§2.5). Imprints run after commit. del Rare[Wizard] runs the Pin's Rip protocols; landed values and Operations stay, sticky, and isinstance(Wizard, Rare) stays True. Pinning again after a Rip is a fresh pinning (§0.7).

A Pin does not alter the pinned Tag's own gate over its Agents. A Tag that should refuse new members while Deprecated writes that as its own Precondition, reading its Pins.


Ring 2 · Contracts

Conditions are TOP's quality control. They run at tagging boundaries, never continuously during play, and they never wrap an ordinary Action call.

2.1 A condition is strictly boolean

A condition is a function whose first input is the Agent. It holds on True or on falling through without a return (an assert-style body), fails on False, and is rejected on anything else.

class Wizard(Tag):
    @Pre
    def Level_Over_Zero(agent):
        return agent.level > 0

    @Post
    def Has_Spellbook(agent):
        assert agent.spellbook is not None

TOP does not coerce truthy and falsy values: a Record of 0 spell slots is a real value, not a failure. return agent.spell_slots raises a Contract Failure telling you to write the comparison you mean. The absence of a return is permission: a condition is a restriction, and saying nothing permits. Legal until written into law.

Contracts should be picky. The Layer's Lawyer

2.2 Preconditions: the gate

A Precondition inspects the incoming materials: may this Agent, with these inputs, enter the line? It runs at step 1, once per call, for the Tags applied in the current call only, as they will be composed: a Shape's gate replaces its Base's. An earlier Tag's gate is not re-asked when a later, unrelated Tag arrives; it already let its Agent in. Because the gate runs before any Base applies, a Precondition sees the Agent as it arrives, never what a Base's Imprint is about to write.

Preconditions, like Record builders and Imprints, receive application inputs by name:

class Coded(Tag):
    @Pre
    def Has_Code(agent, code):
        return code is not None

Coded(bond)              # TagPreconditionError
Coded(bond, code="007")  # applies

A parameter the caller did not supply keeps its declared default, or is None when it has none.

Preconditions relax backward. A Shape may ask for less than its Base, never more, so a Shape can stand in wherever its Base is expected. Override freely; compose with @Underlay when you want the Base's gate too:

class Founder(Guild):
    @Pre
    def Dues_Paid(agent):
        return True                # founders skip the dues gate

class Apprentice(Wizard):
    @Pre
    @Underlay
    def Level_Over_Zero(agent, base):
        assert agent.mentor        # also needs a mentor
        return base()              # then the Base's gate

This is also how synergy is expressed: a feat that requires two other Tags gates on both.

class War_Caster(Tag):
    @Pre
    def Is_A_Caster(agent):
        return agent in Wizard

2.3 Imprints: the writing

An Imprint performs the work of tagging: it runs at step 4, after the Tag has committed, in declaration order, with the application inputs by name.

class MI6(Tag):
    @Imprint
    def SetUp(agent, code):
        agent.code = code
        agent.status = "Full"

An Imprint may apply further Tags to the same Agent; those are ordinary later calls. If an Imprint fails, the Tag stays and the call raises an Imprint Failure: the machine broke while writing, and the product is on the line for inspection.

2.4 Postconditions: the promise

A Postcondition inspects the finished product. Every visible Postcondition runs at step 5, once per call, after the whole Form, so a Base may promise what its Shape delivers. Postconditions take no inputs: they judge the Agent, not the materials.

Every visible Postcondition is re-checked at each later tagging boundary, so a later Tagging can be refused by an earlier Tag's promise. Conditions do not check themselves during play; what must be watched continuously is a Record.

Postconditions strengthen forward. A Shape promises at least what its Base promised: compose with @Underlay and and.

class Knight(Soldier):
    @Post
    @Underlay
    def Is_Equipped(agent, base):
        return base() and agent.oath

Sometimes a specific case really is looser than the rule: "no Strength above 20", except Barbarians. Overriding a Base Postcondition without its Underlay is a weakened promise. TOP allows it, because forbidding it would break the refactoring that is the point of TOP, but it is never silent: a Contract Warning is raised at tagging. If the relaxation is intended, the warning is your receipt; if it was a slip, it is your alarm.

The discipline in three lines:

  • Strengthen a Post → @Post @Underlay, return base() and …. Silent.
  • Relax a Pre → override freely. Silent.
  • Weaken a Post → override without the Underlay. Allowed, diagnosed.

2.5 Defective Agents

A Tag whose Postcondition failed is applied and defective. The Agent is a member; its promise is broken; it must be repaired or Ripped.

An Agent is truthy exactly when every visible Postcondition holds. if is the conditional and Postconditions are conditions:

if agent:               # every promise holds
    proceed(agent)

assert agent            # or raise

Fields partition accordingly:

for wizard in Wizard:        # the sound population: the ones fit to play
for broken in ~Wizard:       # the defective population: repair them
for anyone in Wizard[:]:     # everyone, sound or defective
assert broken in Wizard      # a defective Agent is still a member

The plain loop is the working population, and if Wizard: asks whether it is empty. A broken Agent does not stop being a member (in), does not leave Wizard[:], and waits in ~Wizard for repair or Rip. Membership and the loop deliberately disagree for it: the loop is the line, and a defective product is off the line.

Populations combine (STEP-SPEC-13). | is either, & is both, - is the left without the right, on any population: a whole Field, the sound view, the defective view, or a combination. A Tag in an operator seat is its sound population, as it is in the loop; Wizard[:] | Fighter[:] is everyone who is either; the levels mix. The result is a lazy view that reads its Fields when walked, keeps application order within each side, answers in, len, truth and iteration, and has no complement (~ on a union has no universe). A Tag with anything that is not a population keeps the language's own class union (Wizard | None).

for c in Wizard | Fighter:            # sound in either, each once
for b in Wizard & Fighter:            # sound in both
for u in (Wizard[:] | Fighter[:]) - Sworn:   # anyone with a role who has not sworn

A condition is read on the Agent by its name (STEP-SPEC-14). agent.Has_Book is True while the promise called Has_Book holds and False when it does not: the language's boolean, computed on read, never stored, never callable. A Postcondition answers before a Precondition of the same name; a condition that raises reads False; a non-boolean is a Contract Failure on read, as always. Because the name is read on the Agent, a condition may not share its name with an Action, a Record, a member the host defines or a value the Agent already holds: the tagging is refused at the door with a Composition Failure, and so is an Action or a Record laid over a condition's name. Nothing is silently shadowed. A pinned Tag reads its own conditions the same way (Wizard.Has_Members). A condition that outlived its Tag (§0.7) still reads by name until the author ends it.

Truthiness on a plain object is vacuously true, so this fills an empty seat. A host that defines its own __bool__ or __len__ keeps it until a Postcondition becomes visible on that Agent.

2.6 Naming the culprit

A failed check raises a failure that carries the check's name, as a subclass of the general failure: the Precondition declared as Is_A_Caster refuses with TagPreconditionError.Is_A_Caster, an Imprint called Arm fails with TagImprintError.Arm, a Postcondition called Has_Book with TagPostconditionError.Has_Book. The marks are the short spelling of the same classes, so a program catches a refusal in its own words:

try:
    War_Caster(bruk)
except Precondition.Is_A_Caster:      # this one refusal
    ...
except TagPreconditionError:          # any refusal
    ...

Rules:

  1. The named failure is the general failure (issubclass), so a handler for the general one still catches everything. error.name is the check's name; the general failure's name is None.
  2. A name exists from the moment its Tag class exists, not from its first tagging. A name that no Tag declared is an error at the except line (AttributeError), never a handler that silently fails to match.
  3. Names are per kind, not per Tag: two Tags that each declare Ready share Precondition.Ready. A program that needs to tell them apart reads f"{agent:contract}" or names the checks differently.
  4. A check that raises instead of returning False is named the same way.

bool(agent) is the verdict on a finished Agent. The Contract namespace names the promise that broke:

Contract.Postconditions(agent)   # promises hold, or raise naming one
Contract.Preconditions(agent)    # gates still hold, or raise
Contract.Conditions(agent)       # both, Pre then Post
Contract.Holds(agent)            # the boolean form, spelled out
Contract.Status(agent)           # {condition: holds?}, never raises
print(Contract.Display(agent))
# Hero[Wizard] contract:
#   Pre:
#     OK  Level_Over_Zero
#   Post:
#     XX  Has_Spellbook

2.7 Necessary to enter, necessary to stay

@Pre and @Post stacked on one function make it a condition: checked at the door as a Precondition and kept as a Postcondition. It is one body, named once, and it fails under both names:

class Elf(Tag):

    @Pre
    @Post
    def Alive(agent):
        return agent.alive


Elf(dead)                     # refused: Precondition.Alive
Elf(ari)                      # in; and from now on Ari must stay alive

Being alive is necessary to become an Elf and necessary to remain a sound one; it is not sufficient, since other living things are not Elves. A condition says necessary; membership says is.

@Requirement is the same mark said in one word, for a claim that reads better as a necessity than as a pair of checks:

class Elf(Tag):

    @Requirement
    def Alive(agent):
        return agent.alive

A Requirement names no failure of its own. It fails at the door as Precondition.Alive and afterwards as Postcondition.Alive, because a program repairs the two differently: one is an Agent that may not come in, the other an Agent already in and now broken. Reading Requirement.Alive is a Declaration-time mistake and says so.

2.8 Writing a check

A condition usually asks whether an Agent has something. assert agent.spellbook reads well, but asks two questions at once: defined and truthy. That is fine for a spellbook object; it is a trap for a spell_slots of 0. Separate having a contribution from its value:

  • Is it there at all? assert agent.spell_slots is not None, or assert hasattr(agent, "spell_slots") when None is itself valid.
  • Does it have a particular value? assert agent.spell_slots > 0.

Ring 3 · Lifecycle

3.1 Rip protocols

Imprint and Rip are duals: constructor and destructor, __enter__ and __exit__. A @Rip Action runs when the Agent leaves the Tag's Field. It is also an ordinary, callable Action.

class MI6(Tag):
    @Imprint
    def SetUp(agent, code):
        agent.code = code
        agent.status = "Full"

    @Rip
    def SetDown(agent):
        del agent.code
        agent.status = "Former MI6 Agent"

MI6(bond, code="007")
del MI6[bond]
bond.status          # "Former MI6 Agent"

Teardowns run after membership has ended, in declaration order, every one of them; failures are collected and reported once as a Composition Failure. A @Rip Action with an @Underlay runs composed, like any Action.

Ripping a Tag may apply another Tag, even itself. That is outside good TOP use: it could keep an Agent from ever leaving a Field.

3.2 Deleting an Agent

Deletion of an Agent Rips it from its active Tags, so exit protocols run. An implementation provides three tiers and says which is which:

Tier Guarantee
Finalizer (__del__) best effort: when the Agent is collected, its teardowns run, then its __del__ Layers; at interpreter exit only the __del__ Layers run; the language may not run finalizers at shutdown or inside reference cycles
Scope(agent, *tags) guaranteed: Tags apply on entry and Rip, in reverse, on exit, even if the block raises
At_Exit(agent) opt-in: teardowns also run at normal interpreter exit; registration is weak

Every teardown runs at most once, whichever tier reaches it first.

The Agent's own finalizer is a member in Layers (STEP-SPEC-18). The host's __del__ is its first Layer, found as the language finds it. A Tag's __del__ replaces the Layers beneath it, or with @Underlay extends them; @Delete removes them. With nothing stated, the host's own __del__ runs, as it would for the untagged object. The Layer beneath a __del__ is always callable, and where there is nothing it does nothing. Deletion runs in that order: every teardown still due runs, then the __del__ runs as the Overlay shows it, top Layer first; both run inside the composition door (§1.5). A __del__ never stops a teardown: replacing the Layers replaces only them, and an interrupted teardown does not skip them. At interpreter exit only the Layers run; teardowns at exit are At_Exit's. An error raised by a __del__ Layer is reported as the language reports any finalizer's; the teardowns stay best effort and silent. @Rip on a __del__ is a Declaration Failure: a __del__ Layer already runs at deletion. The language calls __del__, not the program: in Python, agent.__del__ reads the kit's finalizer, and calling it by hand runs the teardowns while the Agent is still a member.

class Lantern:
    def __del__(self):
        log.append("wick out")             # the first Layer

class Carried(Tag):
    @Rip
    def Put_Down(agent):
        log.append("put down")             # a teardown: runs first

class Enchanted(Tag):
    @Underlay
    def __del__(agent, underlay):          # a Layer over the host's own
        log.append("spell fades")
        underlay()

lamp = Lantern()
Carried(lamp)
Enchanted(lamp)
del lamp
assert log == ["put down", "spell fades", "wick out"]
with Scope(agent, Sentry):
    guard_the_gate(agent)
# Sentry's teardown has run here, exception or not

Ring 4 · Edges

What TOP does not promise, stated so nobody has to discover it.

  • Nominal type. type(agent) is Host may be false after tagging. The type's name is unchanged and isinstance(agent, Host) is true. Code that keys on exact type identity is outside the guarantee. A pinned Tag's metaclass may be wrapped the same way; its Shapes' is not.
  • Copying and pickling. Cloning an Agent is domain work: build a new Target and apply its Tags again (Tags(agent) lists them). A Python implementation refuses copy.copy explicitly rather than aliasing state.
  • Threads. One Agent, one thread. Fields are not synchronized.
  • Host descriptors. A Record cannot share a name with a host property or slot; the Tagging fails with a Composition Failure.
  • Raw side effects. Rollback restores TOP-managed state and the Agent's attributes at call entry. It cannot undo an in-place mutation of a pre-existing mutable value.
  • Operations and the door. An Operation called directly on the Tag is outside composition unless it was reached from an Action, Imprint, condition or Rip of the Agent whose secrets it reads.

🚨 Failure model

Every failure names the law it violates. A language profile may use its own types but must keep these distinct.

Failure Meaning Effect
Tag Declaration Failure A Tag is written wrong: illegal mark combination, @Underlay without a parameter to receive it. at class use
Tag Composition Failure Contributions cannot form the Overlay: cross-kind collision, Record over a host descriptor, a Record builder or teardown that failed, a Target that cannot carry state, a Base still required. call rolled back (or Rip refused)
Tag Resolution Failure A required Underlay, view, or membership is unavailable. call rolled back
Tag Rogue Access Failure A Rogue Agent reached a published member of a Tag it has left. A Resolution Failure, and a TOP failure only: never dressed as a host-language attribute failure. use refused
Tag Precondition Failure A gate refused the incoming Agent. call rolled back
Tag Imprint Failure An Imprint failed after commit. Tags stay
Tag Postcondition Failure The finished Agent breaks a promise. Tags stay, Agent defective
Tag Contract Failure A condition returned a non-boolean. call rolled back
Overwrite Warning An independent Tag replaced a visible Action or Record without an Underlay. diagnostic
Contract Warning A Shape weakened a Base Postcondition. diagnostic

🧰 Conformance obligations

A conforming implementation provides, ring by ring:

Ring 0

  • stable object identity under tagging, and preserved host behaviour;
  • membership and Base membership (agent in Tag), closed upward, with a has-been check that survives Rip;
  • non-owning, identity-indexed, iterable Fields;
  • Base-first Form application, each Base once, active reapply a no-op;
  • the five-step tagging sequence with the call boundary: rollback on gate and Record failure, Tags stay on Imprint and Postcondition failure;
  • Rip: sticky contributions and sticky conditions, ended by the author (a guard, or Contract.Delete from the @Rip protocol), refusal while a Shape requires the Base, no cascade;
  • native spellings for every Tag-level act, leaving the Tag's dotted namespace to the program.

Ring 1

  • Agent and Tag scopes, (scope, name) slots, cross-kind collision rules;
  • latest-Layer Overlay; captured, callable Underlays for Actions and conditions; the stored value for Records;
  • Reports and Operations invisible on the Agent;
  • publication: @Secret with a composition door that fails closed; Public Reports as read-only live names and Public Operations as Actions with the Agent as second input; illegal marks rejected at declaration;
  • Flags: opt-in keyword Tags searchable from the Agent's side by name, by the words they list, and by class, a word never standing for membership, colliding in either order with a host or a Tag's Action that already answers in, never matched for ordinary Tags;
  • Pins: opt-in Tags whose Targets are Tags, the pinned Tag as Agent under every Ring 0 act, the receiver rule (Records as Reports, Actions as Operations), Fields never mixed, the Tag's Tag-scope declarations as host members (Underlay, stored seat) with Agent-scope and protocol names refused, @Secret as Pin-private state, @Public as publication onto the Field, Flag Pins as keywords by string;
  • published members answering members only, and only sound ones: a Rogue Access Failure on a Rogue Agent, the broken promise by name on a defective one, open again on membership or repair;
  • @Pre and @Post stacked on one function as one condition, spelled @Requirement in one word;
  • populations combined with |, & and - at every level, lazily, a Tag in an operator seat meaning its sound population;
  • every condition read on the Agent by its name as a plain boolean, with a condition's name refused to Actions, Records and host members;
  • Delete; the three access forms, with Agent-bound views as read-only snapshots requiring active membership.

Ring 2

  • strict boolean conditions; Preconditions gating only the current call, with inputs; Imprints after commit, with inputs; Postconditions once per call after the whole Form, re-checked at every later boundary, without inputs;
  • the contract direction, with weakened Postconditions diagnosed;
  • defective Agents: contract truthiness; the plain loop as the sound population, ~Tag the defective one, Tag[:] everyone, membership unchanged; a namespace that names the culprit.

Ring 3

  • @Rip protocols run after membership ends, once, composed, failures reported; the three deletion tiers; the Agent's __del__ as Layers of its Overlay, run after the teardowns, and alone at interpreter exit.

Everywhere

  • the failure types above, distinct and named.

An implementation may choose runtime type composition, generated wrappers, proxies, trait machinery, or static code generation. Those are its business. TopKit is the Python reference; any gap in TopKit is TopKit's to fix, not a change to TOP.


🧭 TOP among paradigms

TOP was arrived at independently, from storage systems, organizational models, and character creation. Several established ideas share pieces of it:

  • Mixins and traits compose behaviour per class, resolved once. TOP composes per instance, at runtime, and freezes each Layer as an Underlay.
  • The Role Object pattern adds roles to a live identity but treats removal as ordinary. TOP treats removal as exceptional: a role is something you became; only active membership can be rescinded; changing state lives in Records.
  • Entity-Component systems attach data to an identity. TOP keeps behaviour with the category and adds membership: "every Wizard".
  • Aspect-oriented programming layers behaviour across a hierarchy. TOP's Imprints and conditions are a tag-time form of the same idea, scoped to one semantic act.
  • Design by Contract (Meyer, Eiffel; Ada 2012 aspects) gives TOP its contract direction and its refusal to coerce booleans.
  • Ada packages give TOP its specification/body picture: the Tag declares what is shared, the Agent is what acts.

TOP goes beyond these to one vocabulary: sticky runtime contributions, active Field membership, captured Overlays, tag-time contracts, two scopes with publication, and one stable identity.

More importantly, it is a thinking system: a simple idea with analogies to the real world, strong enough to make complex systems intuitive. TOP is not just a library; it is a way to think.


🧯 Commitments

  1. A Target keeps one stable identity.
  2. Tags add semantic context; they never replace inherent structure.
  3. Membership is meaningful even when a Tag contributes nothing else.
  4. Mutable Agent state belongs in Records; shared Tag data in Reports.
  5. Composition is explicit, ordered, and inspectable.
  6. A failed gate publishes nothing; a failed promise is never silent.
  7. Rip ends active membership without pretending history never happened.
  8. What the Agent does is public; what the Agency keeps is internal.
  9. Language profiles feel native to their language.
  10. Implementations serve the paradigm; the paradigm serves no single application.

Surface spellings may evolve. Implementation strategies may evolve. The semantic laws stay.