Skip to content

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:

VerdictMeaningResolved by
FIDELITYthe design specifies it; the implementation divergedcode, no design round
DESIGN GAPthe design does not answer it, or the owner wants what it deliberately refusedClaude 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 way

Why 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.

FieldTierDeclared inputOptionsRenders todayVerdict
About
openingbasictext—single-line box✅ correct
introbasiclong—52dp single-line box⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line
firstName ·reqbasictext—single-line box✅ correct
gender ·reqbasicchoice2pills, commit on tap✅ correct — the design forbids free text here
age ·reqbasicderived—a sentence, no control⚠ design opens the dob sheet; ours shows a dead note
marital ·reqbasicchoice5pills, commit on tap✅ correct — the design forbids free text here
city ·reqbasicsuggest⚠ 0 (needs areas)text box then unlabelled pills🔴 bare text box — the areas source is never passed
photobasicderived—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
fullNamereleasedtext—single-line box✅ correct
heightreleasedchoice21pills, commit on tap✅ correct — the design forbids free text here
bloodGroupreleasedchoice9pills, commit on tap✅ correct — the design forbids free text here
Work
education ·reqbasicsuggest20text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
profession ·reqbasicsuggest17text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
employerreleasedtext—single-line box✅ correct
experiencebasicchoice10pills, commit on tap✅ correct — the design forbids free text here
linkedinreleasedtext—single-line box✅ correct
eduPriorbasicsuggest21text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
Family
fatherreleasedtext—single-line box✅ correct
motherreleasedtext—single-line box✅ correct
siblingsreleasedtext—single-line box✅ correct
mamareleasedtext—single-line box✅ correct
nativereleasedsuggest10text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
familyTypereleasedchoice5pills, commit on tap✅ correct — the design forbids free text here
addressShownreleasedlong—52dp single-line box⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line
mapPlacereleasedsuggest⚠ 0 (needs areas)text box then unlabelled pills🔴 bare text box — the areas source is never passed
referencesreleasedlong—52dp single-line box⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line
soyarereleasedlong—52dp single-line box⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line
Community
religionbasictext—single-line box✅ correct
castebasictext—single-line box✅ correct
communityreleasedsuggest11text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
kuldaivatreleasedsuggest7text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
Kundli
gotrareleasedsuggest10text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
devakreleasedsuggest10text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
rashireleasedsuggest13text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
nakshatrareleasedchoice28pills, commit on tap✅ correct — the design forbids free text here
charanreleasedchoice5pills, commit on tap✅ correct — the design forbids free text here
ganreleasedchoice4pills, commit on tap✅ correct — the design forbids free text here
nadireleasedchoice4pills, commit on tap✅ correct — the design forbids free text here
manglikreleasedchoice4pills, commit on tap✅ correct — the design forbids free text here
placeOfBirthreleasedsuggest⚠ 0 (needs areas)text box then unlabelled pills🔴 bare text box — the areas source is never passed
Expectations
expectationsbasiclong—52dp single-line box⚠ design draws a 5-row textarea; six fields ask for a paragraph in one line
expectAgebasicchoice8pills, commit on tap✅ correct — the design forbids free text here
expectPlacebasicsuggest6text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
expectMorebasictext—single-line box✅ correct
expectHeightbasicchoice22pills, commit on tap✅ correct — the design forbids free text here
expectEducationbasicchoice5pills, commit on tap✅ correct — the design forbids free text here
expectWorkbasicchoice5pills, commit on tap✅ correct — the design forbids free text here
expectDietbasicchoice4pills, commit on tap✅ correct — the design forbids free text here
expectLifestylebasicmulti4text box + pills, comma-joined⚠ same as suggest
expectCommunitybasicsuggest12text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
expectIncomereleasedchoice6pills, commit on tap✅ correct — the design forbids free text here
dietreleasedchoice5pills, commit on tap✅ correct — the design forbids free text here
Who to talk to
repNamereleasedtext—single-line box✅ correct
repRelationreleasedchoice8pills, commit on tap✅ correct — the design forbids free text here
repPhonereleasedtext—single-line box✅ correct
repHoursreleasedsuggest5text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
Contact
dob ·reqprivatedate—3 horizontal pill strips⚠ design draws a 4×4 year pager → month grid → 7-col calendar + a derived age chip
phoneNo ·reqprivatetext—single-line box✅ correct
incomeprivatesuggest6text box then unlabelled pills⚠ order reversed, and the design's "Or write it your way" heading is missing
addressprivatelong—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 choice fields 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 declare optionsFrom: 'areas', the resolver accepts an areas argument, and no call site passes it. The domain even tests both branches. FIDELITY.
  • 16 suggest fields 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 long fields ask for a paragraph in a 52dp single-line box, including intro (420 chars) and expectations (320). The design draws a 5-row textarea. FIDELITY.
  • dob and age are 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:

DesignOurs
Titleoutside the scrollerinside — scrolls away on a 28-option field
Saveoutside the scrollerinside — ~730dp down past the nakshatra grid
Cancelpresent, labelledabsent entirely; the string is used only as the scrim's accessibility label
Tier chipin the sheet headernot rendered; TierChip has one importer, the row
Tier explanationa composed note under the inputdead copy — sectionWhoSees / sectionChangeTier have 0 references
Horizontal padding20dp40dp — 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 ​

SurfaceRendersAvailable
The centre-button drawer ("Make and do")2 tiles, 1 heading23 tiles, 3 groups
The biodata more section ("Fields of your own")an empty bordered cardcustom 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 jsonb bag 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 is validateValues in manage-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.

RuleEnforced
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 keeping private fields off a public page — appears in 0 of 23 pgTAP files, while the migration header states that the tests assert exactly that property.
  • custom_values is 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 ​

  1. ✅ Fix the gates first — check:parity R14 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).
  2. ✅ Send the design prompt — longest lead time, and everything in the DESIGN GAP column waits on it.
  3. This page, as the record.
  4. 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 the suggest and long controls, 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.