Audience: a person running a manual regression before a release, or spot-checking a change. Status: living document — update it in the SAME PR that adds a feature, fixes a bug or changes a flow (§0.6). Covers the POC console after #59.3b (guided apply, end to end), #60 (the daily check-in), #132/#133 (deadline reminders, F8a + F8b), #61 (the fake-notice checker, F2), #62 (the cost meter, F14), #63 (three more debugger rules), #86 (the funnel lens, CRM slice 1), #13 (gating /ops with Google OAuth + a Supabase allowlist), #23 (conversations/messages/cost persisted to Supabase, session-isolated per anonymous visitor), #87 (the guide desk, CRM slice 2 — persisted tickets, claim/note/reply/resolve at /ops/guide), and #139 (a self-service profile correction between the profile card and the document pull). The admit card and the result stage are not built yet and are listed under Known gaps (§15).
This is the manual counterpart to the automated suite (Vitest: console, flow, copy, rules, schema, digilocker). Automated tests gate every PR; this plan is the human pass over what a machine cannot fully judge — tone, ordering, "does it feel right" — plus the reputation events (CLAUDE.md §10) where a wrong step is expensive. Where a case is also guarded by a test, the file is named in Auto so you know what is already covered.
Live at
/qa(www.jivika.dev/qa). The page is generated from THIS file and adds a result button per test, a progress bar, and the expected conversation for tests that have one — that conversation is produced by running the real chat code at build time, so it cannot drift from the product. Recorded results stay in your browser (localStorage).
0 · How to use this plan
0.1 The harness — the console
Everything is driven through the console at /console (local: pnpm build && pnpm -F @jivika/console start, then http://localhost:3000/console; hosted: www.jivika.dev/console). Two equal columns:
- Left — the phone. A number pulldown (top), then the chat window: message bubbles with your local time and timezone, a Type a message box with Send, and Reset (wipes just that number's conversation). Buttons and lists in bot messages are tappable; typing the button's title has the same effect as tapping it.
- Right — the instruments. Guide queue (tickets a person must call about), Debugger (13+ rules watching every message; P0/P1/P2 counts; probes; jump-to-message; Copy report), Events (the event spine: one row per fact each turn, Export JSON).
- Small mono dividers in the thread are the console talking, not the bot:
✦ ask dob → ask qualification(state change),✦ captured: category(smart capture),✦ profile: …(the answers that were confirmed),✦ engine: … → eligible (draft v1)(the rules engine's verdict),✦ guide ticket G-1 queued …. - Scripted access:
window.__jivikaDebugin the browser console exposesfindings,report(),events,active.
0.2 Environment
- Synthetic data only. The site is public. Never type a real name, phone number, Aadhaar, or a real person's date of birth. Numbers cannot be typed — they are minted from the reserved block
+9199999xxxxx. - No login, no accounts — but conversations now persist (#23). A reload keeps your conversation (§10.7); every anonymous visitor gets their own private copy of each persona, so two different browsers never collide (§10.8, a client-generated id in
localStorage, not an account). Clearing storage or switching browsers starts fresh, the same way a different real aspirant would in production. - A DRAFT rule set is in force: eligibility uses the 2026 SSC GD notice as a template for 2027 (crucial date 01-01-2027, assumed). Every result must say so.
0.3 Personas and fixtures
| Persona | Number | Seed | Use for |
|---|---|---|---|
| Rohit | +919999900001 | M · OBC-NCL · graduate · UP | the main happy path |
| Sunaina | +919999900002 | F · SC · graduate · BR | a second profile; shared-phone / leak checks |
| Arjun | +919999900003 | M · UR · postgraduate · MP | over-the-age-limit |
| (new) | +9199999xxxxx | none | + New number — a blank slate |
Rohit's path (used throughout): tap Haan, shuru karein → 14-03-2002 → Graduate (BA/BSc/BCom) → OBC (NCL) → UP → the card → Haan, sahi hai → the eligible list.
Age limits, from the rule set (crucial date 01-01-2027): UR 23, OBC 26, SC/ST 28, minimum 18. Boundary birthdays: UR 02-01-2004 eligible / 01-01-2004 over; OBC 02-01-2001 / 01-01-2001; SC 02-01-1999 / 01-01-1999; youngest 01-01-2009 eligible, 05-05-2010 → "turns 18 on 05-05-2028".
0.4 Conventions
- Handle = the short label used when a message is about one form:
SSC GD · draft(notice · deadline; "draft" until the 2027 notice is out). - The send lines are examples. Tests that name an exact string test a deterministic parser or verb; tests marked (natural) should be re-run with 2–3 phrasings.
- Register: the bot writes Hinglish (romanised Hindi + English). It must never answer in Devanagari or formal Hindi unless you did.
- "Debugger clean" = the Debugger panel shows P0 0 · P1 0 · P2 0 after the steps (the one accepted exception is P1 no-reply after a form is started — §15).
0.5 Recording a result
For each test record PASS / FAIL / BLOCKED / N-A (the /qa page has buttons and a Copy results report) with the build SHA and date. On FAIL capture: the number, the exact text you sent, the actual reply, the divider(s) under it, and Copy report from the Debugger. File a GitHub issue on the jivika board (label qa) and link it in the test's Ref. Severity:
- S1 (blocker) — a reputation event (CLAUDE.md §10): a wrong or prefilled-wrong field; a fake notice marked Verified; a distress message with no human follow-up; another profile's data shown; any document leaving India; a message that tells someone to give up; eligibility shown that disagrees with the rules engine; identity (DigiLocker/Aadhaar) asked before the eligible list.
- S2 (major): a flow dead-ends, an answer is dropped, a wrong form is acted on, a retry loop, a reply in the wrong register.
- S3 (minor): copy/tone, a needless re-ask, cosmetic.
0.6 Maintaining this plan
- New feature → add its tests to the right section (or a new one) in the SAME PR. Number continues the section's sequence; never renumber (IDs are referenced in tickets). Retire a test with
~~x.y~~ (removed — reason), don't delete the block. - Bug fix → add a regression test that reproduces the bug, with the issue in Ref.
- Format matters: each test is
### N.M — Titlefollowed by**Steps:**,**Success:**,**Failure:**and optional**Ref:**,**Auto:**,**Thread:**(a scenario id from/flow, whose real conversation is shown on the page). A test inlib/qa.test.tsfails if a block is malformed or a Thread id does not exist.
1 · Arrival & greeting
The first message
- Load
/console. Select Rohit. - Do not type anything yet — read the first bot message.
- Check the browser console (devtools) for errors.
A greeting in Hinglish ("Namaste! Main Jivika hoon…") with three buttons: Haan, shuru karein, Ye kya hai?, Baad me. No question about age, documents or identity. Step 3: no console errors at all — in particular, no React hydration-mismatch error (minified error #418) from the Debugger panel's clock (#148, fixed 2026-10-06: the clock display now mounts client-only, same pattern DebugPanel's "first seen" timestamps already used).
No buttons (plain text only); Devanagari; formal Hindi; any DigiLocker/Aadhaar/name/phone request; any console error on load.
Auto: apps/console/lib/chat.test.ts
"Ye kya hai?" explains, then offers the choice again
- Tap Ye kya hai?.
A plain explanation (finds notifications you qualify for, helps fill forms) that says it never charges a fee, followed by the choice again — Haan, shuru karein and Baad me only (no "Ye kya hai?" a second time).
A sales pitch; the fee statement missing; the same three buttons again; the flow moves on to the questions by itself.
"Baad me" closes softly; the door stays open
- Tap Baad me.
- Type
hi.
After step 1: a calm, guilt-free line ("koi jaldi nahi… 'hi' likh dena"). After step 2: the questions begin (the date of birth ask).
Any nagging or "are you sure"; a reminder promise; hi is ignored or re-greets instead of starting.
Free text at the greeting just starts
- On a fresh number type
ssc gd form kaise bharein.
The bot starts the questions (asks the date of birth). Nothing is stored; no error.
"Samajh nahi aaya"; a stall; an answer to the question as if it were a form field.
Buttons and typing are the same
- Fresh number. Type
Haan, shuru kareinexactly instead of tapping. - On another fresh number type
haan.
Both start the questions.
Either is treated as a wrong answer to a question.
Auto: apps/console/lib/router.test.ts
`hi` restarts from anywhere except mid-erase
- Rohit: get to the ask state question. Type
hi.
The greeting comes back (fresh), previous answers are not silently used as if confirmed.
hi is read as an answer to the current question; the chat crashes; a second greeting stacked on the first.
2 · The four questions
(Onboarding asks date of birth → education → category → state, one at a time. Each answer is read by a deterministic parser — never guessed.)
Date of birth, the accepted shapes
- At the date question send each of:
14-03-2002,14/03/2002,14.03.2002,1-2-2000,meri dob 14 03 2002 hai.
Each is accepted (the next question — education — appears). The card later shows 14-03-2002 / 01-02-2000 in DD-MM-YYYY.
Any of them re-asks; a date read as month/day swapped.
Auto: packages/flow/src/interpret.test.ts
Impossible or unusable dates are re-asked, never stored
- Try each:
31-02-2001,29-02-2001,14-13-2002,14-03-02,14-03-2030,kal.
Each gets the re-ask "Ye tithi samajh nahi aayi. DD-MM-YYYY me likho, jaise 14-03-2002." Nothing advances; no answer is stored (mere form still says nothing has started).
A rollover (31-02 → 3 Mar) is accepted; a two-digit year is guessed; a future date is accepted.
Auto: packages/flow/src/interpret.test.ts
Education is a list; typing works too
- At the education question tap Chunein and read the rows.
- Choose Graduate (BA/BSc/BCom). On another number type
12th passinstead.
A list (not free text) with the six levels (8th … Post-graduate); both routes advance to the category question.
A free-text question; a level missing; "graduate" ranked as "post-graduate" or the reverse.
Auto: apps/console/lib/chat.test.ts
"Still appearing" is not "passed"
- At education send
10th appearing, thenfinal year BA.
Neither is stored as a qualification; the list is offered again.
"10th appearing" saved as 10th passed.
Auto: packages/flow/src/interpret.test.ts
Category is a list; an ambiguous answer is asked again
- At category send
sc st, thengeneral obc. Then tap OBC (NCL).
The first two are re-asked (never resolved to one of them); the tap works.
"sc st" silently saved as SC.
Auto: packages/flow/src/interpret.test.ts
State: names, codes, spellings
- At state send in turn (fresh each time):
UP,Uttar Pradesh,bihar ka,Gujrat,Orissa.
Each resolves (UP, UP, Bihar, Gujarat, Odisha) and the card shows the full state name.
bihar ka read as Karnataka ("ka"); any of them re-asked.
Auto: packages/flow/src/interpret.test.ts
An ambiguous or unknown state is asked again
- At state send
up bihar, thenmars.
"Ye rajya nahi mila. Rajya ka naam ya code likho, jaise UP ya Bihar." for both; state not stored.
One of the two states picked; "mars" accepted.
Devanagari input is understood
- At state send
उत्तर प्रदेश; at educationस्नातक; at categoryओबीसी.
Each is read correctly; the bot still replies in Hinglish (romanised), not Devanagari.
Not understood; the bot answers in Devanagari without being asked (known limitation: the reverse — a Devanagari user — is flagged by the Debugger as P2 register-mismatch, §15).
No identity before value
- Walk Rohit's path to the card. Read every bot message so far.
Not one message mentions DigiLocker, Aadhaar, documents, OTP, or asks for a name or phone.
Any of those words before the eligible list — S1 (CLAUDE.md §3.1).
Auto: apps/console/lib/debug/rules.test.ts
The answer is stored only when understood
- At the date question send
kal. Typemere form.
"Abhi koi form shuru nahi hua. 'hi' likh kar shuru karein." — nothing was stored.
A partial profile exists; mere form errors.
3 · Smart capture — one message, several answers
A whole sentence in one message
- Fresh number. Send
meri dob 14-03-2002 hai, graduate hu, OBC, UP.
The bot goes straight to one profile card (§4) showing all four values — date 14-03-2002, Graduate (BA/BSc/BCom), OBC (NCL), Uttar Pradesh. Under the reply: ✦ captured: dob, qualification, category, state. No question was re-asked.
Any field asked again; a value differs from what you wrote; a result shown before the card.
Auto: apps/console/lib/chat.test.ts
Partial capture asks only what is missing
- Fresh number: tap Haan, shuru karein, then send
graduate, OBC. - Send
14-03-2002.
After 1: "Samajh gayi — padhai, category note kar liya." then the date question. After 2: skips education and category and asks state only.
Education or category asked again; the date question skipped.
A date's digits are never a qualification
- At the date question send
12-03-2002 graduate.
Date 12-03-2002 and Graduate are both captured — not "12th pass".
Qualification shows 12th.
Auto: packages/flow/src/interpret.test.ts
"B.Sc" is not category SC
- Send
BSc OBC UPat any question.
Graduate + OBC (NCL) + Uttar Pradesh. Category is not SC.
SC anywhere.
What is ambiguous is left out and asked
- Fresh number, tap start. Send
general obc, Bihar.
Only the state (Bihar) is captured; the bot then asks the date, and category is asked later (never guessed from "general obc").
Category filled with UR or OBC.
State codes need capitals inside a sentence
- Send
graduate hu, up me rehta hu(lowercase) then on another numbergraduate UP.
The first captures education only (the lowercase "up" is not taken as a state); the second captures education and state UP.
"up" captured from ordinary words; "UP" ignored.
Distress inside a long sentence still goes to a person
- Send
graduate hu OBC UP lekin mujhe jeene ka mann nahi karta.
The distress path (§8) — acknowledgement with Tele-MANAS 14416, ticket queued — and nothing captured (no card).
The answers are captured and a card shown; no ticket.
Auto: apps/console/lib/chat.test.ts
Free text that answers nothing is not an error
- At the state question send
mujhe pata nahi(or any chatter).
The re-ask (state), not "captured" and not a crash; after three such failures a help ticket opens (§9).
A field captured from chatter; silence.
4 · The profile card and corrections
The card shows what was understood — in your words
- Walk Rohit's path to the card.
One message: "Maine ye samjha:" then Janm tithi 14-03-2002 · Padhai Graduate (BA/BSc/BCom) · Category OBC (NCL) · State Uttar Pradesh, "Sab sahi hai?…", with two buttons Haan, sahi hai and Badlo. "OBC (NCL)" is visible (our reading of "OBC").
Codes instead of words ("OBC-NCL", "UP"); a blank field; more than two buttons; a verdict already shown.
Auto: apps/console/lib/chat.test.ts
No eligibility verdict before you confirm
- Stop at the card. Look at the thread and at the Debugger/dividers.
No ✦ engine: divider and no eligible list yet.
A verdict appears before "Haan, sahi hai".
Confirming shows the result
- Tap Haan, sahi hai (or type
sahi hai).
✦ profile: 2002-03-14 · graduate · OBC-NCL · UP, the eligible list (§5), its source PDF, ✦ engine: …, then the one-time tip.
Confirm ignored; the tip missing or repeated.
A typed correction changes only what it names
- At the card type
category SC hai.
The card is shown again with Category: SC and every other value unchanged.
Other fields cleared or re-asked; category unchanged; a new question instead of the card.
Several corrections at once
- At the card type
state Bihar, 15-03-2002.
State → Bihar and date → 15-03-2002 in the new card; category unchanged.
Only one applied.
"Badlo" offers the four fields; picking one asks only that
- At the card tap Badlo. 2. Choose category from the list. 3. Tap SC.
A list of four fields (janm tithi, padhai, category, state) → the category question → back on the card with SC (dob, education, state untouched, not re-asked).
All four questions re-asked; the card not shown after.
"nahi" at the card means Badlo
- At the card type
nahi.
The same four-field list as Badlo.
Treated as an unreadable answer.
Text it cannot read says how to correct
- At the card type
pata nahi kya likhu.
One line: "Ye samajh nahi aaya. Seedha likho, jaise 'category SC' ya 'state Bihar' — ya 'Badlo' dabao." and the card again. Nothing stored.
Silence; an answer guessed.
A new profile gets a new verdict
- Reach the result as Rohit (born 2002). Then
hi, repeat with date05-05-2010, confirm.
The second result is the too young message (§5), not Rohit's old eligible list.
A stale verdict.
A correction still works AFTER the card, right up to the document pull (#139)
- Reach
show_eligibleas Rohit, say "Badlo". 2. Reachshow_eligible, say "meri dob galat hai, 12-03-2000" (a typed sentence, not the button). 3. Repeat step 1 after tapping a notice (choose_notice), after "Baad me" (notice_saved), and after starting a form but before agreeing to the consent notice (consent). 4. Confirm the corrected card and continue. 5. Agree to the consent notice (reachidentity_card), then say "Badlo" there too.
Steps 1–3 all land back on the profile card (§4.1) with the current values — never "no handler for this input" — and step 2's sentence actually changes the date shown on the card. Step 4's "Haan, sahi hai" returns to show_eligible with eligibility recomputed from the corrected value, and any notice already picked before the correction is still there, re-selectable, not duplicated. Step 5 is UNCHANGED and still guide-only: "Badlo" at identity_card goes to a person (§6.18's "Kuch galat hai" case), never back to the profile card — identity fields are never self-service once documents are pulled.
"No handler for this input" anywhere in steps 1–3; a correction at identity_card or later bypasses the guide; a notice picked before the correction silently disappears or gets duplicated after it.
Auto: apps/console/lib/chat.test.ts, describe block #139: a profile correction between confirm_profile and the document pull is self-service (7 tests, including an explicit check that genuinely unreadable free text at these same states is still "no handler" — nothing is silently swallowed).
5 · Eligibility — decided by the rules engine, labelled DRAFT
Rohit is eligible, and it says DRAFT
- Rohit's path through the card confirm.
"Aap 1 me se 1 ke liye eligible lag rahe ho: SSC GD Constable 2027. Ye abhi DRAFT hai — official notice aana baaki hai…" A list row SSC GD Constable 2027 — Draft — official notice aana baaki; a document notice_01122025.pdf captioned as SSC's 2026 notice; ✦ engine: SSC GD Constable 2027 → eligible (draft v1).
"Eligible" without DRAFT; no source document; a made-up date or vacancy count.
Auto: apps/console/lib/chat.test.ts
The source document is real
- Read the document bubble's file name and (in the Debugger Copy report or by inspecting the link) its URL.
notice_01122025.pdf on ssc.gov.in — SSC's own 2026 notice.
A third-party or placeholder link.
Over the age limit: the rule is named, the door stays open
- Arjun: start →
01-01-1990→ Post-graduate → General (UR) →MP→ confirm.
"…umar 23 saal se kam honi chahiye (01-01-2027 tak), isliye abhi eligible nahi. Aur bhi notices aayenge — hum dekhte rehte hain." No blame, no "give up". ✦ engine: … → not eligible (clause 5…).
A blaming/discouraging line; the limit wrong; no next step.
Too young: it says exactly when
- Fresh number: date
05-05-2010, 10th pass, General (UR),UP, confirm.
"…18 saal ki umar chahiye… Aap 05-05-2028 ko 18 ke ho jaoge…"
No date; the wrong date.
Below 10th
- Date
14-03-2004, 8th pass, General (UR),UP, confirm.
"…kam se kam 10th pass chahiye. Padhai ke baad phir dekhte hain…"
Eligible; a level other than 10th named.
Boundary birthdays (UR)
- Two fresh numbers, General (UR), graduate, UP: dates
02-01-2004and01-01-2004.
02-01-2004 eligible; 01-01-2004 over the limit.
Either reversed.
Auto: apps/console/lib/chat.test.ts
The limit follows the category
- Graduate, UP. OBC (NCL) with
02-01-2001and01-01-2001; SC with02-01-1999and01-01-1999.
OBC: first eligible, second over (26). SC: first eligible, second over (28). The over-limit message quotes 26 / 28.
A 23-year limit applied to OBC/SC.
Sunaina (SC, born 2003)
- Sunaina: date
02-08-2003, Graduate, SC,Bihar, confirm.
Eligible (DRAFT). "mere form" later shows the handle SSC GD · draft.
Rejected on age.
Nothing is asserted that the rules engine did not decide
- For each result you have seen compare the message with the
✦ engine:divider.
The message and the verdict divider agree every time (eligible ⇔ a list; not eligible ⇔ a reason).
A list under a "not eligible" verdict, or the reverse — S1 (the Debugger raises eligibility-mismatch P0).
Auto: apps/console/lib/debug/rules.test.ts
The tip is taught once
- Get a result. Then
hi, run the path again to a second result.
The tip ("'mere form' likh kar… 'madad' likh kar…") appears after the first result only.
Repeated on the second result; missing on the first.
6 · Choose a form
Tapping the row opens that form's card
- As Rohit at the result, tap Chunein, then SSC GD Constable 2027.
A card: "SSC GD Constable 2027 — abhi DRAFT hai, official notice aana baaki. Shuru karein?" with Haan, shuru karein and Baad me. Divider show eligible → choose notice.
No card; a different form; the DRAFT label missing.
Typing does the same
- On separate results type
apply,apply karna hai,form bharo,ssc gd,SSC GD · draft.
Each opens the same card.
Any of them "no handler"; shuru karo is not one of them (it restarts — §7.6).
Auto: apps/console/lib/applications.test.ts
Starting moves the application to "started" and asks for consent
- At the card tap Haan, shuru karein.
Divider application SSC GD · draft: eligible → started, then the consent notice (§6.10) with two buttons. mere form now shows Shuru kiya — draft. Nothing has been pulled yet.
Documents pulled without asking; the divider missing; the application starting twice; a DigiLocker/Aadhaar request before this tap.
"Baad me" saves it without nagging
- At the card tap Baad me. 2. Later tap the row again (or
apply).
"Theek hai, SSC GD · draft save hai — koi jaldi nahi…" then the same card again; still one application (no duplicate); Haan, shuru karein then works.
A reminder promise; guilt; a second application record.
"mere form" shows what you did with it
- After the result type
mere form. Repeat after Baad me, and after Haan, shuru karein.
A list row for SSC GD · draft whose description reads Draft — official notice aana baaki → Save kiya — draft → Shuru kiya — draft respectively.
The description never changes.
Not eligible, asking to apply
- On a "too young" result type
apply.
"Draft rules ke hisaab se abhi koi form aapke liye nahi mila…" — no card, no application created.
A card for a form you are not eligible for.
Only forms the list offered are choices
- At the result type
RRB Group D CEN 09/2025.
Not treated as a choice (a "no handler" note; no card).
A card for a form that was never offered.
With two eligible forms the bot asks, never guesses
- (Not reachable in the console until a second notice has rules — only SSC GD is checked today.) Covered by
applications.test.ts; re-run this test manually when RRB Group D is added.
A bare apply answers "Kaun sa form? Neeche se chuno." with a list of the eligible forms.
One picked silently.
Auto: apps/console/lib/applications.test.ts
Erasing forgets the application
- Start a form, then
delete→ Haan, hata do. Thenmere form.
"Abhi koi form shuru nahi hua…" — the application is gone with the answers.
The form is still listed.
The consent notice is plain, specific, and asked once
- After 6.3 read the consent message.
One message in plain Hinglish: what is taken (only your 10th marksheet and ID from DigiLocker), why (to fill in name, date of birth, roll number), only for this form, and how to withdraw ('delete'). Two buttons: Haan, theek hai / Abhi nahi. No retention period is stated (legal has not fixed one). It is two sentences at most.
Vague ("we take your documents"); more documents than the form needs (photo, signature, address); no withdrawal route; pre-ticked or auto-agreed; asked before the person chose to start a form.
Agreeing pulls the two documents — and shows none of their contents
- Tap Haan, theek hai.
Divider documents pulled (synthetic): marksheet 10, id and application SSC GD · draft: started → docs pulled; the one-line message "DigiLocker se 10th ki marksheet aur ID mil gaye ✓." — no values in it — followed by the identity card (§6.16). The ID (Aadhaar-shaped) number appears nowhere: not in any bubble, not in any event.
The ID number shown or logged; more than two documents; nothing recorded.
Auto: apps/console/lib/chat.test.ts
Declining takes nothing and points to a person
- At consent tap Abhi nahi (or type
nahi).
"Theek hai, koi baat nahi — kuch nahi liya gaya. Bina DigiLocker ke documents haath se dene honge; chaho to 'madad' likho, guide saath me bharwa denge." Nothing pulled; typing madad opens a help ticket. No pressure, no second ask.
A second "are you sure"; guilt; a retry loop; documents pulled anyway.
Auto: apps/console/lib/chat.test.ts
Typed answers work at consent
- On separate runs type
haanandnahiat the consent message.
haan agrees (documents pulled); nahi declines.
Either read as unreadable text.
Every document read is audited — consent first
- After agreeing open the Events panel (or Export JSON).
In order: consent_granted purpose=guided_apply version=v0-draft, then document_pulled type=marksheet_10 why=guided_apply, then document_pulled type=id why=guided_apply, application_status status=docs_pulled, form_card_shown card=identity_education — each with the time (when), the number (who) and the purpose (why). A decline produces only consent_declined. No document value appears in any event.
A document_pulled with no earlier consent_granted; missing events; document contents inside props — S1.
Auto: apps/console/lib/chat.test.ts
Documents belong to one person and go with an erase
- On Rohit agree to consent. Switch to Sunaina and
mere form/ walk her far enough to see any document mention. Switch back anddelete→ Haan, hata do.
Sunaina never sees or holds Rohit's documents. After the erase Rohit has no documents (a fresh consent is needed to pull them again) and the earlier events are attributed to "erased".
Another profile's document data appears anywhere — S1; documents survive the erase.
Auto: apps/console/lib/chat.test.ts
The identity card shows exactly what SSC's form will hold
- After agreeing to consent read the card (Rohit).
"SSC ke form me ye dikhna chahiye:" then Naam ROHIT KUMAR · Pita SURESH KUMAR · Mata SUNITA DEVI · Janm tithi 14-03-2002 · Gender Male · Category OBC (NCL) · 10th: CBSE, roll <digits>, 2018, <percentage>% · Padhai Graduate (BA/BSc/BCom), then "Aapke SSC registration me bhi bilkul yahi hona chahiye — wo badla nahi ja sakta. Sahi hai?" with Haan, sahi hai / Kuch galat hai. No ID number.
A field missing or blank; codes ("OBC-NCL"); the ID number; more than two buttons.
Auto: apps/console/lib/chat.test.ts
Confirming the card moves straight to the claims card
- Tap Haan, sahi hai.
"Ye confirm ho gaya ✓." then the claims list (§6.21). Event form_card_confirmed card=identity_education and form_card_shown card=claims.
A silent stall; the next card missing.
"Kuch galat hai" goes to a person; nothing is typed-corrected
- On the card tap Kuch galat hai (or type
nahi). 2. Typekoi hai?, thenkab tak call aayega.
One help ticket in the Guide queue ("identity card: the candidate says a value is wrong"); "…hamari team se koi aapko 2 ghante ke andar call karega aur saath me check karega." Later messages get a varied "noted" line, never the same words twice, and never a second ticket. Typing a new name is not accepted as a correction.
A typed name accepted; a second ticket; the same reply repeated.
Auto: apps/console/lib/chat.test.ts
Documents that disagree with what you told us: no card at all — **S1 if a card is shown**
- (Needs a tampered document — covered by the automated test, which injects a marksheet with a different date of birth; in the console use the unit test or a debugger build.) Expected behaviour to confirm from the test output:
State identity_wrong; no "Sahi hai?" card, no document values in any bubble; ✦ identity card held back — documents disagree with the answers (dob_differs_from_marksheet); a help ticket whose text holds rule ids only; form_card_held card=identity_education n=1.
A card shown while the documents disagree — a wrong prefill, S1; values in the ticket or the event.
Auto: apps/console/lib/chat.test.ts
A held or wrong card leaves the documents audited, and erase still removes everything
- After 6.18,
delete→ Haan, hata do. Export the Events JSON.
The documents, answers and applications are gone; the reads that happened stay in the spine but attributed to "erased".
Documents survive the erase.
The claims card is a list: none first, only rows this build can record
- After confirming the identity card read the list (Rohit, OBC).
"Kya inme se kuch aap par lagu hota hai? Neeche se chuno." Rows: Kuch nahi — aage chalo, Age me chhoot (description "OBC: 3 saal ki chhoot (code 2)"), NCC certificate, ESM, khel, doosra rajya ("Ye guide ke saath bharenge"). For a General (UR) or EWS candidate there is no age row (the category earns no code by itself). Every title fits 24 characters.
A row that leads nowhere; the age row for UR/EWS; "yes" defaulted for any claim.
Auto: apps/console/lib/chat.test.ts
"Kuch nahi" finishes with every claim an explicit no
- Tap Kuch nahi — aage chalo (or type
nahi).
"Note ho gaya ✓ — koi claim nahi." followed by the domicile question (§6.27). Events form_card_confirmed card=claims then form_card_shown card=domicile. No ticket.
A claim left blank rather than an explicit no; a ticket opened.
Auto: apps/console/lib/chat.test.ts
Age relaxation uses the category's own code, with no extra question
- Rohit (OBC): tap Age me chhoot. Repeat as Sunaina (SC).
"Age me chhoot laga di (OBC: 3 saal ki chhoot (code 2))." — for SC "(SC/ST: 5 saal ki chhoot (code 1))". The menu comes back without the age row and with a fresh header ("Aur kuch lagu hota hai?"). Event claim_selected claim=age.
A code that does not match the category; a second question; the age row still offered.
Auto: apps/console/lib/chat.test.ts
NCC: pick the type, come back to the menu
- Tap NCC certificate, then NCC 'B' certificate, then Age me chhoot, then Kuch nahi — aage chalo.
A three-row list (A, B, C) → "NCC 'B' certificate note kar liya." → the menu (no NCC row) → … → "Note ho gaya ✓ — age chhoot (code 2), NCC B."
The type lost; NCC offered twice; the summary missing a claim.
Auto: apps/console/lib/chat.test.ts
ESM, sports, another state → a person, never a silent "no" — **S1 if defaulted**
- Tap ESM, khel, doosra rajya. 2. Type
koi hai?.
One help ticket ("claims: the candidate says ESM, sports, another state or a 1984-riot dependant applies"); "Ye claim abhi guide ke saath bharenge — hamari team se koi aapko 2 ghante ke andar call karega…". Later messages get a varied line and no second ticket. The application is not advanced with these claims left as "no".
The claim recorded as "no"; the flow carrying on to the next card; a second ticket.
Auto: apps/console/lib/chat.test.ts
The card is validated before it is confirmed
- (Automated test: a form whose age code does not fit the category — e.g. code 1 for an OBC candidate — is finished.)
The card is held (form_card_held card=claims n=1), a ticket carries the rule id (age_relaxation_code_mismatch), no confirmation event.
A mismatched code confirmed — a wrong form value, S1.
Auto: apps/console/lib/chat.test.ts
Domicile: the certificate's state, named, and the district typed
- After the claims card read the message (Rohit told us Uttar Pradesh).
"Aapka domicile certificate Uttar Pradesh ka hai? Haan to zile ka naam likho (jaise Lucknow), warna 'doosra rajya' likho." It asks about the certificate, not where the person lives.
"Where do you live?" wording (domicile ≠ residence — a mismatch cancels the candidature); the state not named; a state guessed silently.
Auto: apps/console/lib/chat.test.ts
A district is stored; the profile is never overwritten
- Type
Lucknow. Then (fresh run) typedoosra rajya, thenBihar, thenPatna.
First run: the exam-centres question appears; events answer_captured field=district via=asked, form_card_confirmed card=domicile, form_card_shown card=centres. Second run: "Kis rajya ka domicile certificate hai?…" → the district question now names Bihar → Patna accepted. mere form and the eligibility answers still say Uttar Pradesh — a guided answer never rewrites the profile.
The profile state changed; the district question still names the old state after "doosra rajya".
Auto: apps/console/lib/chat.test.ts
An unreadable district is re-asked, then a person
- At the district question type
haan, then??, then12345.
"Zile ka naam English letters me likho, jaise Lucknow." → a different "Bas zile ka naam, jaise Kanpur. Dikkat ho to 'madad' likho." → a help ticket on the third failure. haan is not stored as a district.
"HAAN" saved as a district; the same words twice; no ticket.
Auto: apps/console/lib/chat.test.ts
A sentence at a guided question is not profile capture
- At the district question type
meri dob 15-03-2002 hai, graduate hu, SC, Bihar.
The profile answers are unchanged (date 14-03-2002, OBC (NCL), UP); the district question is asked again.
Any profile field changed by a guided-card answer.
Auto: apps/console/lib/chat.test.ts
Exam language: tap two, type the rest
- On separate runs: tap English; tap Hindi; tap Koi aur bhasha then type
marathi; thenfrench.
English/Hindi go straight to the force-preference question (§6.35); the other opens "Bhasha ka naam likho, jaise Tamil ya Marathi." and marathi is stored as Marathi; french → "Ye bhasha exam me nahi mili…". The list has three rows.
A language outside the notice's 15 accepted.
Auto: packages/flow/src/interpret.test.ts
The job-info consent is a real consent
- Read the question. Answer Haan, de sakte ho on one run and
nahion another.
"Ek sawaal: kya sarkar ko aapki jaankari (naam, category, padhai) doosre sarkari naukri ke mauke ke liye dene ki ijazat hai? Ye aapki marzi hai — 'nahi' bhi theek hai." with two buttons. Nothing is recorded until you answer; yes and no both proceed (to the signature card, §6.38); both are stored as an explicit true/false.
Pre-answered "yes"; pressure toward yes; "no" blocks progress — S1 (a consent must be free).
Auto: apps/console/lib/chat.test.ts
Every collected value passes the field map's own validators
- (Automated: a full path is collected, then each card is validated.)
Domicile, medium and consent_jobs cards validate with no errors; a nonexistent state code is caught (not_an_option).
A value the form's rules reject sitting in the collected form.
Auto: apps/console/lib/chat.test.ts
Exam centres: exactly three different cities, in order
- After the district, read the question. Type
Lucknow, Kanpur, Varanasi. Then on separate runs try:Lucknow, Kanpur·Lucknow, Kanpur, Varanasi, Agra·Lucknow, lucknow, Kanpur·Lucknow, 12, Kanpur.
"Exam ke liye 3 shehar chuno, pasand ke order me — jaise Lucknow, Kanpur, Varanasi. Baad me badla nahi ja sakta." The good answer is noted back in one line ("Note kiya: LUCKNOW, KANPUR, VARANASI.") and the language list follows. The bad ones are re-asked ("Teen alag shehar likho…" / "Ek hi shehar do baar aa gaya…") and nothing is stored.
Two or four cities accepted; a repeat accepted; a number taken as a city.
Auto: apps/console/lib/chat.test.ts
Force preferences: all seven, ranked by number
- After the language read the numbered list (1 BSF · 2 CISF · 3 CRPF · 4 SSB · 5 ITBP · 6 Assam Rifles · 7 SSF — the notice's own order). Type
3 1 2 5 4 7 6. On other runs:3125476,3 1 2,1 1 2 3 4 5 6,crpf bsf cisf.
"Note kiya: 1 CRPF, 2 BSF, 3 CISF, 4 ITBP, 5 SSB, 6 SSF, 7 Assam Rifles." then the job-info consent. 3125476 works too. Fewer than seven, a repeat, an out-of-range number or names instead of numbers are re-asked with a different wording the second time, and a help ticket opens on the third failure. Nothing stored until valid.
Six forces accepted; a force listed twice; the order stored reversed.
Auto: apps/console/lib/chat.test.ts
Ordered picks say plainly that they can't be changed later
- Read the centres and forces questions.
Both end with "Baad me badla nahi ja sakta." (the form allows no change after submission — Notice ¶11.2, ¶14.12). Changes are possible only at the review card (built next).
The warning missing; the picks presented as casual.
The events for ordered picks hold no city or force names
- Export the Events JSON after 6.34–6.35.
answer_captured field=centres via=asked and field=forces via=asked, form_card_confirmed card=centres|forces — no city names or force codes anywhere in props.
A city or force named inside an event — S1.
Auto: apps/console/lib/chat.test.ts
The signature card states the whole spec, and the console says how to "upload"
- After the job-info consent read the message and the divider under it.
"Ab signature ki file: JPEG, 10 se 19 KB, lagbhag 6×2 cm (709×236 px), seedhi (horizontal), aur box ka kam se kam 80% bhara hua…". A console divider says no file upload here — type the file's details, e.g. jpg 15 KB 709x236 (the console's voice, not the bot's).
The spec incomplete; the "type the details" hint spoken by the bot as if a real user would do it.
Auto: apps/console/lib/chat.test.ts
A file that meets the spec is accepted; the photo is coached, never requested
- Type
jpg 15 KB 709x236. Also tryJPEG 12.5kb 700 x 240,jpeg 15000 bytes 6.0 x 2.0 cm,jpg 19 KB 770x255.
"Signature theek hai ✓ (jpg, 15 KB, 709×236 px)." then, in the same turn, "Photo SSC ki site/app par live khichti hai, file bhejni nahi hai. Plain background aur achhi roshni rakho; cap, mask, chashma nahi, aur kisi photo ki photo kabhi nahi." then the next-step line (the review card is being built). Events answer_captured field=signature, form_card_confirmed card=signature.
The bot asks for a photo file; a spec-meeting file rejected.
Auto: apps/console/lib/chat.test.ts
Every way of missing the spec gets its own plain coaching
- Try, one per run:
png 15 KB 709x236·jpg 8 KB 709x236·jpg 25 KB 709x236·jpg 20 KB 709x236·jpg 15 KB 300x100·jpg 15 KB 236x709·mera signature hai.
PNG → "JPEG (jpg) me honi chahiye"; 8 KB → "10 KB se badi chahiye"; 25 KB and 20 KB → "19 KB ya usse chhoti" (20 KB is over — the strict end of "10 to 20 KB", flagged in the field map); a tiny or vertical image → "lagbhag 6×2 cm (709×236 px)… chaudai zyada, oonchai kam"; unreadable → "File ki details samajh nahi aayi. Aise likho: jpg 15 KB 709x236." Nothing is stored.
A bad file accepted; one message for every failure; a vertical image accepted.
Auto: apps/console/lib/chat.test.ts
Bad files repeat: different words, then a person
- Send
jpg 8 KB 709x236,jpg 9 KB 709x236,png 15 KB 709x236.
The second reply is a different message ("Signature ek saaf, seedhe box me…"); the third failure opens a help ticket.
The same words twice; no ticket.
Auto: apps/console/lib/chat.test.ts
The file's details are not in the events
- After 6.39 export the Events JSON and search for
709,236,15360,jpeg.
Not found: only field=signature and card=signature.
File attributes inside an event.
Auto: apps/console/lib/chat.test.ts
The review card shows everything collected, in one place
- After the signature is accepted, read the card.
"Sab kuch ek nazar me:" then Naam ROHIT KUMAR (14-03-2002) · Category OBC (NCL) · Padhai Graduate (BA/BSc/BCom) · Claims koi claim nahi · Domicile Uttar Pradesh, LUCKNOW · Exam centres LUCKNOW → KANPUR → VARANASI · Exam bhasha Hindi · Force order 1 CRPF, 2 BSF, 3 CISF, 4 ITBP, 5 SSB, 6 SSF, 7 Assam Rifles · Job-info sharing Haan · Signature jpg, 15 KB, 709×236 px, then "Sab sahi hai?…" with Haan, sahi hai / Badlo. No ID number anywhere. Events form_card_confirmed card=signature then form_card_shown card=review.
A field missing or blank; a code instead of a name; the ID number shown.
Auto: apps/console/lib/chat.test.ts
Confirming moves straight to the packet
- Tap Haan, sahi hai.
State packet_shown; the packet message (§6.52) appears in the same turn. Events, in order: application_status status=reviewed, application_status status=packet_ready, form_card_confirmed card=review, form_card_shown card=packet.
A placeholder ("still being built"); an event out of order.
"Badlo" (or "nahi") offers only what this build can still change
- Tap Badlo (or type
nahi).
"Kya badalna hai? Neeche se chuno." with exactly five rows: Domicile · Exam centres · Exam bhasha · Force order · Job-info sharing. No row for name, category, education or claims.
Identity or claims offered here (they must still go through a guide — §6.18, §6.25); a row missing.
Auto: apps/console/lib/chat.test.ts
Changing the domicile district; nothing else moves
- Badlo → Domicile → type
Kanpur.
Straight back to the card with Uttar Pradesh, KANPUR — the centres, language, forces, consent and signature lines unchanged.
Any other line changed; re-asked from the top.
Auto: apps/console/lib/chat.test.ts
Changing exam centres or force order re-runs the same validators
- Badlo → Exam centres → type
Lucknow, Lucknow, Kanpur(repeat), thenAgra, Gwalior, Bhopal. 2. Badlo → Force order →7 6 5 4 3 2 1.
The repeat is re-asked exactly as at 6.34; the good answer updates the card ("AGRA → GWALIOR → BHOPAL"); the reversed force order updates the card with the new names in order.
A bad centre list accepted from the review path but not from the first-time path (or vice versa) — the same rule must apply both times.
Auto: apps/console/lib/chat.test.ts
Changing the exam language or the job-info consent
- Badlo → Exam bhasha → English (tap). 2. Badlo → Exam bhasha → Koi aur bhasha → type
tamil. 3. Badlo → Job-info sharing → Nahi.
Each returns straight to the card with the new value; the consent change is stored as an explicit false, not blank.
A stale value shown after a change.
Auto: apps/console/lib/chat.test.ts
A wrong name still needs a guide — never a typed fix at the review card
- At the review card type
naam Rohit Sharma hai.
The card is unaffected (not read as a correction); the name is unchanged. To fix a wrong name, the candidate must have used "Kuch galat hai" back at the identity card (§6.18) — the review card cannot do this.
The name silently changes from typed text — S1 (identity must match the certificate; a typed "fix" here is unverified).
Auto: apps/console/lib/chat.test.ts
Unreadable text at the card or the change menu never repeats itself
- At the review card type
pata nahi kya likhu. 2. At the change menu (Badlo) typekuch bhi.
"Ye samajh nahi aaya. 'Haan, sahi hai' dabao, ya 'Badlo'." then the card again; "Ye samajh nahi aaya. Neeche list se ek chuno." then the menu again. The Debugger's repeat-verbatim rule stays quiet no matter how many times you retry.
The exact same bubble twice in a row.
Auto: apps/console/lib/chat.test.ts
Distress and delete still work from inside the review flow
- At the review card send
mujhe jeena nahi hai. 2. On a fresh run,delete→ Nahi, rehne do. 3. Change the domicile, thendelete→ Haan, hata do.
Distress routes as always (§8). Declining a delete returns to the review card with every value intact. Confirming a delete clears the form and the documents together.
Any of the review's values surviving an erase; distress not routed.
Auto: apps/console/lib/chat.test.ts
The packet: fee, the official link, and a plain ask — Jivika never submits
- After the review card confirms, read the message.
"Form taiyaar hai:" then Fee: ₹100 (Rohit, OBC, male) and Link: https://ssc.gov.in, then "Is link par khud submit karo — Jivika submit nahi karta. Ho jaye to yahan application ya registration number bhejo." Plain text, no buttons. No ID number.
A "submit for you" claim; the ID number shown; a missing fee or link.
Auto: apps/console/lib/chat.test.ts
A woman, SC or ST is exempt — ₹0
- Walk the path as Sunaina (F, SC) to the packet.
"Fee: ₹0 (exempt)" — feeFor on the field map, per Notice ¶9.2.
₹100 charged to an exempt category.
Auto: apps/console/lib/chat.test.ts
A valid confirmation number finishes the application
- Type
SSCGD12345.
"Mil gaya ✓ — SSCGD12345 note kar liya. Badhai ho, aapka SSC GD application yahin complete hota hai!" State packet_confirmed. The application walks packet_ready → submitted_by_candidate → confirmed (events application_status twice, then form_card_confirmed card=packet) and the number is stored on the record — never generated or guessed by Jivika.
A number invented; the application not advanced; the number missing from the record.
Auto: apps/console/lib/chat.test.ts
The number is normalised, not corrected
- Type
abc123xy. On a fresh run, type12345678.
Stored as ABC123XY (uppercased) and 12345678 (trimmed) respectively.
Case or whitespace kept verbatim in a way that would make two people's numbers look different when they're the same.
Auto: apps/console/lib/chat.test.ts
An unreadable number is re-asked, then a person — same machinery as every other question
- Send
1234(too short). 2. Sendsubmit ho gaya(a sentence). 3. Send1234 5678(a space inside).
"Ye number samajh nahi aaya. Sirf letters/numbers likho, jaise 12345678." → a different "Bas application ya registration number, jaise 12345678. Dikkat ho to 'madad' likho." → a help ticket on the third failure. Nothing stored.
A malformed number accepted; the same wording twice; no ticket on the third failure.
Auto: apps/console/lib/chat.test.ts
"mere form" shows the submitted status
- After 6.54, type
mere formand expand the list (☰ Chunein).
The row for SSC GD reads Submit ho gaya ✓ — draft.
Still "Shuru kiya" after a number was captured.
Auto: apps/console/lib/chat.test.ts
Distress, delete and the debugger, one more time, at the packet
- At the packet send
mujhe jeena nahi hai. 2. On a fresh run, confirm with a number, thendelete→ Haan, hata do.
Distress routes as always. The delete clears the application, the form and the documents — confirmation number included. The whole path (review → packet → confirmed) leaves the Debugger silent.
Any leftover value after the erase; a finding anywhere on the happy path.
Auto: apps/console/lib/chat.test.ts
7 · Commands and verbs (typed any time)
`madad` opens a guide ticket with a concrete promise
- Mid-form (any question) type
madad.
"Theek hai — hamari team se koi aapko 2 ghante ke andar call karega…". Divider guide ticket G-n queued. The Guide queue shows one open help ticket. The form carries on (the same question is still pending).
No ticket; the form is paused or reset; a vague promise.
Auto: apps/console/lib/chat.test.ts
`madad` twice does not open a second ticket
- Type
madadagain.
"Aapki request mil chuki hai…" (and a different wording a third time). Still one open help ticket.
A second ticket; the same words verbatim.
`band karo` saves the preference and says honestly that no reminder is running
- Type
band karo. Thenband karoagain.
"Theek hai, ab koi reminder nahi bhejungi…"; the second time "Reminders pehle se band hain…". (Reminders are not built — it must not pretend to cancel something.)
The same words twice; a claim that scheduled reminders were cancelled.
`mere form` before any form
- Fresh number:
mere form.
"Abhi koi form shuru nahi hua. 'hi' likh kar shuru karein." A second time: "Kuch badla nahi — abhi koi form shuru nahi hua."
An error; the same words twice.
`delete` asks first, and says what is erased
- Rohit at the result: type
delete.
"Aapki di hui jaankari (janm tithi, padhai, category, rajya) hata dun? Ye wapas nahi aayegi." with Haan, hata do / Nahi, rehne do. Nothing erased yet.
Erased without asking.
Declining a delete puts you back exactly where you were
- Mid-form (say at ask category) type
delete, then Nahi, rehne do.
"Theek hai, kuch nahi hataya." and the category question again; answers intact.
Sent back to the start; answers lost.
Confirming erases end to end
delete→ Haan, hata do. Thenmere form. Then typehi.
"Ho gaya — aapki jaankari hata di gayi…". ✦ erased: dob, qualification, category, state (lists what went). mere form says nothing started. The Events panel shows erasure_requested and erasure_completed, and all earlier events for that number are now attributed to "erased" (Export JSON: no phone number). hi starts fresh; the tip can be taught again.
Anything of the old profile survives; the phone number remains on old events.
Auto: apps/console/lib/events.test.ts
`hi` / `namaste` restarts
- At the result type
namaste.
The greeting again; a fresh start.
Read as an answer.
Commands only match short messages
- At the date question send
mujhe madad chahiye kyunki main bahut confused hoon aur yeh form samajh nahi aa raha.
A re-ask/normal handling (long text is not swallowed by the keyword madad), no ticket.
A ticket opened from a long sentence that merely contains "madad".
Yes / no / change verbs, typed
- At the card type
ok, then (fresh)theek hai,bilkul. At a card typegalat.
ok/theek hai/bilkul confirm; galat opens the four-field list.
Any read as unreadable text.
Auto: packages/flow/src/verbs.test.ts
Typed words work only where the bot offered the choice
- At the date question type
haan.
Not accepted as a date (re-ask); does not skip ahead.
"haan" advances the flow.
Unhandled input after the last built step is honest
- After a confirmation number is captured (§6.54) type
kal mausam kaisa rahega.
No fake reply; a divider no handler for this input in "packet confirmed" yet; the Debugger raises P1 no-reply (accepted, §15).
A confident wrong reply; a crash.
The daily check-in (F9, #60): a synthetic stub, reachable from anywhere
- On a fresh number type
aaj ka kaam(before answering anything else). Also trycheck-in,daily task,आज का काम.
"Aaj ka kaam: 3 sawaal, 2 minute se kam." then a buttons question (3 options, no free text). This is a synthetic stub — not real exam content — standing in for a licensed/affiliate question bank (CLAUDE.md §2).
More than 3 buttons; a free-text question; a real-looking exam question.
Auto: apps/console/lib/chat.test.ts
Mid-question, it still works — and hands you right back
- Mid-onboarding (say at the date question) type
aaj ka kaam. Answer all 3 questions correctly.
The 3 questions in turn, then "3/3 sab sahi ✓! Streak: 1 din — kal phir milte hain." and the conversation is back at the exact question that was interrupted (the date question again) — nothing lost, nothing skipped.
The interrupted question forgotten; landing somewhere else; the answers given before the check-in lost.
Auto: apps/console/lib/chat.test.ts
A wrong answer still finishes; the streak counts completion, not a perfect score
- Answer the first question wrong, the rest correctly.
"2/3 sahi ho gaye. Streak: 1 din — kal phir milte hain." The streak still went to 1.
The task blocked or restarted on a wrong answer; the streak not incremented.
Auto: apps/console/lib/chat.test.ts
The next day: a fresh task; a missed day: a lighter one, never a guilt message
- Complete a task, click the Debugger's +1 day, start again. 2. Complete a task, click +7 days, start again.
After +1 day: the same 3-question task, streak continues. After +7 days: "Aaj ek chhota sa kaam — bas 1 sawaal." — one question, the streak carries over unchanged, and nothing says a day was missed.
Any wording like "you missed" or "you didn't do it yesterday" — S1 (CLAUDE.md §3.5, never guilt); the streak reset to 0 for a miss.
Auto: apps/console/lib/chat.test.ts (passes a later now directly); live in the console via §7.21's clock control.
Already done today: no new task, never re-scored
- Complete a task, then type
aaj ka kaamagain the same session.
"Aaj ka kaam ho chuka ✓ — streak {n} din. Kal phir milte hain." No new questions; the streak unchanged.
A second task offered the same day; the streak counted twice.
Auto: apps/console/lib/chat.test.ts
Unreadable input mid-task: a nudge, the same question again — never a repeat
- Mid-task type something that isn't one of the three options, three times in a row.
"Neeche diye options me se ek dabao." then the same question again, every time. The Debugger's repeat-verbatim rule stays quiet no matter how many times you retry.
The same exact bubble twice in a row; an answer guessed from free text.
Auto: apps/console/lib/chat.test.ts
Events, "mere form" during figuring, distress, and erase
- Complete a task and check the Events panel. 2. Send a distress phrase mid-task. 3.
delete→ Haan, hata do after a completed check-in.
Events command_used command=checkin, checkin_started lighter=false, checkin_answered correct=… ×3, checkin_completed score=… total=3 streak=… — no question or option text in any event. Distress still routes to a person from mid-task. Erasing drops the streak with everything else.
Question/answer text inside an event — S1; distress not routed; the streak surviving an erase.
Auto: apps/console/lib/chat.test.ts
"Baad me" at the eligible list — finally a real message, and a door into the check-in
- At the eligible list type
baad me(declining to apply for now).
"Theek hai — jab man kare, 'aaj ka kaam' likh kar ek chhota kaam try karo, roz ka streak banao." (This state, first_task, said nothing at all before #60 — a real gap, fixed here.)
Silence; a P1 no-reply finding.
Auto: apps/console/lib/chat.test.ts
The Debugger's clock control (#133, F8a): the only way to move time in the console
- Note the Debugger's Clock row (real time, no
+Ndsuffix). 2. Click +1 day, then +7 days — the label updates each time ((+8d)). 3. Send a message; check its timestamp reflects the advanced clock. 4. Click Reset.
The clock label always shows the simulated time, with an offset suffix once advanced; every message sent after advancing is timestamped at the simulated time, not real time; Reset returns to real time and the button disappears at offset 0. No other flow is affected by an unadvanced clock (offset 0 behaves identically to before this ticket).
A sent message timestamped at real time despite an advanced clock; the offset failing to reset.
Auto: packages/flow/src/reminders.test.ts (the pure engine); no console-level automated test yet (it's a debug affordance, not a user flow) — this row is manual-only.
Deadline reminders (F8b, #132): opt-in, a labelled test deadline, `band karo` stops it
- Type
reminder chahiye. 2. Type it again. 3. Use the Debugger's clock to jump to the T-7 day (or +7/+14 days from opt-in), send any message. 4. Jump to T-3, then T-1, sending a message each time. 5. Typeband karo, jump forward again. 6. Typereminder chahiyeagain.
(1) "Theek hai — jab deadline paas aayegi, main yaad dila dungi..." (2) "Reminders pehle se on hain." (3)–(4) one "Test reminder: SSC GD (DRAFT)..." message per crossed window, each exactly once, clearly saying "test" — never presented as a real official date; the turn's own reply still follows right after. (5) reminders stop; the opt-in itself is remembered. (6) reminders resume from wherever they left off — the deadline anchor never resets.
A reminder claiming to be the notice's real deadline (a reputation-event shape, CLAUDE.md §10); a reminder firing twice; band karo wiping the opt-in instead of just pausing it; a reminder mixed into the same turn that first triggers distress.
Auto: apps/console/lib/chat.test.ts ("deadline reminders (F8b, #132)").
The fake-notice checker (F2, #61): Tracked / Old / Not found, deterministic, from any state
- Type
ssc gd 2027 sahi hai kya. 2. TypeRRB ALP 2024 bharti nikli hai, sahi hai kya. 3. Type something unrecognisable, e.g.kisi ne mujhe ek naukri ka notice bheja hai, sahi hai kya. 4. Mid-onboarding (after "haan", before answering DOB), type an old-notice check, then answer the DOB question normally. 5. Typessc gd form kaise bharein(no year, no "sahi hai kya", no notice word).
(1) SSC GD 2027 is named as tracked but not published — never "Verified" — plus its 2026 source PDF as a document card. (2) "Ye purana notice hai: RRB ALP (2024). Abhi koi naya RRB ALP notification nahi hai." (3) "Ye notice mujhe pehchaan nahi aaya — ise unverified maano..." — never guessed either way. (4) the notice reply lands, then the DOB question is answered exactly as if nothing interrupted it — the check is a pure self-loop, never a committing step. (5) ordinary onboarding capture — no notice reply at all; this is a genuine "how do I apply" question, not a forward.
Any notice ever called "Verified" (nothing in this POC's data is actually published yet — CLAUDE.md §10, a fake notice marked Verified is a P0-class reputation event, and so is the inverse); a real notice-choice tap or an "apply" intent swallowed by the checker instead; the verdict changing the conversation's state; the checked text appearing in an event's props.
Auto: packages/flow/src/notice-check.test.ts (the pure matcher), apps/console/lib/chat.test.ts ("the fake-notice checker (F2, #61)").
The Cost panel (F14, #62): a live, honest per-conversation number, never inflated
- Note the Cost panel right after the greeting (one message, 0 turns). 2. Walk through onboarding to the eligible list (step by step, no smart capture). 3. Check the breakdown rows. 4. Opt into reminders and let one fire (§7.22), then re-check the breakdown.
(1) 0 turns ⇒ no ₹/10-turns figure shown (nothing to project from yet). (2) the panel updates live every turn; the ₹/10-turns badge turns amber once it crosses ₹2.50 — expect it to, since the step-by-step path is a known, already-quantified gap (§7 turn/cost budget). (3) every line is "Service (replies)" plus a final "LLM / voice (not called yet) — ₹0.00" line, always present, never silently omitted. (4) after a reminder fires, a new "Utility (reminders)" line appears, separate from "Service" — a reminder is never folded into the free-flowing service-conversation cost, because in production it would be a genuine WhatsApp utility template send.
A ₹/10-turns figure shown at 0 turns; the LLM/voice line disappearing instead of reading ₹0; a reminder counted as "service"; the badge never turning amber even on a conversation that's visibly gone over ₹2.50/10 turns.
Auto: packages/whatsapp/src/cost.test.ts (the pure rate math), apps/console/lib/cost.test.ts (per-conversation derivation).
A closer after a resolved turn is acknowledged, not ignored (#63, L2/L6)
- Reach the eligible list, then type
thanks. 2. Repeat withok,theek hai,dhanyavad. 3. At the very start, typeokin answer to the greeting's "Haan, shuru karein?".
(1)–(2) a brief, warm reply ("Koi baat nahi!...") — never the old "no handler for this input... (DigiLocker steps not built)" console note, and the state doesn't move (a self-loop). (3) "ok" still works exactly as a real "yes" to the pending question — moves straight to asking DOB, never swallowed as a bare closer.
Silence or the stale "no handler" note for a plain "thanks"/"ok"; a genuine "yes" answer at a pending question mistaken for a closer and not advancing.
Auto: apps/console/lib/chat.test.ts ("a closer after a resolved turn is acknowledged"), packages/flow/src/verbs.test.ts (isCloser).
Three new debugger rules (#63): quiet by default, real when something's actually wrong
- Note the Debugger's rule count (19). 2. Drive any scripted scenario to completion and confirm 0 findings. 3. (Developer-only) feed the debugger a bot message containing "just", "quick", "As an AI", or "Oops" and confirm
filler-voicefires. 4. (Developer-only) a conversation at ≥10 turns whose cost projects over ₹2.50 should tripcost-over-budget; the same shape at <10 turns should not.
Every existing scenario stays debugger-clean — these are additive checks, not stricter ones. filler-voice and cost-over-budget only fire on genuinely bad content/cost, never on real product copy or any scenario measured today.
A new rule firing on existing, correct copy or an existing scenario (a false positive); cost-over-budget judging a short conversation (the volatility the ≥10-turn gate exists to avoid).
Auto: apps/console/lib/debug/rules.test.ts ("filler-voice", "closer-not-acknowledged", "cost-over-budget").
The last two debugger rules from #63: `no-next-step` (L8) and `unconfirmed-commit` (L4/L5)
- Note the Debugger's rule count (21). 2. Drive any scripted scenario that reaches
distress,guide_holdoridentity_wrong(the Distress probe; "kuch galat hai" at the identity card; an unrecordable claim) and confirm the hand-off message says a person is coming (a timeframe, or Tele-MANAS 14416) and that 0 findings fire. 3. Drive thedeletecommand toconfirm_deleteand confirm the ask is the real yes/no card, 0 findings. 4. Reachpacket_confirmed(the completed application) and confirm it is not flagged byno-next-stepeven though it names no further command. 5. (Developer-only, console-internal) feed the debugger a hand-off toidentity_wrong/guide_hold/distresswhose message never says a person is coming, and aconfirm_delete/consentask that is bare text instead of a card — both should fire.
Every existing scenario stays debugger-clean (additive checks, not stricter ones) — no-next-step and unconfirmed-commit both start at zero live findings, same as every debugger rule shipped in #63 before them. no-next-step only applies to the 3 kind: "human" states (a genuine dead end), never the 2 kind: "terminal" ones (a graceful, successful close is a status update, not a roadblock — L8's own wording). unconfirmed-commit only applies to the 2 yes/no gates before a consequential, hard-to-reverse action (confirm_delete, consent) — packet_shown's typed confirmation number is deliberately out of scope, since it is a value entry, not a yes/no gate, and is separately guarded by construction (GUIDED_TYPED keeps it out of the general smart-capture sweep, so it can never be inferred from unrelated free text).
Either rule firing on existing, correct copy or an existing scenario (a false positive); a future hand-off or consequential-action copy change silently removing the next-step line or the yes/no card without the debugger catching it.
Auto: apps/console/lib/debug/rules.test.ts ("no-next-step", "unconfirmed-commit") — each with a hand-built positive case, a hand-built negative case, and a real end-to-end scenario check (distress, the identity-card "kuch galat hai" path, and the real delete flow).
8 · Distress and the guide desk
(P0 area — a distress message with no human follow-up is a reputation event.)
A distress phrase routes to a person at once
- Any state, type
mujhe jeena nahi hai(or use the Distress probe).
"Main sun rahi hoon. Hamari team se koi aapko thodi der me call karega. Abhi baat karni ho to Tele-MANAS 14416 par call karein — 24x7, free." Divider: ticket queued, due within an hour. The Guide queue shows a distress ticket (red).
No ticket; no 14416; a cheer-up, emoji or motivation; the form question repeated.
Auto: apps/console/lib/chat.test.ts
Nothing else is asked while a person owns it
- Right after 8.1 type
koi hai?, then14-03-2002.
Only the informational line "Hamari team aapko jaldi call karegi. Tele-MANAS 14416 par kabhi bhi baat kar sakte ho." — no question, no parsing of the date, no reassurance.
The date is accepted; a new question; "everything will be fine".
Follow-up messages are added to the ticket for the guide
- After 8.1, send another message.
Divider guide ticket G-n updated — message added for the guide. The ticket in the queue shows the note.
The message is lost.
Resolving hands the form back where they were
- In the Guide queue click Resolve on the distress ticket.
Divider guide ticket … resolved … conversation resumes at "<state>"; the ticket turns resolved; the next message continues the form from that exact state.
Restart from the beginning; the conversation stays locked.
Distress works from every stage
- Trigger
mujhe jeena nahi haiat: the greeting, a question, the card, the result, and after a form is started.
The same handling every time.
Any stage where it is read as an answer or ignored.
Distress in other phrasings *(natural)*
- Try
main sab khatam kar dunga,jeene ka mann nahi karta,I want to end my life.
Each goes to the distress path.
Any is treated as chatter — S1.
Ordinary frustration is not distress
- At a question send
I give up on this form, andye form bahut mushkil hai.
Normal handling (re-ask / help), no distress ticket. And the bot never says "give up" itself.
A distress ticket for frustration; the bot telling the user to give up.
The guide queue clock
- Open a distress ticket and a help ticket.
Distress "call by" ≈ 60 minutes out; help ≈ 2 hours; times in your local timezone; counters show "n open".
Both the same SLA; times in another timezone.
Reset closes that number's tickets
- With an open ticket for Rohit click Reset.
The conversation restarts and that ticket is closed.
An orphan ticket for a conversation that no longer exists.
Auto: apps/console/lib/workspace.test.ts
9 · Repair — re-asks and escalation
A re-ask never repeats the question word for word
- At the date question send
kal, thenphir kuch.
First: "Ye tithi samajh nahi aayi…" (a note, not the question). Second: a different message with an example and "Dikkat ho to 'madad' likho."
The original question repeated verbatim.
The third failure opens a person
- Fail the same question three times.
"Ye thoda atak raha hai — maine hamari team ko bata diya hai. Koi aapko 2 ghante ke andar call karega…" and a help ticket in the queue whose text names the field and the attempts. Divider "3 failures at this question → help ticket requested".
A fourth identical re-ask; no ticket; the attempts lost.
The ticket is not opened twice
- Keep failing after 9.2.
Later replies vary; no second help ticket while one is open.
A ticket per failure.
A list question's re-ask points at the list
- At education type
xyz, thenabc.
"Ye list me nahi mila. Neeche 'Chunein' dabao aur ek chuno." then a second, different wording.
The question text repeated.
A correct answer resets the count
- Fail the date twice, answer it correctly, then fail the state question once.
The state failure is counted as 1 (plain re-ask), not 3.
Escalates immediately.
Gibberish probe
- Use the Gibberish probe 3 times.
As 9.1–9.2; the Debugger shows retry-loop if a 4th identical reply happened (it must not).
A loop without a ticket.
10 · Numbers and shared phones
Each number has its own conversation
- Walk Rohit to the card. Switch (pulldown) to Sunaina. Switch back.
Sunaina is at the greeting; Rohit is exactly where he was.
Any state or answer carried across numbers.
Another number never sees your answers — **S1**
- Give Rohit his profile. On Sunaina type
mere formandhi.
Nothing from Rohit (date, category, form) appears in Sunaina's thread, buttons or Events tagged to her.
Another profile's data shown — a reputation event.
Auto: apps/console/lib/debug/rules.test.ts
Events belong to the right number
- Events panel: this number vs all numbers while switching.
The counts and rows follow the number; all numbers shows the last 5 digits per row.
Events of one number listed under another.
+ New number mints a synthetic number
- Open the pulldown → + New number.
A new persona in the +9199999xxxxx block, greeting shown, becomes active. There is no way to type a number.
Any way to enter a real phone number; a number outside the block.
Auto: apps/console/lib/personas.test.ts
The pulldown shows tickets and findings per number
- Give Rohit a help ticket and a Debugger finding (e.g. Devanagari probe).
The pulldown shows an open-ticket badge and the finding count against Rohit only.
Badges on the wrong number.
Reset only affects the active number
- Progress two numbers. Reset one.
Only that thread restarts; the other stays.
Both reset.
A reload keeps your conversation (#23)
- Give Rohit a few turns (past the profile card). Reload the page (same browser tab or a new tab in the same browser).
Within a few seconds, the full transcript and current state reappear exactly as left — including the very first greeting message. A second browser tab on the same conversation picks up a reply sent from the first within ~4 seconds (polling), without reloading.
The conversation resets to the greeting; the greeting message itself is missing from the restored transcript even though later messages are present (a narrower, easy-to-miss variant of the same failure — check the very first bubble specifically); a stale copy that never updates from the other tab.
Auto: apps/console/lib/db/threads.test.ts (the persistence/reconstruction logic — no live Supabase needed to run it).
A different visitor never sees or shares your conversation (#23)
- Progress Rohit in one browser (or clear
localStorage→ reload → note you're now a "different visitor"). Open the same URL in a second, unrelated browser (or an incognito window) and also pick Rohit.
The second visitor's "Rohit" starts at the greeting, completely independent of the first's progress — each anonymous visitor gets their own private copy of every persona, not a shared one.
Two unrelated visitors both picking "Rohit" see or edit the same live conversation.
11 · The Debugger and the event spine
A clean run is clean
- Walk Rohit's full path to a started form. Watch the panel.
P0 0 · P1 0 · P2 0 (until you type something unhandled after the start).
Any finding on a happy path — file it, it is either a product bug or a bad rule.
Probes trip their rules
- Click Distress, Devanagari, Gibberish (×3).
Distress → no finding when routed correctly; Devanagari → a P2 register-mismatch; Gibberish×3 → the escalation (§9.2) and no P0.
A P0 on a correct path.
Findings can be jumped to
- Trigger a finding. Click jump →.
The thread scrolls to the offending message and highlights it.
Nothing happens.
Filters
- Filter P0/P1/P2/all; toggle all numbers / this number.
The list follows the filter and the counts.
A stale list.
Copy report
- Click Copy report and paste it somewhere.
A plain-text report: counts, each finding with its source, the transcripts involved, your timezone.
An empty report; the button says "Copied ✓" but nothing was copied.
Every turn emits events; the Events panel follows
- Rohit: one-sentence capture (§3.1), then
Haan, sahi hai.
Rows in order, e.g. conversation_started source=console · answer_captured field=dob via=asked … profile_card_shown · profile_confirmed · eligible_list_shown n=1 k=1. Times in your timezone.
Missing steps; out of order; wrong via (asked / sentence / correction).
Auto: apps/console/lib/events.test.ts
Events hold ids and codes only — **S1 if not**
- Send messages that look like personal data:
Mera naam Rohit Kumar hai, phone 9876543210,aadhaar 1234 5678 9012,rohit.kumar@example.com. - Export JSON from the Events panel and search it.
None of that text — no name, number, date of birth or email — appears in any props. Only codes (field=dob, n=1 k=1, from=ask_state).
Anything a user typed inside an event — S1 (props are ids and codes only).
Auto: apps/console/lib/events.test.ts
Export JSON
- Click Export JSON.
A file jivika-events-…json downloads; it parses; rows are numbered from 1 with seq, at, phone, name, props.
No download; invalid JSON.
Scripted access
- In the browser console run
window.__jivikaDebug.events.lengthandwindow.__jivikaDebug.findings.
A number and an array.
undefined.
12 · The design pages
`/flow` renders every conversation
- Open
/flow. Scroll the field guide.
15+ phone-style threads, each a heading, a sentence and a conversation; four diagrams render (no raw Mermaid text, no "Syntax error"); a contents list works.
A thread blank; a diagram shown as code.
Auto: apps/console/lib/flowdoc.test.ts
The flow page tells the truth
- Pick three threads (e.g. the eligible result, "mere form", the card correction) and replay them in the console.
The console produces the same messages (the page is generated from the same code).
A difference.
`/scope` renders
- Open
/scope.
Sections, three diagrams, the "Built (slice 0)" note under the event spine.
A build/render error.
Internal pages stay out of search
- View source of
/flow,/scope,/qa.
<meta name="robots" content="noindex, nofollow"> on each.
Indexable.
Navigation
- Use the top nav: Simulator, Gallery, Flow, Scope, QA, Ops.
Each goes where it says; the active item is highlighted; works on a phone-width window. Since #13: "Ops" is still a public nav link on every page (the label itself isn't a secret — it's documented in this file and in HANDOFF.md), but clicking it when signed out now 404s rather than showing the shell — that's the intended gate, not a dead link.
A dead link for a signed-in ops user; /ops rendering real content for a signed-out visitor.
Dark theme everywhere
- Visit
/,/console,/flow,/scope,/qa.
A consistent dark theme; readable text; diagrams legible.
A white flash or unreadable section.
`/ops/funnel`: the leak map (#86, CRM slice 1)
- Sign in at
/ops/loginwith an allowlisted account (§12.8). 2. Open/ops— note the "Funnel lens" card (not a "not built yet" placeholder like the other three). 3. Click through to/ops/funnel. 4. Read the stage bars top to bottom. 5. Read the five stalls.
A non-empty, non-increasing funnel (registered ≥ profiled ≥ … ≥ retained); "profiled" and "saw value" show the same count every time (they fire in the same turn); all five stalls render, including "profiled, never saw a list" at 0 with its own one-line explanation (never hidden); a footnote explicitly labels "Retained" as a streak-≥2 proxy and says Credentialed/Advocate aren't built.
An empty or all-zero funnel; a stall silently omitted instead of explained; "Retained" presented as if it were real D30 activity; any count that isn't traceable to a real, re-runnable synthetic lead.
Auto: apps/console/lib/funnel/cohort.test.ts (each lead stops where it claims), apps/console/lib/funnel/stages.test.ts (the pure derivation/stall logic).
`/ops` sign-in: Google OAuth + the Supabase allowlist (#13)
- Signed out, visit
/opsand/ops/funneldirectly. 2. Visit/ops/loginand click "Continue with Google." 3. Sign in with a Google account that is not on the allowlist. 4. Sign in again with an allowlisted account (the founder's own, already seeded). 5. With the session from step 4, revisit/opsand/ops/funnel.
Step 1 → a plain 404 for both, not a redirect and not the shell (the surface isn't advertised). Step 3 → bounced back to /ops/login?denied=1 naming the rejected account, app still otherwise fully usable (nothing crashes). Step 4 → lands on /ops. Step 5 → both pages render real content.
/ops renders anything for a signed-out visitor; a non-allowlisted Google account reaches /ops; the denied banner doesn't name which account was rejected; a crash instead of a clean bounce.
Auto: apps/console/lib/auth/ops.test.ts (the allowlist hash/fail-closed logic, pure). There is no automated test for the live OAuth round trip or the DB-side grant_ops/RLS behavior — no live-Supabase-integration-test harness exists anywhere in this repo yet (consistent with the rest of the codebase, not a gap unique to this ticket); that side was checked once, manually, via mcp__supabase__execute_sql when #13 shipped. **Note:** all five steps are fully verified live (2026-10-04) — the founder signed in with their allowlisted Google account and landed on /ops; confirmed in the DB (ops_users.is_ops = true).
`/ops/guide`: the guide desk (#87, CRM slice 2)
- In
/console, on a synthetic number, say a distress phrase ("mujhe jeena nahi hai"). 2. Send a second message while still in distress. 3. Sign in at/ops, open the "Guide desk" card. 4. Claim the distress ticket. 5. Add an internal note. 6. Reply into the thread. 7. Go back to/consoleon that same number and wait ~4 s (the existing poll). 8. Return to/ops/guideand resolve the ticket. 9. Check the/consolethread again.
Step 1 → a guide_tickets row appears (priority P0, due in 60 min, excerpt = the distress line). Step 2 → the SAME row's "while waiting" notes grow by one — no duplicate ticket. Step 3 → the card reads "live", not "not built yet". Step 4 → status flips to claimed. Step 6 → the reply shows up in the candidate's /console thread as a normal assistant bubble, signed "— Jivika guide se" — the candidate never sees the internal note from step 5. Step 8 → the ticket leaves the open queue. Step 9 → the conversation has resumed exactly where distress interrupted it (the next assistant message answers whatever question was pending, not a repeat of the distress-hold line) — never left stuck.
A second distress message creates a second ticket instead of appending a note; an internal note leaks into the candidate's thread; a reply never reaches /console even after a hard reload; resolving a distress ticket leaves the candidate's conversation still parked in the distress state; /api/ops/tickets or /ops/guide return anything but a 404 when signed out.
Auto: apps/console/lib/db/tickets.test.ts — kind/priority/SLA mapping for all 7 §4.4 kinds, loadOpenTickets reconstruction, and a closeTicket test that asserts the real resolveTicket() actually ran (the resulting state is no longer the distress state), not a mock standing in for it. **Known gaps, not bugs (see §15):** only distress and help→form_error are ever actually created today; the other 5 §4.4 kinds render in the inbox but nothing opens one yet. /console's own local "Mark called" button and /ops/guide's server-side resolve are two independent paths that don't sync with each other.
13 · Voice, register and copy
Register matching
- Read the whole thread of a full run.
Romanised Hinglish throughout, in the voice of a helpful senior — short sentences. Never formal Hindi, never Devanagari, no emoji storms.
Devanagari or stiff Hindi ("kripya…"); English-only lecture.
Never guilt, never "give up"
- After Baad me, band karo, a "not eligible" result and an over-limit result, read the reply.
Calm and forward-looking ("koi jaldi nahi", "hum dekhte rehte hain").
Any sentence that blames, shames, pressures or tells the person to give up — S1.
No automated cheer-up on the hard day
- After a distress message read every bot line.
Acknowledgement, a person is coming, Tele-MANAS 14416 — nothing else.
"Sab theek ho jayega", motivation, emoji.
Buttons before prose
- Note every structured decision (greeting, education, category, card, form card, delete).
Each is buttons or a list; free text is used only for the date, state and corrections.
A yes/no or a pick that requires typing.
Short messages
- Read each bot message.
Two sentences or fewer (the card and consent-style notices excepted), no stacked bubbles saying the same thing.
A wall of text; the same content sent twice in a row.
Timestamps carry your timezone
- Look at message times and the Guide queue.
e.g. 10:33 AM PDT — your local timezone next to the time.
No zone; the server's zone.
Placeholders never leak
- Read every bot message across a full run.
No {n}, {first}, {date} or undefined in any text.
A raw placeholder — the Debugger shows unfilled-placeholder.
14 · Cross-cutting invariants (spot-check every release)
Synthetic data only
- Search the page, the report and the events export for anything that looks like a real number/name.
Only +9199999xxxxx, "(synthetic)" names.
Real personal data anywhere — S1.
Value before identity
- Any run: find the first mention of DigiLocker/Aadhaar/documents.
After the eligible list, and only after the person chose to start a form.
Earlier — S1.
The bot never automates a portal or takes money
- Read every message about forms.
No fee, payment, "I submitted for you" or a link that submits. (Fee guidance and links arrive with guided apply.)
Any charge, convenience fee, paid tier or claim of submitting — S1.
Government jobs only
- Look for any employer-facing wording or private-job pathway.
None.
Any — out of Phase I scope.
The eligibility engine, not the chat, decides
- Repeat 5.3, 5.6 and 5.7 and check the message against the
✦ engine:divider.
Always consistent, limits come from the rules (23/26/28).
A mismatch — S1.
Every eligibility claim is labelled
- Read each result and each form row.
"DRAFT" / "Draft — official notice aana baaki" everywhere until a real notice exists, and the source document is the official 2026 PDF.
An unlabelled claim — a fake-notice-marked-Verified class of bug, S1.
Shared-phone safety
- Repeat 10.2 with the same number after Reset and after delete.
No trace of the previous profile.
Leftover answers or documents — S1.
Erasure is end to end
- Repeat 7.7 and inspect thread,
mere form, Events (Export JSON) andwindow.__jivikaDebug.
Nothing attributable to the number remains.
Any leftover.
The Debugger is quiet after the whole suite
- After a full run of this plan check the panel across all numbers.
Only the findings you intentionally provoked (Devanagari P2, unhandled-after-start P1).
An unexplained P0.
Production parity
- Repeat 5.1, 4.4 and 6.3 on
www.jivika.dev/consoleafter a deploy.
Identical to local. (Check the deployed content, not just the deploy status — the domain lags ~20 s.)
A difference.
15 · Known gaps — these are not bugs
| Gap | Where | Tracked |
|---|---|---|
| After a confirmation number is captured, nothing more is built; further input → P1 no-reply. No admit-card or result stage, no screenshot OCR for the confirmation number (typed only). Identity and claims cannot be changed at the review card — only through a guide (by design, §6.49). ESM / sports / another-state / 1984-dependant claims still go to a person (not yet recorded by the bot) | §6.49, §6.57, §7.12 | #59.2b-2 |
Deadline reminders (F8, #132) are opt-in-only, not proactively offered. remind/stop work, but nothing in the live conversation ever tells an aspirant the command exists — unlike mere form/madad, which are taught once at the eligible list. The reminder itself also targets one fixed notice (SSC GD 2027) against a labelled test deadline, since the real 2027 notice still has none — not the general "any eligible or applied-for notice" F8 describes | §7.22 | a future ticket: teach the trigger, and generalise past one hard-coded notice once a real deadline exists |
| The fake-notice checker (F2, #61) can never say "Verified." Every notice this repo tracks is still DRAFT or awaiting publication — there is no genuinely published, ingested notice in this POC's data to demonstrate that outcome against. Same root cause as the reminders gap above: a data-readiness gap, not a code gap. The checker also only recognises a small, hand-picked registry of real historical exam names (RRB ALP 2024, SSC CHSL 2019, Railway Group D 2019) and the two notices this repo already tracks — a real ingestion pipeline is out of POC scope (CLAUDE.md §5A) | §7.23 | demonstrable once a real notice is actually ingested (production, not POC) |
The console has no real file upload: the signature's format/size/dimensions are typed (jpg 15 KB 709x236). The same field-map validator will judge a real file; whether the strokes fill 80% of the box cannot be checked from those three facts | §6.38 | real WhatsApp media (post-POC) |
| District names are free text (the portal's district lists are not in the notice), so a typo in a real district is not caught by the bot | §6.29 | field-map needsReview |
| DigiLocker is a mock: the documents are synthetic and deterministic per number | §6.11 | #59 → real requester later |
Persistence (#23) covers conversations/messages/cost; the event spine (ws.events) still resets on reload. Guide tickets are now persisted (#87) — only the in-memory event spine remains a per-tab/per-reload thing | §10.8 | a future ticket, lower priority than tickets were |
/api/health is publicly reachable | — | By design — uptime checks need it reachable without auth |
| Replies are always romanised Hinglish; a Devanagari-typing user gets a P2 register-mismatch | §2.8 | — |
No reminders exist; band karo only saves the preference | §7.3 | #60 |
Only distress and help→form_error tickets are ever actually opened (#87). The other 5 §4.4 kinds (stuck_application, fake_notice_review, data_request, payment_guidance, abuse) render correctly in the guide desk's schema/UI but nothing in the app detects them yet — each is its own future ticket. The three 24h SLA numbers for those kinds are a POC placeholder, not researched compliance figures (data_request especially — the spec calls it "mandated"). /console's own local "Mark called" (client-only resolve) and /ops/guide's server-side resolve are two independent, unsynced paths; there's no realtOS-style ack/assignee split — claim does both | §12.9 | #87 (follow-ups) |
| One shared phone has one profile (F11 multi-profile per number not built; schema has a UNIQUE on the number) | §10 | #24 |
| Only SSC GD is checked; "Kaun sa form?" is unit-tested only | §6.8 | RRB rules |
Copy distress.holding is not yet reviewed by the guide desk | §8.2 | #45 |
| Rules are a DRAFT (2026 notice as a template; crucial date assumed) | §0.2 | re-verify ~Dec 2026 |
| Hindi/English registers exist in copy but the console always uses Hinglish | §13.1 | — |
The Cost panel's LLM and STT lines always read ₹0 and the per10Turns figure is a linear projection, not a benchmark run. Neither an LLM nor STT is called anywhere in this console yet, so real per-conversation cost is understated (WhatsApp-send cost only). The projection is also volatile for a short, front-loaded conversation — see §7.24 | §7.24 | LLM/STT lines land whenever an LLM or STT step is actually wired in |
no-next-step only covers the 3 kind: "human" hand-off states (guide_hold, identity_wrong, distress) — it does not audit every bot text message in the app for a "next step," which would risk false-positiving ordinary mid-flow utility lines (e.g. "Centres noted: ..."). L3 ("never ask a question you can't field") remains a candidate, untouched by this ticket | §6 L3, L8 | a future ticket, if the narrower scope proves insufficient |
unconfirmed-commit only covers 2 of the 3 consequential actions (confirm_delete, consent) — packet_shown's typed confirmation number is deliberately excluded (a value entry, not a yes/no gate; separately guarded by construction, not by this rule) | §6 L4/L5 | — |
The funnel lens (/ops/funnel, #86) is still a synthetic, build-time snapshot, not a live view. The 9-lead cohort is hardcoded and regenerated only on rebuild. #23 (persistence) is done, but the funnel lens itself was never rewired to read real /console sessions from it — that's separate follow-up work, not an automatic side effect of #23 shipping. Retained is a proxy (check-in streak ≥ 2; there is no real wall-clock D30 in a one-shot conversation). Credentialed and Advocate (from the scope doc's §5.2 stage table) are not shown at all — neither has a built feature to derive them from (no admit-card/result flow, no credential card). "Profiled, never saw a list" is structurally always zero — profile_confirmed and eligible_list_shown fire in the same reply() turn — and the lens explains this rather than hiding the row | §12.7 | a future ticket to point the funnel lens at real persisted sessions; Credentialed/Advocate need the admit-card/result/credential features |
16 · Changelog
- 2026-10-06 · v1.23 — #63 (closed out): the last two debugger rules,
no-next-step(L8) andunconfirmed-commit(L4/L5).no-next-stepchecks everykind: "human"hand-off state (guide_hold,identity_wrong,distress) says what happens next (a person is coming, a timeframe, Tele-MANAS 14416) — the 2kind: "terminal"states (packet_confirmed,first_task) are deliberately exempt, since a graceful, successful close is a status update, not a roadblock (L8's own wording).unconfirmed-commitchecks the 2 yes/no gates before a consequential, hard-to-reverse action (confirm_deleteerasing everything;consentpulling real DigiLocker documents) are a structured buttons/list card, never bare text —packet_shown's typed confirmation number is deliberately out of scope, since it's a value entry guarded separately by construction (§7.27 added; the Known Gaps row narrowed to the two rules' now-precise scope rather than "not built"). Both rules start at zero live findings — measured against every existing scripted scenario (including the real delete flow and everykind: "human"state) before shipping, same discipline ascost-over-budget/closer-not-acknowledged. 21 debugger rules total; 402 console tests. - 2026-10-06 · v1.22 — #148: fixed the React hydration-mismatch console error (minified #418) that fired on every single
/consoleload, filed during #23's testing but accidentally auto-closed by GitHub on merge without ever actually being fixed (a PR body saying "not fixed: #148" still tripped the closing-keyword matcher — reopened, then fixed for real).apps/console/app/components/DebugPanel.tsx's clock display now mounts client-only (useState(null)+useEffect), the same pattern the panel's own "first seen" finding timestamps already used — no render-timenew Date()is ever baked into the statically-prerendered server HTML anymore./consolestays statically prerendered (unchanged build output); the debug clock's behavior changes only in that it now updates on mount and on a clock-offset change, not on every incidental re-render — aDatefor a debug-only display, never functionally load-bearing. (§1.1 updated with a console-error check.) - 2026-10-05 · v1.21 — #139: a profile-field correction now works self-service anywhere between the profile card and the document pull (
show_eligible,choose_notice,notice_saved, pre-agreeconsent) — both "Badlo"/"galat hai" and a typed correction sentence ("meri dob galat hai, 12-03-2000") land back on the profile card instead of "no handler for this input" (§4.10 added; the Known Gaps row from v1.13 removed).identity_cardonward is unaffected — its own pre-existing "change" →identity_wrongedge already sent corrections to a guide, by design (§6.49), and this fix reuses that same state-machineon-map mechanism rather than touching it. Flow bumped to v15 (3 new edges, no new states). - 2026-10-05 · v1.20 — #87 (CRM slice 2): the guide desk —
guide_tickets/guide_ticket_notesnow persisted (realtOS's "Option B": the ticket is the incident, signals attach to it), a real/ops/guide(claim, internal note, reply-into-thread which shows up in the candidate's own/consolethread via #23's existing poll, resolve/dismiss — resolving a held distress ticket actually resumes the conversation, reusing the existingresolveTicket()rather than reimplementing it) (§12.9 added; Known Gaps rows updated — the event-spine-only persistence gap narrowed now that tickets persist, and a new row for the 5 still-undetected §4.4 kinds). Two real bugs caught before shipping, not after: the in-memory ticket id ("G-1"-style) was never written to the DB'suuidcolumn (new tickets insert without specifyingid, letting the DB mint its own); the distress-resume reconstruction initially used an emptyentries: [], which would have made the persistence layer's diff re-insert a held conversation's entire history on every resolve — fixed by reconstructing real entries frommessages, the same way existing code already does elsewhere. - 2026-10-04 · v1.19 — #23: console threads (conversations, messages, cost) now persist to Supabase and survive a reload; every anonymous
/consolevisitor gets their own private copy of each persona via a client-generatedlocalStorageid, so two unrelated visitors never collide (§0.2 corrected, §10.7/§10.8 added). Guide tickets and the event spine deliberately still reset on reload — that's #87's job, not retrofitted here (Known Gaps row updated). Found and fixed live, the hard way, during this ticket's own verification (not left as silent gaps):Entry.atis a display string, not a timestamp, and broke a raw DB write; a message-insert failure could desync a conversation's snapshot from its actual messages; the opening greeting was dropped from persistence on a fresh conversation AND again after a reset; an un-percent-encoded+in a synthetic phone silently became a space in every query string. Also filed (not fixed, pre-existing, unrelated to #23): #148, a React hydration-mismatch console error on every/consoleload, traced to the Debugger panel's clock reading the real clock during a statically-prerendered render. - 2026-10-04 · v1.18 — #13 fully closed out: the founder completed all three manual setup steps and a real Google sign-in, confirmed live end to end —
/ops404s signed out and works signed in as an allowlisted account,ops_users.is_ops = trueconfirmed in the DB. The Known Gaps row from v1.17 is removed (no longer a gap); §12.8's note updated to reflect the completed verification. - 2026-10-02 · v1.17 — #13: gate
/opswith Google OAuth + a Supabase-held email allowlist (ported from the sibling realtOS repo's ownops_allowlist/is_opspattern) —/opsand/ops/funnel404 for a signed-out request; RLS turned on (no policies yet) for the 12 pre-existing tables as a zero-risk bundled fix (§12.5/12.7/12.8 updated or added; the Known Gaps row for the old "publicly reachable" state replaced with one for the three manual founder-only setup steps still pending — a Google Cloud OAuth client, the Supabase Auth provider toggle, andSUPABASE_SERVICE_ROLE_KEYin Vercel). - 2026-10-01 · v1.16 — #86 (CRM slice 1): the funnel lens at
/ops/funnel— 8-stage conversion (registered → … → retained) and "the five stalls" (leak map), derived purely from the domain event spine (#85) of a synthetic, deterministic 9-lead cohort driven through the realreply()engine (§12.7 added; a Known Gaps row for the three honest departures — the build-time/non-live snapshot, the Retained proxy, and the omitted Credentialed/Advocate stages). - 2026-09-30 · v1.15 — #63: three more debugger rules —
filler-voice(P2),closer-not-acknowledged(P2, which came with an actual fix: a closer now gets a warm reply instead of the stale "no handler" note), andcost-over-budget(P2, gated at ≥10 turns to avoid judging a naturally volatile short conversation) (§7.25–7.26 added; a Known Gaps row for the two rules still not built —no-next-step,unconfirmed-commit— both need more design thought to avoid false-positiving good copy). - 2026-09-30 · v1.14 — #62 (F14): a live per-conversation Cost panel — total ₹, a service/utility breakdown, a ₹/10-turns projection (§7.24 added; a Known Gaps row replaces the stale "not built" one — LLM/STT always read ₹0 since neither is wired in yet). Measured a real, already-flagged gap: the step-by-step arrive→eligible-list path projects to ₹2.71/10 turns, over the ≤₹2.5 target.
- 2026-09-29 · v1.13 — new Known Gaps row: a profile-field correction (DOB etc.) is unhandled between the profile card and the identity card — found by inspection, reproduced, filed as #139, not fixed yet.
- 2026-09-29 · v1.12 — #61 (F2): the fake-notice checker — Tracked-unpublished / Old / Not found, deterministic, from any state (§7.23 added; a new Known Gaps row — no "Verified" outcome is demonstrable yet, same root cause as F8's missing real deadline).
- 2026-09-29 · v1.11 — #132 (F8b):
remind/stopopt-in, a labelled test deadline for SSC GD 2027, T-7/T-3/T-1 delivery (§7.22 added; gaps row narrowed — reminders now wired, just not proactively taught). - 2026-09-28 · v1.10 — #133 (F8a): the due-reminder engine + the Debugger's clock control (§7.21 added; §7.16 now testable live; gaps updated).
- 2026-09-28 · v1.9 — #60: the daily check-in (§7.13–7.20 added, gaps updated; deadline reminders F8 scoped out as a separate ticket).
- 2026-09-28 · v1.8 — 59.3b: the packet — #59 guided apply done end to end (§6.44 rewritten, §6.52–6.58 added, §7.12/gaps updated).
- 2026-09-27 · v1.7 — 59.3a: the review card (§6.35 note, §6.43–6.51 added, §7.12/gaps updated).
- 2026-09-26 · v1.6 — 59.2d: the signature card + photo coaching (§6.32 note, §6.38–6.42 added, gaps updated).
- 2026-09-25 · v1.5 — 59.2c-2: exam centres and force preferences (§6.27/6.28/6.31 updated, §6.34–6.37 added).
- 2026-09-25 · v1.4 — 59.2c-1: domicile, exam language, job-info consent (§6.22 updated, §6.27–6.33 added, §7.12 updated).
- 2026-09-25 · v1.3 — 59.2b: the claims card (§6.17 rewritten, §6.21–6.26 added, §7.12 updated).
- 2026-09-25 · v1.2 — 59.2a: the identity card (§6.11 updated, §6.16–6.20 added, §7.12 updated).
- 2026-09-25 · v1.1 — 59.1: consent notice + mock DigiLocker (§6.3 rewritten, §6.10–6.15 added, §7.12 updated).
- 2026-09-25 · v1 — first plan: sections 1–14 for the console as of #57 (arrival, the four questions, smart capture, the profile card, eligibility, choose a form, commands, distress, repair, numbers, debugger and events, design pages, voice, invariants).