package mapping import ( "strings" sdk "code.gitea.io/sdk/gitea" "git.noodles.cam/claude-skills/marketplace/cli/internal/issue" ) // How the taxonomy is painted in Gitea's UI. A hex code says nothing about what // an issue IS, which is exactly why the table lives here and not in the domain // — internal/issue/taxonomy.go says as much where the labels themselves are. // // The keys are the canonical set and nothing else. TestEveryCanonicalLabelHasA // Color walks issue.CanonicalLabels() and fails on a gap, so a type or a // severity added over there cannot quietly arrive here as grey. var labelColors = map[string]string{ "type/bug": "#ee0701", "type/task": "#0e8a16", "type/refactor": "#1d76db", "type/test": "#fbca04", "type/feature": "#5319e7", "type/draft": "#cccccc", "severity/low": "#c2e0c6", "severity/medium": "#fbca04", "severity/high": "#eb6420", "severity/showstopper": "#ee0701", "severity/critical": "#b60205", } // DefaultColor paints everything outside the canonical set. `tech/*` and // `comp/*` are project-specific and have no preset, so guessing a color for one // would be inventing a meaning it does not have. const DefaultColor = "#ededed" // LabelColor is the hex code a label is painted with in the tracker. func LabelColor(name string) string { if c, ok := labelColors[name]; ok { return c } return DefaultColor } // LabelSpecs is the request body for each name, in the order given. // // The SDK's own CreateLabelOption and not a shape of this package's own: it is // field for field what a label create takes, and a second spelling of it would // mean the bootstrap command copying four fields across on its way to the // transport. It is what an EDIT is built from too — see Client.EditLabel — so // one value says what a label should be, whether or not it exists yet. // Exclusivity and meaning come from the domain taxonomy; only the color is // decided here. // // A slice and not a map: the order is the taxonomy's, and a bootstrap prints // its plan in that order — a map would shuffle the plan on every run and make // two identical runs look like different ones. func LabelSpecs(names []string) []sdk.CreateLabelOption { ns := exclusiveNamespaces() out := make([]sdk.CreateLabelOption, 0, len(names)) for _, name := range names { out = append(out, sdk.CreateLabelOption{ Name: name, Color: LabelColor(name), Description: typeMeaning(name), Exclusive: hasAnyPrefix(name, ns), }) } return out } // CanonicalLabelSpecs is the set a repository needs before a push can attach // anything. // // Derived from the domain's own list rather than restated: add a type over in // the taxonomy and the next bootstrap creates it, with no line changing here // except the color it is painted with. func CanonicalLabelSpecs() []sdk.CreateLabelOption { return LabelSpecs(issue.CanonicalLabels()) } // exclusiveNamespaces are the namespaces at most one label may come from, read // off the canonical set rather than listed again — the domain publishes exactly // the exclusive namespaces there, in full, and that is what makes the set // canonical. // // A prefix test and not a membership test, on purpose: a project's own // `type/spike` is still exclusive. Being one of a set of alternatives is a // property of the namespace, not of the members the taxonomy happens to know. func exclusiveNamespaces() []string { var out []string seen := map[string]bool{} for _, name := range issue.CanonicalLabels() { ns, _, ok := strings.Cut(name, "/") if !ok || seen[ns] { continue } seen[ns] = true out = append(out, ns+"/") } return out } // typeMeaning is the description a `type/*` label carries into the tracker, so // the meaning a reader needs is on the chip rather than in this repository. // Nothing else gets one: a severity explains itself, and a project's own // namespaces are not ours to describe. func typeMeaning(name string) string { tail, ok := strings.CutPrefix(name, "type/") if !ok { return "" } for _, t := range issue.Types { if t.Name == tail { return t.Meaning } } return "" } func hasAnyPrefix(s string, prefixes []string) bool { for _, p := range prefixes { if strings.HasPrefix(s, p) { return true } } return false }