Basefox v0 · Section A follow-up · 25 Aug 2026
Acting on the two notes from the Section A test pass: the Home redesign (mockup 1), the card system (mockup 2), and the search-intent problem.
All six types and three densities from mockup 2 exist as one component. The five-part anatomy (eyebrow, headline, descriptor, visual, status) is the same shape at every density, and boards now compose cards instead of hand-rolling rows.
| Type | Leads with | What triggers it in the data | Live in v0? |
|---|---|---|---|
| A · Brand offer | the value | a usable best route | yes |
| B · Exchange event | the event | a live ExchangeEvent | yes |
| C · Category event | the category | a live event seen from a category board | yes |
| D · New benefit | what’s new | a newRoute change inside 14 days | yes |
| E · Ending soon | urgency | endsOn or event ends inside 3 days | events only |
| F · Fallback | what we do know | no usable route, or a verified negative | yes |
Type E fires from exchange events today. It cannot fire from a brand offer yet, because nothing extracts an offer expiry — see the gap below.
Four tiers, best first. The app asks for the best it could have and takes the best that exists, so a missing file costs richness and never correctness.
| Tier | Source | Where it comes from | Ships today |
|---|---|---|---|
| 1 · Campaign | from source | registry/assets/events/ | no files |
| 2 · Brand | curated by Basefox | registry/assets/brands/ | no files |
| 3 · Category | Basefox library | SF Symbol per category | yes |
| 4 · Generic | Basefox abstract | drawn in-app | yes |
larchpipe assets sync copies whatever is in the registry into the app’s
asset catalog, names what is still missing, and flags files nothing references. It reports
0 of 13 today and is safe to run against an empty directory.
Sampled from your card-system mockup and assigned per brand in brands.yaml.
Every accent comes from this fixed set, so no card can introduce a colour the rest of the
app does not already speak.
Your instinct was right, and the old behaviour proved it: typing “hiking boots”
matched nothing and dropped to the empty state. The index still holds brands and categories
only. A new productTerms lexicon bridges the two.
The old board had four mutually-exclusive chips that mixed sorting with filtering, so choosing “Online only” silently discarded the discount ranking. They are now two separate controls, and the filters combine.
6 of 9 brands line states the effect, so a filter that hides most
of the board says so instead of looking like a thin category.Before this work, grep for image/logo/asset/photo across
/pipeline, /registry, and the spec returned zero hits. The
“Visual Asset Hierarchy” box in your mockup described a system with no supply
of any of its four tiers.
I built tiers 3 and 4 so the app is complete and intentional-looking today, and wired
tiers 1 and 2 end to end. What remains is the licensed material itself: 10 brand
lockups and 3 campaign heroes. Specs and filenames are in
registry/assets/README.md; drop the files in, run
larchpipe assets sync, rebuild, and every card upgrades on its own.
Route.endsOn now exists in all four halves of the contract, but no source
extracts it — the Tier-2 extractor never asked for an expiry date. I left it
null across the bundled snapshot rather than inventing dates in shipped
sample data. Teaching the extractor to pull expiry is the next step if
“Ends tomorrow” matters for launch.
| Area | Files | Change |
|---|---|---|
| Contract | brands.yaml, categories.yaml, events.yaml, models.py, registry.py, dataset.schema.json, Models.swift, dataset.json | Six new fields carried through all four mirrors: accent, logo, glyph, productTerms, hero, endsOn |
| Card system | Views/Cards/ (new: CardModel, CardView, CardBuilders, AssetResolver) | The six types, three densities, the anatomy, and the four-tier resolver |
| Home | HomeView.swift | Hero event cards, circular glyph tiles, compact change feed, one card tap handler |
| Search | Domain.swift | CategoryMatch with the matched term; scored name/synonym/product-term ranking |
| Filters | BoardFilter.swift, FilterBar.swift, CategoryBoardView.swift (new + rewired) | Sort split from filters; route-level filtering with best-route re-pick |
| Assets | larchpipe/assets.py, cli.py, registry/assets/ | larchpipe assets sync plus the file spec |
| Tests | CardSystemTests.swift, test_assets.py | 27 new Swift tests, 11 new pipeline tests |
| Spec | basefox-spec-v03.md | New §Card system; §Screens, §Design system, §Data model, and the search order updated |
One tidy-up worth naming: FlowChips was left with no callers after the
category chips became tiles, so it is gone. FlowLayout survived and moved to
Components.swift with the other shared primitives.
ChangeItem carries a routeID but no event id, so an
eventOpened row cannot reach its event’s category glyph and shows
the house mark instead.High on the parts with a clear right answer: the four-tier resolver, splitting sort from filters, filtering at the route level, and keeping the contract mirrored. Lower on two judgement calls that only a real asset will settle — the single stacked hero variant, and how a licensed hero image behaves in a short band. The lexicon’s coverage is a guess until someone uses it.