hraness

an icon pipeline agents can read: manifest, provenance, immutable releases

icons as checked data, not pasted svg

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

Most products treat icons as artwork pasted into a repository by hand. That is fine for a blog and a liability for a product family. A pasted SVG has no provenance, no guarantee it is the same file on the favicon as on the app icon, and no defense against the class of SVG that is not an image at all: scripts, external references, embedded raster payloads. When an agent, a build, or a new product needs “the brand icon for this domain,” a directory of hand-placed files cannot answer.

The Hraness answer is to make icons data. One catalog declares each brand’s identity; checked, constrained source assets feed a compositor; every output is a registered derivative recorded in a manifest with hashes on both sides. The result is a pipeline that agents can read, because reading it requires no judgment: the catalog says what the icon is, the manifest says what was produced, and the check command says whether the two still agree.

icons are checked data

The rule in the monorepo behind this site is blunt: do not add emoji, mark, or domain maps to an app. Application code never decides what its own icon is. Identity lives in brands.yaml, the checked brand catalog, which owns each domain, display name, emoji, and any optional authored mark source. Every derivative, from a Next.js app/icon.png to a desktop assets/icon.png, is generated from that catalog and nothing else.

This is the same move a database migration makes: the schema is the source of truth and the rows are derived. An agent that needs to change a brand does not edit a PNG; it edits the catalog and regenerates. An agent that needs to read the current identity does not guess from assets on disk; it reads the catalog and the manifest, both of which are ordinary checked files.

one catalog, one source

The catalog sits at packages/brand-catalog/brands.yaml and is the only place a domain’s identity is declared. Two asset directories feed it, and both are constrained on purpose. assets/emoji/ holds committed, path-only duotone source for every non-null catalog emoji, regenerated from a local Apple fixture adapter so the emoji family is consistent across products. assets/marks/ holds checked, path-only authored marks for the cases where an icon should not derive from its emoji at all: a product with real authored artwork registers icon_source pointing at a mark path rather than accepting the emoji fallback.

The split matters because it keeps the honest cases honest. A brand that is genuinely “the 📷 emoji” says so in the catalog. A brand with authored geometry, like Slopcamera’s camera mark, says that instead, and the catalog records which is which. There is no third option where an app quietly ships a file nobody reviewed.

targets are inferred or registered

Targets are declared in exactly two places, matching the two ways a product can exist. vercel-projects.json already registers every Next.js deployment, so web targets are inferred from it automatically; adding a site to the portfolio adds its icon target for free. Non-web app roots cannot be inferred that way, so brand-icon-targets.json registers them explicitly, and only them. A utility subdomain that is not a separate public brand sets iconBrandDomain to an existing catalog domain and reuses that identity instead of duplicating it.

Each target kind has a fixed output contract:

target generated files treatment
Next.js app/icon.png, app/apple-icon.png 512px and 180px PNG, black background, 12.5% inset
Expo assets/icon.png, assets/adaptive-icon.png 1024px black full icon plus a transparent 25%-inset adaptive foreground
Desktop assets/icon.png 1024px black full icon, the platform-neutral master for packaging

Generation is three commands in the monorepo, run in order:

bun run emoji:brands
bun run --cwd packages/brand-assets emoji:export:brands
bun run --cwd packages/brand-assets brand-icons:generate

The first refreshes the catalog source, the second exports the emoji fixtures, and the third composes every registered target. The composition stage is portable and can run alone from a clean clone, which is what makes the check below meaningful.

the compositor is strict

The reason source assets are “path-only” is that the compositor enforces it. It accepts only inert path elements drawn in the checked two-color palette and rejects everything else: references, embedded images, scripts, styles, event handlers, unsupported elements, and off-palette fills. An SVG that carries behavior cannot become an icon, because the input format cannot express behavior.

This is a security boundary disguised as a format constraint, and it is the right place for it. Favicons and app icons are served at origin scope; a compromised or careless asset there is not a cosmetic problem. By making the accepted input a deliberately inert subset, the pipeline turns “is this SVG safe” from a review question into a parse.

The honesty rules extend to what does not ship. Raw strikes, faithful upscales, remasters, and hybrids stay local. Full-fidelity duotone vectors stay outside deployable public roots; public surfaces receive flattened previews and the registered PNG derivatives. Slopcamera’s registered mark, for the record, is a path-only duotone adaptation of repository-authored camera geometry, with the canonical full-color SVG owned in its own repository and the source SHA-256 recorded in the runbook. The mark, the emoji fallback, and the historical receipts are three different artifacts, and the pipeline keeps them distinct rather than letting one silently become another.

adding a brand or a target

Because every input is checked data, the recipe for a new product is mechanical. To change identity you edit brands.yaml: add or update the emoji, or, when the icon should carry authored artwork, commit a path-only mark under assets/marks/ and set icon_source to its domain-derived path. To add a target you either register a Next.js deployment in vercel-projects.json (which is already required for the site to exist) or add an expo/desktop root to brand-icon-targets.json. Then you point the non-web app’s platform configuration at the generated files, run the generation commands, and commit the outputs alongside the updated manifest.

Notice what is absent from that list: no step involves drawing, exporting from a design tool into a product repo, or choosing a size. The sizes are the contract; the compositor is the only renderer; the manifest is the only record. A new product gets a correct favicon, apple-touch icon, and app icon not because someone was careful but because there is no other output the pipeline can produce.

On the consumption side, the kit keeps rendering equally narrow. @hraness/ui’s Icon component renders checked marks inline at currentColor with aria-hidden decoration semantics, so the same constrained pipeline that produces a favicon also produces the glyphs inside buttons and links, and products never reach for a raw <img> or an inline SVG blob of their own.

the manifest makes it immutable

Everything above would still be a convention without brand-icons.manifest.json. The manifest records the recipe, the input hashes, the pixel toolchain, and the hash of every generated derivative, so the pipeline’s claim is checkable after the fact rather than trusted at the time. bun run brand-icons:check validates every PNG against its registered recipe and manifest; a drifted icon, a stale output, or an input that changed without regeneration fails the check.

That is the property the title means by “immutable releases.” An icon is not a file someone exported once; it is the output of a recorded recipe over hashed inputs, and the check can prove the outputs on disk match the recipe. For an agent, the whole pipeline reduces to three readable artifacts: brands.yaml says what the identity is, the target registries say where it ships, and brand-icons.manifest.json says what was actually produced and whether it still agrees. No repository spelunking, no “which of these seven icon.png files is current,” and no pasted SVG that turns out to be a script. The broader lesson is that provenance is a build property, not a documentation habit: if you cannot regenerate it, you do not own it.

sources

  • The monorepo behind this site (projects): packages/brand-catalog/brands.yaml and its assets/ sources, vercel-projects.json and brand-icon-targets.json as the target registries, brand-icons.manifest.json as the recorded recipe, and the brand-icons:generate / brand-icons:check commands.
  • slopcamera: the product whose registered mark is an authored duotone adaptation rather than an emoji derivative.
  • ui: the shared Icon component that renders checked marks as portable currentColor primitives.