Project 02
Aainaआईना
You already know more than you think.
Ninety-one questions and no score at the end. What you get instead is every place your own answers contradict each other, quoted back to you.

A friend sent me one of those relationship quizzes. It told her she was "78% compatible" with someone she had been with for two years. I went looking for where that number comes from and found the answer is: nowhere. In 2017 three researchers threw over a hundred measures at exactly this question across thousands of couples and found that the compatibility part — the bit that is about these two specific people — predicted almost none of the variance. The number on the screen is decoration.
So I built the version that refuses. Aaina will not score you. What it does instead is hold your own answers against each other and show you where they disagree. You say you are allowed to take up space; you also say your needs come after everyone else's. It does not resolve that for you — it puts both sentences next to each other, with the questions they came from, and lets you sit with it.
The hard part was not the psychology. It was stopping the language model from being charming. Ask any model for relationship feedback and it produces horoscope prose that people rate as *more* accurate the moment you tell them it was written for them. So the model in Aaina is not allowed to think. Every number, every contradiction, every verdict is computed in TypeScript first; the model receives a sealed bundle of that evidence and may only write the connective sentences — and any paragraph whose citations do not resolve is thrown away in three separate places on the way to your screen.
Why it was hard
The easy product is a quiz that returns "you are 72% compatible". That number is the industry standard and it is fabricated: Joel, Eastwick and Finkel threw over a hundred measures at dyad-specific compatibility in 2017 and predicted none of the variance. So the problem I set myself was producing a reading that is specific and emotionally weighty while structurally refusing the one claim everybody wants. The second problem is worse. Ask a language model for relationship feedback and it will hand you a horoscope — and the research is blunt about what happens next: the same paragraph is rated as more accurate the instant somebody is told it was written for them personally.
How it actually works
The model is not allowed to think. Every number and every contradiction is worked out in ordinary code first; the model only gets to write the joining sentences.
I made genericness inexpressible rather than discouraged. Every number, contradiction and verdict is computed in TypeScript; the model receives a closed evidence bundle and may only write connective prose.
1 receipt
- derive.ts:28One deterministic, fingerprinted function is the sole authority — no component recomputes.
A paragraph that cannot point back at your own answers is thrown away three separate times on its way to your screen — once on the server, once in the browser, and once more at the moment of drawing it.
Any paragraph whose citations do not resolve is discarded in three independent places — in the serverless function, in the client orchestrator, and at the component, which renders nothing for unresolvable evidence.
2 receipts
- write.ts:139Server-side allowlist built per request from that reader’s own findings.
- Claim.tsx:48“A guard that exists in one place is a guard the second call site will bypass.”
It writes from the places where you disagree with yourself. You said you are allowed to take up space. You also said your needs come after everyone else’s.
The report is written from contradictions, not scores — collisions between a person’s own answers, such as rating “I am allowed to take up space” high while rating “my needs come after everyone else’s” low.
1 receipt
- contradictions.ts:215Seven attitude-versus-behaviour rules, each with the direction it requires.
Wanting to stay and being unable to leave are not the same feeling, so they get two separate axes. That split is the only reason it can ever say: this is being held together by what leaving would cost.
I measure Pull and Hold on separate axes, because Rusbult and Martz found that what predicted staying in abusive relationships was investment and poor alternatives — not satisfaction. That separation is what lets the report reach “this is being held in place by what leaving would cost.” Safety is orthogonal and never moves a number.
2 receipts
- axes.tsQuality, Pull, Hold and Safety computed independently.
- sources.tsRusbult, Martz & Agnew 1998 cited with the load it bears.
Disagree with something it said, press the cross, and it does not simply grey the line out — it rebuilds everything that was leaning on it.
Rejecting a claim recomputes the plan. A reader’s ✗ on a finding is treated as new evidence: the finding is dropped and everything leaning on it is rebuilt.
1 receipt
- derive.ts:170“A claim the reader has rejected must not still be holding up a verdict three sections later.”
Two people can compare answers with no server involved anywhere. Both halves ride inside the link itself, and anything either of you said about safety is physically blocked from travelling.
Couple mode has no backend at all. Both halves travel in the URL fragment, which is never sent to a server, and every safety answer is hard-blocked from travelling in either direction — with a test that poisons a link and asserts the disclosure comes out missing.
2 receipts
- couple.ts:92mayTravel() blocks all 15 safety items outbound and inbound.
- couple.test.ts:102The adversarial test that tries to smuggle one through.




The numbers
receipts
- src/items/Counted and de-duplicated from source.
receipts
- sources.ts:23A registry test fails on any citation that does not resolve, and on any source nothing cites.
receipts
- _contract.ts:197The ban list, with a comment explaining each failure mode.
receipts
- practices.ts:104Gottman, IBCT, EFT, Bowen, WOOP and more, each cited.
receipts
- audit.ts:21The transplant problem has to be solved for the hard case, not the obvious one.
The part most people would have skipped
I refused to reproduce copyrighted instruments. Rather than administering published scales verbatim, it names the published construct and its interpretation rules and writes its own item text. MBTI, the Five Love Languages, the Dyadic Adjustment Scale and the HITS screen I considered and rejected in writing, with my reasons recorded.
What it can't do
- The current version replaced an earlier careless-responding detector I had built; it now surfaces dwell time and answer revisions instead, and says plainly that no self-report assessment is fake-proof.
- Vite 8
- React 19
- TypeScript
- Tailwind 4
- Zustand
- one serverless function
- Groq
- Vitest
- Playwright
On a phone
All of it, on a 412-pixel phone.
Not a shrunken desktop with a hamburger bolted on — the reading measure, the receipt drawers and the couple-mode comparison all re-set for a thumb. Most people who take this will take it in bed on a phone, so that is the size it was designed at.
The door
Why no score
The plan
The report
On your own
Both of you
Open a receipt
A glimpse
Not one stock image
Every picture in it is a real painting.
Ten of them, all Indian, all CC0, all named — Company School bird studies from Calcutta and Lucknow, a Rajput falcon, Ragamala miniatures from Mewar and Amber. Each one carries its museum and its accession number inside the app, because an uncredited image is the same failure as an uncited sentence. Click any of them to open the museum's own record.










