Appearance
Consumer design to implementation gap assessment
Measured 2026-09-08 against design round 48, fetched live. Raised by the owner after reviewing an Android build: three items in the Marriage Biodata editor looked absent, and the Account/Profile implementation was being treated as correct against a stale reading of the design.
THE OWNER'S CORRECTION WAS RIGHT ABOUT THE THING THAT MATTERS
Account/Profile was being reported against a design reading that does not match prototype/consumer/Account.dc.html, and the screen ledger's own note for my-qr-setu describes three entry points that do not exist in the app. Both are recorded below with the measurement.
Two of the three biodata items were a different problem: they are built and correct, and the APK the owner tested was built two hours and forty minutes before they landed. That is a build staleness failure on my side, not a design gap, and it is why the fourth item (App language) was worth finding: it is genuinely absent.
0 · How this was measured, so every row can be rechecked
| What | How |
|---|---|
| Design | DesignSync live pull, 2026-09-08: SCREENS.md, prototype/consumer/Account.dc.html, prototype/consumer/Biodata.dc.html, prototype/consumer/consumer-data.js. Never a cached copy; the local mirror was measured stale (Biodata 09-07, ConsumerHome 09-06) and was not used. |
| Implementation | Direct file measurement of apps/mobile/src/tiers/consumer/**, packages/domain/src/**, packages/i18n/src/index.ts |
| Server rules | supabase/migrations/20260905091500_v2_get_public_biodata.sql read directly, because a disclosure rule enforced only in the client is not enforced |
| Ledger and contracts | npm run check:screens, screen-conformance.json, parity-contracts/consumer-*.json |
Design round drift. The screen ledger says "registry transcribed 2026-09-04" and the design is at round 48. Rounds 41 to 48 are almost entirely a new consumer surface (invitations, occasions, a template library), so the ledger's denominator of 51 approved screens predates it.
1 · The four items the owner raised
1.1 How the page opens — BUILT, and not in the build that was tested
| Class | Stale build, not a gap |
| Design | The opening composer: four routes (collection, words, own picture, off), kuldaivat offered first, up to three emblems, ordered |
| Implementation | BiodataHubScreen/OpeningSheet.tsx, landed e92c520 at 19:59 on 2026-09-08 |
| The APK | app-release.apk mtime 17:18 on 2026-09-08 |
| Contract | 9 of 11 opening rows pass; 2 blocked (QRS-1200 collection art does not exist on either side, QRS-1201 an own emblem stores a media id and the preview shows a slot) |
The tested APK also predates People and access (09cfadc, 18:59) and the release-length fix (3284c29, 18:16). Required change: rebuild before any further device review (R-1 below).
1.2 Photographs — BUILT, faithful, and correctly enforced on the server
| Class | Existing functionality, reusable as is |
| Design | Up to four slots in About; each with cover / show or hide / move / remove in its own sheet; the first shown one is the cover and is the only photograph a forwarded link carries |
| Implementation | PhotoBlock.tsx (289 lines) + PhotoSheet.tsx (197), mounted at SectionScreen.tsx:586 under section.id === 'about'; 36 copy keys across three languages; domain model packages/domain/src/biodata/photos.ts |
| Landed | 777f91b, 2026-09-07, so it was in the tested APK |
Every clause of the requirement is present:
- four slots —
profiles_photos_within_cap check (jsonb_array_length(photos) <= 4) - cover, show/hide, move earlier, move later, remove —
PhotoSheetaction keyscover,shown,up,down,remove - the first shown one is the cover —
biodataPhotoOrderputs exactly onecoverflag on index 0 - the cover is the only photograph a forwarded link carries — enforced in SQL, not in the client:
limit case when v_tier = 'released' then 4 else 1 end, ordered cover-first, filtered toshownand a non-nullmediaId
⚠ One real difference, and it is the design's seed data rather than a requirement. The artboard shows four dashed placeholders because the demo subject is seeded with four slot entries holding no image. A real new profile has zero slots, so the block renders its heading, the "no photographs" line and one add tile. The design agrees: photoList(B, subj, started) returns an empty list for a starting profile. No change required; recorded so the screenshot is not mistaken for a gap later.
1.3 App language — GENUINELY MISSING, and it is not a chip
| Class | Missing functionality (client only, no backend) |
| Tracker | QRS-1202 |
The design carries two language controls inside the link card, immediately after Copy link / Share with someone / Save as a picture, and the artboard comment names the distinction:
TWO LANGUAGES, TWO DIFFERENT THINGS. Above: the language the FAMILY writes in, which we never touch. Here: the language the APP shows its labels in, which opens on the family's own language rather than making them find a setting.
Row 1, "Written in mr": a pill with a globe. ⚠ It is a help affordance, not a chooser.scriptHelp(contentLanguage) decides whether it gets a chevron; the sheet is titled "Writing in mr" and says the family already types Marathi in WhatsApp, so write it there and paste it in, with a helpline link. Beside it: "We never translate your own words."
Row 2, "App language": a bordered card with three pills (mr / hi / en) and a note reading "Labels only. What you write stays in your language and is never translated."
⚠⚠ uiLang is not decoration: it drives every label on the hub — fieldLabel(id, uiLang), groupTitle(s.id, uiLang, w), sectionStatusLabel(stx, uiLang), stakeLabel(s.stake, uiLang), metrics(uiLang) and the suggest option lists. It is editor-scoped component state seeded from the biodata's content language (uiLang = st.uiLang || langForContent(subj.contentLanguage)), and picking a pill never writes back to the record or to the device.
What the implementation does instead, and why it is not equivalent. The hub has no language control at all (its render order is ViewTabs, JourneyCard, NextCard, SectionCard*, SeeAsTheySee, LookStrip, LinkCard, Declaration, HubFooter), and labels come from the global device locale via useT(). content_language is written once at creation from that same locale (useCreateBiodata.ts:46) and is then never shown and never changeable.
So a Marathi family on an English phone gets English labels over Marathi content, where the design gives them Marathi labels. The design's own seed comment calls the reverse case correct and says the page states it, which is the tell that this axis was designed deliberately.
⚠ This is an enumeration hole, not just an unbuilt control. The contract has a content_language_chip row (blocked, QRS-1103) whose note opens "Two languages, two different things" and then enumerates only the first one. There is no row for App language anywhere in the 82. A silent design omission inside the row written about it.
⚠ And content_language_chip is mis-blocked. It is marked blocked on QRS-1103 "needs the language sheet", as though the sheet were undesigned. The sheet is fully specified in the artboard (title, body, helpline link, note) and is a help sheet, not a chooser. It is buildable today.
1.4 Save as a picture — absent, honestly, and correctly
The design's link card has a third button. LinkCard.tsx:13 records why it is absent: rasterising needs react-native-view-shot, which is an app-size callout under the dependency policy (QRS-1065). Owner decision, not a defect.
2 · Account and Profile: the correction
The design is prototype/consumer/Account.dc.html, and its declared props are the denominator:
accountState: Registered | Guest | Loading | Error (4)
view: hub | details | areas | notifs | appearance | privacy | help | about (8)32 designed renderings. The implementation has 2 views and 1 state.
ts
// AccountScreen/index.tsx:42
type AccountView = 'hub' | 'notifs';
const view: AccountView = params.view === 'notifs' ? 'notifs' : 'hub';2.1 Incorrect implementation (not merely missing)
| # | Finding | Why it is "incorrect" rather than "missing" | Tracker |
|---|---|---|---|
| A1 | Five rows on the hub are inert. areas, appearance, privacy, help, about are SettingRows with no onPress. SettingRow shows a chevron only when onPress is set, so they render as flat rows that do nothing. | A visible control that teaches nothing and goes nowhere is the fifth rule's exact failure, and the presence-is-not-destination class again. Not an absence: an affordance that lies. | QRS-1203 |
| A2 | The guest banner is unconditional. function HubView() takes no arguments and always renders consumerAccount.guest and guestBody. | A registered consumer is told they are browsing as a guest. The accountState axis is not merely unimplemented, it is pinned to the wrong value. | QRS-1204 |
| A3 | ?view=details silently renders the hub. Anything not notifs collapses to hub. | A deep link, a notification tap or a back-navigation lands on the wrong screen with no signal. The fifth rule says render the state on the route, never bounce. | QRS-1205 |
| A4 | Your areas is present as a row. It is marketplace (D-a). | D-a says OFF means ABSENT. Rendering it, inert, is the one thing the decision forbids. | QRS-1206 |
| A5 | Create your card button has no onPress. | Same class as A1, on the hub's only primary control. | QRS-1203 |
| A6 | The screen reads only consumerPrefs. No identity or context query. | It therefore cannot render the profile card, the counters, or a registered/guest distinction. The gap is structural, so A2 cannot be fixed without this. | QRS-1207 |
2.2 Missing functionality, by design element
| Design element (hub) | Present? | Tracker |
|---|---|---|
| Profile card: avatar, name, email, "Confirmed account" chip, Edit profile | absent | QRS-1207 |
| Counters: Saved, Orders, Scans | absent (Saved/Orders are marketplace; Scans is not and reads the device history) | QRS-1208 |
| Group structure: Personal, Your activity, Preferences, Support | absent (one flat card) | QRS-1203 |
| Personal group row My QR setu | absent | QRS-1209 |
| Personal group row Personal details | absent | QRS-1205 |
| Personal group row Your contact code | absent | QRS-1210 |
| Activity rows Saved / Orders / Recently scanned | absent (first two marketplace; Recently scanned is not) | QRS-1208 |
| Merchant pitch card | absent | QRS-1203 |
| Sign out with confirm and toast | absent | QRS-1211 |
Views details, appearance, privacy, help, about | absent | QRS-1205 |
⚠ appearance is the one that compounds. It holds theme (Light/Dark/System) and app language (mr/hi/en). Those stores exist (themeStore, localeStore) and are wired elsewhere, so the view is glue over existing functionality rather than new capability, and it is also where the biodata's app language belongs at the account level.
2.3 Stale design and stale record
| # | What was stale | The measurement | Class |
|---|---|---|---|
| S1 | The ledger's my-qr-setu note asserts it is "reached from three entries that already point here (the Your QR setu heading on Home, the Make-your-own tile in the More sheet, and a My QR setu row atop the Account hub)". | Zero of the three exist. A grep for my-qr-setu across apps/mobile/src returns exactly one hit, a comment in SavedScreen explaining that the CTA is absent because the screen is not built. | Stale record read as an implementation fact. The note is transcribed from the design registry, where all three sentences are true of the design. It then propagated into project-state.md. |
| S2 | The ledger's account note: "hub + notifs built; the rest are rows onto the same module." | The rest are inert rows, which is not a partial state. | Stale note |
| S3 | The earlier record's account note "Stub-only, in-memory" | Already corrected: account, consumerPrefs, consumerActivity, consumer are all Supabase-bound and barrel-bound. | Corrected 2026-09-08 |
| S4 | The plan's Home section count (9 non-market) | The design has 10: round 45 added invites. | Stale plan |
⚠ S1 is the generalisable one and it is why the owner's complaint is correct in substance. A ledger note that describes the DESIGN in the present tense, sitting in a field that is read as a statement about the CODE, is indistinguishable from a measurement. It is the same register failure QRS-871 was written about: measured and unmeasured claims written identically.
3 · ConsumerHome, measured against round 48
Better than the record said, and short by one whole section.
| Design (round 48) | Implementation | |
|---|---|---|
HOME_SECTIONS | 16, of which 6 are market: true | 15 in packages/domain/src/consumer/home.ts |
| Non-market sections | 10: identity, stories, focus, today, quickMake, progress, yourCards, create, invites, weekRecap | 9 — invites is absent |
| Rendered today | identity (pinned), stories, focus, quickMake, progress, yourCards, create | |
| Contract | consumer-home.json 62 rows: 53 pass, 4 gap, 5 blocked, and no invites row |
today and weekRecap remain correctly absent: they read progress check-ins and coach announcements that have no schema, and the self-hiding rule covers them.
⚠ invites is new scope, not a missing row. Round 45 gave Home an invitations discovery surface, and rounds 46 to 48 grew it: one auto-scrolling rail per occasion at a ceiling of four rails, each with icon, name, Marathi name, live design count and its own See more, drawing real poster art from template-catalog.js. It cannot be built without the library behind it, so it is an owner decision about scope (D-y below), not a defect. Tracker QRS-1212.
4 · What rounds 41 to 48 added that no plan or ledger contains
| Design | Round | Ledger row? | Class |
|---|---|---|---|
prototype/consumer/InviteTemplates.dc.html | 45 | none | New scope |
prototype/consumer/Invitation.dc.html | 41, 45 | none | New scope |
prototype/consumer/TemplatePreview.dc.html | 46 | none | New scope |
prototype/consumer/BirthdayCard.dc.html | 41 | none | New scope |
prototype/setu-card/InvitationPage.dc.html | 41 | none | New scope, web |
prototype/setu-card/BirthdayCardPage.dc.html | 41 | none | New scope, web |
prototype/templates/ — 22 designs plus TemplateGallery | 43 to 48 | none (a new top-level section) | New scope |
Six occasions are live in the catalog (marriage, sakharpuda, shop opening, vastu shanti, barsa, birthday and anniversary), three more are planned rows. PERSONAL_KINDS is now four (biodata, invite, birthday, intro) and round 45 filters invite out of the Make-your-own grid because Home carries the rails instead.
⚠ This is not being absorbed silently. It is a second consumer product surface arriving after the end-to-end plan was approved on 2026-09-04, and the launch scope is the owner's call (D-y).
5 · Required changes, classified and sequenced
Nothing below is started. Ordered so that each item is unblocked when it is reached.
R-1 · Rebuild the device build (client, no code)
The tested APK predates three feature commits. Rebuild before any further device review, and report the commit the bundle carries rather than the build's exit code.
R-2 · Biodata language, the two rows (client only, no backend)
| Work | Class |
|---|---|
AppLanguageRow inside LinkCard: three pills, the note, editor-scoped state seeded from content_language | New client |
Thread uiLang through the hub's label resolvers so it drives fieldLabel, groupTitle, sectionStatusLabel, stakeLabel, metrics and the suggest option lists | New client, and it is the load-bearing half |
WrittenInChip plus the help sheet ("Writing in mr", the WhatsApp paste route, the helpline link) | New client; scriptHelp is a domain port |
Retire the content_language_chip block: the sheet is designed, so QRS-1103 does not gate it | Correction to the contract |
| Add the missing App language contract row | Correction to the contract |
| Backend | None — but verified rather than assumed, and the check nearly went the other way |
⚠ THE READ PATH WAS THE THING TO CHECK, AND IT SPLITS. content_language exists on biodata.subjects and the Edge Function validates it, but that proves only that it can be WRITTEN. Measured on both owner reads:
get_my_biodata_overviewprojects the subject asid · first_name · owner_is_subjectand does not carry it. Home and My QR setu read this one.get_my_biodatausesto_jsonb(s.*), so it carries every subject column including this one. The editor reads this one, which is why R-2 needs no migration.
That to_jsonb is CLAUDE.md's forward-compat rule paying off years after it was written: a column list there would have needed editing by this change. A migration was one step from being written against the narrow projection — the fix would have been real, additive and completely unnecessary. Recorded because the general form recurs: "the column exists" and "the client can read it" are different claims, and only the second one lets a screen be built.
⚠ One decision inside R-2 (D-z). The design keeps uiLang in component state, so it resets on every open. That is right for a prototype and questionable for a family filling a biodata over days. Recommended: persist it per biodata alongside content_language, which is additive and needs no migration if it rides in an existing bag. Flagged rather than decided.
Two things the build turned up that are worth carrying (both measured, neither a blocker):
- 22 of the 440
consumerBiodatakeys have no Marathi or Hindi — the same 22 in both, all of them explanatory copy (the field hints, and the why/tip pair forwork,moreandtalk), while every label, status word, stake word, section title and value map is done. So a Marathi family gets a fully Marathi form with three English explanations in it.t()falls back silently and that is correct here — the sentence renders in English rather than showing a key, so nothing is unreadable. QRS-1213. ⚠ It was invisible until this control existed: before it, the catalog followed the device, so nobody could tell "not translated" from "not selected". A control that reveals a data gap is the control working. - In Marathi the two rows print the SAME sentence, by design.
uiLangNotefor a non-English UI resolves tocontentNever, which is exactly what row one prints beside its chip. In English they are two distinct sentences. Recorded because the first version of the test usedgetByTextand failed on it: the product was right and the assertion was wrong.
R-3 · Account, the six incorrect behaviours before the five missing views
Fixing presentation first would leave A2 and A3 lying to a registered user on more surfaces.
| # | Item | State |
|---|---|---|
| 1 | QRS-1207 — read identity and context, so a registered/guest distinction exists at all | ✅ done. get_my_context existed and was not being called |
| 2 | QRS-1204 — branch on it; render the profile card | ✅ done, and mutation-proven: pinning the card back to guest fails the new regression case |
| 3 | QRS-1205 — make ?view= total over the eight designed values; render the state on the route | ✅ done. The switch names all eight, so a ninth is a compile error rather than a silent fallback |
| 4 | QRS-1206 — remove Your areas (absent, per D-a) | ✅ done |
| 5 | QRS-1203 — the four named groups, and no row without a destination | ✅ done. Only Preferences renders, because it is the only group with a member that has a destination; a heading over an empty card would be worse |
| 6 | QRS-1211 — Sign out with confirm and toast | ⛔ blocked on a tier boundary, and the assessment above was wrong about it |
⚠⚠ CORRECTION TO THIS PLAN'S OWN ROW 6. It said "reuse the merchant useSignOut", which is not possible: useSignOut lives in tiers/user/features/settings/hooks/useAccountActions.ts and consumer ⇎ user is lint-gated in guardrails.js (all six directions, derived from a TIERS array). So the row named a reuse the architecture forbids.
And copying it would be worse than the boundary. It carries the PIN teardown, which is a real correctness rule: a PIN is per device, so without clearing it person A sets a PIN and signs out, person B signs in on the same phone and declines the offer, and B is then asked for A's PIN — recoverable only through Forgot PIN and completely inexplicable (QRS-1001). Two copies of that is the duplicate-source-of-truth class on a control that locks people out of their own accounts.
The right answer is to lift it to src/lib, where useMyContext already lives for the same reason (app-level, needed by every tier). Both tiers then consume one implementation. That is a small refactor which touches merchant code, so it is its own change rather than a rider on this one.
⚠ What its absence means today, stated because it is not cosmetic: a consumer who signs in on a shared handset has no way to sign out from the UI at all.
| Backend needed for R-3 | Status |
|---|---|
get_my_context | exists |
manage-account: update_email, change_password, delete_account, claim_slug | exists |
set_avatar, update_phone (OTP path, not a field write), export_data | missing — gates parts of details and privacy |
So details ships partially: name and email today, avatar and phone need backend (QRS-1016 and the OTP path). appearance, help and about need no backend at all and are pure glue.
R-4 · My QR setu (client, plus one RPC)
Unchanged from the plan's F3 except one correction: it has no entry point today, so the three entries have to be built with it rather than assumed. Needs get_my_biodata_overview().
R-5 · Re-transcribe the consumer ledger from round 48
The same QRS-1017 work, recurring, because the design moved eight rounds. Six consumer screens and a new top-level section have no row, so check:screens is green over a stale denominator.
R-6 · Decisions owed before scope is settled
| # | Decision | Recommended |
|---|---|---|
| D-y | Do invitations and the occasion library enter the consumer launch, or follow it? | Follow it. The biodata journey is not finished on a device yet, and this is 25 or more designs, a public page family and a catalog. Home's invites section stays absent by the self-hiding rule, which costs nothing and lies about nothing. |
| D-z | Persist the biodata editor's label language, or keep it per session as the design does? | Persist per biodata. A family fills this over days. |
| D-aa | Save as a picture and the maker image export: install react-native-view-shot? | Owner call under the app-size policy (QRS-1065). |
6 · What this changes in the plan of record
consumer/end-to-end-plan.md stands, with these amendments:
- F4 gains the two language rows and the
uiLanglabel axis (R-2). Previously the plan carried only a "content-language chip", matching the contract's single row, so it inherited the same omission. - F11 is rewritten against
Account.dc.html: eight views by four states, and the six incorrect behaviours are listed before the missing views, because presentation work over A2 and A3 would spread the lie. - F2 gains
invitesas a section that is absent by decision (D-y) rather than unenumerated. - F3 drops the assumption that three entry points exist.
- A new section for rounds 41 to 48 (invitations, occasions, templates) held behind D-y.
- R22 recurs: the ledger is stale again, so R-5 is a standing item after every design round, not a one-off.