From fbd4b3a18d5b7e671f70be032d55af2bd1df9feb Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 23:17:19 +0200 Subject: [PATCH] refactor(meta): label categories with one hieroglyph A deploy job name repeats the category path on every row of the matrix. One Egyptian hieroglyph costs four UTF-8 bytes where the two Chinese characters it replaces cost six. Unicode names each sign by its Gardiner code, so the opaque value stays identifiable without a comment per entry. A label has to stay one codepoint: a sign written with a variation selector costs seven bytes and is worse than what it replaced. tests/integration/meta/categories/test_glyph.py enforces that. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/README.md | 2 +- meta/categories.yml | 90 ++++++++++--------- .../files/python/infinito_api/roles.py | 2 +- tests/integration/meta/categories/README.md | 2 +- .../{test_hanzi.py => test_glyph.py} | 33 ++++--- .../integration/meta/categories/test_paths.py | 2 +- .../cli/administration/deploy/ci/test_runs.py | 2 +- .../files/infinito_api/test_roles.py | 2 +- tests/unit/python/utils/i18n/test_extract.py | 2 +- tests/unit/python/utils/roles/test_display.py | 4 +- utils/github/variant/selection.py | 2 +- utils/roles/display.py | 10 +-- 12 files changed, 83 insertions(+), 70 deletions(-) rename tests/integration/meta/categories/{test_hanzi.py => test_glyph.py} (58%) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index d3f92854fe..45e8ded99b 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -135,7 +135,7 @@ row would otherwise be assigned. Two spellings are accepted: | Form | Example | |---|---| | ASCII | `web-app-nextcloud#0,2@swarm+tor%debian/zfs:arm64` | -| the job label, pasted back out of the failed run | `🐳🧅🌀🦓🦾网络应用·Nextcloud#0` | +| the job label, pasted back out of the failed run | `🐳🧅🌀🦓🦾𓏴𓏛·Nextcloud#0` | | Separator | Axis | Values | |---|---|---| diff --git a/meta/categories.yml b/meta/categories.yml index 5846277d77..b0bb9712e6 100644 --- a/meta/categories.yml +++ b/meta/categories.yml @@ -1,17 +1,19 @@ --- -# Hanzi labels are compact hierarchical category fragments. -# Every label uses exactly two Chinese characters. +# Glyph labels are compact hierarchical category fragments. +# Every label is a single Egyptian hieroglyph; its Unicode name carries the +# Gardiner sign code. # -# The labels prioritize short, consistent, left-to-right navigation over -# complete or grammatically natural Chinese terminology. +# A glyph has to stay one codepoint: four UTF-8 bytes. A sign written with a +# variation selector costs seven and is worse than the two Chinese characters +# it replaced, at six. # # Display paths: # parent · child · grandchild # # Examples: -# sys-ctl-cln -> 系统·控制·清理 -# web-app -> 网络·应用 -# svc-ai -> 服务·智能 +# sys-ctl-cln -> 𓉐·𓌀·𓈖 +# web-app -> 𓏴·𓏛 +# svc-ai -> 𓊵·𓅝 # # Deploy stages in call order (tasks/stages/0*.yml): the constructor pass # runs first, then the per-host-type passes (workstation, then server), and @@ -27,27 +29,27 @@ stages: roles: dev: title: "Software Development Utilties" - hanzi: "开发" # Means "development". + glyph: "𓌸" stage: workstation invokable: false sys: title: "System" - hanzi: "系统" # Means "system". + glyph: "𓉐" description: "System near components. Will be automaticly called if necessary from other roles." stage: server invokable: false ctl: title: "Control" - hanzi: "控制" # Means "control" or "steering". + glyph: "𓌀" description: "Control layer for system lifecycle management—handling cleanup, monitoring, backups, alerting, maintenance, and repair tasks." icon: "fas fa-cogs" invokable: false cln: title: "Cleanup" - hanzi: "清理" # Means "cleanup" or "clearing". + glyph: "𓈖" description: "Roles for cleaning up various system resources—old backups, unused certificates, temporary files, Docker volumes, disk caches, deprecated domains, and more." icon: "fas fa-trash-alt" @@ -55,7 +57,7 @@ roles: hlth: title: "Monitoring" - hanzi: "监控" # Means "monitoring" or "surveillance". + glyph: "𓁹" description: "Roles for system monitoring and health checks—encompassing bot-style automated checks and core low-level monitors for logs, containers, disk usage, and more." icon: "fas fa-chart-area" @@ -63,56 +65,56 @@ roles: bkp: title: "Backup & Restore" - hanzi: "备份" # Means "backup" or "copy". + glyph: "𓎟" description: "Backup strategies & restore procedures" icon: "fas fa-hdd" invokable: false alm: title: "Alerting" - hanzi: "告警" # Means "warning" or "alarm". + glyph: "𓌉" description: "Notification handlers for system events" icon: "fas fa-bell" invokable: false mtn: title: "Maintenance" - hanzi: "维护" # Means "maintenance" or "upkeep". + glyph: "𓍋" description: "Maintenance roles for certificates, system upkeep, and recurring operational tasks." icon: "fas fa-tools" invokable: false rpr: title: "Repair" - hanzi: "修复" # Means "repair", "fix", or "restoration". + glyph: "𓋹" description: "Repair and recovery roles—handling hard/soft recovery of Docker, Btrfs balancers, and other low-level system fixes." icon: "fas fa-wrench" invokable: false dns: title: "DNS Automation" - hanzi: "域名" # Means "domain name". + glyph: "𓏞" description: "DNS providers, records, and rDNS management (Cloudflare, Hetzner, etc.)." icon: "fas fa-network-wired" invokable: false stk: title: "Stack" - hanzi: "堆栈" # Means "stack". + glyph: "𓉻" description: "Stack levels to setup the server" icon: "fas fa-bars-staggered" invokable: false front: title: "System Frontend Helpers" - hanzi: "前端" # Means "frontend" or literally "front end". + glyph: "𓂋" description: "Frontend helpers for reverse-proxied apps (injection, shared assets, CDN plumbing)." icon: "fas fa-wand-magic-sparkles" invokable: false inj: title: "Injection" - hanzi: "注入" # Means "injection" or "insertion". + glyph: "𓆑" description: "Composable HTML injection roles (CSS, JS, logout interceptor, analytics, dashboard iframe) for Nginx/OpenResty via sub_filter/Lua with CDN-backed assets." icon: "fas fa-filter" @@ -120,7 +122,7 @@ roles: update: title: "Updates & Package Management" - hanzi: "更新" # Means "update" or "renewal". + glyph: "𓇳" description: "OS & package updates" icon: "fas fa-sync" stage: constructor @@ -131,7 +133,7 @@ roles: drv: title: "Drivers" - hanzi: "驱动" # Means "driver", "drive", or "actuation". + glyph: "𓃒" description: "Roles for installing and configuring hardware drivers—covering printers, graphics, input devices, and other peripheral support." icon: "fas fa-microchip" stage: constructor @@ -143,7 +145,7 @@ roles: gen: title: "Generic" - hanzi: "通用" # Means "general-purpose" or "universal". + glyph: "𓏤" description: "Helper roles & installers (git, locales, timer, etc.)" icon: "fas fa-wrench" stage: constructor @@ -152,7 +154,7 @@ roles: dsk: title: "Desktop" - hanzi: "桌面" # Means "desktop" or literally "desk surface". + glyph: "𓊨" description: "Desktop environment roles & apps (GNOME, browser, LibreOffice, etc.)" icon: "fas fa-desktop" stage: workstation @@ -160,27 +162,27 @@ roles: gnt: title: "Agents" - hanzi: "代理" # Means "agent" or "proxy". + glyph: "𓁐" description: "Coding agents installed on the workstation (Claude Code, Cursor, Pi)" icon: "fas fa-robot" web: title: "Web Infrastructure" - hanzi: "网络" # Means "network" or "web". + glyph: "𓏴" description: "Roles for managing web infrastructure—covering static content services and deployable web applications." icon: "fas fa-globe" stage: server svc: title: "Services" - hanzi: "服务" # Means "service". + glyph: "𓊵" description: "Static content servers (assets, HTML, legal, files)" icon: "fas fa-file" invokable: true app: title: "Applications" - hanzi: "应用" # Means "application" or "use". + glyph: "𓏛" description: "Deployable web applications (GitLab, Nextcloud, Mastodon, etc.)" icon: "fas fa-docker" run_after: [web-svc] @@ -188,21 +190,21 @@ roles: opt: title: "Webserver Optimation" - hanzi: "优化" # Means "optimization". + glyph: "𓅱" description: "Tools which help to optimize webservers" run_after: [web-app] invokable: true svc: title: "Services" - hanzi: "服务" # Means "service". + glyph: "𓊵" description: "Infrastructure services like databases" stage: constructor invokable: false db: title: "Databases" - hanzi: "数据" # Means "data". + glyph: "𓎼" description: "Database applications (DBMS, LDAP, Redis, etc.)" icon: "fas fa-database" run_after: [svc-net] @@ -210,7 +212,7 @@ roles: dns: title: "DNS Resolvers" - hanzi: "解析" # Means "resolution", "parsing", or "analysis". + glyph: "𓇋" description: "Central recursive/forwarding DNS resolver engines (Unbound, etc.)" icon: "fas fa-globe" run_after: [svc-db] @@ -218,7 +220,7 @@ roles: opt: title: "Service Optimation" - hanzi: "优化" # Means "optimization". + glyph: "𓅱" description: "Optimation Services to improve your system" icon: "fas fa-database" stage: destructor @@ -226,7 +228,7 @@ roles: prx: title: "Proxy Servers" - hanzi: "代理" # Means "proxy", "agent", or "representation". + glyph: "𓊛" description: "Reverse-proxy roles for routing and load-balancing traffic to backend services" icon: "fas fa-project-diagram" run_after: [svc-dns] @@ -234,7 +236,7 @@ roles: net: title: "Network" - hanzi: "网络" # Means "network" or "web". + glyph: "𓈐" description: "Network setup (DNS, Let's Encrypt HTTP, WireGuard, etc.)" icon: "fas fa-globe" run_after: [gen] @@ -245,7 +247,7 @@ roles: ai: title: "AI Services" - hanzi: "智能" # Means "intelligence" or "smart capability". + glyph: "𓅝" description: "Core AI building blocks—model serving, OpenAI-compatible gateways, vector databases, orchestration, and chat UIs." icon: "fas fa-brain" run_after: [svc-prx] @@ -253,7 +255,7 @@ roles: bkp: title: "Backup Services" - hanzi: "备份" # Means "backup" or "copy". + glyph: "𓎟" description: "Service-level backup and recovery components—handling automated data snapshots, remote backups, synchronization services, and backup orchestration across databases, files, and containers." icon: "fas fa-database" @@ -262,7 +264,7 @@ roles: cache: title: "Package Caches" - hanzi: "缓存" # Means "cache" or "buffer". + glyph: "𓏏" description: "Pull-through caches for package upstreams - proxy repositories and the TLS frontend that answers for them." icon: "fas fa-warehouse" bootstrap: true @@ -273,7 +275,7 @@ roles: registry: title: "Container Registries" - hanzi: "仓库" # Means "warehouse" or "repository". + glyph: "𓊖" description: "Container-registry services - local registry and pull-through cache." icon: "fas fa-box-archive" bootstrap: true @@ -284,7 +286,7 @@ roles: storage: title: "Shared Storage" - hanzi: "存储" # Means "storage" or "to store". + glyph: "𓉔" description: "Shared persistent-storage backends used by multi-node deployments — NFSv4 server and client roles." icon: "fas fa-hdd" bootstrap: true @@ -292,14 +294,14 @@ roles: swarm: title: "Container Orchestration" - hanzi: "编排" # Means "orchestration", "arrangement", or "coordination". + glyph: "𓅧" description: "Docker Swarm cluster bootstrap and node-membership roles for multi-host service deployment." icon: "fas fa-network-wired" bootstrap: true invokable: true virt: title: "Sandboxed Runtimes" - hanzi: "沙箱" # Means "sandbox". + glyph: "𓈒" description: "Kernel-isolating container runtimes on the container host: Kata Containers microVMs and the gVisor userspace fallback." icon: "fas fa-shield-halved" bootstrap: true @@ -310,7 +312,7 @@ roles: runner: title: "CI Runner" - hanzi: "运行" # Means "running", "operation", or "execution". + glyph: "𓂻" description: "Self-hosted GitHub Actions runner deployment and configuration" icon: "fas fa-play" run_after: [svc-bkp] @@ -318,7 +320,7 @@ roles: user: title: "Users & Access" - hanzi: "用户" # Means "user". + glyph: "𓀀" description: "User accounts & access control" icon: "fas fa-users" invokable: false diff --git a/roles/web-svc-api/files/python/infinito_api/roles.py b/roles/web-svc-api/files/python/infinito_api/roles.py index d2879dbde9..49813a9de1 100644 --- a/roles/web-svc-api/files/python/infinito_api/roles.py +++ b/roles/web-svc-api/files/python/infinito_api/roles.py @@ -14,7 +14,7 @@ ROLE_FILE = re.compile( HEADING = re.compile(r"^#{1,3}\s+(.+?)\s*$", re.MULTILINE) CATEGORY_FILES = ("meta/categories.yml", "roles/categories.yml") CATEGORY_METADATA = frozenset( - {"title", "description", "icon", "invokable", "modes", "hanzi", "stage"} + {"title", "description", "icon", "invokable", "modes", "glyph", "stage"} ) BUNDLE_FILE = re.compile( r"^inventories/bundles/(?P[^/]+)/(?P[^/]+)/inventory\.yml$" diff --git a/tests/integration/meta/categories/README.md b/tests/integration/meta/categories/README.md index c2a2663f28..a739ef8539 100644 --- a/tests/integration/meta/categories/README.md +++ b/tests/integration/meta/categories/README.md @@ -1,6 +1,6 @@ # Category Tests 🗂️ -Integration tests that validate `meta/categories.yml`: every declared category path MUST correspond to an existing directory under `roles/`, `invokable: true` MUST NOT appear on any descendant of an already-invokable ancestor, and every category MUST declare a `hanzi` label that is unique among its siblings. +Integration tests that validate `meta/categories.yml`: every declared category path MUST correspond to an existing directory under `roles/`, `invokable: true` MUST NOT appear on any descendant of an already-invokable ancestor, and every category MUST declare a `glyph` label that is unique among its siblings. Tests in this directory MUST only cover the category hierarchy itself. Tests for `application_id` classification driven by `invokable_paths.py` MUST live under `tests/integration/roles/applications/id/`. diff --git a/tests/integration/meta/categories/test_hanzi.py b/tests/integration/meta/categories/test_glyph.py similarity index 58% rename from tests/integration/meta/categories/test_hanzi.py rename to tests/integration/meta/categories/test_glyph.py index 0eecff214b..60ffadd504 100644 --- a/tests/integration/meta/categories/test_hanzi.py +++ b/tests/integration/meta/categories/test_glyph.py @@ -1,3 +1,4 @@ +import unicodedata import unittest from utils.cache.yaml import load_yaml_any @@ -7,7 +8,7 @@ ATTRIBUTE_KEYS = frozenset( { "bootstrap", "description", - "hanzi", + "glyph", "icon", "invokable", "modes", @@ -17,6 +18,8 @@ ATTRIBUTE_KEYS = frozenset( } ) +HIEROGLYPHS = range(0x13000, 0x13430) + def _categories(node, path=()): """Yield ``(path, attrs)`` for every category node below *node*. @@ -32,32 +35,42 @@ def _categories(node, path=()): yield from _categories(value, (*path, key)) -class TestCategoryHanzi(unittest.TestCase): +class TestCategoryGlyph(unittest.TestCase): @classmethod def setUpClass(cls): data = load_yaml_any(str(categories_file())) cls.categories = list(_categories(data["roles"])) - def test_every_category_declares_a_hanzi(self): + def test_every_category_declares_a_glyph(self): missing = sorted( - "-".join(path) for path, attrs in self.categories if not attrs.get("hanzi") + "-".join(path) for path, attrs in self.categories if not attrs.get("glyph") ) - self.assertEqual([], missing, f"Categories without a hanzi label: {missing}") + self.assertEqual([], missing, f"Categories without a glyph: {missing}") - def test_hanzi_is_unique_among_siblings(self): + def test_every_glyph_is_one_hieroglyph(self): + wrong = [] + for path, attrs in self.categories: + glyph = attrs.get("glyph", "") + if len(glyph) == 1 and ord(glyph) in HIEROGLYPHS: + continue + name = unicodedata.name(glyph[0], "?") if glyph else "empty" + wrong.append(f"{'-'.join(path)}: {glyph!r} ({len(glyph)} cp, {name})") + self.assertEqual([], wrong, f"Not a single Egyptian hieroglyph: {wrong}") + + def test_glyph_is_unique_among_siblings(self): """Sibling scope, not tree scope: the labels are path fragments and - repeat across branches on purpose (web-svc and svc both carry 服务).""" + repeat across branches on purpose (web-svc and svc both carry 𓊵).""" seen = {} collisions = [] for path, attrs in self.categories: - key = (path[:-1], attrs.get("hanzi")) + key = (path[:-1], attrs.get("glyph")) name = "-".join(path) if key in seen: - collisions.append(f"{attrs.get('hanzi')}: {seen[key]} vs {name}") + collisions.append(f"{attrs.get('glyph')}: {seen[key]} vs {name}") seen[key] = name - self.assertEqual([], collisions, f"Duplicate hanzi labels: {collisions}") + self.assertEqual([], collisions, f"Duplicate glyph labels: {collisions}") if __name__ == "__main__": diff --git a/tests/integration/meta/categories/test_paths.py b/tests/integration/meta/categories/test_paths.py index 7adb2dd09d..30be45a477 100644 --- a/tests/integration/meta/categories/test_paths.py +++ b/tests/integration/meta/categories/test_paths.py @@ -25,7 +25,7 @@ class TestCategoryPaths(unittest.TestCase): for sub_key in attrs: if sub_key in ( "title", - "hanzi", + "glyph", "description", "icon", "children", diff --git a/tests/unit/python/cli/administration/deploy/ci/test_runs.py b/tests/unit/python/cli/administration/deploy/ci/test_runs.py index 59297e65c9..4a8e04c8fe 100644 --- a/tests/unit/python/cli/administration/deploy/ci/test_runs.py +++ b/tests/unit/python/cli/administration/deploy/ci/test_runs.py @@ -429,7 +429,7 @@ class TestConfigFromTitle(unittest.TestCase): def test_a_truncated_priority_entry_aborts_instead_of_passing_through(self) -> None: with self.assertRaises(SystemExit): - runs.untriggered_priority("web-app-a 网络应用·Funkwha...", {}) + runs.untriggered_priority("web-app-a 𓏴𓏛·Funkwha...", {}) def test_the_tor_mode_is_carried_over_from_the_job_log(self) -> None: title = render({"mode": "swarm"}) diff --git a/tests/unit/python/roles/web-svc-api/files/infinito_api/test_roles.py b/tests/unit/python/roles/web-svc-api/files/infinito_api/test_roles.py index 79bce277e9..a97d4c29da 100644 --- a/tests/unit/python/roles/web-svc-api/files/infinito_api/test_roles.py +++ b/tests/unit/python/roles/web-svc-api/files/infinito_api/test_roles.py @@ -30,7 +30,7 @@ FILES = { roles: web: title: Web - hanzi: 网络 + glyph: 𓏴 app: title: Applications description: Apps for users diff --git a/tests/unit/python/utils/i18n/test_extract.py b/tests/unit/python/utils/i18n/test_extract.py index d601856d11..fe803e25a0 100644 --- a/tests/unit/python/utils/i18n/test_extract.py +++ b/tests/unit/python/utils/i18n/test_extract.py @@ -35,7 +35,7 @@ class TestCoreMessages(unittest.TestCase): roles: web: title: Web - hanzi: 网络 + glyph: 𓏴 app: title: Applications description: Apps for users diff --git a/tests/unit/python/utils/roles/test_display.py b/tests/unit/python/utils/roles/test_display.py index 048f11758d..07ed0a7cc5 100644 --- a/tests/unit/python/utils/roles/test_display.py +++ b/tests/unit/python/utils/roles/test_display.py @@ -28,9 +28,7 @@ class TestRoleDisplayName(unittest.TestCase): ), self.codec.encode(long_heading), ) - self.assertEqual( - "系统控制告警·compose", self.codec.encode("sys-ctl-alm-compose") - ) + self.assertEqual("𓉐𓌀𓌉·compose", self.codec.encode("sys-ctl-alm-compose")) def test_the_id_section_drops_what_the_categories_already_say(self): self.assertEqual("nextcloud", self.codec.id_fragment(APP)) diff --git a/utils/github/variant/selection.py b/utils/github/variant/selection.py index 9e7a50a136..8c73619ba8 100644 --- a/utils/github/variant/selection.py +++ b/utils/github/variant/selection.py @@ -12,7 +12,7 @@ filesystem and the architecture on both lines. Two spellings are accepted, so an operator can either type the token or paste back the job title of the run they want repeated: -* the job label CI emits (``🐳🧅🌀🦓🦾网络应用·Nextcloud#2``) -- the glyphs carry +* the job label CI emits (``🐳🧅🌀🦓🦾𓏴𓏛·Nextcloud#2``) -- the glyphs carry mode, onion state, mesh state, distro, filesystem and architecture, the ``#`` shard the variants; * an ASCII form (``web-app-nextcloud#0,2@swarm+tor%debian/zfs:arm64&vpn``). diff --git a/utils/roles/display.py b/utils/roles/display.py index e096673642..a27f12e0c5 100644 --- a/utils/roles/display.py +++ b/utils/roles/display.py @@ -2,7 +2,7 @@ A role id is the machine handle (``web-app-nextcloud``); the display name is what a human reads off a GitHub Actions job or run title -(``网络应用·Nextcloud``). The category path renders as the ``hanzi`` labels +(``𓏴𓏛·Nextcloud``). The category path renders as the ``glyph`` labels ``meta/categories.yml`` declares, run together into one block, then a ``·``, then the role's own ``README.md`` heading or the part of the id the categories do not already carry. @@ -13,7 +13,7 @@ make a two-role list unparseable. :meth:`RoleDisplayName.decode` accepts a raw role id unchanged, so every consumer can decode unconditionally and a human dispatching a run by hand -never has to type hanzi. +never has to type a glyph. """ from __future__ import annotations @@ -98,7 +98,7 @@ class RoleDisplayName: return "-".join(parts) or app_id def category_labels(self, app_id: str) -> list[str]: - """The ``hanzi`` label of every category the role id walks through. + """The ``glyph`` label of every category the role id walks through. Args: app_id: role id, e.g. ``'web-app-nextcloud'``. @@ -107,9 +107,9 @@ class RoleDisplayName: labels: list[str] = [] for part in app_id.split("-"): child = node.get(part) - if not isinstance(child, dict) or "hanzi" not in child: + if not isinstance(child, dict) or "glyph" not in child: break - labels.append(child["hanzi"]) + labels.append(child["glyph"]) node = child return labels