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,viewidentity.league_id,identity.sport,identity.namecontexts(may be empty array)templates(may be empty array)enrollment(may be empty object)emission_log.total,emission_log.accepted,emission_log.rejectedspec.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"
}
]
}
}