Commit 6815967f by PLN (Algolia)

fleet color language + triangle UX redesign (impeccable pass)

ontology in models (SSOT) → generated tokens:
- models.py: OntologyTerm + ColorFamily; ROLE/LIFECYCLE/AGREE terms (color+glyph+
  label) and an authored 12-family SAMPLE-WORLD model (hue-anchored). Variation =
  OKLCH lightness ladder + ±8° hue-fan → 8 shades/family = 96 sample slots. Sample
  axis (conceptual) kept separate from the measured-register axis (ROLE_FAMILIES).
- tools/gen_tokens.py: OKLCH→sRGB (no deps) → tokens.css (armada/ui) + tokens.json
  (triangle fetches at runtime, overrides :root). Validated vs DesignTokens. New
  tide.py 'tokens' step. Never hue alone (glyph+label per term).

triangle UX (low cognitive load, clear hierarchy):
- header calm at rest: one title line, one quiet coverage line, and the agreement
  bar is now the interactive sea-state instrument (overview + legend + level filter
  in one) — replaces the duplicated chips+stack+pills.
- faceted full-text search: free terms + scopes (sample:/gig:/phrase:/take:/style:/
  name:), covers the pattern registry, with 'matched-in' hints so results never feel
  mysterious; '/' focuses, clear button, teaching empty state.
- sound chips colored by sample family (shared lexical classifier from tokens),
  glyph-paired; agreement painted from the ontology (unparsed no longer red).
- tests: 51 green (OKLCH math, shade ladder greyscale-distinctness, ontology
  integrity, unparsed≠conflict, DRY tokens contract, classifier examples).
parent aa0de028
...@@ -417,3 +417,149 @@ class PatternRegistry(BaseModel): ...@@ -417,3 +417,149 @@ class PatternRegistry(BaseModel):
n_repeated: int n_repeated: int
patterns: list[PatternEntry] = Field(default_factory=list) patterns: list[PatternEntry] = Field(default_factory=list)
tracks: list[TrackPatternSig] = Field(default_factory=list) tracks: list[TrackPatternSig] = Field(default_factory=list)
# ── THE ONTOLOGY: the fleet's shared visual vocabulary (concept → color + glyph) ─
# Principle 3 (DESIGN.md): "One language across the fleet." Every L'Armada surface
# (triangle.html, armada/ui, future tools) must paint the SAME concept the SAME way.
# So the vocabulary lives HERE, beside the data models — one machine-readable source
# of truth. tools/gen_tokens.py derives tokens.json + tokens.css from it; the design
# RATIONALE stays human-readable in armada/DESIGN.md (the two must agree).
#
# Never-Hue-Alone Rule: every term carries a glyph + label, so meaning survives in
# greyscale and is colour-blind safe. Colours are the DESIGN.md fleet palette.
class Lifecycle(str, Enum):
idea = "idea"
wip = "wip"
ready = "ready"
released = "released"
blocked = "blocked"
archived = "archived"
class OntologyTerm(BaseModel):
"""One named concept in the fleet vocabulary, with its canonical paint."""
key: str
label: str
color: str # hex — DESIGN.md fleet palette
glyph: str # greyscale-safe reinforcement (never hue alone)
description: str = ""
# Role families — an orbit's measured register group (DESIGN.md §2 Secondary).
# The MAP is per-track and derived from analysis (Measured-Register Rule); these are
# only the family identities + paint, shared by every tool that shows an orbit.
ROLE_FAMILIES = [
OntologyTerm(key="percs", label="Percs", color="#ff8c00", glyph="▰", description="kicks, snares, hats, percussion — transient, warm"),
OntologyTerm(key="bass", label="Bass", color="#7c5cff", glyph="▂", description="sub, acid, reese/wobble, bass-spine — low, deep"),
OntologyTerm(key="melodic", label="Melodic", color="#36c5f0", glyph="♪", description="leads, keys, riffs, high plucks — bright"),
OntologyTerm(key="tops", label="Tops", color="#2dd4bf", glyph="≈", description="breaks, chops, texture loops — busy, mid-high"),
OntologyTerm(key="atmos", label="Atmos", color="#8a93a6", glyph="◌", description="pads, drones, ambient, fx — diffuse, background"),
OntologyTerm(key="vox", label="Vox", color="#ff3d7b", glyph="◍", description="vocal samples and chops — human, forward"),
]
# Lifecycle status — catalog/triage/release state (DESIGN.md §2 Tertiary).
LIFECYCLE_TERMS = [
OntologyTerm(key="idea", label="Idea", color="#737373", glyph="·", description="raw / sketch"),
OntologyTerm(key="wip", label="WIP", color="#e0a82e", glyph="◐", description="in progress"),
OntologyTerm(key="ready", label="Ready", color="#5bc091", glyph="●", description="mastered, ship-ready"),
OntologyTerm(key="released", label="Released", color="#d900ff", glyph="★", description="out in the world"),
OntologyTerm(key="blocked", label="Blocked", color="#ff5252", glyph="✕", description="needs a fix before it moves"),
OntologyTerm(key="archived", label="Archived", color="#4a4a4a", glyph="▢", description="parked / superseded"),
]
# A↔C agreement (the triangle) — REUSES the lifecycle paint so a green/amber/red means
# the same thing fleet-wide. unparsed is FAINT GREY, never red: a parser miss is not a
# conflict (the cardinal rule). divergent gets a bespoke violet (wrong file link).
AGREE_TERMS = [
OntologyTerm(key="agree", label="Agree", color="#5bc091", glyph="●", description="≥60% of claimed sounds are in the score"),
OntologyTerm(key="partial", label="Partial", color="#e0a82e", glyph="◐", description="30–60% — some claims unmatched"),
OntologyTerm(key="conflict", label="Conflict", color="#ff5252", glyph="✕", description="0–30% — real partial disagreement"),
OntologyTerm(key="divergent", label="Divergent", color="#b06cff", glyph="⤬", description="zero overlap, both rich → wrong file link"),
OntologyTerm(key="no-claim", label="No-claim", color="#737373", glyph="·", description="metadata lists no parseable sound"),
OntologyTerm(key="unparsed", label="Unparsed", color="#6a6a70", glyph="?", description="WE couldn't read the score — not a conflict"),
OntologyTerm(key="empty", label="Empty", color="#4a4a4a", glyph="∅", description="neither side has parseable sounds"),
]
# Neutral surfaces + brand (DESIGN.md §2 Neutral / Primary) — the rest of the palette.
NEUTRALS = {
"surface": "#0a0a0a", "raised": "#111111", "overlay": "#171717",
"hairline": "#ffffff1f", "ink": "#e8e8ea", "mute": "#9a9aa0", "faint": "#6a6a70",
}
BRAND = {"magenta": "#d900ff", "magenta-low": "#a700d1", "magenta-deep": "#8900b3"}
# ── the scalable color LANGUAGE: families (hue) × variations (shades) ──────────
# To paint 50-100 concepts coherently we need families with systematic variations,
# not 100 hand-picked hexes. A family anchors an OKLCH hue; gen_tokens derives N
# distinguishable shades by walking lightness DOWN while fanning the hue ±SHADE_FAN
# degrees (PLN's choice) — so members read as one family yet stay separable, and the
# lightness ladder keeps them greyscale-safe. Each concept = (family, shade index)
# + its glyph + label (never hue alone). This is the SAMPLE-WORLD axis: a coherent
# AUTHORED model of sample kinds — deliberately SEPARATE from the analysis/measured
# register axis (ROLE_FAMILIES), which carries its own labels and colors.
SHADE_COUNT = 8 # shades generated per family (≥ members we expect in one)
SHADE_L = (0.80, 0.52) # OKLCH lightness ladder: lightest → darkest
SHADE_CHROMA = 0.15 # base chroma (slightly eased at the ends by gen_tokens)
SHADE_FAN = 8.0 # ± degrees of hue fan across the ladder
class ColorFamily(BaseModel):
"""A hue-anchored family; gen_tokens expands it into SHADE_COUNT shades."""
key: str
label: str
glyph: str
hue: float # OKLCH hue anchor (degrees)
chroma: float = SHADE_CHROMA
match: list[str] = Field(default_factory=list) # lexical cues → classify a sample name
description: str = ""
# Sample-world families (authored coherent model). `match` keywords let any surface
# map a sample NAME to its family lexically — legitimate here: this is the sample's
# conceptual identity, NOT a measured register claim (that's ROLE_FAMILIES, by audio).
SAMPLE_FAMILIES = [
ColorFamily(key="kick", label="Kick", glyph="●", hue=25, match=["kick", "kik", "bd", "808bd", "909", "bassdrum"]),
ColorFamily(key="snare", label="Snare", glyph="◆", hue=50, match=["snare", "sn", "sd", "clap", "cp", "rim", "rs"]),
ColorFamily(key="perc", label="Perc", glyph="▴", hue=80, match=["perc", "conga", "bongo", "tom", "clave", "shaker", "tabla", "cowbell"]),
ColorFamily(key="hat", label="Hat", glyph="✦", hue=110, match=["hat", "hh", "oh", "ch", "hihat", "cymbal", "cym", "ride", "crash"]),
ColorFamily(key="break", label="Break", glyph="≈", hue=150, match=["break", "amen", "loop", "jungle", "dnb", "jazz"]),
ColorFamily(key="pad", label="Pad", glyph="◌", hue=180, match=["pad", "drone", "choir", "string", "ambient", "atmos"]),
ColorFamily(key="keys", label="Keys", glyph="♬", hue=205, match=["key", "keys", "piano", "rhodes", "epiano", "organ", "fpiano", "qstab", "nujazz"]),
ColorFamily(key="lead", label="Lead", glyph="♪", hue=230, match=["lead", "arp", "pluck", "stab", "blip", "saw", "square"]),
ColorFamily(key="synth", label="Synth", glyph="◈", hue=260, match=["synth", "commodore", "chip", "fm", "moog", "reese"]),
ColorFamily(key="bass", label="Bass", glyph="▂", hue=290, match=["bass", "sub", "808", "acid", "wobble", "meth_bass", "ramplem", "bassline"]),
ColorFamily(key="fx", label="FX", glyph="✺", hue=320, match=["fx", "riser", "sweep", "noise", "impact", "downlifter", "uplifter", "glitch", "vinyl", "foley"]),
ColorFamily(key="vox", label="Vox", glyph="◍", hue=355, match=["vox", "vocal", "voc", "acap", "speech", "voice"]),
]
# The whole vocabulary, for gen_tokens (and anyone who needs the fleet paint).
ONTOLOGY = {
"role": ROLE_FAMILIES,
"lifecycle": LIFECYCLE_TERMS,
"agree": AGREE_TERMS,
}
# ── design tokens artifact (generated by tools/gen_tokens.py) ──────────────────
# The machine-readable fleet palette every surface loads. tokens.css for CSS-var
# consumers (armada/ui), tokens.json for runtime consumers (triangle.html fetches
# it and overrides its :root). Validated against this model on emit (DRY contract).
class TokenFamily(BaseModel):
key: str
label: str
glyph: str
base: str # the family's representative hex
shades: list[str] = Field(default_factory=list) # light → dark
match: list[str] = Field(default_factory=list)
class DesignTokens(BaseModel):
model_config = ConfigDict(populate_by_name=True)
schema_: str = Field(alias="schema")
as_of: str
neutrals: dict[str, str]
brand: dict[str, str]
role: list[OntologyTerm] = Field(default_factory=list)
lifecycle: list[OntologyTerm] = Field(default_factory=list)
agree: list[OntologyTerm] = Field(default_factory=list)
sample: list[TokenFamily] = Field(default_factory=list)
"""Fleet color language — the generated tokens must be valid, distinguishable, and
greyscale-safe (parsers-over-copy: ontology in models → tokens, tested mechanically).
gen_tokens lives in tools/, so add it to the path like the ts-types generator."""
import re
import sys
from pathlib import Path
import models as M
ROOT = Path(__file__).resolve().parent.parent.parent.parent # …/Sound/Tidal
sys.path.insert(0, str(ROOT / "tools"))
import gen_tokens as G # noqa: E402
HEX = re.compile(r"^#[0-9a-f]{6}$")
def _rellum(hexs):
"""WCAG relative luminance, for greyscale-distinctness checks."""
def lin(c):
c /= 255
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = (int(hexs[i:i + 2], 16) for i in (1, 3, 5))
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
# ── UT: the OKLCH→hex math + shade ladder ────────────────────────────────────
def test_oklch_hex_is_valid_and_clamped():
for L in (0.0, 0.5, 1.0):
for H in (0, 90, 200, 359):
assert HEX.match(G.oklch_hex(L, 0.15, H))
def test_shades_are_valid_and_monotonically_darker():
for fam in M.SAMPLE_FAMILIES:
base, sh = G.shades(fam)
assert HEX.match(base)
assert len(sh) == M.SHADE_COUNT
assert all(HEX.match(s) for s in sh)
lums = [_rellum(s) for s in sh]
assert lums == sorted(lums, reverse=True), f"{fam.key} ladder not light→dark"
# light and dark ends are clearly separated (greyscale-safe)
assert lums[0] - lums[-1] > 0.15, f"{fam.key} ladder too flat in greyscale"
# ── ontology integrity ───────────────────────────────────────────────────────
def test_every_ontology_term_has_glyph_and_hex():
for axis in (M.ROLE_FAMILIES, M.LIFECYCLE_TERMS, M.AGREE_TERMS):
for t in axis:
assert t.glyph, t.key
assert HEX.match(t.color), (t.key, t.color)
def test_agree_terms_cover_the_enum():
keys = {t.key for t in M.AGREE_TERMS}
assert keys == {lv.value for lv in M.AgreeLevel}
def test_unparsed_is_not_painted_like_conflict():
"""Cardinal rule: a parser miss must never look like a data conflict (red)."""
by = {t.key: t.color for t in M.AGREE_TERMS}
assert by["unparsed"] != by["conflict"]
assert by["empty"] != by["conflict"]
def test_sample_families_have_distinct_hues_and_match_cues():
hues = [f.hue for f in M.SAMPLE_FAMILIES]
assert len(set(hues)) == len(hues) # no two families share a hue
for f in M.SAMPLE_FAMILIES:
assert f.match, f"{f.key} has no lexical cues"
assert f.glyph
# ── DRY contract: emitted tokens validate against the model ──────────────────
def test_tokens_validate_against_model():
from models import DesignTokens
tok = G.build()
dt = DesignTokens.model_validate(tok)
assert len(dt.sample) == len(M.SAMPLE_FAMILIES)
# every sample slot is a valid hex
for f in tok["sample"]:
assert HEX.match(f["base"]) and all(HEX.match(s) for s in f["shades"])
def test_sample_classifier_examples():
"""The lexical sample-family rules should land obvious cases (coherent model)."""
fam = {f.key: f for f in M.SAMPLE_FAMILIES}
def classify(name):
s = name.lower()
for f in M.SAMPLE_FAMILIES:
if any(s == m or s.startswith(m) for m in f.match):
return f.key
return None
assert classify("kick") == "kick"
assert classify("808bd") == "kick"
assert classify("hats") == "hat"
assert classify("meth_bass") == "bass"
assert classify("jazz") == "break"
assert classify("superpiano") is None or classify("piano") == "keys"
...@@ -49,6 +49,13 @@ def _patterns(): ...@@ -49,6 +49,13 @@ def _patterns():
return m.OUT return m.OUT
def _tokens():
sys.path.insert(0, str(ROOT / "tools"))
import gen_tokens as t
t.main() # validates against DesignTokens on emit
return t.OUT
def _ts_types(): def _ts_types():
sys.path.insert(0, str(ROOT / "tools")) sys.path.insert(0, str(ROOT / "tools"))
import gen_ts_types as g import gen_ts_types as g
...@@ -62,6 +69,7 @@ STEPS = [ ...@@ -62,6 +69,7 @@ STEPS = [
("catalog_view", "triangle · A=score ⋈ C=metadata ⋈ B=recording (validated)", _catalog_view), ("catalog_view", "triangle · A=score ⋈ C=metadata ⋈ B=recording (validated)", _catalog_view),
("catalog", "GENERATED catalog · catalog_view ⊕ authored overlay", _catalog), ("catalog", "GENERATED catalog · catalog_view ⊕ authored overlay", _catalog),
("patterns", "pattern registry · phrases-as-things + n-gram clustering", _patterns), ("patterns", "pattern registry · phrases-as-things + n-gram clustering", _patterns),
("tokens", "fleet color language · ontology → tokens.css + tokens.json", _tokens),
("ts_types", "pydantic models → generated TypeScript (DRY)", _ts_types), ("ts_types", "pydantic models → generated TypeScript (DRY)", _ts_types),
] ]
NAMES = [n for n, _, _ in STEPS] NAMES = [n for n, _, _ in STEPS]
......
{
"schema": "design-tokens v1 (fleet color language: ontology → families × shades)",
"as_of": "2026-06-06",
"neutrals": {
"surface": "#0a0a0a",
"raised": "#111111",
"overlay": "#171717",
"hairline": "#ffffff1f",
"ink": "#e8e8ea",
"mute": "#9a9aa0",
"faint": "#6a6a70"
},
"brand": {
"magenta": "#d900ff",
"magenta-low": "#a700d1",
"magenta-deep": "#8900b3"
},
"role": [
{
"key": "percs",
"label": "Percs",
"color": "#ff8c00",
"glyph": "▰",
"description": "kicks, snares, hats, percussion — transient, warm"
},
{
"key": "bass",
"label": "Bass",
"color": "#7c5cff",
"glyph": "▂",
"description": "sub, acid, reese/wobble, bass-spine — low, deep"
},
{
"key": "melodic",
"label": "Melodic",
"color": "#36c5f0",
"glyph": "♪",
"description": "leads, keys, riffs, high plucks — bright"
},
{
"key": "tops",
"label": "Tops",
"color": "#2dd4bf",
"glyph": "≈",
"description": "breaks, chops, texture loops — busy, mid-high"
},
{
"key": "atmos",
"label": "Atmos",
"color": "#8a93a6",
"glyph": "◌",
"description": "pads, drones, ambient, fx — diffuse, background"
},
{
"key": "vox",
"label": "Vox",
"color": "#ff3d7b",
"glyph": "◍",
"description": "vocal samples and chops — human, forward"
}
],
"lifecycle": [
{
"key": "idea",
"label": "Idea",
"color": "#737373",
"glyph": "·",
"description": "raw / sketch"
},
{
"key": "wip",
"label": "WIP",
"color": "#e0a82e",
"glyph": "◐",
"description": "in progress"
},
{
"key": "ready",
"label": "Ready",
"color": "#5bc091",
"glyph": "●",
"description": "mastered, ship-ready"
},
{
"key": "released",
"label": "Released",
"color": "#d900ff",
"glyph": "★",
"description": "out in the world"
},
{
"key": "blocked",
"label": "Blocked",
"color": "#ff5252",
"glyph": "✕",
"description": "needs a fix before it moves"
},
{
"key": "archived",
"label": "Archived",
"color": "#4a4a4a",
"glyph": "▢",
"description": "parked / superseded"
}
],
"agree": [
{
"key": "agree",
"label": "Agree",
"color": "#5bc091",
"glyph": "●",
"description": "≥60% of claimed sounds are in the score"
},
{
"key": "partial",
"label": "Partial",
"color": "#e0a82e",
"glyph": "◐",
"description": "30–60% — some claims unmatched"
},
{
"key": "conflict",
"label": "Conflict",
"color": "#ff5252",
"glyph": "✕",
"description": "0–30% — real partial disagreement"
},
{
"key": "divergent",
"label": "Divergent",
"color": "#b06cff",
"glyph": "⤬",
"description": "zero overlap, both rich → wrong file link"
},
{
"key": "no-claim",
"label": "No-claim",
"color": "#737373",
"glyph": "·",
"description": "metadata lists no parseable sound"
},
{
"key": "unparsed",
"label": "Unparsed",
"color": "#6a6a70",
"glyph": "?",
"description": "WE couldn't read the score — not a conflict"
},
{
"key": "empty",
"label": "Empty",
"color": "#4a4a4a",
"glyph": "∅",
"description": "neither side has parseable sounds"
}
],
"sample": [
{
"key": "kick",
"label": "Kick",
"glyph": "●",
"base": "#df6862",
"shades": [
"#f6a4a7",
"#f59091",
"#f07e7d",
"#e66f6a",
"#d8635a",
"#c65a4e",
"#b15546",
"#995243"
],
"match": [
"kick",
"kik",
"bd",
"808bd",
"909",
"bassdrum"
]
},
{
"key": "snare",
"label": "Snare",
"glyph": "◆",
"base": "#d97230",
"shades": [
"#f4a98c",
"#f2966e",
"#eb8653",
"#e0783a",
"#d16d26",
"#bf641b",
"#aa5e1e",
"#92592a"
],
"match": [
"snare",
"sn",
"sd",
"clap",
"cp",
"rim",
"rs"
]
},
{
"key": "perc",
"label": "Perc",
"glyph": "▴",
"base": "#c18500",
"shades": [
"#e5b476",
"#dfa54f",
"#d69725",
"#c98b00",
"#b98000",
"#a87600",
"#956d00",
"#806517"
],
"match": [
"perc",
"conga",
"bongo",
"tom",
"clave",
"shaker",
"tabla",
"cowbell"
]
},
{
"key": "hat",
"label": "Hat",
"glyph": "✦",
"base": "#999900",
"shades": [
"#cac075",
"#beb550",
"#b0aa2b",
"#a19e00",
"#919300",
"#818800",
"#727c0e",
"#647028"
],
"match": [
"hat",
"hh",
"oh",
"ch",
"hihat",
"cymbal",
"cym",
"ride",
"crash"
]
},
{
"key": "break",
"label": "Break",
"glyph": "≈",
"base": "#3eab5e",
"shades": [
"#9ace94",
"#7ec67e",
"#63bc6e",
"#4ab162",
"#33a55b",
"#239857",
"#228954",
"#2d7a53"
],
"match": [
"break",
"amen",
"loop",
"jungle",
"dnb",
"jazz"
]
},
{
"key": "pad",
"label": "Pad",
"glyph": "◌",
"base": "#00af96",
"shades": [
"#77d2b7",
"#46caac",
"#00c0a2",
"#00b59a",
"#00a892",
"#009a89",
"#008b7f",
"#007a73"
],
"match": [
"pad",
"drone",
"choir",
"string",
"ambient",
"atmos"
]
},
{
"key": "keys",
"label": "Keys",
"glyph": "♬",
"base": "#00abbd",
"shades": [
"#68d1d3",
"#26c8cf",
"#00bdc9",
"#00b1c2",
"#00a4b8",
"#0095ac",
"#00869d",
"#00778a"
],
"match": [
"key",
"keys",
"piano",
"rhodes",
"epiano",
"organ",
"fpiano",
"qstab",
"nujazz"
]
},
{
"key": "lead",
"label": "Lead",
"glyph": "♪",
"base": "#00a1db",
"shades": [
"#70cceb",
"#43c1eb",
"#00b5e8",
"#00a8e1",
"#009ad5",
"#008cc6",
"#007eb2",
"#26709a"
],
"match": [
"lead",
"arp",
"pluck",
"stab",
"blip",
"saw",
"square"
]
},
{
"key": "synth",
"label": "Synth",
"glyph": "◈",
"base": "#5991ed",
"shades": [
"#8fc2fb",
"#79b4fe",
"#68a6fb",
"#5d98f3",
"#568ae6",
"#537dd3",
"#5271bd",
"#5166a1"
],
"match": [
"synth",
"commodore",
"chip",
"fm",
"moog",
"reese"
]
},
{
"key": "bass",
"label": "Bass",
"glyph": "▂",
"base": "#927fe7",
"shades": [
"#b3b6fb",
"#a8a6fc",
"#9f96f8",
"#9686ee",
"#8e79df",
"#856dcb",
"#7b63b4",
"#705c9a"
],
"match": [
"bass",
"sub",
"808",
"acid",
"wobble",
"meth_bass",
"ramplem",
"bassline"
]
},
{
"key": "fx",
"label": "FX",
"glyph": "✺",
"base": "#ba71cb",
"shades": [
"#d3acec",
"#ce99e8",
"#c787df",
"#bf78d2",
"#b46bc2",
"#a860b0",
"#99599b",
"#875485"
],
"match": [
"fx",
"riser",
"sweep",
"noise",
"impact",
"downlifter",
"uplifter",
"glitch",
"vinyl",
"foley"
]
},
{
"key": "vox",
"label": "Vox",
"glyph": "◍",
"base": "#d76797",
"shades": [
"#eda4ca",
"#eb90bd",
"#e67dae",
"#dd6d9f",
"#d0618f",
"#c0587f",
"#ad5270",
"#974f63"
],
"match": [
"vox",
"vocal",
"voc",
"acap",
"speech",
"voice"
]
}
]
}
\ No newline at end of file
/* AUTO-GENERATED from armada/tide-table/models.py ONTOLOGY by
tools/gen_tokens.py — DO NOT EDIT. The fleet color language. */
:root{
--surface:#0a0a0a;
--raised:#111111;
--overlay:#171717;
--hairline:#ffffff1f;
--ink:#e8e8ea;
--mute:#9a9aa0;
--faint:#6a6a70;
--brand-magenta:#d900ff;
--brand-magenta-low:#a700d1;
--brand-magenta-deep:#8900b3;
--role-percs:#ff8c00;
--role-bass:#7c5cff;
--role-melodic:#36c5f0;
--role-tops:#2dd4bf;
--role-atmos:#8a93a6;
--role-vox:#ff3d7b;
--lifecycle-idea:#737373;
--lifecycle-wip:#e0a82e;
--lifecycle-ready:#5bc091;
--lifecycle-released:#d900ff;
--lifecycle-blocked:#ff5252;
--lifecycle-archived:#4a4a4a;
--agree-agree:#5bc091;
--agree-partial:#e0a82e;
--agree-conflict:#ff5252;
--agree-divergent:#b06cff;
--agree-no_claim:#737373;
--agree-unparsed:#6a6a70;
--agree-empty:#4a4a4a;
--sample-kick:#df6862;
--sample-kick-0:#f6a4a7;
--sample-kick-1:#f59091;
--sample-kick-2:#f07e7d;
--sample-kick-3:#e66f6a;
--sample-kick-4:#d8635a;
--sample-kick-5:#c65a4e;
--sample-kick-6:#b15546;
--sample-kick-7:#995243;
--sample-snare:#d97230;
--sample-snare-0:#f4a98c;
--sample-snare-1:#f2966e;
--sample-snare-2:#eb8653;
--sample-snare-3:#e0783a;
--sample-snare-4:#d16d26;
--sample-snare-5:#bf641b;
--sample-snare-6:#aa5e1e;
--sample-snare-7:#92592a;
--sample-perc:#c18500;
--sample-perc-0:#e5b476;
--sample-perc-1:#dfa54f;
--sample-perc-2:#d69725;
--sample-perc-3:#c98b00;
--sample-perc-4:#b98000;
--sample-perc-5:#a87600;
--sample-perc-6:#956d00;
--sample-perc-7:#806517;
--sample-hat:#999900;
--sample-hat-0:#cac075;
--sample-hat-1:#beb550;
--sample-hat-2:#b0aa2b;
--sample-hat-3:#a19e00;
--sample-hat-4:#919300;
--sample-hat-5:#818800;
--sample-hat-6:#727c0e;
--sample-hat-7:#647028;
--sample-break:#3eab5e;
--sample-break-0:#9ace94;
--sample-break-1:#7ec67e;
--sample-break-2:#63bc6e;
--sample-break-3:#4ab162;
--sample-break-4:#33a55b;
--sample-break-5:#239857;
--sample-break-6:#228954;
--sample-break-7:#2d7a53;
--sample-pad:#00af96;
--sample-pad-0:#77d2b7;
--sample-pad-1:#46caac;
--sample-pad-2:#00c0a2;
--sample-pad-3:#00b59a;
--sample-pad-4:#00a892;
--sample-pad-5:#009a89;
--sample-pad-6:#008b7f;
--sample-pad-7:#007a73;
--sample-keys:#00abbd;
--sample-keys-0:#68d1d3;
--sample-keys-1:#26c8cf;
--sample-keys-2:#00bdc9;
--sample-keys-3:#00b1c2;
--sample-keys-4:#00a4b8;
--sample-keys-5:#0095ac;
--sample-keys-6:#00869d;
--sample-keys-7:#00778a;
--sample-lead:#00a1db;
--sample-lead-0:#70cceb;
--sample-lead-1:#43c1eb;
--sample-lead-2:#00b5e8;
--sample-lead-3:#00a8e1;
--sample-lead-4:#009ad5;
--sample-lead-5:#008cc6;
--sample-lead-6:#007eb2;
--sample-lead-7:#26709a;
--sample-synth:#5991ed;
--sample-synth-0:#8fc2fb;
--sample-synth-1:#79b4fe;
--sample-synth-2:#68a6fb;
--sample-synth-3:#5d98f3;
--sample-synth-4:#568ae6;
--sample-synth-5:#537dd3;
--sample-synth-6:#5271bd;
--sample-synth-7:#5166a1;
--sample-bass:#927fe7;
--sample-bass-0:#b3b6fb;
--sample-bass-1:#a8a6fc;
--sample-bass-2:#9f96f8;
--sample-bass-3:#9686ee;
--sample-bass-4:#8e79df;
--sample-bass-5:#856dcb;
--sample-bass-6:#7b63b4;
--sample-bass-7:#705c9a;
--sample-fx:#ba71cb;
--sample-fx-0:#d3acec;
--sample-fx-1:#ce99e8;
--sample-fx-2:#c787df;
--sample-fx-3:#bf78d2;
--sample-fx-4:#b46bc2;
--sample-fx-5:#a860b0;
--sample-fx-6:#99599b;
--sample-fx-7:#875485;
--sample-vox:#d76797;
--sample-vox-0:#eda4ca;
--sample-vox-1:#eb90bd;
--sample-vox-2:#e67dae;
--sample-vox-3:#dd6d9f;
--sample-vox-4:#d0618f;
--sample-vox-5:#c0587f;
--sample-vox-6:#ad5270;
--sample-vox-7:#974f63;
}
{
"schema": "design-tokens v1 (fleet color language: ontology → families × shades)",
"as_of": "2026-06-06",
"neutrals": {
"surface": "#0a0a0a",
"raised": "#111111",
"overlay": "#171717",
"hairline": "#ffffff1f",
"ink": "#e8e8ea",
"mute": "#9a9aa0",
"faint": "#6a6a70"
},
"brand": {
"magenta": "#d900ff",
"magenta-low": "#a700d1",
"magenta-deep": "#8900b3"
},
"role": [
{
"key": "percs",
"label": "Percs",
"color": "#ff8c00",
"glyph": "▰",
"description": "kicks, snares, hats, percussion — transient, warm"
},
{
"key": "bass",
"label": "Bass",
"color": "#7c5cff",
"glyph": "▂",
"description": "sub, acid, reese/wobble, bass-spine — low, deep"
},
{
"key": "melodic",
"label": "Melodic",
"color": "#36c5f0",
"glyph": "♪",
"description": "leads, keys, riffs, high plucks — bright"
},
{
"key": "tops",
"label": "Tops",
"color": "#2dd4bf",
"glyph": "≈",
"description": "breaks, chops, texture loops — busy, mid-high"
},
{
"key": "atmos",
"label": "Atmos",
"color": "#8a93a6",
"glyph": "◌",
"description": "pads, drones, ambient, fx — diffuse, background"
},
{
"key": "vox",
"label": "Vox",
"color": "#ff3d7b",
"glyph": "◍",
"description": "vocal samples and chops — human, forward"
}
],
"lifecycle": [
{
"key": "idea",
"label": "Idea",
"color": "#737373",
"glyph": "·",
"description": "raw / sketch"
},
{
"key": "wip",
"label": "WIP",
"color": "#e0a82e",
"glyph": "◐",
"description": "in progress"
},
{
"key": "ready",
"label": "Ready",
"color": "#5bc091",
"glyph": "●",
"description": "mastered, ship-ready"
},
{
"key": "released",
"label": "Released",
"color": "#d900ff",
"glyph": "★",
"description": "out in the world"
},
{
"key": "blocked",
"label": "Blocked",
"color": "#ff5252",
"glyph": "✕",
"description": "needs a fix before it moves"
},
{
"key": "archived",
"label": "Archived",
"color": "#4a4a4a",
"glyph": "▢",
"description": "parked / superseded"
}
],
"agree": [
{
"key": "agree",
"label": "Agree",
"color": "#5bc091",
"glyph": "●",
"description": "≥60% of claimed sounds are in the score"
},
{
"key": "partial",
"label": "Partial",
"color": "#e0a82e",
"glyph": "◐",
"description": "30–60% — some claims unmatched"
},
{
"key": "conflict",
"label": "Conflict",
"color": "#ff5252",
"glyph": "✕",
"description": "0–30% — real partial disagreement"
},
{
"key": "divergent",
"label": "Divergent",
"color": "#b06cff",
"glyph": "⤬",
"description": "zero overlap, both rich → wrong file link"
},
{
"key": "no-claim",
"label": "No-claim",
"color": "#737373",
"glyph": "·",
"description": "metadata lists no parseable sound"
},
{
"key": "unparsed",
"label": "Unparsed",
"color": "#6a6a70",
"glyph": "?",
"description": "WE couldn't read the score — not a conflict"
},
{
"key": "empty",
"label": "Empty",
"color": "#4a4a4a",
"glyph": "∅",
"description": "neither side has parseable sounds"
}
],
"sample": [
{
"key": "kick",
"label": "Kick",
"glyph": "●",
"base": "#df6862",
"shades": [
"#f6a4a7",
"#f59091",
"#f07e7d",
"#e66f6a",
"#d8635a",
"#c65a4e",
"#b15546",
"#995243"
],
"match": [
"kick",
"kik",
"bd",
"808bd",
"909",
"bassdrum"
]
},
{
"key": "snare",
"label": "Snare",
"glyph": "◆",
"base": "#d97230",
"shades": [
"#f4a98c",
"#f2966e",
"#eb8653",
"#e0783a",
"#d16d26",
"#bf641b",
"#aa5e1e",
"#92592a"
],
"match": [
"snare",
"sn",
"sd",
"clap",
"cp",
"rim",
"rs"
]
},
{
"key": "perc",
"label": "Perc",
"glyph": "▴",
"base": "#c18500",
"shades": [
"#e5b476",
"#dfa54f",
"#d69725",
"#c98b00",
"#b98000",
"#a87600",
"#956d00",
"#806517"
],
"match": [
"perc",
"conga",
"bongo",
"tom",
"clave",
"shaker",
"tabla",
"cowbell"
]
},
{
"key": "hat",
"label": "Hat",
"glyph": "✦",
"base": "#999900",
"shades": [
"#cac075",
"#beb550",
"#b0aa2b",
"#a19e00",
"#919300",
"#818800",
"#727c0e",
"#647028"
],
"match": [
"hat",
"hh",
"oh",
"ch",
"hihat",
"cymbal",
"cym",
"ride",
"crash"
]
},
{
"key": "break",
"label": "Break",
"glyph": "≈",
"base": "#3eab5e",
"shades": [
"#9ace94",
"#7ec67e",
"#63bc6e",
"#4ab162",
"#33a55b",
"#239857",
"#228954",
"#2d7a53"
],
"match": [
"break",
"amen",
"loop",
"jungle",
"dnb",
"jazz"
]
},
{
"key": "pad",
"label": "Pad",
"glyph": "◌",
"base": "#00af96",
"shades": [
"#77d2b7",
"#46caac",
"#00c0a2",
"#00b59a",
"#00a892",
"#009a89",
"#008b7f",
"#007a73"
],
"match": [
"pad",
"drone",
"choir",
"string",
"ambient",
"atmos"
]
},
{
"key": "keys",
"label": "Keys",
"glyph": "♬",
"base": "#00abbd",
"shades": [
"#68d1d3",
"#26c8cf",
"#00bdc9",
"#00b1c2",
"#00a4b8",
"#0095ac",
"#00869d",
"#00778a"
],
"match": [
"key",
"keys",
"piano",
"rhodes",
"epiano",
"organ",
"fpiano",
"qstab",
"nujazz"
]
},
{
"key": "lead",
"label": "Lead",
"glyph": "♪",
"base": "#00a1db",
"shades": [
"#70cceb",
"#43c1eb",
"#00b5e8",
"#00a8e1",
"#009ad5",
"#008cc6",
"#007eb2",
"#26709a"
],
"match": [
"lead",
"arp",
"pluck",
"stab",
"blip",
"saw",
"square"
]
},
{
"key": "synth",
"label": "Synth",
"glyph": "◈",
"base": "#5991ed",
"shades": [
"#8fc2fb",
"#79b4fe",
"#68a6fb",
"#5d98f3",
"#568ae6",
"#537dd3",
"#5271bd",
"#5166a1"
],
"match": [
"synth",
"commodore",
"chip",
"fm",
"moog",
"reese"
]
},
{
"key": "bass",
"label": "Bass",
"glyph": "▂",
"base": "#927fe7",
"shades": [
"#b3b6fb",
"#a8a6fc",
"#9f96f8",
"#9686ee",
"#8e79df",
"#856dcb",
"#7b63b4",
"#705c9a"
],
"match": [
"bass",
"sub",
"808",
"acid",
"wobble",
"meth_bass",
"ramplem",
"bassline"
]
},
{
"key": "fx",
"label": "FX",
"glyph": "✺",
"base": "#ba71cb",
"shades": [
"#d3acec",
"#ce99e8",
"#c787df",
"#bf78d2",
"#b46bc2",
"#a860b0",
"#99599b",
"#875485"
],
"match": [
"fx",
"riser",
"sweep",
"noise",
"impact",
"downlifter",
"uplifter",
"glitch",
"vinyl",
"foley"
]
},
{
"key": "vox",
"label": "Vox",
"glyph": "◍",
"base": "#d76797",
"shades": [
"#eda4ca",
"#eb90bd",
"#e67dae",
"#dd6d9f",
"#d0618f",
"#c0587f",
"#ad5270",
"#974f63"
],
"match": [
"vox",
"vocal",
"voc",
"acap",
"speech",
"voice"
]
}
]
}
\ No newline at end of file
#!/usr/bin/env python3
"""gen_tokens — the fleet color language, generated from the ontology (DRY).
armada/DESIGN.md is the design RATIONALE; armada/tide-table/models.py is the
machine-readable ontology (concepts → color + glyph + label, and the hue-anchored
SAMPLE_FAMILIES). This derives the actual paint every L'Armada surface uses:
• families × variations — each ColorFamily is expanded into SHADE_COUNT shades by
walking OKLCH lightness DOWN while fanning the hue ±SHADE_FAN° (PLN's scheme),
so ~50-100 concepts get stable, distinguishable, greyscale-safe colors.
• two artifacts —
tokens.css (:root custom properties) for CSS-var consumers (armada/ui)
tokens.json (the structured palette) for runtime consumers (triangle.html
fetches it and overrides its :root, so it can never drift)
OKLCH→sRGB is implemented inline (Björn Ottosson's matrices) — no dependencies, so
this runs under the same system python3 as the rest of the pipeline. Validated
against models.DesignTokens on emit.
python3 tools/gen_tokens.py # writes tokens.css (ui) + tokens.json (×2)
"""
from __future__ import annotations
import json
import math
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "armada" / "tide-table"))
import models as M # noqa: E402
AS_OF = "2026-06-06"
CSS_OUT = ROOT / "armada" / "ui" / "src" / "tokens.css"
JSON_OUT = ROOT / "armada" / "ui" / "src" / "tokens.json"
# a served copy so the standalone triangle can fetch it from its own dir
TRIANGLE_OUT = ROOT / "armada" / "tide-table" / "tokens.json"
# ── OKLCH → sRGB hex (Ottosson) ───────────────────────────────────────────────
def _lin_to_srgb(c: float) -> float:
c = max(0.0, min(1.0, c))
return 1.055 * (c ** (1 / 2.4)) - 0.055 if c > 0.0031308 else 12.92 * c
def oklch_hex(L: float, C: float, H_deg: float) -> str:
h = math.radians(H_deg)
a, b = C * math.cos(h), C * math.sin(h)
l_ = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
m_ = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
s_ = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3
r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_
g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_
bb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_
return "#" + "".join(f"{round(_lin_to_srgb(x) * 255):02x}" for x in (r, g, bb))
def shades(fam) -> tuple[str, list[str]]:
"""Family base + SHADE_COUNT shades: lightness ladder + ±SHADE_FAN° hue fan.
Chroma eases slightly at the light/dark ends so extremes don't look muddy."""
n = M.SHADE_COUNT
lo, hi = M.SHADE_L[1], M.SHADE_L[0]
out = []
for i in range(n):
t = i / (n - 1) # 0 (light) … 1 (dark)
L = hi + (lo - hi) * t
H = fam.hue + (t - 0.5) * 2 * M.SHADE_FAN
C = fam.chroma * (1 - 0.35 * (2 * t - 1) ** 2) # bell: full in the middle
out.append(oklch_hex(L, C, H))
base = oklch_hex(0.66, fam.chroma, fam.hue) # representative swatch
return base, out
def build() -> dict:
def terms(ts):
return [{"key": t.key, "label": t.label, "color": t.color,
"glyph": t.glyph, "description": t.description} for t in ts]
sample = []
for fam in M.SAMPLE_FAMILIES:
base, sh = shades(fam)
sample.append({"key": fam.key, "label": fam.label, "glyph": fam.glyph,
"base": base, "shades": sh, "match": fam.match})
return {
"schema": "design-tokens v1 (fleet color language: ontology → families × shades)",
"as_of": AS_OF,
"neutrals": dict(M.NEUTRALS),
"brand": dict(M.BRAND),
"role": terms(M.ROLE_FAMILIES),
"lifecycle": terms(M.LIFECYCLE_TERMS),
"agree": terms(M.AGREE_TERMS),
"sample": sample,
}
def to_css(tok: dict) -> str:
out = ["/* AUTO-GENERATED from armada/tide-table/models.py ONTOLOGY by",
" tools/gen_tokens.py — DO NOT EDIT. The fleet color language. */",
":root{"]
for k, v in tok["neutrals"].items():
out.append(f" --{k}:{v};")
for k, v in tok["brand"].items():
out.append(f" --brand-{k}:{v};")
for axis in ("role", "lifecycle", "agree"):
for t in tok[axis]:
out.append(f" --{axis}-{t['key'].replace('-', '_')}:{t['color']};")
for f in tok["sample"]:
out.append(f" --sample-{f['key']}:{f['base']};")
for i, s in enumerate(f["shades"]):
out.append(f" --sample-{f['key']}-{i}:{s};")
out.append("}")
return "\n".join(out) + "\n"
def main():
tok = build()
from models import DesignTokens # DRY contract: validate before writing
DesignTokens.model_validate(tok)
js = json.dumps(tok, ensure_ascii=False, indent=1)
JSON_OUT.write_text(js)
TRIANGLE_OUT.write_text(js)
CSS_OUT.write_text(to_css(tok))
n_sh = sum(len(f["shades"]) for f in tok["sample"])
print(f"✓ {CSS_OUT.relative_to(ROOT)} + {JSON_OUT.name} (×2: ui + tide-table)")
print(f" {len(tok['sample'])} sample families × {M.SHADE_COUNT} shades = {n_sh} sample slots")
print(f" + role {len(tok['role'])} · lifecycle {len(tok['lifecycle'])} · agree {len(tok['agree'])}")
OUT = CSS_OUT
if __name__ == "__main__":
main()
...@@ -14,9 +14,9 @@ from pathlib import Path ...@@ -14,9 +14,9 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "armada" / "tide-table")) sys.path.insert(0, str(ROOT / "armada" / "tide-table"))
from models import CatalogView, Note, PlayerData # noqa: E402 from models import CatalogView, Note, PatternRegistry, PlayerData # noqa: E402
ROOTS = [PlayerData, Note, CatalogView] # top-level UI-facing models ROOTS = [PlayerData, Note, CatalogView, PatternRegistry] # top-level UI-facing models
OUT = ROOT / "armada" / "ui" / "src" / "types.gen.ts" OUT = ROOT / "armada" / "ui" / "src" / "types.gen.ts"
SCALARS = {"string": "string", "number": "number", "integer": "number", SCALARS = {"string": "string", "number": "number", "integer": "number",
"boolean": "boolean", "null": "null"} "boolean": "boolean", "null": "null"}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment