Appearance
Marriage Biodata editor — systematic audit
Measured 2026-09-07 against develop. This is the record the owner asked for: every section, every field, once — rather than rediscovering the same class of defect on each Android build.
Two primary actions in this feature cannot succeed today
No family can publish a biodata, and no family can add a family member. Both were verified by running the real code, not by reading it, and both shipped with green tests. See §1.
How this was produced. Four independent enumerations over disjoint file sets (family/people · drawer structure and "More" · the end-to-end persistence path · the approved design artboard), each reporting only measured facts with citations, plus direct verification by the author of every P0 before it was recorded here. Where something could not be established it says so.
The sorting rule, which decides who fixes what:
| Verdict | Meaning | Resolved by |
|---|---|---|
| FIDELITY | the design specifies it; the implementation diverged | code, no design round |
| DESIGN GAP | the design does not answer it, or the owner wants what it deliberately refused | Claude Design first |
1 · The two dead primary actions
P0-A · Publish is impossible for every family
age is declared required: true, kind: 'derived'. The field sheet renders a note and no Save button for a derived field — correctly, because it is meant to be computed. But withBiodataDerived(), the only function that computes it, has zero call sites outside its own test file. Nothing derives it and nothing can type it.
Run against the real journey function, with every required field a family can fill:
required fields : firstName · gender · age(derived) · marital · city
education · profession · dob · phoneNo
left unfillable : age
journey.ready : false → publishable is false, always
with age present: true → age is the only thing in the wayWhy the gate said otherwise: Publish.test.tsx builds its fixture by mapping every required field to the literal 'x', age included. It synthesises a state the product cannot reach and then asserts publish works from it.
The design already says how this should work: "THE DATE IS THE SOURCE OF TRUTH AND THE AGE IS ALWAYS DERIVED… it is never stored and never typed: it is computed from the date every time anything reads it." So age is a computed header chip in the dob sheet, not a value in the bag — FIDELITY, and the fix is the same one that rebuilds the date control.
P0-B · The add-person button is rejected on every tap
PeopleSheet's add() constructs a person with an empty name and saves immediately. The Edge Function does str(p.name, "a person's name", 1, 48), which trims and requires at least one character. Every add returns 400 and the family sees "Could not save that".
Because the add button is the only way a person is created, every downstream control — name, detail, seniority, reorder, remove — is unreachable. The sheet was written against a contract that already refused its own opening move: the name rule landed in the write-API commit, before the sheet that violates it.
A second, independent bug would still hide the row: the editor reads people through biodataPeopleOf, which drops every row with a blank name, and there is no optimistic update.
2 · Every field, measured
Generated from the domain port (biodataFieldInput), never transcribed. "Renders today" is measured from FieldInput.tsx; the verdict compares it against Biodata.dc.html.
| Field | Tier | Declared input | Options | Renders today | Verdict |
|---|---|---|---|---|---|
| About | |||||
opening | basic | text | — | single-line box | ✅ correct |
intro | basic | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
firstName ·req | basic | text | — | single-line box | ✅ correct |
gender ·req | basic | choice | 2 | pills, commit on tap | ✅ correct — the design forbids free text here |
age ·req | basic | derived | — | a sentence, no control | ⚠ design opens the dob sheet; ours shows a dead note |
marital ·req | basic | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
city ·req | basic | suggest | ⚠ 0 (needs areas) | text box then unlabelled pills | 🔴 bare text box — the areas source is never passed |
photo | basic | derived | — | a sentence, no control | 🔴 shares age's note, so it reads "Worked out from the date of birth" — false, and the sheet has no action |
fullName | released | text | — | single-line box | ✅ correct |
height | released | choice | 21 | pills, commit on tap | ✅ correct — the design forbids free text here |
bloodGroup | released | choice | 9 | pills, commit on tap | ✅ correct — the design forbids free text here |
| Work | |||||
education ·req | basic | suggest | 20 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
profession ·req | basic | suggest | 17 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
employer | released | text | — | single-line box | ✅ correct |
experience | basic | choice | 10 | pills, commit on tap | ✅ correct — the design forbids free text here |
linkedin | released | text | — | single-line box | ✅ correct |
eduPrior | basic | suggest | 21 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
| Family | |||||
father | released | text | — | single-line box | ✅ correct |
mother | released | text | — | single-line box | ✅ correct |
siblings | released | text | — | single-line box | ✅ correct |
mama | released | text | — | single-line box | ✅ correct |
native | released | suggest | 10 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
familyType | released | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
addressShown | released | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
mapPlace | released | suggest | ⚠ 0 (needs areas) | text box then unlabelled pills | 🔴 bare text box — the areas source is never passed |
references | released | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
soyare | released | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
| Community | |||||
religion | basic | text | — | single-line box | ✅ correct |
caste | basic | text | — | single-line box | ✅ correct |
community | released | suggest | 11 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
kuldaivat | released | suggest | 7 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
| Kundli | |||||
gotra | released | suggest | 10 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
devak | released | suggest | 10 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
rashi | released | suggest | 13 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
nakshatra | released | choice | 28 | pills, commit on tap | ✅ correct — the design forbids free text here |
charan | released | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
gan | released | choice | 4 | pills, commit on tap | ✅ correct — the design forbids free text here |
nadi | released | choice | 4 | pills, commit on tap | ✅ correct — the design forbids free text here |
manglik | released | choice | 4 | pills, commit on tap | ✅ correct — the design forbids free text here |
placeOfBirth | released | suggest | ⚠ 0 (needs areas) | text box then unlabelled pills | 🔴 bare text box — the areas source is never passed |
| Expectations | |||||
expectations | basic | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
expectAge | basic | choice | 8 | pills, commit on tap | ✅ correct — the design forbids free text here |
expectPlace | basic | suggest | 6 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
expectMore | basic | text | — | single-line box | ✅ correct |
expectHeight | basic | choice | 22 | pills, commit on tap | ✅ correct — the design forbids free text here |
expectEducation | basic | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
expectWork | basic | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
expectDiet | basic | choice | 4 | pills, commit on tap | ✅ correct — the design forbids free text here |
expectLifestyle | basic | multi | 4 | text box + pills, comma-joined | ⚠ same as suggest |
expectCommunity | basic | suggest | 12 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
expectIncome | released | choice | 6 | pills, commit on tap | ✅ correct — the design forbids free text here |
diet | released | choice | 5 | pills, commit on tap | ✅ correct — the design forbids free text here |
| Who to talk to | |||||
repName | released | text | — | single-line box | ✅ correct |
repRelation | released | choice | 8 | pills, commit on tap | ✅ correct — the design forbids free text here |
repPhone | released | text | — | single-line box | ✅ correct |
repHours | released | suggest | 5 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
| Contact | |||||
dob ·req | private | date | — | 3 horizontal pill strips | ⚠ design draws a 4×4 year pager → month grid → 7-col calendar + a derived age chip |
phoneNo ·req | private | text | — | single-line box | ✅ correct |
income | private | suggest | 6 | text box then unlabelled pills | ⚠ order reversed, and the design's "Or write it your way" heading is missing |
address | private | long | — | 52dp single-line box | ⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line |
What that table says in aggregate
- 19
choicefields are correct as built. The design forbids free text on them, in prose: "It is a fixed list and never free text." The owner's Custom/Other requirement is therefore a DESIGN GAP, not a fidelity fix — it is G1 of the design prompt. - 3 fields are bare text boxes that should offer suggestions —
city(required),mapPlace,placeOfBirth. They declareoptionsFrom: 'areas', the resolver accepts anareasargument, and no call site passes it. The domain even tests both branches. FIDELITY. - 16
suggestfields render their two halves in the wrong order and omit the design's own heading, "Or write it your way" — so the custom-value affordance is present but unannounced. FIDELITY, and it is a direct cause of the owner's "scattered" report. - 6
longfields ask for a paragraph in a 52dp single-line box, includingintro(420 chars) andexpectations(320). The design draws a 5-row textarea. FIDELITY. dobandageare covered above.
3 · The drawers
What the design specifies and we did not build
The artboard defines twelve sheets and a precise skeleton for the seven that have sticky footers: display:flex; flex-direction:column, header flex:none, body flex:1; min-height:0; overflow-y:auto, footer flex:none. Per-sheet max-height 86–96%. Asymmetric radius 36px 36px 48px 48px so the sheet's lower corners follow the phone shell. Footer order Clear · Cancel · Save at a 1:2 flex ratio, so Save is twice Cancel.
Measured divergences in the field sheet alone:
| Design | Ours | |
|---|---|---|
| Title | outside the scroller | inside — scrolls away on a 28-option field |
| Save | outside the scroller | inside — ~730dp down past the nakshatra grid |
| Cancel | present, labelled | absent entirely; the string is used only as the scrim's accessibility label |
| Tier chip | in the sheet header | not rendered; TierChip has one importer, the row |
| Tier explanation | a composed note under the input | dead copy — sectionWhoSees / sectionChangeTier have 0 references |
| Horizontal padding | 20dp | 40dp — the shell's 20 plus its own 20, so the answer area is 280dp on a 360dp phone |
Six interactive treatments in one drawer
Pill · text field · horizontal pill scroller · plain sentence · switch · button pair. Across the four consumer drawers an "option" is a pill, a 56dp row card, a 20px-radius row card, or a 4-column tile. Eleven distinct corner radii are in use, six of them off the token scale (12, 15, 19, 20, 22, 26 against a scale of 10/14/18/24/30/40/999). Two styling idioms sit in three sibling files, and three sheet titles are implemented three ways — none of them using the primitive's own title prop.
That is the measurable content of "scattered and inconsistent".
4 · "More" is three surfaces, and two are broken
| Surface | Renders | Available |
|---|---|---|
| The centre-button drawer ("Make and do") | 2 tiles, 1 heading | 23 tiles, 3 groups |
The biodata more section ("Fields of your own") | an empty bordered card | custom fields: 8 max, 6 types, 5 presentations |
expectMore, a plain field | — | not a "More" surface |
The drawer drops every kind: 'route' intent unconditionally, including "Make a marriage profile", whose route exists and is marked built — so the whole Make something group disappears, and so does Tools (scan, order code, recently scanned, contact). This is the dead-CTA bug from Home repeating a day later in a second file, because the availability list was copied instead of imported; there are now two BUILT_MAKERS lists that can disagree.
The custom-field capability is finished everywhere except the client. 227 tested lines in the domain, full validation in the Edge Function, custom_schema/custom_values patchable — and BiodataRecordModel has no custom member. biodataFieldsIn('more') returns [], so the screen maps over nothing.
5 · The end-to-end flow
The good news, and it is the answer to the owner's "can the backend support this" question:
No migration is needed to store richer field values. Values live in a
jsonbbag keyed by field id. The column, the write RPC, the public projection (->, not->>) and the Zod contract are all shape-agnostic — an object or array passes through intact. The only thing forbidding structure isvalidateValuesinmanage-biodata/helpers.ts, which rejects anything that is not a string, number or boolean.
So a structured location, an income range, an ISO date with a derived age, or a real multi-select array are each a one-function Edge Function change, not a schema change.
The bad news: the server validates the bag, never the field.
| Rule | Enforced |
|---|---|
| Unknown field key | ✅ Edge Function |
| People caps, relations, name/detail length | ✅ Edge Function and a DB constraint |
| Tier may only tighten | ✅ Edge Function |
| Photo ownership | ✅ Edge Function |
Value length (max) | ❌ UI only — a disabled Save button |
Option membership on a choice field | ❌ nowhere — gender: "banana" stores and publishes |
| Kind agreement | ❌ nowhere — values.age can contradict the private dob |
| Date range 18–75 | ❌ UI only |
| "No phone number in a person's detail" | ❌ nowhere — rendered as a warning, enforced by nothing |
The option lists live in @qrsetu/domain, which the Deno function cannot import, and biodata.fields has no options column. Making these real needs one expand-only migration adding input_kind · options · value_max · value_shape to the registry — and get_biodata_fields() already returns to_jsonb(f.*), so it picks them up with no function change.
Three more, each with a consequence
- The write REPLACES the values bag while three comments promise a merge. The SQL is
coalesce(p_patch -> 'values', p.values). The mobile client accidentally does the right thing by sending the whole bag; a second writer built against the documented contract would silently delete every field it omitted. - The disclosure boundary has zero test coverage.
get_public_biodata— the three redundant guards keepingprivatefields off a public page — appears in 0 of 23 pgTAP files, while the migration header states that the tests assert exactly that property. custom_valuesis written by nothing and read by nothing, while the domain documents it as the home of custom answers. The public page reads the value off the schema entry instead.
6 · What the design cannot answer, and why
The artboard renders in a desktop preview. Grepped: zero occurrences of safe-area, env(, scrollIntoView, visualViewport, autoFocus or enterkeyhint; one inputmode in the whole file. Meanwhile five of the twelve sheets have keyboard input above a sticky footer.
This is the class the owner's 2026-09-07 rule names, and it is permanently outside design parity: a screen can sit at 100% design fidelity and be unusable on a phone. It goes to Claude Design as a design request (G2), not as an implementation detail — see the drawer prompt.
Nine smaller gaps are listed there too: what dense does for the 21/22/28-option lists, the partial-year age boundary, the orphan yesno branch, the missing inline error slot, Clear's mismatched height, person removal bypassing the shared confirm sheet, the unconditional people-sheet Done, and two sheets missing .qrc-scroll.
7 · Order of work
- ✅ Fix the gates first —
check:parityR14 skipped 45% of the app and R15 did not exist, so the rework would have shipped with the same blind side (QRS-1156, QRS-1157). - ✅ Send the design prompt — longest lead time, and everything in the DESIGN GAP column waits on it.
- This page, as the record.
- Implement the FIDELITY column, backend first where the Edge Function must accept structure: the two dead primary actions, then the date control and derived age, then
areas, then thesuggestandlongcontrols, then the sheet skeleton, then custom fields and the More drawer.
Nothing in the DESIGN GAP column is implemented before the design round returns. That is the owner's instruction and the reason this page separates the two columns at all.