diff --git a/docs/superpowers/contracts/app-icons-daily-limits-hourglass-2026-10-09.json b/docs/superpowers/contracts/app-icons-daily-limits-hourglass-2026-10-09.json new file mode 100644 index 00000000..9890e993 --- /dev/null +++ b/docs/superpowers/contracts/app-icons-daily-limits-hourglass-2026-10-09.json @@ -0,0 +1,15 @@ +{ + "title": "app_icons: daily_limits gets the hourglass glyph in the shared family", + "objective": "The daily-limits phone app ships a family launcher icon (charcoal #211D1B field, shared accent #B8862E) instead of the stock Flutter icon, with no change to any other app's icon.", + "acceptance_criteria": [ + "`generate --app daily_limits --android` writes assets/icon and the Android mipmaps for ~/src/daily-limits/app", + "The registry keeps one accent (#B8862E) and one field (style.BACKGROUND) for every app", + "The diff to python_pkg/app_icons is additive only, so every other app renders byte-identically", + "python_pkg/app_icons tests pass at 100% line and branch coverage; pre-commit is clean" + ], + "out_of_scope": [ + "A per-app background or accent override (decided 2026-10-09: the generator's shared colours are right)", + "Committing the daily-limits app itself" + ], + "verifier": ".venv/bin/python -m pytest -q python_pkg/app_icons --cov=python_pkg/app_icons --cov-branch; pre-commit run" +} diff --git a/docs/superpowers/evidence/app-icons-daily-limits-hourglass-2026-10-09.json b/docs/superpowers/evidence/app-icons-daily-limits-hourglass-2026-10-09.json new file mode 100644 index 00000000..100ebaf1 --- /dev/null +++ b/docs/superpowers/evidence/app-icons-daily-limits-hourglass-2026-10-09.json @@ -0,0 +1,41 @@ +{ + "intent": "Give the daily-limits phone app an on-style launcher icon: an hourglass (a daily allowance that runs out) in the shared accent on the family charcoal.", + "scope": [ + "python_pkg/app_icons/_glyph_art_more.py, glyphs.py: the `hourglass` glyph", + "python_pkg/app_icons/apps.py: the `daily_limits` registry entry", + "python_pkg/app_icons/tests/test_registry.py: no glyph reused across apps; hourglass sand takes the accent marker" + ], + "changes": [ + "New `hourglass` glyph: capped bars, crossed sides, sand filled in the accent so the monochrome layer recolours it", + "daily_limits registered with accent #B8862E and the default style.BACKGROUND field", + "Invariant test: apps are told apart by glyph, so a new app may not reuse one (track-bars is the pre-existing exception)" + ], + "verification": [ + { + "command": "PATH=~/sdk/fvm/versions/3.47.7/bin:$PATH python3 -m python_pkg.app_icons generate --app daily_limits --android", + "result": "pass", + "evidence": "daily_limits: wrote 6 asset files; regenerated Android mipmaps; ic_launcher_background #211D1B" + }, + { + "command": "phone_deploy.sh ~/src/daily-limits/app (adb install -r on 23181JEGR08034)", + "result": "pass", + "evidence": "Install Success; the APK's mipmap-xxxhdpi ic_launcher.png is byte-identical to the generated one (hourglass in #B8862E on #211D1B); confirmed by kuhy" + }, + { + "command": ".venv/bin/python -m pytest -q python_pkg/app_icons --cov=python_pkg/app_icons --cov-branch", + "result": "pass", + "evidence": "47 passed; every python_pkg/app_icons module 100% line and branch" + }, + { + "command": "git diff python_pkg/app_icons | grep -c '^-[^-]'", + "result": "pass", + "evidence": "0 removed lines: renderer and other registry entries untouched, so other apps' icons are byte-identical" + } + ], + "risks": [ + "`generate` runs `dart run`, which needs a Dart SDK satisfying the app's ^3.13.5; the system dart 3.13.3 fails, so put the fvm 3.47.7 SDK first on PATH" + ], + "rollback": [ + "git revert this commit; daily_limits disappears from the registry and the glyph library" + ] +} diff --git a/docs/superpowers/evidence/hosts-guard-rollback-fresh-install-2026-08-24.json b/docs/superpowers/evidence/hosts-guard-rollback-fresh-install-2026-08-24.json deleted file mode 100644 index 0e386a08..00000000 --- a/docs/superpowers/evidence/hosts-guard-rollback-fresh-install-2026-08-24.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "intent": "Give a freshly installed machine a working escape hatch from the hosts file-guards. Until now --rollback refused to run on exactly the machines the previous commit created.", - "scope": [ - "linux_configuration/fixes/lib/hosts_guard_rollback.sh only.", - "Follow-up to 4c426eae, which made hosts-blocker's install.sh register the guard-lib instances directly.", - "Non-goal: changing what the migration path records; a migrated machine keeps its existing legacy backup and restore behaviour unchanged." - ], - "changes": [ - "do_rollback no longer exits when /var/lib/guard-lib-migration is absent. That state dir is written by the one-shot migration, but a machine set up by hosts-blocker's install.sh has no legacy layer to back up and therefore no state dir -- so --rollback bailed out while the very instances it exists to remove were live. It now proceeds whenever a guard-lib instance is registered, warns that there is no legacy layer to restore, and still refuses when there is genuinely nothing to roll back.", - "After uninstalling the instances, re-assert chattr +i on /etc/hosts when no legacy unit did it. The existing re-assertion relied on starting hosts-guard.service / hosts-bind-mount.service, which do not exist on a fresh machine, so every systemctl cat skipped and the rollback left /etc/hosts with no immutable flag and no bind mount -- strictly less protected than before the rollback." - ], - "verification": [ - { - "command": "sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback --dry-run (pristine vmbox guest, guards installed by install_core_system.sh --all)", - "result": "pass", - "evidence": "DRYRUN_RC=0, reporting it would uninstall the hosts, nsswitch and resolved instances. Before this change the same command printed 'no rollback state at /var/lib/guard-lib-migration - nothing to roll back to' and exited 1." - }, - { - "command": "sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback (same guest)", - "result": "pass", - "evidence": "ROLLBACK_RC=0. Uninstalled file-guard hosts, nsswitch and resolved, then logged 're-asserted chattr +i on /etc/hosts (no legacy unit to do it)'." - }, - { - "command": "ls /etc/guard-lib/targets; lsattr /etc/hosts; findmnt /etc/hosts; wc -l /etc/hosts (after rollback)", - "result": "pass", - "evidence": "Only shutdown-schedule.conf remains, which belongs to a different feature and must not be touched. /etc/hosts shows ----i----------------- and still holds all 178410 lines; the bind mount is gone, which is the intended post-rollback state." - }, - { - "command": "shellcheck -x linux_configuration/fixes/lib/hosts_guard_rollback.sh", - "result": "pass", - "evidence": "Clean. An initial A && B || C form was rewritten as if/else after SC2015 pointed out the C branch can run when A succeeds, which matters because `run` is a no-op under --dry-run." - }, - { - "command": "bash ./linux_configuration/fixes/lib/tests/run_all.sh", - "result": "pass", - "evidence": "hosts_guard_rollback: 31 passed, 0 failed; whole suite exits 0. The first attempt failed one case: the refusal message is asserted verbatim by test_hosts_guard_rollback.sh:115, and the new no-instance branch had dropped the trailing 'to'. The wording was restored rather than the assertion loosened, since the test is checking the user-visible explanation." - } - ], - "risks": [ - "Rollback is now reachable in more situations, so an accidental invocation can unregister live guards. It still refuses when no instance is registered, and it keeps the canonical copy via --keep-canonical, so the file itself is never lost.", - "The re-assertion only covers /etc/hosts. nsswitch.conf and resolved.conf are left to their plugins, matching the behaviour the migration path already had." - ], - "rollback": [ - "git revert this commit; the previous behaviour (refuse without a state dir) returns.", - "After reverting, confirm with sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --status that the instance list matches expectations before relying on it." - ] -} diff --git a/python_pkg/app_icons/_glyph_art_more.py b/python_pkg/app_icons/_glyph_art_more.py index 06379c35..2e715750 100644 --- a/python_pkg/app_icons/_glyph_art_more.py +++ b/python_pkg/app_icons/_glyph_art_more.py @@ -207,3 +207,25 @@ _OPEN_BOOK = """\ """ + + +# An hourglass with sand still in the top bulb -- daily-limits, today's PC +# shutdown time and gaming budget: a daily allowance that runs out. Chosen over +# a clock-with-a-cap because `clock` is already wake_alarm_sync's glyph. +# +# Geometry, inside SAFE_BOX (ink x=284..740, y=236..788): +# caps bars at y=272 and y=752, x=320..704. +# glass straight sides (352,272)->(512,512)->(352,752) and mirror; the +# sides cross at the neck, so the waist is solid, not a sliver. +# sand filled to the glass centrelines, so it fuses with the outline: +# top bulb from the neck up to y=392, leaving an empty band 84 tall +# and 73+ wide below the cap; bottom bulb a pile up to y=680. Every +# gap clears MIN_NEGATIVE_SPACE=36. +_HOURGLASS = """\ + + + + + """ diff --git a/python_pkg/app_icons/apps.py b/python_pkg/app_icons/apps.py index e85d3e7b..a2594d20 100644 --- a/python_pkg/app_icons/apps.py +++ b/python_pkg/app_icons/apps.py @@ -211,6 +211,14 @@ APPS: Final[dict[str, AppIcon]] = { icon_name="book-guard", linux=False, ), + AppIcon( + key="daily_limits", + repo=_HOME / "src/daily-limits/app", + accent="#B8862E", + glyph="hourglass", + icon_name="daily-limits", + linux=False, + ), ) } diff --git a/python_pkg/app_icons/glyphs.py b/python_pkg/app_icons/glyphs.py index 80aa4b56..e1c86205 100644 --- a/python_pkg/app_icons/glyphs.py +++ b/python_pkg/app_icons/glyphs.py @@ -31,6 +31,7 @@ from python_pkg.app_icons._glyph_art import ( ) from python_pkg.app_icons._glyph_art_more import ( _BOWL_STAR, + _HOURGLASS, _HOUSE_TICK, _NOTE_ASCENDER, _OPEN_BOOK, @@ -94,6 +95,7 @@ GLYPHS: Final[dict[str, Glyph]] = { Glyph("house-tick", "House outline with a tick inside", _HOUSE_TICK), Glyph("umbrella", "Filled umbrella canopy over a J-handle", _UMBRELLA), Glyph("open-book", "Open book, two filled pages", _OPEN_BOOK), + Glyph("hourglass", "Capped hourglass with sand still in the top", _HOURGLASS), Glyph( "remote-wave", "Handset with punched buttons under a broadcast arc", diff --git a/python_pkg/app_icons/tests/test_registry.py b/python_pkg/app_icons/tests/test_registry.py index b21b8cc5..9ea3df65 100644 --- a/python_pkg/app_icons/tests/test_registry.py +++ b/python_pkg/app_icons/tests/test_registry.py @@ -45,6 +45,13 @@ class TestFamilyInvariants: assert len(app.accent) == 7 int(app.accent[1:], 16) + def test_no_glyph_is_reused(self) -> None: + """Apps are told apart by glyph; a new app must not borrow one.""" + shared_by_design = {"track-bars"} # kuhylog + betting_sim, pre-existing + used = [app.glyph for app in apps.APPS.values()] + reused = {glyph for glyph in used if used.count(glyph) > 1} + assert reused <= shared_by_design + def test_safe_box_fits_inside_canvas(self) -> None: assert style.SAFE_BOX < style.CANVAS assert style.MIN_NEGATIVE_SPACE == style.STROKE_WIDTH / 2 @@ -64,3 +71,14 @@ class TestFamilyInvariants: f"{glyph.name} paints style.BACKGROUND; cut the hole with " f'fill-rule="evenodd" instead' ) + + +class TestHourglassGlyph: + def test_daily_limits_uses_it(self) -> None: + assert apps.get_app("daily_limits").glyph == "hourglass" + + def test_sand_is_filled_in_the_accent(self) -> None: + """The sand takes the accent marker, so the monochrome layer recolours it.""" + body = glyphs.get_glyph("hourglass").body + assert f'fill="{style.ACCENT_MARKER}"' in body + assert 'stroke="none"' in body