Skip to content

README: League

Profile version: net.plantange/v1 Kind: league Surface root: league::<league_id>/


What is a League?

A League is the coordinator-local policy authority for team-branded speech. Where a Team publishes freely into any context, a League is a gatekeeping surface: it defines which teams are enrolled, which emission contexts are valid, and which parameterized templates teams may project through.

The League is the trust boundary between participating teams and published brand output. Only enrolled teams can use the league's templates. The league validates every request. Rejections are audited.


Three Concepts

LeagueBrand             — identity + context registry + template catalog
LeagueParticipation     — team enrollment record (league ↔ team, optional season)
LeagueTemplate          — parameterized projection unit ({slot} → filled text)
EmissionLog             — append-only audit trail: accepted + rejected requests

The request flow:

Team → LeagueProjectionRequest → validate_league_projection() → LeagueProjectionOutcome → EmissionLog

Surface Organisation

league::<league_id>/
  identity/
    league_id    sport    name    description
  contexts[]
    context_id    description
  templates[]
    template_id    text_template
    allowed_contexts[]
    allowed_slots[]
    policy_tags[]
    decal_targets[]
  enrollment/
    <team_id>/
      team_id    active    season_id (optional)
  emission_log/
    total    accepted    rejected
    recent_rejections[]
      team_id    template_id    context_id    reason
  spec/
    ref    genome
  engine/
    name    version
  view
  frame

Key Properties

Contexts (contexts)

Named emission arenas recognised by this league. A team's projection request must name a context that exists here. League templates may further restrict themselves to a subset of these contexts via allowed_contexts.

Template Catalog (templates)

League-owned, inspectable, parameterized projection templates. The text_template contains {slot_name} placeholders. Teams fill these at request time. The league validates:

  • Template exists (TemplateNotFound)
  • Context is permitted for this template (ContextNotPermitted)
  • No disallowed slot names used (SlotNotPermitted)
  • All required slots (present in {...} in the template) are filled (MissingRequiredSlot)

Enrollment (enrollment)

Teams admitted into this league. Keyed by team_id. A team must be enrolled before it can project through any league template (NotEnrolled). Enrollment carries an optional season_id and an active flag.

Emission Log (emission_log)

The primary audit surface. Accepted outputs stay attributable to team + request. Rejected outputs are logged to the remote actor, not the league brand.

Rejection reasons reference:

Reason Description
LeagueMismatch Request names the wrong league
NotEnrolled Team is not enrolled in this league
TemplateNotFound Template does not exist in the catalog
ContextNotPermitted Template does not allow the requested context
SlotNotPermitted Slot name not in the template's allowed list
MissingRequiredSlot A {slot} in the template was not filled

League vs Team

Property Team League
Requires mandate? No No
Agent attachment? Yes (opt-in) No — enrolls Teams, not agents
Validation gate? validate_projection (4 checks) validate_league_projection (6 checks)
Audit log? No Yes — EmissionLog
Template system? No — plain slogan text Yes — {slot} parameterization
Can be bypassed? No — validate is the gate No — all requests go through the log

A Team enrolled in a League can project via either path: directly (Team slogans into any permitted context) or through the League (parameterized templates with league-enforced policy).


MVP Presence

A minimal valid League KNAT snapshot:

  • id, frame, engine, view
  • identity.league_id, identity.sport, identity.name
  • contexts (may be empty array)
  • templates (may be empty array)
  • enrollment (may be empty object)
  • emission_log.total, emission_log.accepted, emission_log.rejected
  • spec.ref, spec.genome

Example: Curling League

{
  "id": "curling_league",
  "spec": {
    "ref": "file://brands/sports/curling/league.json",
    "genome": "1122334455667788"
  },
  "engine": { "name": "janet-executor", "version": "0.4.0" },
  "view": "full",
  "frame": 900,
  "identity": {
    "league_id": "curling_league",
    "sport": "curling",
    "name": "Curling League",
    "description": "The coordinator authority for curling team speech."
  },
  "contexts": [
    { "context_id": "curling", "description": "In-session curling events." },
    { "context_id": "post_match", "description": "Post-match commentary." }
  ],
  "templates": [
    {
      "template_id": "sweep_call",
      "text_template": "We {verb} together, always.",
      "allowed_contexts": ["curling"],
      "allowed_slots": ["verb"],
      "policy_tags": ["in-play"],
      "decal_targets": ["scoreboard"]
    }
  ],
  "enrollment": {
    "slug-bears": {
      "team_id": "slug-bears",
      "active": true,
      "season_id": "s2026"
    },
    "bear-slugs": {
      "team_id": "bear-slugs",
      "active": true,
      "season_id": "s2026"
    }
  },
  "emission_log": {
    "total": 83,
    "accepted": 80,
    "rejected": 3,
    "recent_rejections": [
      {
        "team_id": "bear-slugs",
        "template_id": "sweep_call",
        "context_id": "post_match",
        "reason": "ContextNotPermitted"
      }
    ]
  }
}