app_icons: umbrella glyph for the Umbrella? app

Filled scalloped canopy over a stroked J-handle (filled because a stroked
dome with ribs reads as a jellyfish at 48dp). AppIcon moves to _app_icon.py,
re-exported from apps.py, so the registry stays under the 250-line cap.
Prunes the two oldest evidence artifacts (60-file cap; still in history).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ajzz98xksbfak8CaunZWC
This commit is contained in:
Krzysztof Rudnicki 2026-09-25 15:04:07 +02:00
parent b7e537fe01
commit baa50cdb60
8 changed files with 90 additions and 146 deletions

View File

@ -0,0 +1,12 @@
{
"title": "app_icons: umbrella glyph for the Umbrella? app",
"objective": "~/src/umbrella ships a family-style launcher icon (charcoal field, shared #B8862E accent, umbrella glyph) generated by the shared generator.",
"acceptance_criteria": [
"`python3 -m python_pkg.app_icons generate --app umbrella --android` writes the assets and mipmaps with no safe-box warning.",
"The preview sheet shows the umbrella legibly at 48dp.",
"apps.py stays under 250 lines and `from python_pkg.app_icons.apps import AppIcon` keeps working.",
"pytest python_pkg/app_icons passes at 100% coverage."
],
"out_of_scope": ["Any other app's glyph or the family style."],
"verifier": "generate + preview run and inspected; pytest python_pkg/app_icons 44 passed; pre-commit on the changed files."
}

View File

@ -0,0 +1,13 @@
{
"intent": "The new Umbrella? Flutter app (~/src/umbrella, com.kuhy.umbrella) gets a launcher icon from the shared family instead of the stock Flutter icon.",
"scope": ["python_pkg/app_icons: new `umbrella` glyph and registry entry", "AppIcon dataclass moved to _app_icon.py so apps.py stays under the 250-line cap (re-exported, API unchanged)", "No change to style.py or any other app's icon"],
"changes": ["_glyph_art_more.py: _UMBRELLA, a filled scalloped canopy over a stroked J-handle, geometry inside SAFE_BOX with a 40px hook opening (>= MIN_NEGATIVE_SPACE)", "glyphs.py: registers the umbrella glyph", "apps.py: registers the umbrella app; AppIcon now imported from _app_icon.py and listed in __all__", "_app_icon.py: the AppIcon record, Path under TYPE_CHECKING"],
"verification": [
{"command": "PYTHONPATH=. python3 -m python_pkg.app_icons generate --app umbrella --android", "result": "pass", "evidence": "umbrella: wrote 6 asset files to ~/src/umbrella/assets/icon; regenerated Android mipmaps; no safe-box warning"},
{"command": "PYTHONPATH=. python3 -m python_pkg.app_icons preview -o /tmp/sheet.png", "result": "pass", "evidence": "umbrella renders in the family row at full size, circle mask and 48dp; reads as an umbrella"},
{"command": "pytest python_pkg/app_icons", "result": "pass", "evidence": "44 passed; registry invariants (real glyph, shared accent, hex accent) cover the new entry"},
{"command": "pre-commit run --files <changed-files>", "result": "pass", "evidence": "all hooks passed"}
],
"risks": ["Importers of AppIcon from apps.py keep working via the re-export; generate.py and test_generate.py still import it from there."],
"rollback": ["git revert the commit; the umbrella app keeps its already-generated PNGs", "Re-run pytest python_pkg/app_icons"]
}

View File

@ -1,63 +0,0 @@
{
"intent": "Test both hosts-guard migration libraries to 100% line coverage, take them off the allowlist, and teach the coverage instrument that a loop terminator carrying a redirect is not a statement.",
"scope": [
"linux_configuration/scripts/single_use/fixes/lib/hosts_guard_migrate.sh (two test seams)",
"linux_configuration/scripts/single_use/fixes/lib/hosts_guard_rollback.sh (one test seam)",
"linux_configuration/scripts/single_use/fixes/lib/tests/hosts_guard_harness.sh and three test files (new)",
"meta/scripts/lib/shell_coverage_lines.py (_LOOP_END_OP predicate)",
"meta/shell-coverage-allowlist.txt (two lines removed)"
],
"changes": [
"Added SYSTEMD_UNIT_DIR and PACMAN_DB_LCK seams to hosts_guard_migrate.sh and HOSTS_FILE to hosts_guard_rollback.sh, each defaulting to the real path, so validate_requirements and do_rollback can be driven without a real /etc or a real pacman lock",
"Added hosts_guard_harness.sh, the fifth family harness, supplying the path globals, the LEGACY_HOOKS/LEGACY_UNITS/INSTANCES tables and the msg/note/warn/err/run/instance_* helpers that live in the entry script",
"Added 82 assertions over 36 cases across three test files covering both libs",
"INSTRUMENT FIX: added _LOOP_END_OP to continuation_lines(), excluding a loop terminator that carries a redirect (`done >file`, `done <file`, `done | cmd`). bash attributes the redirected compound statement to the loop's OPENING line, so kcov instruments the `done` line but it can never be hit",
"Removed both libs from meta/shell-coverage-allowlist.txt, taking it from 88 to 86 entries"
],
"verification": [
{
"command": "Minimal reproduction of the instrument artifact: a three-iteration `for` loop ending in `done >\"$1\"`, measured with kcov directly",
"result": "pass",
"evidence": "The loop body reports hits=3 and the `done >\"$1\"` line reports hits=0 in the same run, confirming bash attributes the redirect to the loop's opening line. This is the same defect _CLOSING_BRACE_OP already handles for `} |`."
},
{
"command": "Audit: print every line the new predicate newly excludes, with its text, across all of fixes/lib",
"result": "pass",
"evidence": "Exactly two lines: hosts_guard_rollback.sh:69 'done >\"$STATE_DIR/units.state\"' and :108 'done <\"$STATE_DIR/units.state\"'. No over-exclusion, which is the only direction that could inflate coverage."
},
{
"command": "Acceptance suite for any instrument change, through features/lib/tests/run_all.sh with its jail_args",
"result": "pass",
"evidence": "dot_resolver_install.sh: 39/39 = 100.00% and rpi_nc_ca.sh: 73/73 = 100.00% -- both exactly the documented numbers, so the predicate changed no other measurement."
},
{
"command": "shell_coverage_jail.sh --measure hosts_guard_migrate.sh / hosts_guard_rollback.sh --min 100 --fail-on-case-error",
"result": "pass",
"evidence": "hosts_guard_migrate.sh: 81/81 = 100.00%. hosts_guard_rollback.sh: 65/65 = 100.00% (denominator 67 -> 65 from the predicate; it measured 97.01% with lines 69 and 108 uncovered beforehand)."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh on both libs, AFTER deleting their allowlist lines",
"result": "pass",
"evidence": "'every checked shell library meets the coverage bar', EXIT 0."
},
{
"command": "bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh",
"result": "pass",
"evidence": "All fixes lib test files passed; 98s bare with the three new files."
},
{
"command": "pre-commit run ruff --files meta/scripts/lib/shell_coverage_lines.py",
"result": "pass",
"evidence": "Passed. A bare `ruff check` reports CPY001 on this file, but it does so on the unmodified file from HEAD as well -- it is a config-less invocation artifact, not a regression."
}
],
"risks": [
"The instrument change lowers denominators wherever `done <redirect>` appears. A lib already at 100% cannot regress from excluding more lines (covered_set is intersected with the denominator), and the audit shows only two lines repo-wide are affected, both in this commit's own lib.",
"The predicate REQUIRES a redirect after `done`; a bare `done` stays in the denominator, so ordinary uncovered loops are still caught.",
"hosts_guard_harness.sh redefines instance_spec with tmpdir targets rather than /etc paths, so the tests exercise the real control flow against fake targets; a change to the real instance table would not be caught by these tests."
],
"rollback": [
"git revert the commit, then re-add both hosts_guard lines to meta/shell-coverage-allowlist.txt",
"Re-run the acceptance pair (dot_resolver_install.sh 39/39, rpi_nc_ca.sh 73/73) to confirm the instrument is back to its prior behaviour"
]
}

View File

@ -1,57 +0,0 @@
{
"intent": "Test thorium_repairs.sh to 100% line coverage, take it off the shell coverage allowlist, and raise the coverage gate's per-case timeout, which the growing suite had outgrown.",
"scope": [
"linux_configuration/scripts/single_use/fixes/lib/tests/thorium_harness.sh (new)",
"linux_configuration/scripts/single_use/fixes/lib/tests/test_thorium_repairs.sh and test_thorium_repairs_deep.sh (new)",
"meta/scripts/lib/shell_coverage_bar.sh (--timeout added to the gate's jail invocation)",
"meta/shell-coverage-allowlist.txt (one line removed)",
"Non-goal: thorium_repairs.sh itself, which needed NO production change -- it already takes every path from $THORIUM_CONFIG_DIR"
],
"changes": [
"Added thorium_harness.sh, the fourth family harness, supplying THORIUM_CONFIG_DIR plus the backup_if_exists/remove_if_exists helpers that live in the entry script fix_thorium.sh rather than in any lib",
"Added 55 assertions over 23 cases across both files: all five profile repairs plus test_thorium, including the dry-run arm of every function that has one",
"Raised the gate's per-case timeout from the jail's 120s default to 600s in shell_coverage_bar.sh, because the suite is now 93s bare and kcov instrumentation roughly doubles that",
"Removed thorium_repairs.sh from meta/shell-coverage-allowlist.txt, taking it from 89 to 88 entries"
],
"verification": [
{
"command": "shell_coverage_jail.sh --subject .../tests/run_all.sh --measure thorium_repairs.sh --min 100 --fail-on-case-error",
"result": "pass",
"evidence": "thorium_repairs.sh: 75/75 lines = 100.00%, exit 0."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh .../thorium_repairs.sh, AFTER deleting its allowlist line",
"result": "pass",
"evidence": "'every checked shell library meets the coverage bar', EXIT 0. Checked after removal so the pass cannot be coming from the exemption."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh .../bt_audio.sh .../arch_cpu.sh",
"result": "pass",
"evidence": "EXIT 0. Before the timeout fix bt_audio.sh FAILED the gate while measuring a clean 127/127 -- the jail printed the number and then 'warn: case timed out after 120s', and --fail-on-case-error turned that into a non-zero exit."
},
{
"command": "time bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh",
"result": "pass",
"evidence": "All fixes lib test files passed; 93s bare with the two new files (76s before them). That runtime against the 120s default is what the fix addresses."
},
{
"command": "shellcheck + shfmt on the harness, both test files and shell_coverage_bar.sh",
"result": "pass",
"evidence": "No findings, zero suppressions."
},
{
"command": "shell_coverage_jail.sh --timeout 600s --measure bt_audio.sh, with test_arch_sysctl.sh temporarily restored",
"result": "fail",
"evidence": "bt_audio.sh: 'kcov instrumented no lines', with NO timeout warning at 600s. This rules out runtime as the cause of that file's contagion and confirms the quarantine in b2779059 was correct for the reason its header states."
}
],
"risks": [
"A genuine hang now takes 10 minutes per pass to surface instead of 2. test_thorium's `read -r -p` is exactly that shape, which is why every case feeding it supplies stdin explicitly.",
"The gate's timeout is now the binding constraint on how large this suite can grow; at 93s bare it has roughly a 3x margin left.",
"test_thorium backgrounds a stub that sleeps 30s so the process is still alive when the lib's `kill -0` runs; a much slower machine could in principle race it."
],
"rollback": [
"git revert the commit, then re-add the thorium_repairs.sh line to meta/shell-coverage-allowlist.txt",
"Note the revert also restores the 120s gate timeout, which will make bt_audio.sh fail the gate again on this suite; re-run check_shell_coverage.sh on bt_audio.sh to confirm that is understood"
]
}

View File

@ -0,0 +1,36 @@
"""The :class:`AppIcon` record, split out of :mod:`apps` for the 250-line cap."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
@dataclass(frozen=True)
class AppIcon:
"""One app's icon configuration.
Parameters:
key (str): Short identifier used on the command line.
repo (Path): Flutter project root, i.e. the directory holding pubspec.yaml.
accent (str): Hex accent colour. Shared across every app in the family
(the `unified-design-system` accent) — not app-specific.
glyph (str): Glyph name from :data:`~python_pkg.app_icons.glyphs.GLYPHS`.
icon_name (str): Basename for Linux hicolor/desktop installation.
linux (bool): Whether the app has a Linux desktop target.
"""
key: str
repo: Path
accent: str
glyph: str
icon_name: str
linux: bool
@property
def asset_dir(self) -> Path:
"""Return the directory holding this app's icon source assets."""
return self.repo / "assets" / "icon"

View File

@ -174,3 +174,20 @@ M 512 640 A 40 40 0 0 0 512 720 A 40 40 0 0 0 512 640 Z"/>"""
_HOUSE_TICK = """\
<path d="M 272 752 L 272 480 L 512 280 L 752 480 L 752 752 Z"/>
<path d="M 400 560 L 480 640 L 630 500"/>"""
# Umbrella: a filled canopy with a scalloped hem over a stroked J-handle -- the
# app answers exactly one question, "take it or not". Filled rather than
# stroked because a stroked dome with ribs reads as a jellyfish at 48dp.
#
# Geometry, inside SAFE_BOX (232..792) and clear of MIN_NEGATIVE_SPACE=36:
# canopy dome r=240 about (512,512): x 272..752, top y=272; hem is three
# r=80 scallops bulging up to y=432.
# handle shaft x=512 from the hem (y=440) to y=700, then a hook r=56 about
# (456,700): ink reaches y=792 and x=364; the hook's inner opening
# is 112-72 = 40 wide.
_UMBRELLA = """\
<path fill="{{ACCENT}}" stroke="none" d="\
M 272 512 A 240 240 0 0 1 752 512 A 80 80 0 0 0 592 512 \
A 80 80 0 0 0 432 512 A 80 80 0 0 0 272 512 Z"/>
<path d="M 512 440 L 512 700 A 56 56 0 0 1 400 700"/>"""

View File

@ -9,36 +9,12 @@ own accent.
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Final
from python_pkg.app_icons._app_icon import AppIcon
@dataclass(frozen=True)
class AppIcon:
"""One app's icon configuration.
Parameters:
key (str): Short identifier used on the command line.
repo (Path): Flutter project root, i.e. the directory holding pubspec.yaml.
accent (str): Hex accent colour. Shared across every app in the family
(the `unified-design-system` accent) — not app-specific.
glyph (str): Glyph name from :data:`~python_pkg.app_icons.glyphs.GLYPHS`.
icon_name (str): Basename for Linux hicolor/desktop installation.
linux (bool): Whether the app has a Linux desktop target.
"""
key: str
repo: Path
accent: str
glyph: str
icon_name: str
linux: bool
@property
def asset_dir(self) -> Path:
"""Return the directory holding this app's icon source assets."""
return self.repo / "assets" / "icon"
__all__ = ["APPS", "AppIcon", "get_app"]
_HOME = Path.home()
@ -219,6 +195,14 @@ APPS: Final[dict[str, AppIcon]] = {
# it already has, served on loopback.
linux=False,
),
AppIcon(
key="umbrella",
repo=_HOME / "src/umbrella",
accent="#B8862E",
glyph="umbrella",
icon_name="umbrella",
linux=False,
),
)
}

View File

@ -37,6 +37,7 @@ from python_pkg.app_icons._glyph_art_more import (
_PUNCH_CARD,
_REMOTE_WAVE,
_TRACK_BARS,
_UMBRELLA,
)
@ -90,6 +91,7 @@ GLYPHS: Final[dict[str, Glyph]] = {
),
Glyph("bowl-star", "Filled star above an open bowl", _BOWL_STAR),
Glyph("house-tick", "House outline with a tick inside", _HOUSE_TICK),
Glyph("umbrella", "Filled umbrella canopy over a J-handle", _UMBRELLA),
Glyph(
"remote-wave",
"Handset with punched buttons under a broadcast arc",