diff --git a/docs/superpowers/evidence/app-icons-anvil-octoforge-2026-08-09.json b/docs/superpowers/evidence/app-icons-anvil-octoforge-2026-08-09.json new file mode 100644 index 00000000..f446baab --- /dev/null +++ b/docs/superpowers/evidence/app-icons-anvil-octoforge-2026-08-09.json @@ -0,0 +1,52 @@ +{ + "intent": "Give OctoForge (~/octoforge) an on-style launcher icon from the shared family, replacing the stock Flutter logo it was still shipping. Registers it as app #10 with a new anvil glyph.", + "scope": [ + "python_pkg/app_icons/glyphs.py: adds the _ANVIL fragment and its GLYPHS entry", + "python_pkg/app_icons/apps.py: adds the octoforge AppIcon registry entry", + "Non-goal: no change to style.py, the renderer, or any existing app's icon - a style change would restyle the whole family", + "Non-goal: no iOS assets; OctoForge cannot be built for iOS here (no Xcode)" + ], + "changes": [ + "Anvil drawn as a filled silhouette rather than a stroked outline: at the family's 72px stroke weight the narrow waist closes up solid at 48dp. Same reasoning as the existing quill-nib and receipt-split glyphs.", + "The horn is a separate wedge projecting off the face, tapering to a 40px blunt tip (above MIN_NEGATIVE_SPACE so it survives at launcher size). Two earlier drafts folded the taper into the face slab and rendered as a bump, leaving the shape reading as an I-beam.", + "Registered with linux=True: OctoForge has a real desktop target with a .desktop entry, unlike the phone-first apps that scaffolded linux/ but ship no entry.", + "Accent is the shared #B8862E, matching all nine existing apps - apps are distinguished by glyph shape, not colour." + ], + "verification": [ + { + "command": "pre-commit run --files python_pkg/app_icons/glyphs.py python_pkg/app_icons/apps.py", + "result": "pass", + "evidence": "All hooks Passed or Skipped, including ruff, mypy, pylint, bandit, 'pytest with coverage enforcement' and 'Block binary/image files'." + }, + { + "command": "python3 -m pytest python_pkg/app_icons/tests/ -q --cov=python_pkg.app_icons", + "result": "pass", + "evidence": "44 passed; every app_icons module at 100% (apps.py, glyphs.py, cli.py, generate.py, preview.py, render.py, style.py). Registry tests are data-driven so they cover the new entry." + }, + { + "command": "python3 -m python_pkg.app_icons generate --app octoforge --android --linux-out /desktop/icons", + "result": "pass", + "evidence": "Wrote 6 asset files, 8 hicolor PNGs and regenerated the Android mipmaps with no safe-box warning. All five mipmap-*/ic_launcher.png md5s changed from the stock Flutter values." + }, + { + "command": "python3 -m python_pkg.app_icons preview -o sheet.png", + "result": "pass", + "evidence": "Inspected the three rows (full size, circle-masked, 48dp). The anvil is legible at 48dp and sits with the family in weight and style." + }, + { + "command": "adb -s 23181JEGR08034 install -r app-debug.apk && open the app drawer", + "result": "pass", + "evidence": "Icon renders correctly in the launcher on the physical device, next to todo's checklist glyph on the same charcoal field." + } + ], + "risks": [ + "Glyph legibility is a visual judgement; it was checked on the preview sheet at 48dp and in the real device app drawer, not asserted by a test.", + "Committed from a temporary git worktree because the working tree held another session's in-flight kuhylog registry entry in the same hunk. Only the anvil and octoforge additions are in this commit; the kuhylog work is untouched and still uncommitted in the main checkout.", + "app-icon/SKILL.md still documents charcoal #1B1D21 and accent #4C7DF0, which the live generator and all ten registered apps contradict. Following the skill text literally would produce an off-palette icon. Not fixed here." + ], + "rollback": [ + "git revert the commit; it only adds a glyph and a registry entry, so nothing else depends on it.", + "After rollback, run python3 -m python_pkg.app_icons list and confirm nine apps and nine glyphs, then re-run the app_icons test suite.", + "OctoForge's already-generated icon assets live in its own repo and are unaffected by a revert here; regenerating them would then fail with 'unknown app octoforge', which is the expected signal." + ] +} diff --git a/python_pkg/app_icons/apps.py b/python_pkg/app_icons/apps.py index 345fbe46..70568f1b 100644 --- a/python_pkg/app_icons/apps.py +++ b/python_pkg/app_icons/apps.py @@ -130,6 +130,18 @@ APPS: Final[dict[str, AppIcon]] = { # the table, so it ships no desktop entry and gets no hicolor set. linux=False, ), + AppIcon( + key="octoforge", + # The Flutter app is a subdirectory of the repo: the repo root + # holds the pure-Dart core and the tooling alongside it. + repo=_HOME / "octoforge" / "octoforge" / "app", + accent="#B8862E", + glyph="anvil", + icon_name="octoforge", + # Unlike the other phone-first apps, this one is genuinely used on + # the desktop too, so it gets the hicolor set and a desktop entry. + linux=True, + ), ) } diff --git a/python_pkg/app_icons/glyphs.py b/python_pkg/app_icons/glyphs.py index de259835..7321816f 100644 --- a/python_pkg/app_icons/glyphs.py +++ b/python_pkg/app_icons/glyphs.py @@ -182,6 +182,36 @@ M 472 470 A 40 40 0 1 0 552 470 A 40 40 0 1 0 472 470 Z \ M 512 700 L 492 580 L 532 580 Z"/>""" +# A blacksmith's anvil: OctoForge is a GitHub client, and the forge is the +# name. Filled rather than stroked, for the same reason as the nib — a stroked +# anvil outline is a long thin horn plus a narrow waist, and at the family's +# 72px weight the waist closes up solid at 48dp. +# +# The obvious alternatives were rejected: an octopus (the Octocat) is both +# GitHub trade dress and a tangle of thin curves that blurs at launcher size, +# and a hammer alone collides with nothing in the family but reads as a generic +# tool rather than a forge. The anvil silhouette is unmistakable and shares no +# shape language with the other nine glyphs. +# +# Geometry: the body is a face slab (x=336..768, y=316..420), a waist +# (x=444..596, y=420..592) and a flared foot (x=304..716, y=592..708) — total +# ink 512x392 including the horn, inside SAFE_BOX once the renderer scales it. +# +# The horn is what makes this an anvil rather than an I-beam, so it is a +# separate wedge projecting left off the face, not a rounded corner on it: it +# leaves the slab at full 104px depth (x=336) and tapers to a 40px blunt tip +# at x=256, dropping the underside from y=420 to y=392 on the way. A sharp +# point would disappear at 48dp; 40px is above MIN_NEGATIVE_SPACE so it +# survives. The first two drafts folded the taper into the slab's left end and +# both read as a bump. +_ANVIL = """\ + """ + + GLYPHS: Final[dict[str, Glyph]] = { glyph.name: glyph for glyph in ( @@ -194,6 +224,7 @@ GLYPHS: Final[dict[str, Glyph]] = { Glyph("storage-box", "Isometric storage carton", _STORAGE_BOX), Glyph("receipt-split", "Torn receipt with a perforated split", _RECEIPT_SPLIT), Glyph("quill-nib", "Dip-pen nib with vent hole and slit", _QUILL_NIB), + Glyph("anvil", "Blacksmith's anvil with horn and flared foot", _ANVIL), ) }