docs: plan birth time evidence rectification

This commit is contained in:
Jesse_Chen
2026-07-18 07:45:41 +08:00
parent f08c043bb5
commit c388338bd6
@@ -0,0 +1,370 @@
# Birth-Time Evidence Rectification Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Continue the completed birth-time questionnaire into structured life-event scoring, candidate review, and guarded user confirmation.
**Architecture:** Extend the pure TypeScript journey state machine first, then add one isolated Python event adjudicator behind the existing Jyotish API. The authenticated Next.js service owns persistence and confirmation; React renders only the server-returned input state.
**Tech Stack:** Python 3.12, pytest, Next.js 16 App Router, React 19, TypeScript 5, Zod 3, Supabase/PostgreSQL, Node test runner.
## Global Constraints
- Agent prose never determines state, candidate score, confidence, route, or application permission.
- `reported_birth_time` is immutable; only a confirmed server-side result may update `active_birth_time` and legacy `birth_time`.
- Rectification events use the free journey route and never call `/api/consult`.
- Life-event dates and domains are structured; free-form text never affects scoring.
- Low and medium results cannot apply a time. High results require an explicit confirmation event.
- The UI must say “候选时间” or “当前排盘使用时间”, never “真实出生时间”.
- Preserve unrelated dirty-worktree changes and do not reset the repository.
---
### Task 1: Deterministic Journey State and Event Contracts
**Files:**
- Modify: `frontend/src/lib/birth-time-journey.ts`
- Test: `frontend/tests/birth-time-journey.test.ts`
**Interfaces:**
- Produces: `lifeEventSchema`, `candidateResultSchema`, extended `JourneySnapshot`, `withCompletedQuestionnaire()`, `withCandidateResult()`, and `withConfirmedCandidate()`.
- Consumes: existing `JourneySnapshot` and `RectificationScoring`.
- [ ] **Step 1: Write failing state-transition tests**
```ts
test("the final questionnaire answer requests dated life events", () => {
const next = withRectificationScoring(initial, {
answeredCount: 8,
candidateClusterRankings: [{ cluster: "middle_candidate_cluster", score: 5 }],
nextRoundQuestions: [],
});
assert.equal(next.input, "life_events");
assert.equal(next.assistantIntent, "collect_dated_life_events");
assert.equal(next.canApply, false);
});
test("only a high candidate enters confirmation", () => {
assert.equal(withCandidateResult(eventSnapshot, mediumResult).input, "candidate_actions");
assert.equal(withCandidateResult(eventSnapshot, highResult).input, "candidate_confirmation");
});
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `cd frontend && node --test tests/birth-time-journey.test.ts`
Expected: FAIL because the new input, intent, schemas, and transition functions do not exist.
- [ ] **Step 3: Implement the minimal typed state model**
```ts
export const lifeEventSchema = z.object({
id: z.string().uuid(),
domain: z.enum(["education", "relocation", "relationship", "career", "health_pressure"]),
date: z.string(),
precision: z.enum(["year", "month", "day"]),
}).strict().readonly();
export function withCandidateResult(snapshot: JourneySnapshot, result: CandidateResult): JourneySnapshot {
switch (result.confidence) {
case "low": return { ...snapshot, state: "rectifying", input: "life_events", canApply: false };
case "medium": return { ...snapshot, state: "candidate", input: "candidate_actions", canApply: false };
case "high": return { ...snapshot, state: "confirming", input: "candidate_confirmation", canApply: true };
default: return assertNever(result.confidence);
}
}
```
- [ ] **Step 4: Run the focused test and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-journey.test.ts`
Expected: all birth-time journey domain tests pass.
---
### Task 2: Local Candidate Event Adjudicator
**Files:**
- Create: `scripts/active_rectification_events.py`
- Test: `tests/test_active_rectification_events.py`
**Interfaces:**
- Produces: `score_life_events(request: RectificationEventRequest) -> CandidateResult`.
- Consumes: stored birth date/range/location, structured events, `domain_calculation_service`, `varga`, `jaimini`, Vimshottari timeline, and Narayana Dasha.
- [ ] **Step 1: Write failing Python tests for segments, thresholds, and abstention**
```python
def test_high_confidence_requires_four_events_three_domains_and_narrow_leader() -> None:
result = score_life_events(high_fixture())
assert result["confidence"] == "high"
assert result["winning_segment"]["start_time"] <= result["winning_segment"]["end_time"]
assert result["can_apply"] is True
def test_tied_candidates_abstain() -> None:
result = score_life_events(tied_fixture())
assert result["confidence"] == "low"
assert "tied_leader" in result["reasons"]
assert result["can_apply"] is False
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_events.py`
Expected: collection fails because `active_rectification_events` does not exist.
- [ ] **Step 3: Implement frozen request models and fixed rule tables**
```python
class LifeEvent(TypedDict):
id: str
domain: EventDomain
date: str
precision: EventPrecision
PRECISION_WEIGHTS: Final = {"day": 1.0, "month": 0.8, "year": 0.5}
DOMAIN_LAYERS: Final = {
"education": ("D24", (4, 5, 9)),
"relocation": ("D4", (4, 12)),
"relationship": ("D9", (7,)),
"career": ("D10", (10,)),
"health_pressure": ("D30", (6, 8, 12)),
}
```
- [ ] **Step 4: Implement minute scanning, contiguous signature segments, dual-Dasha rule IDs, and confidence thresholds**
Every candidate row records actual D1/D4/D9/D10/D24/D30 data. Adjacent equal signatures collapse into segments. Each event score emits rule IDs for Vimshottari lord/domain-house, Narayana sign/domain-house, and domain-Varga support; unavailable mandatory layers produce a low-confidence abstention.
- [ ] **Step 5: Run the focused test and verify GREEN**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_events.py`
Expected: all event adjudicator tests pass without network access.
---
### Task 3: Python API Boundary
**Files:**
- Modify: `scripts/jyotish_api_server.py`
- Modify: `tests/test_active_rectification_api.py`
**Interfaces:**
- Produces: `POST /api/active_rectification_events`.
- Consumes: `score_life_events()` from Task 2.
- [ ] **Step 1: Add a failing API contract test**
```python
def test_active_rectification_events_api_scores_structured_events() -> None:
result = _handler()._compute_active_rectification_events(valid_payload())
assert result["success"] is True
assert result["endpoint"] == "active_rectification_events"
assert result["candidate_result_id"]
```
- [ ] **Step 2: Run and verify RED**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_api.py -k events`
Expected: FAIL because the handler and route are missing.
- [ ] **Step 3: Add strict payload parsing and the new handler route**
The HTTP method validates date/range/location and three-to-six events before calling `score_life_events`; invalid variants raise `BadRequest`. Register the path in POST dispatch, endpoint catalog, and capability metadata.
- [ ] **Step 4: Run and verify GREEN**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_api.py`
Expected: all active rectification API tests pass.
---
### Task 4: Journey Service, Engine, Store, and SQL
**Files:**
- Modify: `frontend/src/lib/birth-time-journey-service.ts`
- Modify: `frontend/src/lib/birth-time-journey-engine.ts`
- Modify: `frontend/src/lib/birth-time-journey-adapters.ts`
- Modify: `frontend/src/lib/birth-time-journey-store.ts`
- Create: `frontend/supabase/migrations/20260718010000_birth_time_evidence_rectification.sql`
- Modify: `frontend/tests/birth-time-journey-service.test.ts`
- Modify: `frontend/tests/birth-time-journey-adapters.test.ts`
- Modify: `tests/test_birth_time_journey_contract.py`
**Interfaces:**
- Produces: engine `scoreEvents`, store `saveCandidateResult`/`confirmCandidate`, service `submitLifeEvents`/`saveCandidate`/`confirmCandidate`.
- Consumes: Task 1 schemas and Task 3 API.
- [ ] **Step 1: Write failing service tests**
```ts
test("submitting life events persists the server result", async () => {
const result = await service.submitLifeEvents("user-1", "case-1", events);
assert.equal(result.candidateResult?.confidence, "medium");
assert.equal(result.snapshot.input, "candidate_actions");
});
test("confirmation rejects a stale result id", async () => {
await assert.rejects(
service.confirmCandidate("user-1", "case-1", "stale", "14:24"),
StaleCandidateConfirmationError,
);
});
```
- [ ] **Step 2: Run focused frontend tests and verify RED**
Run: `cd frontend && node --test tests/birth-time-journey-service.test.ts tests/birth-time-journey-adapters.test.ts`
Expected: FAIL because the new ports and methods are missing.
- [ ] **Step 3: Implement engine adapter and service transitions**
`scoreEvents` posts only stored assessment/range/location plus parsed events. `submitLifeEvents` reloads the owner-scoped case, computes the result, transitions through `withCandidateResult`, and persists atomically. `confirmCandidate` rechecks state, result ID, confidence, time, and ownership before store confirmation.
- [ ] **Step 4: Write the failing SQL contract assertions and verify RED**
Run: `.venv/bin/python -m pytest -q tests/test_birth_time_journey_contract.py`
Expected: FAIL because event/result columns and grants are absent.
- [ ] **Step 5: Add the migration and store persistence**
The migration adds `life_events`, `candidate_result`, `event_scoring_version`, `candidate_result_id`, `candidate_saved_at`, and `confirming` status. Browser grants exclude score/result/confirmation writes. The admin store updates profile active time only inside `confirmCandidate`.
- [ ] **Step 6: Run service, adapter, and SQL tests and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-journey-service.test.ts tests/birth-time-journey-adapters.test.ts && cd .. && .venv/bin/python -m pytest -q tests/test_birth_time_journey_contract.py`
Expected: all selected tests pass.
---
### Task 5: Route and Client Boundary
**Files:**
- Modify: `frontend/src/app/api/birth-time-journey/route.ts`
- Modify: `frontend/src/lib/birth-time-journey-client.ts`
- Modify: `frontend/tests/birth-time-journey-client.test.ts`
**Interfaces:**
- Produces: client calls `submitBirthTimeLifeEvents`, `saveBirthTimeCandidate`, `confirmBirthTimeCandidate`.
- Consumes: Task 4 service methods.
- [ ] **Step 1: Write failing parser and request tests**
```ts
test("client accepts only a guarded high-confirmation response", () => {
const parsed = parseJourneyResponse(highConfirmationResponse);
assert.equal(parsed.snapshot.input, "candidate_confirmation");
});
test("client rejects a rectification response that applies without confirmation state", () => {
assert.throws(() => parseJourneyResponse(unsafeResponse));
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test tests/birth-time-journey-client.test.ts`
Expected: FAIL on missing response fields and client functions.
- [ ] **Step 3: Extend strict event schemas and exhaustive route dispatch**
The route accepts only `submit_life_events`, `save_candidate`, and `confirm_candidate` shapes defined by Zod `.strict()`. It maps stale confirmation and insufficient evidence to 409 and keeps all server failures fail-closed.
- [ ] **Step 4: Run and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-journey-client.test.ts`
Expected: all client boundary tests pass.
---
### Task 6: Life-Event and Candidate UI
**Files:**
- Create: `frontend/src/components/birth-time-life-events.tsx`
- Create: `frontend/src/components/birth-time-candidate-result.tsx`
- Modify: `frontend/src/components/birth-time-rectification.tsx`
- Modify: `frontend/src/app/page.tsx`
- Modify: `frontend/src/app/globals.css`
- Modify: `frontend/DESIGN.md`
- Modify: `frontend/tests/birth-time-rectification-contract.test.ts`
**Interfaces:**
- Produces: accessible event form and low/medium/high result actions.
- Consumes: Task 5 client calls and parsed `JourneyClientResponse`.
- [ ] **Step 1: Add failing UI contract assertions**
```ts
test("rectification renders the life-event step from the server input", () => {
assert.match(component, /snapshot\.input === "life_events"/);
assert.match(component, /BirthTimeLifeEvents/);
assert.match(component, /BirthTimeCandidateResult/);
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test tests/birth-time-rectification-contract.test.ts`
Expected: FAIL because the new components and input branches are missing.
- [ ] **Step 3: Document and implement the component states**
Extend the existing Birth time intake component in `DESIGN.md` with life-event rows, candidate action states, and confirmation copy. Implement three initial event rows, a six-row maximum, persistent labels, precision-dependent native inputs, live errors, and existing design tokens only.
- [ ] **Step 4: Wire page handlers and ready transition**
`page.tsx` submits events, saves candidates, and confirms only through Task 5 client functions. A ready response updates profile state and proceeds to existing onboarding; no client code sets `activeTime` or `canApply`.
- [ ] **Step 5: Run and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-rectification-contract.test.ts`
Expected: the full UI contract passes.
---
### Task 7: Final Verification and Browser QA
**Files:**
- Review all files changed in Tasks 16.
**Interfaces:**
- Produces: fresh automated and browser evidence for the approved design.
- [ ] **Step 1: Run focused Python and frontend tests**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_events.py tests/test_active_rectification_api.py tests/test_birth_time_journey_contract.py`
Run: `cd frontend && npm test`
Expected: zero failures.
- [ ] **Step 2: Run lint, TypeScript, and production build**
Run: `cd frontend && npm run lint && npx tsc --noEmit && npm run build`
Expected: exit code 0 for every command.
- [ ] **Step 3: Run no-excuse checks on changed Python and TypeScript sources**
Run the programming skill checkers against the changed source files and repair every new violation without refactoring unrelated legacy code.
- [ ] **Step 4: Run real-browser visual QA**
Start the verified application, drive the final questionnaire answer, event validation, low/medium/high candidate cards, and confirmation fixture at 375px, 768px, and 1280px. Verify keyboard labels, live regions, overflow, motion, and that no consultation request is issued before ready.
- [ ] **Step 5: Review the final diff**
Confirm every spec section has implementation evidence, `reported_birth_time` remains immutable, client payloads cannot inject score/confidence/application fields, and unrelated dirty files were not overwritten.