domain

package
v0.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MPL-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package domain, glossary.go owns the canonical kendo-term dictionary used by the Preact <Term> tooltip component (web-mobile/js/glossary.jsx) AND by API error responses (handlers_*.go ResolveReasonHuman).

The source of truth is specs/003-tournament-gap-closure/glossary.md. When entries are added or tooltip wording changes, update both this file AND the spec. A unit test (glossary_test.go) parses glossary.md and asserts the Go map covers every term in it, so a forgotten entry fails the build.

The JS side reads the same data via web-mobile/js/glossary.js, which is generated from this file by `go generate ./internal/domain/...` (see the go:generate directive below). Running `make go/build` regenerates it; CI catches drift.

Package domain defines the core domain models for the bracket creator

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCompetitorStatusMissingPlayerID = errors.New("competitor_status: playerId must be set")
	ErrCompetitorStatusMissingReason   = errors.New("competitor_status: reason required when not eligible")
)

Sentinel errors so callers can switch on the validation failure without parsing strings.

View Source
var (
	ErrLineupMissingSenpo    = errors.New("team_lineup: senpo must be present")
	ErrLineupMissingTaisho   = errors.New("team_lineup: taisho must be present")
	ErrLineupTooManyMissing  = errors.New("team_lineup: 3+ missing positions, team is disqualified")
	ErrLineupTeamSizeInvalid = errors.New("team_lineup: teamSize must be positive")
)
View Source
var Glossary = map[string]Term{

	"hikiwake": {
		ID:      "hikiwake",
		Kanji:   "引き分け",
		Short:   "Draw",
		Tooltip: "The match ended tied, both competitors finished with equal points and no winner is declared. Common in pool matches; knockout matches go to overtime (encho) instead.",
		SeeAlso: []string{"encho"},
	},
	"encho": {
		ID:      "encho",
		Kanji:   "延長",
		Short:   "Overtime",
		Tooltip: "Overtime, when a knockout match is tied at the end of regulation, an extra period is played until someone scores. Scoring in Encho follows ippon-shobu rules: first to one point wins.",
		SeeAlso: []string{"ippon-shobu", "ippon"},
	},
	"kiken": {
		ID:      "kiken",
		Kanji:   "棄権",
		Short:   "Withdrawal",
		Tooltip: "The competitor withdraws. Two sub-types: Voluntary (FIK Art. 31), permanent disqualification from the competition; Injury (FIK Art. 30), can be reinstated if a doctor and shinpan permit it. The opponent wins 2–0 by default.",
	},
	"kiken-voluntary": {
		ID:      "kiken-voluntary",
		Kanji:   "棄権",
		Short:   "Voluntary withdrawal",
		Tooltip: "FIK Article 31, the competitor voluntarily withdraws. They forfeit this match (opponent wins 2–0) and are permanently barred from all later matches in this competition.",
	},
	"kiken-injury": {
		ID:      "kiken-injury",
		Kanji:   "棄権",
		Short:   "Injury withdrawal",
		Tooltip: "FIK Article 30, the competitor withdraws due to injury or illness. They forfeit this match (opponent wins 2–0) and are blocked from later matches, but can be reinstated if a doctor and shinpan-in approve.",
	},
	"fusenpai": {
		ID:      "fusenpai",
		Kanji:   "不戦敗",
		Short:   "No-show forfeit",
		Tooltip: "The competitor didn't appear when called to court. The opponent wins 2–0 by default. (Literal meaning: no-fight loss.)",
	},
	"fusensho": {
		ID:      "fusensho",
		Kanji:   "不戦勝",
		Short:   "Bye-win",
		Tooltip: "In a team match, when one team is short a player for a bout, the present opponent wins that bout 2–0 without fighting. (Literal meaning: no-fight win.)",
	},
	"daihyosen": {
		ID:      "daihyosen",
		Kanji:   "代表選",
		Short:   "Representative bout",
		Tooltip: "Tiebreaker for team knockout matches. When two teams finish equal on both individual wins and points, each picks one player to fight a single-point bout (ippon-shobu) to decide the match.",
		SeeAlso: []string{"ippon-shobu", "ippon"},
	},
	"hansoku": {
		ID:      "hansoku",
		Kanji:   "反則",
		Short:   "Penalty",
		Tooltip: "A foul. Two hansoku awarded to a competitor give the opponent one free point.",
	},
	"shiro": {
		ID:      "shiro",
		Kanji:   "白",
		Short:   "White",
		Tooltip: "The competitor wearing a white ribbon at the back. Always shown on the left in this app.",
	},
	"aka": {
		ID:      "aka",
		Kanji:   "赤",
		Short:   "Red",
		Tooltip: "The competitor wearing a red ribbon at the back. Always shown on the right in this app.",
	},
	"shiaijo": {
		ID:      "shiaijo",
		Kanji:   "試合場",
		Short:   "Court",
		Tooltip: "The marked floor area where matches are fought. Labelled A, B, C, etc.",
	},
	"ippon": {
		ID:      "ippon",
		Kanji:   "一本",
		Short:   "Point",
		Tooltip: "A valid point, a clean strike with correct form, posture, and spirit. First competitor to two ippon wins the match (or one in overtime).",
	},

	"ippon-shobu": {
		ID:      "ippon-shobu",
		Kanji:   "一本勝負",
		Short:   "Single-point match",
		Tooltip: "First competitor to score one ippon wins. Used in overtime (encho) and tiebreaker (daihyosen) bouts.",
		SeeAlso: []string{"ippon", "encho", "daihyosen"},
	},
	"kachinuki": {
		ID:      "kachinuki",
		Kanji:   "勝ち抜き",
		Short:   "Winner-stays-on",
		Tooltip: "A team format where the winner of each bout stays on to face the next opponent. A draw retires both. The match ends when one team has no more players left.",
	},
	"senpo": {
		ID:      "senpo",
		Kanji:   "先鋒",
		Short:   "First player",
		Tooltip: "The team's first player, fights bout 1 in a 5-person team. Senpo must be filled; this position cannot be left vacant, even when a teammate is withdrawn.",
	},
	"jiho": {
		ID:      "jiho",
		Kanji:   "次鋒",
		Short:   "Second player",
		Tooltip: "The team's second player, fights bout 2. If a team is short one player, FIK rules require that the Jiho position is the one left vacant.",
	},
	"chuken": {
		ID:      "chuken",
		Kanji:   "中堅",
		Short:   "Middle player",
		Tooltip: "The team's middle player, fights bout 3 in a 5-person team.",
	},
	"fukusho": {
		ID:      "fukusho",
		Kanji:   "副将",
		Short:   "Vice-captain",
		Tooltip: "The team's vice-captain, fights bout 4. If a team is short two players, FIK rules require jiho and Fukusho to be the two vacant positions.",
		SeeAlso: []string{"jiho"},
	},
	"taisho": {
		ID:      "taisho",
		Kanji:   "大将",
		Short:   "Captain",
		Tooltip: "The team's captain, fights the final bout (bout 5). Like senpo, Taisho must be filled; this position cannot be left vacant.",
		SeeAlso: []string{"senpo"},
	},

	"zekken": {
		ID:      "zekken",
		Kanji:   "ゼッケン",
		Short:   "Name tag (nafuda)",
		Tooltip: "The name tag affixed to the centre of the tare (waist protector). Also called nafuda. The display can show this name (instead of the registered name) so spectators see what's physically visible.",
	},
	"dan": {
		ID:      "dan",
		Kanji:   "段",
		Short:   "Rank",
		Tooltip: "A competitor's grade, shodan (1st), nidan (2nd), sandan (3rd), up to 8th. Optional in the entry form.",
	},
	"dojo": {
		ID:      "dojo",
		Kanji:   "道場",
		Short:   "Club or school",
		Tooltip: "The training school or club a competitor represents. The pool generator avoids placing two competitors from the same dojo in the same pool when it can.",
	},
	"waza": {
		ID:      "waza",
		Kanji:   "技",
		Short:   "Technique",
		Tooltip: "The specific type of strike scored, for example, men, kote, or do. Recorded with each ippon for the match record.",
		SeeAlso: []string{"ippon"},
	},
	"kachinuki-exhaustion": {
		ID:      "kachinuki-exhaustion",
		Kanji:   "",
		Short:   "Team exhausted",
		Tooltip: "A kachinuki match ended because one team has no remaining players to send. The opposing team wins.",
		SeeAlso: []string{"kachinuki"},
	},

	"sune": {
		ID:      "sune",
		Kanji:   "すね",
		Short:   "Shin strike",
		Tooltip: "A strike to the shin. Scores a point in Naginata competitions. Not a valid scoring technique in Kendo.",
	},
}

Glossary is the full term map keyed by lowercase romaji ID. The entries here mirror specs/003-tournament-gap-closure/glossary.md verbatim, tooltips are copied as-is so the spec and code never drift. Don't paraphrase.

Functions

func AssignSeeds

func AssignSeeds(players []Player, assignments []SeedAssignment) error

AssignSeeds applies valid seed assignments to a list of players It swaps seeds if a collision occurs. Returns error if a seeded participant is not found.

func IsKikenDecision added in v0.16.0

func IsKikenDecision(d Decision) bool

IsKikenDecision reports whether d is any kiken variant (legacy, voluntary, or injury). Use this instead of comparing against DecisionKiken alone, the legacy value is kept for backward compatibility but new code should use the specific sub-types.

func IsKikenDecisionStr added in v0.16.0

func IsKikenDecisionStr(s string) bool

IsKikenDecisionStr is the string-argument twin of IsKikenDecision, for call sites that hold the wire value as a string (e.g. MatchResult.Decision).

func ResolveReasonHuman added in v0.16.0

func ResolveReasonHuman(reason string) string

ResolveReasonHuman returns the human-friendly gloss for an engine-emitted reason string, or "" when no pattern matches.

Callers wire this into error responses alongside the raw `reason` field so UIs can show "withdrew from match m_12" instead of "kiken at m_12". Returning "" rather than echoing the input lets the caller decide whether to omit the field or fall back to the raw reason, the JSON omitempty tag on ReasonHuman handles the typical case.

func ValidateAssignments

func ValidateAssignments(assignments []SeedAssignment) error

ValidateAssignments checks a list for duplicate seed ranks, valid properties, and gapless sequences.

Types

type CompetitorStatus added in v0.16.0

type CompetitorStatus struct {
	PlayerID      string    `json:"playerId" yaml:"playerId"`
	Eligible      bool      `json:"eligible" yaml:"eligible"`
	Reinstateable bool      `json:"reinstateable,omitempty" yaml:"reinstateable,omitempty"`
	Reason        string    `json:"reason,omitempty" yaml:"reason,omitempty"`
	MatchID       string    `json:"matchId,omitempty" yaml:"matchId,omitempty"`
	RecordedAt    time.Time `json:"recordedAt" yaml:"recordedAt"`
}

CompetitorStatus tracks whether a player is eligible to compete after a kiken/fusenpai withdrawal recorded earlier in the tournament.

FR-034, data-model §3.

func (CompetitorStatus) Validate added in v0.16.0

func (c CompetitorStatus) Validate() error

Validate enforces FR-034: PlayerID must be set, and an ineligible status must carry a non-empty Reason.

type Decision added in v0.16.0

type Decision string

Decision identifies how a match was concluded.

FR-030, NFR-011, FR-044, see data-model §1.

const (
	DecisionNone                Decision = ""
	DecisionFought              Decision = "fought"
	DecisionHikiwake            Decision = "hikiwake"
	DecisionKiken               Decision = "kiken"
	DecisionKikenVoluntary      Decision = "kiken-voluntary"
	DecisionKikenInjury         Decision = "kiken-injury"
	DecisionFusenpai            Decision = "fusenpai"
	DecisionFusensho            Decision = "fusensho"
	DecisionDaihyosen           Decision = "daihyosen"
	DecisionKachinukiExhaustion Decision = "kachinuki-exhaustion"
	DecisionIpponShobu          Decision = "ippon-shobu"
)

func (*Decision) UnmarshalYAML added in v0.16.0

func (d *Decision) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML migrates legacy `decision` values (NFR-025, R6):

  • bool true → DecisionHikiwake (the historical "draw" flag)
  • bool false → DecisionFought (legacy YAML only persisted the field on completed matches; "not a draw" meant the match was fought to a result)
  • any defined string wire value → that Decision
  • any other string → DecisionNone (schema-tolerant load)

func (Decision) Valid added in v0.16.0

func (d Decision) Valid() bool

Valid reports whether d is one of the defined Decision constants (including the empty DecisionNone sentinel). Unknown wire values return false.

type EnchoMetadata added in v0.16.0

type EnchoMetadata struct {
	PeriodCount int `json:"periodCount" yaml:"periodCount"`
}

EnchoMetadata records overtime (encho) periods played to resolve a tied match.

type Match

type Match struct {
	ID     string
	SideA  *Player
	SideB  *Player
	Winner *Player
}

Match represents a match between two players

type MatchResult added in v0.16.0

type MatchResult struct {
	Decision       Decision       `json:"decision,omitempty" yaml:"decision,omitempty"`
	DecisionBy     Side           `json:"decisionBy,omitempty" yaml:"decisionBy,omitempty"`
	DecisionReason string         `json:"decisionReason,omitempty" yaml:"decisionReason,omitempty"`
	Encho          *EnchoMetadata `json:"encho,omitempty" yaml:"encho,omitempty"`
}

MatchResult captures the outcome metadata of a match.

FR-030, FR-032, data-model §2.

type MatchWinner

type MatchWinner struct {
	PlayerID string
	MatchID  string
}

MatchWinner represents a player who has won a match

type Player

type Player struct {
	ID          string   `json:"id,omitempty"` // stable UUID assigned at first persist
	Name        string   `json:"name"`
	DisplayName string   `json:"displayName"`
	Dojo        string   `json:"dojo"`
	Metadata    []string `json:"metadata,omitempty"`
	Source      string   `json:"source,omitempty"` // registration source, how the entry was created: registered (self-registered), manual (operator-added), transfer (imported)
	CheckedIn   bool     `json:"checkedIn"`

	PoolPosition int64 `json:"-"` // internal: used for Excel output and pool-draw ordering; not serialised to JSON because the value is inconsistently indexed across producer paths (0-based in handlers, 1-based in helper). Draw order on the wire is conveyed by pool.players array ordering.
	Seed         int   `json:"seed"`
	// Number is the player's "tag", their assigned competitor number / on-court
	// identifier (zekken-style), optionally prefixed via the competition's
	// numberPrefix (e.g. "A1"). This is distinct from the registration Source field.
	Number string `json:"number,omitempty"` // e.g. "K1", assigned when --number-prefix is set
}

Player represents a tournament participant. It is the canonical participant type across state, engine, mobileapp, and helper packages; internal/helper re-exports it under helper.Player as a type alias for rendering-side ergonomics (NFR-007).

func (*Player) Matches

func (p *Player) Matches(name string) bool

Matches checks if the player's name exactly matches the given name (case-sensitive)

type Pool

type Pool struct {
	ID      string
	Name    string
	Players []Player
	Matches []Match
}

Pool represents a group of players in the tournament

type Position added in v0.16.0

type Position string

Position names a slot in a team lineup. For 5-person teams the named FIK constants apply; for other sizes positions are numeric strings "1".."N" produced by PositionNumbered.

FR-040, data-model §4.

const (
	PosSenpo   Position = "senpo"
	PosJiho    Position = "jiho"
	PosChuken  Position = "chuken"
	PosFukusho Position = "fukusho"
	PosTaisho  Position = "taisho"
)

func PositionNumbered added in v0.16.0

func PositionNumbered(n int) Position

PositionNumbered returns the canonical Position value for a non-5 team size, where positions are 1-indexed numeric strings.

type SeedAssignment

type SeedAssignment struct {
	Name     string `json:"name"`
	Dojo     string `json:"dojo,omitempty"`
	SeedRank int    `json:"seedRank"`
}

SeedAssignment represents the mapping of a previous winner to a seed position.

func (*SeedAssignment) Validate

func (s *SeedAssignment) Validate() error

Validate checks if the seed assignment is valid.

type Side added in v0.16.0

type Side string

Side identifies which side of a match (Shiro = white, Aka = red).

FR-032, data-model §2.

const (
	SideShiro Side = "shiro"
	SideAka   Side = "aka"
)

type TeamLineup added in v0.16.0

type TeamLineup struct {
	TeamID        string              `json:"teamId" yaml:"teamId"`
	CompetitionID string              `json:"competitionId" yaml:"competitionId"`
	Round         int                 `json:"round" yaml:"round"`
	MatchID       string              `json:"matchId,omitempty" yaml:"matchId,omitempty"`
	Positions     map[Position]string `json:"positions" yaml:"positions"`
	LockedAt      *time.Time          `json:"lockedAt,omitempty" yaml:"lockedAt,omitempty"`
	// ChangeReason is a mandatory audit justification when an operator
	// edits a lineup after the match has started (force=true). Format:
	// "<category>: <note>" (e.g. "Substitution: injury to jiho").
	// Omitted for pre-match lineup submissions.
	ChangeReason string `json:"changeReason,omitempty" yaml:"changeReason,omitempty"`
}

TeamLineup pins which player occupies each Position for a team in a given round OR for a specific match. The lineup is replaceable up until its match (match-scoped) or its round's first match (round-scoped) starts, at which point LockedAt is set and further PUT/PATCH operations are rejected.

Keying (mp-825): when MatchID is non-empty the lineup is match-scoped, a team may field a different order/roster for each encounter (e.g. successive pool matches), and each entry locks independently when its own match starts. When MatchID is empty the lineup is round-scoped (the legacy behavior, still used by bracket rounds and pre-mp-825 data): one lineup per (team, round), frozen when the round's first match starts. The two scopes coexist; a match-scoped entry shadows the round-scoped fallback for that match.

FR-040, data-model §4.

func (TeamLineup) Validate added in v0.16.0

func (t TeamLineup) Validate(teamSize int) error

Validate enforces FR-037 / FR-041 / R4 / CHK012: a 5-person lineup must include Senpo and Taisho, and any kiken vacancies must follow the FIK back-fill rule (the missing position is Jiho first, then Jiho+Fukusho). For non-5 sizes positions must be numeric "1".."N".

Returning a non-nil error signals the team should not be allowed to take the court (either reject the lineup PUT or DQ the team via CompetitorStatus).

func (TeamLineup) ValidatePositions added in v0.17.0

func (t TeamLineup) ValidatePositions(teamSize int) error

ValidatePositions checks only that the position KEYS are valid for the team size; it does NOT enforce the FIK completeness/vacancy rule. Lineups are entered incrementally while bouts run, so a partial lineup must be persistable, completeness is surfaced as a non-blocking UI warning, not enforced at write time.

type Term added in v0.16.0

type Term struct {
	ID      string   `json:"id"`
	Kanji   string   `json:"kanji"`
	Short   string   `json:"short"`
	Tooltip string   `json:"tooltip"`
	SeeAlso []string `json:"seeAlso,omitempty"`
}

Term is a single dictionary entry. ID is the lowercase romaji ("kiken", "ippon-shobu", "kachinuki-exhaustion"); it doubles as the map key and as the `name` prop of the JSX <Term> component. Kanji is the Japanese spelling (or empty for the few entries without one, e.g. "kachinuki-exhaustion"). Short is the one-word English gloss the /glossary page renders next to the term. Tooltip is the full volunteer-facing sentence (rendered inside the popover and the /glossary page). SeeAlso lists the IDs of cross-referenced terms; the JSX component uses it to render nested <Term> spans inside the tooltip so hovered cross-references reveal their own tooltips.

func Lookup added in v0.16.0

func Lookup(id string) (Term, bool)

Lookup returns the Term for an ID (case-insensitive) and a found boolean. Centralised so callers don't need to remember the lowercase convention or import the raw map.

type Tournament

type Tournament struct {
	Name               string
	Pools              []Pool
	EliminationMatches []Match
}

Tournament represents the complete tournament structure

Directories

Path Synopsis
internal
glossarygen command
Package main, glossarygen emits web-mobile/{js,dist}/glossary_data.js from internal/domain/glossary.go (the Go-side dictionary).
Package main, glossarygen emits web-mobile/{js,dist}/glossary_data.js from internal/domain/glossary.go (the Go-side dictionary).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL