Skip to content

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 ​

WhatHow
DesignDesignSync 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.
ImplementationDirect file measurement of apps/mobile/src/tiers/consumer/**, packages/domain/src/**, packages/i18n/src/index.ts
Server rulessupabase/migrations/20260905091500_v2_get_public_biodata.sql read directly, because a disclosure rule enforced only in the client is not enforced
Ledger and contractsnpm 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 ​

ClassStale build, not a gap
DesignThe opening composer: four routes (collection, words, own picture, off), kuldaivat offered first, up to three emblems, ordered
ImplementationBiodataHubScreen/OpeningSheet.tsx, landed e92c520 at 19:59 on 2026-09-08
The APKapp-release.apk mtime 17:18 on 2026-09-08
Contract9 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 ​

ClassExisting functionality, reusable as is
DesignUp 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
ImplementationPhotoBlock.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
Landed777f91b, 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 — PhotoSheet action keys cover, shown, up, down, remove
  • the first shown one is the cover — biodataPhotoOrder puts exactly one cover flag 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 to shown and a non-null mediaId

⚠ 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 ​

ClassMissing functionality (client only, no backend)
TrackerQRS-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) ​

#FindingWhy it is "incorrect" rather than "missing"Tracker
A1Five 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
A2The 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
A4Your 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
A5Create your card button has no onPress.Same class as A1, on the hub's only primary control.QRS-1203
A6The 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 profileabsentQRS-1207
Counters: Saved, Orders, Scansabsent (Saved/Orders are marketplace; Scans is not and reads the device history)QRS-1208
Group structure: Personal, Your activity, Preferences, Supportabsent (one flat card)QRS-1203
Personal group row My QR setuabsentQRS-1209
Personal group row Personal detailsabsentQRS-1205
Personal group row Your contact codeabsentQRS-1210
Activity rows Saved / Orders / Recently scannedabsent (first two marketplace; Recently scanned is not)QRS-1208
Merchant pitch cardabsentQRS-1203
Sign out with confirm and toastabsentQRS-1211
Views details, appearance, privacy, help, aboutabsentQRS-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 staleThe measurementClass
S1The 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.
S2The 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
S3The 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
S4The 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_SECTIONS16, of which 6 are market: true15 in packages/domain/src/consumer/home.ts
Non-market sections10: identity, stories, focus, today, quickMake, progress, yourCards, create, invites, weekRecap9 — invites is absent
Rendered todayidentity (pinned), stories, focus, quickMake, progress, yourCards, create
Contractconsumer-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 ​

DesignRoundLedger row?Class
prototype/consumer/InviteTemplates.dc.html45noneNew scope
prototype/consumer/Invitation.dc.html41, 45noneNew scope
prototype/consumer/TemplatePreview.dc.html46noneNew scope
prototype/consumer/BirthdayCard.dc.html41noneNew scope
prototype/setu-card/InvitationPage.dc.html41noneNew scope, web
prototype/setu-card/BirthdayCardPage.dc.html41noneNew scope, web
prototype/templates/ — 22 designs plus TemplateGallery43 to 48none (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) ​

WorkClass
AppLanguageRow inside LinkCard: three pills, the note, editor-scoped state seeded from content_languageNew client
Thread uiLang through the hub's label resolvers so it drives fieldLabel, groupTitle, sectionStatusLabel, stakeLabel, metrics and the suggest option listsNew 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 itCorrection to the contract
Add the missing App language contract rowCorrection to the contract
BackendNone — 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_overview projects the subject as id · first_name · owner_is_subject and does not carry it. Home and My QR setu read this one.
  • get_my_biodata uses to_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):

  1. 22 of the 440 consumerBiodata keys have no Marathi or Hindi — the same 22 in both, all of them explanatory copy (the field hints, and the why/tip pair for work, more and talk), 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.
  2. In Marathi the two rows print the SAME sentence, by design. uiLangNote for a non-English UI resolves to contentNever, 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 used getByText and 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.

#ItemState
1QRS-1207 — read identity and context, so a registered/guest distinction exists at all✅ done. get_my_context existed and was not being called
2QRS-1204 — branch on it; render the profile card✅ done, and mutation-proven: pinning the card back to guest fails the new regression case
3QRS-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
4QRS-1206 — remove Your areas (absent, per D-a)✅ done
5QRS-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
6QRS-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-3Status
get_my_contextexists
manage-account: update_email, change_password, delete_account, claim_slugexists
set_avatar, update_phone (OTP path, not a field write), export_datamissing — 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 ​

#DecisionRecommended
D-yDo 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-zPersist 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-aaSave 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 uiLang label 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 invites as 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.