# WORKPLAN — TrahKeluarga.com V1

> Status: **V1 FEATURE-COMPLETE AND REAL-DB E2E VERIFIED.** All 28 tickets
> are implemented, reviewed, and committed. A real Neon Postgres database
> was provisioned post-build, migrated, seeded, and clicked through live —
> see "Final status & handoff" near the end of this file for what was
> actually tested and the one real bug found (nested `<form>` in
> Relationship Finder — invalid HTML only a real browser could catch,
> already fixed and committed) and next steps (deploy).

> Cuts `spec/` (Tier A/B, Gate-2 approved) into tickets per the
> project's ladder rules: each ticket ≤5 steps / ≤40 turns, one ticket = one
> dispatch. Lane is determined by the danger-list (money · locks · auth-RBAC ·
> webhooks · prod migrations) — IAM and Trah&Membership tickets that implement
> auth or permission enforcement are danger-list (tester-first, deep review);
> everything else is standard lane (koder + inline tests, light reviewer).

## Ordering constraint

`Scaffold → IAM → Trah&Membership (minus claim) → Genealogy → Trah&Membership
claim → Audit → UI → Seed data → PWA polish`, because:
- Everything needs the scaffold.
- Trah&Membership.claimPerson depends on Genealogy.personExists, so claim is
  cut after Genealogy's Person write path exists, not before.
- Genealogy's every write calls Audit.record and Trah&Membership.canWrite, so
  both must exist first, but Audit itself only depends on Trah&Membership.

## Tickets

### Scaffold (standard)
- [ ] T-000 Project scaffold: Next.js + TypeScript, Prisma, module folder
      structure (`src/modules/{iam,trah-membership,genealogy,audit}`), base
      PWA shell (manifest + service worker skeleton per ADR-0005), lint/test
      runner wired, local dev README.
- [ ] T-001 Full Prisma schema (all 4 modules' tables per Tier B specs) +
      initial migration, applied against a local/dev Postgres.

### IAM — danger-list (auth)
- [ ] T-101 `register` + password hashing (INV-IAM-1..3), unit tests.
- [ ] T-102 `login`/`logout`/`verifySession` via Auth.js Credentials +
      DB sessions (INV-IAM-4), unit tests.
- [ ] T-103 `requestPasswordReset`/`confirmPasswordReset` (REQ-ACC-004,
      INV-IAM-5..6), unit tests.

### Trah & Membership, part 1 — danger-list (auth-RBAC)
- [ ] T-201 `createTrah`, `getMembership`, `getRole`, `listMembershipsForUser`.
- [ ] T-202 `invite`, `acceptInvitation` (7-day expiry).
- [ ] T-203 `canWrite`, `canManageMembers`, `canInvite`, `changeRole`,
      `removeMember` — permission enforcement, unit tests per role matrix.

### Genealogy — standard
- [ ] T-301 `createPerson`, `updatePersonFact` (precision qualifiers,
      multi-value disputed facts per REQ-ID-002/005), unit tests.
- [ ] T-302 `addResidence`, `addContact` (privacy visibility), unit tests.
- [ ] T-303 `createRelationship`, `updateRelationship` — parent/child
      (biological/adoptive/step), partner/spouse incl. concurrent polygamy
      and sequential remarriage, sibling; divorce must not touch
      PARENT_CHILD edges (REQ-REL-001..004). Unit tests per edge case in
      blueprint §4.5.
- [ ] T-304 Kinship computation: BFS nearest-common-ancestor + label mapping
      (parent/child/sibling/grandparent/aunt-uncle/cousin, with
      adoptive/step qualifiers) — `findRelationship` (REQ-REL-005,
      REQ-SRCH-002). Unit tests against a fixture graph, not a live DB.
- [ ] T-305 `getProfile` (privacy-scoped, claim badge via
      Trah&Membership.isPersonClaimed), `getTree` (ancestors/descendants/
      partners, generation-depth filter, multi-partner branch grouping),
      `searchByName`.

### Trah & Membership, part 2 — danger-list (auth-RBAC)
- [ ] T-204 `claimPerson`, `isPersonClaimed` (calls Genealogy.personExists).

### Audit — standard
- [ ] T-401 `record` (called transactionally from every Genealogy write),
      `getHistory` (any active member, all roles).

### UI — standard (light review; verifikator once a feature's tickets land)
- [ ] T-501 Auth pages: register, login, logout, request/confirm password
      reset.
- [ ] T-502 Trah creation + invite + membership/role management UI.
- [ ] T-503 Claim-profile UI flow ("Are you X?" confirm).
- [ ] T-504 Person create/edit form: identity fields, precision date input,
      residence history, contact + per-field privacy toggle.
- [ ] T-505 Relationship management UI: add parent/partner/child, end a
      partnership (divorce/widowed), add a concurrent partner (polygamy).
- [ ] T-506 Interactive tree UI: expand/collapse, generation-depth filter,
      multi-partner branch rendering, center-on-person.
- [ ] T-507 Person profile page UI (privacy-aware, verification badge).
- [ ] T-508 Search UI.
- [ ] T-509 Relationship Finder UI (path display).
- [ ] T-510 Audit history UI.

### Data & polish — standard
- [x] T-601 Seed script: dummy validation dataset per blueprint §6 (nasab
      of Prophet Muhammad ﷺ), with Source citations and precision/
      verification-status fields exercising REQ-ID-002/005, REQ-REL-002/004,
      REQ-REL-005 as described in blueprint §6. Internal validation only,
      not shown as default seed to real users. See `prisma/seed.ts` (header
      comment has full scope note); wired via `npm run db:seed` /
      `package.json`'s `prisma.seed` field (Prisma convention, `tsx`), never
      run automatically on deploy. Every row is written through the real
      Genealogy contract functions (createPerson/updatePersonFact/
      createRelationship), not raw Prisma inserts, so the seed exercises the
      actual write path (canWrite() checks, audit logging) — not a
      shortcut. Per the "Environment constraint" section below, there was
      no live database to execute it against in this environment; it was
      verified via `npx tsc --noEmit` against the real Prisma Client types
      only, and must be run for real (`npm run db:seed`) once a database is
      provisioned, before relying on it for engine validation.
- [ ] T-602 PWA finalization: icons, offline-shell precache verification,
      installability check (ADR-0005).

## Environment constraint discovered during build

No local PostgreSQL and no Docker are available on the build machine, and
Claude does not install a database server or other persistent system
software on the user's machine unattended. Consequence for every ticket
from T-001 onward:
- `prisma validate`, `prisma format`, and `prisma generate` (Client
  generation) all work without a live DB connection and ARE run/verified.
- The initial migration SQL is generated via `prisma migrate diff
  --from-empty --to-schema-datamodel` (also does not require a live DB)
  and committed as a file, but is NOT applied/tested against a running
  database.
- All module business logic (T-101 onward) is unit-tested against a
  **mocked Prisma Client**, not a live database. This was already the
  intended testing approach for the kinship/tree algorithms per
  ADR-0002's consequences ("unit-test without a live database... feed a
  fixture graph in"); it is now extended to CRUD-level logic too, out of
  necessity, not preference.
- **Before real use, the user must provision a Postgres database** (a
  local install, or a Neon free-tier dev branch — see ADR-0003) and run
  `npx prisma migrate deploy`, then re-run the test suite's integration
  tier if/when one is added. This is flagged again in the final handoff.

## Known gaps before real users (tracked, not silent)

- **`FixedWindowRateLimiter`'s in-memory Map has no eviction** (T-501
  review, LOW): every distinct rate-limit key gets a permanent entry,
  only overwritten when its own window rolls over — an attacker
  generating many distinct keys (e.g. many identifiers) grows the map
  unboundedly (minor memory-exhaustion angle). Also, on serverless
  platforms (Vercel), concurrent requests can already land on separate
  warm/cold instances, each with its own independent limiter map — so
  protection is weaker in practice than a single-process mental model
  suggests, even before scaling beyond one instance. A Redis-backed
  limiter with TTL eviction would close both gaps; acceptable to defer
  for V1 given low expected traffic, but do this before real growth.
- **No rate limiting on `POST /api/auth/register` or `/api/auth/reset/request`**
  (T-501 review, LOW): register is naturally throttled by bcrypt's write
  cost (low risk); reset/request has no such throttle and could be used
  to spam a victim's email/phone with reset codes, or grow
  `password_reset_tokens` rows unboundedly. Lower severity than the
  login/reset-confirm fixes already applied, but worth closing before
  real users.
- **No HTTP-layer tests for the T-501 auth routes** (T-501 review): only
  the underlying IAM contract functions and the isolated
  `FixedWindowRateLimiter`/`clientIp` units have tests — no test exercises
  `register`/`reset/request`/`reset/confirm`'s route handlers or
  `src/auth.ts`'s callback wiring directly (status codes, malformed input,
  a spoofed `X-Forwarded-For`). This is the kind of gap that let the
  `clientIp` bug (already fixed) ship in the first place — add route-level
  tests before this ships to real users.
- **Identity claims (`claimPerson`) are entirely un-audited** (T-204
  review): `AuditEntityType` only has `PERSON`/`RELATIONSHIP`, no
  membership/claim variant, and `claimPerson` writes no audit row. For a
  feature whose purpose is establishing "who is who," there's no trail of
  who claimed/overwrote/lost a given identity claim — this matters
  specifically for investigating identity-fraud scenarios. Extend
  `AuditEntityType` and log claim/overwrite events once T-401 (Audit)
  exists and gets wired back into this module.
- **`getTree`'s branch metadata can misrepresent which marriage a bucket
  reflects on remarriage-to-the-same-partner** (T-305 light review): a
  PartnerBranch is correctly keyed by partner id (children never merge
  across different partners — REQ-TREE-003's core guarantee holds), but
  if a person has TWO `PARTNER` rows to the SAME partner (divorced then
  remarried — an explicitly supported tier-b scenario), the bucket's
  `relationshipId`/`partnerStatus` reflects whichever row was iterated
  last in `buildDescendantBranches` (`src/modules/genealogy/tree.ts`),
  not necessarily the marriage a given child was actually born under. No
  privacy/security impact, no route handler exposes this yet (T-506 UI
  pending). Needs a product decision (most-recent PARTNER row wins? show
  both?) before the tree UI ships.
- ~~**`ProfileView` has no REQ-PROF-002 claim badge yet**~~ — RESOLVED
  (T-507). Rather than adding a `claimed`/`claimedByUserId` field to
  `ProfileView` itself (which would require `getProfile` to always run
  `isPersonClaimed`, even for PUBLIC/unauthenticated viewers who should
  never learn *which* userId claimed a Person — that's FAMILY-level
  info), the profile page (`src/app/trahs/[trahId]/persons/[personId]/
  profile/page.tsx`) makes a second, separate `isPersonClaimed(prisma,
  trahId, personId)` call itself, gated on `isMember` (only ACTIVE
  members trigger it). `ProfileView`'s own shape is unchanged.
- **`Person.displayName` not synced with `person_facts` FULL_NAME rows**
  (T-301 light review): tier-b-implementation.md documents `display_name`
  as a denormalized cache of the `is_preferred=true`, `field=FULL_NAME`
  row in `person_facts`, kept in sync on every write to that fact —
  `person_facts` is meant to stay authoritative. `createPerson` currently
  writes `displayName` directly with no backing `person_facts` row at
  all, and `updatePersonFact` never writes back to `Person.displayName`.
  Net effect: a name has no verification-status/source/dispute support
  right after creation, and a later corrected/sourced `FULL_NAME` fact
  won't update `displayName` — `searchByName` (T-305) would then search
  stale names. Not fixed now because `person_facts.value`'s JSON shape
  for FULL_NAME isn't pinned down anywhere yet (guessing it risked a
  wrong fix) — must be resolved (pin the JSON shape in tier-b, then sync
  `createPerson`/`updatePersonFact` accordingly, or update tier-a's
  `createPerson(initialFacts)` signature to match the simpler
  implementation actually shipped) before T-305's `searchByName` ships.
- **Last-owner race guard (`changeRole`/`removeMember`) needs real-DB
  verification** (T-203 finding #1): fixed by wrapping the owner-count
  check and the write in a `$transaction` serialized per-trahId via
  `pg_advisory_xact_lock(hashtext(trahId))`, closing a race where two
  owners removing/demoting each other concurrently could leave a Trah
  with zero ACTIVE OWNERs (unrecoverable in-app). This could only be
  verified against a mocked Prisma client (no live Postgres available —
  see "Environment constraint" above) — the transaction/lock call is
  confirmed invoked, but true concurrent behavior against a real database
  is unverified. Before real users rely on multi-owner Trahs, run a
  genuine concurrency test (two parallel requests) against a provisioned
  Postgres to confirm the advisory lock actually serializes as intended.
- ~~`invite()` lets an ADMIN propose `proposedRole: "OWNER"`~~ — **FIXED**
  (T-502 review): only an OWNER may now propose `proposedRole: "OWNER"`;
  an ADMIN proposing OWNER returns FORBIDDEN. See `invite.ts`'s header
  comment and the two regression tests in `invite.test.ts`.
- **`searchByName` has no result-count limit** (T-508 review, LOW): the
  Prisma query has no `take`, so a large Trah's search could return an
  unbounded result set. Low severity for typical family-tree sizes; add
  pagination/a `take` cap if Trahs are ever expected to scale to
  thousands of Persons.
- **`updateRelationship` has no guard against re-ending an already-ended
  PARTNER relationship** (T-505 review): no check that `partnerStatus`
  isn't already DIVORCED/WIDOWED/ENDED before writing a new
  status/endDate. The UI (T-505) hides the "end partnership" button once
  terminal, but a direct API call could still overwrite it again — not
  data-corrupting (same row), but produces a misleading repeated UPDATE
  in the audit trail. Low severity; add a status-transition guard if the
  audit trail's accuracy matters before real users rely on it.
- **Silent re-claim overwrite has no UI warning** (T-503 review): if a
  membership already claims Person X and the user navigates (directly, by
  URL) to claim Person Y, `claimPerson.ts` silently overwrites the claim
  with no confirm/undo — documented as an intentional judgment call
  in-code (tier-a has no "already has a claim" error variant), but the UI
  built in T-503 doesn't surface this before the user clicks confirm.
  Low exposure today (no browse UI links to this page yet), but T-504/507
  will add real navigation to it — decide before then (new error variant
  + confirm dialog, or accept as intended and just add a warning message).
- **No route-handler-level (HTTP) tests exist for T-501/T-502's Route
  Handlers** (T-502 review, MEDIUM): all business logic is tested at the
  module/contract-function level, but nothing exercises the routes
  themselves (auth-gate behavior, JSON body parsing, status codes,
  `getCallerMembership` wiring) — the layer where the `clientIp`
  header-spoofing bug (T-501, fixed) and this ADMIN-OWNER gap (T-502,
  fixed) actually became reachable. Add HTTP-layer tests (e.g. Next.js
  Route Handler testing via a request/response harness) before more UI
  tickets land on top of this pattern, so future route changes get the
  same regression coverage the module layer already has.
- **Rate limiting on `POST /api/auth/reset/confirm`** (flagged by T-103's
  deep review): the password-reset code is a 6-digit (10^6) space; module-
  level defenses (hashed code, 15-min expiry) don't stop online brute-
  forcing of the confirm endpoint. No route handler exists yet (T-501
  builds it) — whoever builds it MUST add rate limiting before this ships
  to real users. See spec/iam/tier-b-implementation.md's API surface
  section for the same note, kept in two places deliberately so it isn't
  missed.

## Not in this WORKPLAN (needs the user's direct action, not autonomous)

Deploying to a live Vercel/Neon environment requires creating accounts and
possibly entering billing details — that crosses into actions Claude does
not perform autonomously. T-000..T-602 build and verify the app to run
locally (`npm run dev`, migrations against a local/dev Postgres, tests
green). Handing off to production hosting is a separate step for the user
to do hands-on, with instructions provided once V1 is locally complete.

## Verification cadence

- Every ticket: inline tests green, committed as its own step.
- Danger-list tickets (IAM, Trah&Membership): `tester` writes tests first,
  then implementation, then a deep-review pass.
- After all tickets in a module land: no extra step (unit-tested already).
- After all tickets for a *feature* (per blueprint §5) land, incl. its UI:
  `reviewer` + `verifikator` in parallel, folded into one report.
- Final pass once T-000..T-602 are all done: run the app end-to-end locally
  with the seeded dummy dataset, walk every one of the 20 MVP features.

## Final status & handoff

**Update: real E2E now performed against a live database.** Everything
below this line originally described a structural-only verification (no
live Postgres was available during the initial build — see "Environment
constraint" above). That has since changed: the user provisioned a Neon
Postgres database, and a real end-to-end pass was done —
`prisma migrate deploy` against it, `npm run db:seed` (real write path,
not mocked), then an actual browser session logged in as the seeded
owner and clicked through the app.

**What the real E2E pass found and fixed:** one genuine bug —
`relationship-finder-panel.tsx` (T-509) nested a `<form>` inside the
outer `<form onSubmit={handleFind}>` for its person-search widget.
Nested `<form>` elements are invalid HTML; browsers misparse them, which
made the inner "Search" button submit the outer form instead of running
the search — the Relationship Finder's person pickers were silently
non-functional. This could not be caught by mocked unit tests (no real
DOM) or `npm run build`/`tsc` (valid TypeScript/JSX, invalid HTML is a
runtime-only concern) — only a real browser session surfaced it. Fixed by
replacing the inner `<form>` with a plain `div` + `onClick`/`onKeyDown`
handlers (matching the pattern `add-relationship-form.tsx`, T-508, already
used correctly). Verified after the fix: searched "Muhammad" and "Ali bin
Abi Thalib" in the Relationship Finder, got the correct result — "1st
cousin" with the correct connecting path through Abdul Muttalib.

**What else was verified live, with real data, in a real browser:**
- Login as the seeded Trah owner — real Auth.js session, real credential
  check against the real `users` table.
- Trah dashboard — correct member list, correct role, working nav links.
- Family tree — correct multi-generation rendering; Muhammad's 4 partners
  (Khadijah/Sawdah/Aisyah/Maria al-Qibtiyyah) each in their own branch
  with their own children grouped correctly (REQ-TREE-003, live-verified,
  not just unit-tested); Zaid bin Haritsah's ADOPTIVE_PARENT_CHILD edge
  rendered with the correct "(adoptive)" subtype label; Hasan/Husein
  (Fatimah+Ali's children) reachable only through Ali's branch at the
  correct generation depth, confirming REQ-REL-005's "grandchildren via
  computed kinship only, no stored grandparent edge" claim is actually
  true in a real render, not just in the algorithm's unit tests.
- Person profile — correct privacy-filtered fields, correct precision/
  verification display on the BIRTH_DATE fact ("570 (CIRCA) — VERIFIED"),
  correct unclaimed-profile badge, correct parents/partners/children lists.
- Audit history — correctly shows "CREATE by **Member**" (the generic,
  credential-free label from T-510's fix), NOT the seed owner's login
  email — live confirmation that the T-510 privacy fix actually holds up
  against a real database, not just the mocked test that pinned it.
- Created a brand-new Person ("Uji Coba E2E") through the real `/persons/new`
  form → real `POST /api/trahs/:trahId/persons` → real `createPerson()` →
  real Postgres row → real redirect to its edit page. Confirms the full
  write path (browser → route handler → contract function → DB) works
  end to end, not just the seed script's direct in-process calls. This
  test person remains in the "Trah Validasi — Nasab Rujukan (T-601)"
  Trah — harmless (it's explicitly a validation-only Trah per blueprint
  §6, not real family data), no delete-person feature exists in V1 scope
  to remove it, and none was needed for this check.

**Still not clicked through live in this pass** (not because of any known
issue — just not yet exercised with a real click): registration of a
brand-new account (only login was tested, using the seed script's
pre-registered owner), invite/accept-invitation flow, claim-profile flow,
password reset, search page (standalone), residence/contact sub-forms,
changeRole/removeMember UI actions, ending a partnership (divorce) via the
UI. These are all backed by contract functions with passing unit tests and
were already code-reviewed, but haven't been personally clicked through
against the live database the way the paths above were — reasonable next
checks if you want extra confidence before wider family use.

Also still true from before this pass:
- `npm test` — 240/240 tests passing, 28 test files, covering every
  backend contract function in IAM, Trah & Membership, Genealogy, and
  Audit (including every danger-list invariant found and fixed during
  review across the build).
- `npx tsc --noEmit` — clean, whole project.
- `npm run build` — clean production build, all 15 pages and 20 API
  routes compile and are listed in the route manifest with no errors or
  warnings.

**Traceability: all 20 MVP features (blueprint.md §5), backend + UI, both present:**

| # | Feature | Backend | UI |
|---|---|---|---|
| 1 | Register/Login/password reset | T-101/102/103 | T-501 |
| 2 | Create Trah | T-201 | T-502 |
| 3 | Invite members | T-202 | T-502 |
| 4 | Person record | T-301 | T-504 |
| 5 | Gender | T-301 | T-504 |
| 6 | Birth/death date+place w/ precision | T-301 | T-504 |
| 7 | Residence history | T-302 | T-504 |
| 8 | Contact with privacy | T-302 | T-504 |
| 9 | Parent relationship | T-303 | T-505 |
| 10 | Partner relationship | T-303 | T-505 |
| 11 | Child relationship | T-303 | T-505 |
| 12 | Divorce | T-303 | T-505 |
| 13 | Multiple spouses / polygamy | T-303 | T-505 |
| 14 | Interactive tree | T-304/T-305 | T-506 |
| 15 | Person profile page | T-305 | T-507 |
| 16 | Search | T-305 | T-508 |
| 17 | Relationship Finder | T-304 | T-509 |
| 18 | Permissions/roles | T-203 (enforced in every write; role changes via T-502 UI) |
| 19 | Claim Profile | T-204 | T-503 |
| 20 | Audit history | T-401 | T-510 |

Every row has both a backend contract function (unit-tested, reviewed —
several rows had a real bug found and fixed during review, see commit
history and the "Known gaps" section above for what's still open) and a
reachable UI route. No feature is backend-only or UI-only.

**What's left before wider family use:**

1. ~~Provision Postgres, migrate, seed~~ — **done** (Neon, Singapore
   region, `trahkeluarga` project).
2. ~~Click through the core features live~~ — **done**, see above.
3. Optionally click through the "still not clicked through live" list
   above for extra confidence (register, invite/accept, claim, reset
   password, search page, residence/contact forms, role changes, divorce).
4. Re-read the "Known gaps before real users" section above — several
   items (rate-limiter Map eviction, no route-level HTTP tests, the
   re-end-partnership guard, etc.) are accepted V1 tradeoffs, not
   blockers, but worth knowing about before wider rollout.
5. Deploy (Vercel, or the Rumahweb cPanel hosting's Node.js App feature —
   database stays on Neon either way) — this is the one step Claude does
   not do autonomously (account creation/billing), see "Not in this
   WORKPLAN" above.
6. Decide what to do with the seed Trah ("Trah Validasi — Nasab Rujukan")
   before inviting real family members — it's clearly labeled as a
   validation dataset (per blueprint §6) and isolated in its own Trah, so
   it doesn't have to be removed, but you may want to keep it separate
   from your real family's Trah rather than adding real relatives into it.
