docs: plan agent guided birth time rectification

This commit is contained in:
Jesse_Chen
2026-07-18 11:02:11 +08:00
parent 9dc31c8112
commit e5520f6bb8
@@ -0,0 +1,707 @@
# Agent-Guided Birth-Time 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:** Replace the fixed questionnaire/manual comparison path with a deterministic, versioned JourneyTurn that asks one high-information question at a time, lets an Agent create review-only evidence drafts, and automatically advances to the next question or guarded result.
**Architecture:** Extend the existing `BirthTimeJourney` as the only state authority. A pure planner ranks canonical evidence domains from actual candidate Varga differences; a constrained Mastra Agent may phrase a server-selected question and extract a draft, but only authenticated structured UI actions can confirm evidence, run scoring, save a candidate, or confirm an active time. Persist `nextAction`, progress, optimistic version, idempotency receipts, drafts, and scoring jobs so refresh/resume cannot produce a dead end.
**Tech Stack:** TypeScript 5, Zod 3, Next.js 16.2 Route Handlers, React 19 Client Components, Mastra 1.50, Supabase/PostgreSQL, Python 3.11+, Node test runner, pytest.
## Global Constraints
- Preserve all existing dirty work; inspect the diff before every edit and never reset, restore, or overwrite unrelated changes.
- Agent prose never determines candidate ranking, confidence, route, progress, or permission.
- Every scored event is structured and explicitly confirmed by the user.
- Baseline scoring requires at least three confirmed events across two domains.
- Low-confidence adaptive questioning is capped at three displayed questions; skip consumes the displayed round.
- Medium confidence saves only; high confidence still requires explicit confirmation of the matching result ID and representative time.
- `reported_birth_time` remains immutable; only guarded confirmation may update `active_birth_time`.
- Keep the legacy response `canApply` compatibility parser, but new UI and Agent permissions use `canConfirmCandidate`.
- Use “候选时间” and “当前排盘使用时间”; never claim a proven true birth minute.
- Read `frontend/node_modules/next/dist/docs/01-app/01-getting-started/15-route-handlers.md` and `05-server-and-client-components.md` before editing Next.js code.
- No new runtime dependency is permitted.
- Each implementation task follows red → green TDD and stages only files owned by that task.
---
### Task 1: Deterministic Candidate-Difference Question Planner
**Files:**
- Modify: `scripts/active_rectification_questions.py`
- Modify: `frontend/src/lib/birth-time-journey-adapters.ts`
- Create: `frontend/src/lib/birth-time-question-planner.ts`
- Modify: `tests/test_active_rectification_questions.py`
- Create: `frontend/tests/birth-time-question-planner.test.ts`
**Interfaces:**
- Consumes: candidate scan samples with D4/D9/D10/D24/D30 Ascendant signs.
- Produces: `planEvidenceQuestion(input: QuestionPlannerInput): QuestionSpec | null` and `QuestionSpec` for Tasks 2, 4, and 6.
- [ ] **Step 1: Add failing Python coverage for all canonical domain Vargas**
```python
def test_candidate_recast_contains_all_evidence_domain_vargas(monkeypatch):
report = build_questionnaire(
"1993-04-17 14:30", 30, 30,
lat=31.2304, lon=121.4737, tz=8,
)
sample = report["candidate_scan"]["samples"][0]
assert {"D4", "D9", "D10", "D24", "D30"}.issubset(sample["varga_lagna"])
```
- [ ] **Step 2: Run the Python test and verify RED**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_questions.py -k evidence_domain_vargas`
Expected: FAIL because `_candidate_recast()` currently omits division 4.
- [ ] **Step 3: Add D4 to the recast and expose five parsed signs**
Change `varga.calc_all_vargas(... divisions=[4, 9, 10, 24, 30, 60])`. Extend `RectificationQuestionnaire.samples` and the adapter with `d4Sign`, `d9Sign`, `d10Sign`, `d24Sign`, and `d30Sign`.
- [ ] **Step 4: Add failing planner tests**
```ts
test("planner chooses the unasked domain with the largest candidate split", () => {
const question = planEvidenceQuestion({
phase: "baseline",
samples: [
{ d4Sign: "Aries", d9Sign: "Cancer", d10Sign: "Leo", d24Sign: "Gemini", d30Sign: "Virgo" },
{ d4Sign: "Taurus", d9Sign: "Cancer", d10Sign: "Leo", d24Sign: "Gemini", d30Sign: "Virgo" },
{ d4Sign: "Gemini", d9Sign: "Cancer", d10Sign: "Leo", d24Sign: "Gemini", d30Sign: "Virgo" },
],
askedDomains: [],
coveredDomains: [],
adaptiveRound: 0,
});
assert.equal(question?.domain, "relocation");
assert.equal(question?.phase, "baseline");
});
test("planner never repeats a domain and returns null after canonical domains are exhausted", () => {
assert.equal(planEvidenceQuestion({
phase: "baseline",
samples: [],
askedDomains: ["education", "relocation", "relationship", "career", "health_pressure"],
coveredDomains: [],
adaptiveRound: 0,
}), null);
});
```
- [ ] **Step 5: Run the planner test and verify RED**
Run: `cd frontend && node --test tests/birth-time-question-planner.test.ts`
Expected: FAIL because the planner module does not exist.
- [ ] **Step 6: Implement the pure planner**
```ts
export const evidenceDomains = [
"education", "relocation", "relationship", "career", "health_pressure",
] as const;
const layerByDomain = {
education: "d24Sign",
relocation: "d4Sign",
relationship: "d9Sign",
career: "d10Sign",
health_pressure: "d30Sign",
} as const;
export function planEvidenceQuestion(input: QuestionPlannerInput): QuestionSpec | null {
const available = evidenceDomains.filter((domain) => !input.askedDomains.includes(domain));
const ranked = available.map((domain) => ({
domain,
split: new Set(input.samples.map((sample) => sample[layerByDomain[domain]]).filter(Boolean)).size,
coverageBonus: input.coveredDomains.includes(domain) ? 0 : 1,
})).sort((left, right) => right.split - left.split
|| right.coverageBonus - left.coverageBonus
|| evidenceDomains.indexOf(left.domain) - evidenceDomains.indexOf(right.domain));
const winner = ranked[0];
return winner ? questionSpecFor(winner.domain, input.phase, input.adaptiveRound) : null;
}
```
- [ ] **Step 7: Run focused tests and verify GREEN**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_questions.py && cd frontend && node --test tests/birth-time-question-planner.test.ts tests/birth-time-journey-adapters.test.ts`
Expected: all selected tests pass.
- [ ] **Step 8: Commit the isolated planner change**
```bash
git add scripts/active_rectification_questions.py tests/test_active_rectification_questions.py frontend/src/lib/birth-time-question-planner.ts frontend/src/lib/birth-time-journey-adapters.ts frontend/tests/birth-time-question-planner.test.ts frontend/tests/birth-time-journey-adapters.test.ts
git commit -m "feat: plan adaptive birth time evidence questions"
```
---
### Task 2: JourneyTurn, NextAction, Progress, and Permission Protocol
**Files:**
- Create: `frontend/src/lib/birth-time-journey-turn.ts`
- Modify: `frontend/src/lib/birth-time-journey-service.ts`
- Modify: `frontend/src/lib/birth-time-journey-client.ts`
- Create: `frontend/tests/birth-time-journey-turn.test.ts`
- Modify: `frontend/tests/birth-time-journey-client.test.ts`
**Interfaces:**
- Consumes: `QuestionSpec`, `CandidateResult`, and confirmed `LifeEvent[]`.
- Produces: `NextAction`, `JourneyProgress`, `JourneyPermissions`, `JourneyTurnState`, `deriveNextAction()`, and parsed response fields for later tasks.
- [ ] **Step 1: Write failing invariants tests**
```ts
test("a fresh rectification turn asks exactly one baseline evidence question", () => {
const turn = createInitialJourneyTurn(question("career"));
assert.equal(turn.nextAction.kind, "ask_baseline_evidence");
assert.equal(turn.progress.confirmedEvidenceCount, 0);
assert.equal(turn.progress.maxAdaptiveRounds, 3);
assert.equal(turn.permissions.canConfirmCandidate, false);
});
test("the third low adaptive result becomes terminal", () => {
const next = deriveNextAction({
progress: { phase: "adaptive", baselineDomainCount: 3, confirmedEvidenceCount: 6, adaptiveRound: 3, maxAdaptiveRounds: 3 },
candidateResult: lowResult,
nextQuestion: question("health_pressure"),
});
assert.equal(next.kind, "present_low_result");
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test tests/birth-time-journey-turn.test.ts`
Expected: FAIL because the protocol module does not exist.
- [ ] **Step 3: Implement strict Zod schemas and pure transitions**
```ts
export const nextActionSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("ask_baseline_evidence"), question: questionSpecSchema }),
z.object({ kind: z.literal("ask_adaptive_evidence"), question: questionSpecSchema }),
z.object({ kind: z.literal("review_evidence_draft"), draftId: z.string().uuid() }),
z.object({ kind: z.literal("score_pending"), jobId: z.string().uuid() }),
z.object({ kind: z.literal("retry_scoring"), jobId: z.string().uuid() }),
z.object({ kind: z.literal("present_low_result"), resultId: z.string().uuid().nullable() }),
z.object({ kind: z.literal("present_medium_result"), resultId: z.string().uuid() }),
z.object({ kind: z.literal("request_candidate_confirmation"), resultId: z.string().uuid() }),
z.object({ kind: z.literal("ready"), activeTime: z.string() }),
z.object({ kind: z.literal("paused") }),
]);
```
`deriveNextAction()` must exhaustively map: baseline incomplete → one baseline question; low and adaptive round < 3 → one adaptive question; low at round 3 → terminal low; medium → terminal medium; high → confirmation; confirmed → ready.
- [ ] **Step 4: Extend service and client response types**
Add `nextAction`, `progress`, `permissions`, `turnVersion`, and nullable `evidenceDraft` to `JourneyResponse` and its client Zod schema. Keep defaults only in the legacy-normalization path; new responses must provide all fields.
- [ ] **Step 5: Add parser rejection coverage**
```ts
test("client rejects a nonterminal turn without nextAction", () => {
assert.throws(() => parseJourneyResponse({ ...validTurn, nextAction: undefined }));
});
test("client does not expose legacy canApply as Agent permission", () => {
const parsed = parseJourneyResponse(highConfirmationTurn);
assert.equal(parsed.permissions.canConfirmCandidate, true);
assert.equal("canApply" in parsed.permissions, false);
});
```
- [ ] **Step 6: Run focused tests and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-journey-turn.test.ts tests/birth-time-journey-client.test.ts`
Expected: all selected tests pass.
- [ ] **Step 7: Commit the protocol**
```bash
git add frontend/src/lib/birth-time-journey-turn.ts frontend/src/lib/birth-time-journey-service.ts frontend/src/lib/birth-time-journey-client.ts frontend/tests/birth-time-journey-turn.test.ts frontend/tests/birth-time-journey-client.test.ts
git commit -m "feat: define versioned birth time journey turns"
```
---
### Task 3: Persisted Turn Version, Drafts, and Idempotency Receipts
**Files:**
- Create: `frontend/supabase/migrations/20260718020000_agent_guided_birth_time_rectification.sql`
- Modify: `frontend/src/lib/birth-time-journey-store.ts`
- Modify: `frontend/src/lib/birth-time-journey-service.ts`
- Modify: `tests/test_birth_time_journey_contract.py`
- Modify: `frontend/tests/birth-time-journey-service.test.ts`
**Interfaces:**
- Produces: `saveTurn(value, expectedVersion, actionId)`, `StaleJourneyTurnError`, stored `turnVersion`, `turnState`, `evidenceDraft`, `processedActionIds`.
- Consumed by Tasks 4 and 5.
- [ ] **Step 1: Add failing migration contract assertions**
```python
def test_agent_guided_rectification_migration_versions_turns_and_jobs():
sql = MIGRATION.read_text()
assert "turn_version bigint not null default 0" in sql
assert "turn_state jsonb not null default" in sql
assert "evidence_draft jsonb" in sql
assert "processed_action_ids uuid[]" in sql
assert "birth_time_rectification_scoring_jobs" in sql
```
- [ ] **Step 2: Run and verify RED**
Run: `.venv/bin/python -m pytest -q tests/test_birth_time_journey_contract.py -k agent_guided`
Expected: FAIL because the migration does not exist.
- [ ] **Step 3: Create the additive migration**
The migration must add typed JSON checks, a bounded `processed_action_ids` array, a service-role-only scoring job table with random UUID primary key, ownership, status, expiry, and unique `(case_id, evidence_fingerprint, algorithm_version)`. Do not grant job-table access to `authenticated` or `anon`.
- [ ] **Step 4: Add a failing optimistic-concurrency service test**
```ts
test("stale turn versions cannot overwrite the current action", async () => {
await assert.rejects(
service.skipEvidenceQuestion("user-1", "case-1", actionId, 4),
StaleJourneyTurnError,
);
assert.equal(memory.savedCase()?.turnVersion, 5);
});
```
- [ ] **Step 5: Implement atomic store writes**
Use one Supabase update constrained by `.eq("turn_version", expectedVersion)` and owner ID. Append the action ID and increment the version in the same statement. If no row is returned, reload: return the current case when `processedActionIds` already includes the action ID; otherwise throw `StaleJourneyTurnError`.
- [ ] **Step 6: Run focused tests and verify GREEN**
Run: `.venv/bin/python -m pytest -q tests/test_birth_time_journey_contract.py && cd frontend && node --test tests/birth-time-journey-service.test.ts`
Expected: migration and concurrency tests pass.
- [ ] **Step 7: Commit persistence**
```bash
git add frontend/supabase/migrations/20260718020000_agent_guided_birth_time_rectification.sql frontend/src/lib/birth-time-journey-store.ts frontend/src/lib/birth-time-journey-service.ts tests/test_birth_time_journey_contract.py frontend/tests/birth-time-journey-service.test.ts
git commit -m "feat: persist versioned rectification turns"
```
---
### Task 4: Draft Confirmation, Skip, Pause, Resume, and Automatic Service Progression
**Files:**
- Modify: `frontend/src/lib/birth-time-evidence.ts`
- Modify: `frontend/src/lib/birth-time-evidence-service.ts`
- Modify: `frontend/src/lib/birth-time-journey-service.ts`
- Modify: `frontend/src/app/api/birth-time-journey/route.ts`
- Modify: `frontend/src/lib/birth-time-journey-client.ts`
- Modify: `frontend/tests/birth-time-evidence.test.ts`
- Modify: `frontend/tests/birth-time-journey-service.test.ts`
- Modify: `frontend/tests/birth-time-journey-client.test.ts`
**Interfaces:**
- Produces: `proposeEvidenceDraft`, `confirmEvidenceDraft`, `skipEvidenceQuestion`, `pause`, `finishWithCurrentRange`, and legacy `resume` normalization.
- Calls `planEvidenceQuestion()` and Task 3 store writes.
- [ ] **Step 1: Add failing end-to-end service tests with a memory store**
```ts
test("confirmed drafts automatically advance from baseline to scoring", async () => {
const first = await service.proposeEvidenceDraft(userId, caseId, actionId1, 0, careerDraft);
assert.equal(first.nextAction.kind, "review_evidence_draft");
const confirmed = await service.confirmEvidenceDraft(userId, caseId, actionId2, first.turnVersion, first.evidenceDraft!.id);
assert.equal(confirmed.progress.confirmedEvidenceCount, 1);
assert.equal(confirmed.nextAction.kind, "ask_baseline_evidence");
});
test("a third confirmed baseline event starts scoring without a compare action", async () => {
const result = await confirmThirdDraft();
assert.equal(result.nextAction.kind, "score_pending");
assert.equal(engine.scoreEventsCalls, 0);
});
test("resume reconstructs one deterministic action for a legacy dead-end snapshot", async () => {
const result = await service.resume(userId, legacyCaseId);
assert.equal(result.nextAction.kind, "ask_baseline_evidence");
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test --test-name-pattern="draft|third confirmed|legacy dead-end" tests/birth-time-journey-service.test.ts`
Expected: FAIL because these actions do not exist.
- [ ] **Step 3: Add a strict evidence draft schema**
Drafts carry `id`, server-selected `questionId`/`domain`, nullable precision/date, `status: "draft"`, and `needsReview`. `confirmEvidenceDraft` must parse the final draft through `lifeEventSchema`; incomplete or domain-mismatched drafts fail closed.
- [ ] **Step 4: Implement automatic transition rules**
On confirmation: append the event; if baseline minimum is not met, persist the next baseline question; if met, create `score_pending`; after a low completed score, persist the next adaptive question and increment the displayed round exactly once; at round 3 persist terminal low. Skip marks the domain/question asked, consumes adaptive round only in the adaptive phase, and plans the next question. Pause persists `paused` without changing evidence.
- [ ] **Step 5: Normalize legacy cases on resume**
Legacy questionnaire and `life_events` snapshots without turn state must derive one current `nextAction` from stored evidence/result. Resume may repair derived turn state but must not call the external scoring engine.
- [ ] **Step 6: Add authenticated structured API actions**
Add strict `confirm_evidence_draft`, `skip_evidence_question`, `pause_rectification`, and `finish_rectification` request variants. Every mutation includes `caseId`, UUID `actionId`, and non-negative `turnVersion`; confirmation additionally includes only `draftId`. The client exposes `confirmBirthTimeEvidenceDraft()`, `skipBirthTimeEvidenceQuestion()`, `pauseBirthTimeRectification()`, and `finishBirthTimeRectification()` and never submits candidate score, confidence, or permissions.
- [ ] **Step 7: Run focused tests and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-evidence.test.ts tests/birth-time-question-planner.test.ts tests/birth-time-journey-turn.test.ts tests/birth-time-journey-service.test.ts tests/birth-time-journey-client.test.ts`
Expected: all focused service-flow tests pass.
- [ ] **Step 8: Commit the orchestration**
```bash
git add frontend/src/lib/birth-time-evidence.ts frontend/src/lib/birth-time-evidence-service.ts frontend/src/lib/birth-time-journey-service.ts frontend/src/app/api/birth-time-journey/route.ts frontend/src/lib/birth-time-journey-client.ts frontend/tests/birth-time-evidence.test.ts frontend/tests/birth-time-journey-service.test.ts frontend/tests/birth-time-journey-client.test.ts
git commit -m "feat: advance rectification from confirmed evidence"
```
---
### Task 5: Idempotent Score-Pending Jobs and Polling
**Files:**
- Modify: `frontend/src/lib/birth-time-journey-store.ts`
- Modify: `frontend/src/lib/birth-time-evidence-service.ts`
- 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-service.test.ts`
- Modify: `frontend/tests/birth-time-journey-client.test.ts`
- Modify: `tests/test_birth_time_journey_contract.py`
**Interfaces:**
- Produces: `createScoringJob`, `pollScoringJob`, `completeScoringJob`, `failScoringJob`, API action `poll_scoring`, and client `pollBirthTimeScoring()`.
- [ ] **Step 1: Add failing job lifecycle tests**
```ts
test("polling a pending job scores exactly once and atomically stores the next action", async () => {
const first = await service.pollScoringJob(userId, caseId, jobId);
const second = await service.pollScoringJob(userId, caseId, jobId);
assert.equal(engine.scoreEventsCalls, 1);
assert.deepEqual(second.nextAction, first.nextAction);
});
test("a failed job preserves evidence and exposes retry_scoring", async () => {
engine.scoreEventsError = new Error("offline");
const result = await service.pollScoringJob(userId, caseId, jobId);
assert.equal(result.nextAction.kind, "retry_scoring");
assert.equal(result.lifeEvents.length, 3);
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test --test-name-pattern="pending job|failed job" tests/birth-time-journey-service.test.ts`
Expected: FAIL because job APIs do not exist.
- [ ] **Step 3: Implement owner-scoped job claim and completion**
Only one poll may change `pending``processing`. A completed job returns the stored result. A failed job may be retried with the same evidence fingerprint without duplicating evidence or consuming an adaptive round. Job expiry and ownership are checked before engine invocation.
- [ ] **Step 4: Add strict API and client contracts**
```ts
z.object({
type: z.literal("poll_scoring"),
caseId: z.string().uuid(),
jobId: z.string().uuid(),
}).strict()
```
The route authenticates first and never accepts candidate score, confidence, result, or active time from this action.
- [ ] **Step 5: Run focused tests and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-journey-service.test.ts tests/birth-time-journey-client.test.ts && cd .. && .venv/bin/python -m pytest -q tests/test_birth_time_journey_contract.py`
Expected: all job and API contracts pass.
- [ ] **Step 6: Commit the scoring job path**
```bash
git add frontend/src/lib/birth-time-journey-store.ts frontend/src/lib/birth-time-evidence-service.ts frontend/src/app/api/birth-time-journey/route.ts frontend/src/lib/birth-time-journey-client.ts frontend/tests/birth-time-journey-service.test.ts frontend/tests/birth-time-journey-client.test.ts tests/test_birth_time_journey_contract.py
git commit -m "feat: resume idempotent birth time scoring jobs"
```
---
### Task 6: Constrained BirthTimeGuideAgent and Unbilled Guide API
**Files:**
- Create: `frontend/src/lib/birth-time-guide-agent.ts`
- Modify: `frontend/src/mastra/index.ts`
- Create: `frontend/src/app/api/birth-time-guide/route.ts`
- Modify: `frontend/src/lib/birth-time-journey-client.ts`
- Create: `frontend/tests/birth-time-guide-agent.test.ts`
- Create: `frontend/tests/birth-time-guide-route.test.ts`
**Interfaces:**
- Produces: `getBirthTimeGuideAgent(model)`, `parseEvidenceDraftOutput()`, deterministic `fallbackQuestionCopy()`, `requestBirthTimeGuidePrompt()`, and `draftBirthTimeEvidence()`.
- Consumes only server-loaded `QuestionSpec` and current case identifiers; does not expose score/save/confirm/apply tools.
- [ ] **Step 1: Add failing pure safety tests**
```ts
test("draft parser cannot change the server-selected domain", () => {
assert.throws(() => parseEvidenceDraftOutput(
{ domain: "relationship", precision: "month", date: "2023-04" },
{ requiredDomain: "career" },
));
});
test("ambiguous dates stay incomplete instead of being invented", () => {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: null, date: null },
{ requiredDomain: "career" },
);
assert.equal(draft.needsReview, true);
assert.equal(draft.date, null);
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test tests/birth-time-guide-agent.test.ts tests/birth-time-guide-route.test.ts`
Expected: FAIL because guide modules do not exist.
- [ ] **Step 3: Implement the constrained guide agent**
Agent instructions must require concise Simplified Chinese, one neutral question, no candidate-support disclosure, JSON-only drafts, no missing-date invention, and no astrology result. Register only a draft-structure tool; do not register consultation, scoring, candidate, profile, or confirmation tools.
- [ ] **Step 4: Implement the authenticated no-credit route**
Supported actions:
```ts
type GuideRequest =
| { type: "render_question"; caseId: string }
| { type: "draft_evidence"; caseId: string; actionId: string; turnVersion: number; message: string };
```
The route loads the owner-scoped current turn itself. `render_question` returns Agent copy or deterministic fallback. `draft_evidence` constrains extraction to the current question domain, then calls `proposeEvidenceDraft`; it never calls score/save/confirm/apply and never touches consultation credits.
- [ ] **Step 5: Add source/contract assertions for the tool boundary**
Assert that the guide route does not import `begin_consultation_credit`, `confirmBirthTimeCandidate`, `saveBirthTimeCandidate`, or the consultation Agent, and that fallback output is returned when no model is configured.
- [ ] **Step 6: Run focused tests and verify GREEN**
Run: `cd frontend && node --test tests/birth-time-guide-agent.test.ts tests/birth-time-guide-route.test.ts tests/birth-time-journey-client.test.ts`
Expected: all guide safety tests pass.
- [ ] **Step 7: Commit the Agent boundary**
```bash
git add frontend/src/lib/birth-time-guide-agent.ts frontend/src/mastra/index.ts frontend/src/app/api/birth-time-guide/route.ts frontend/src/lib/birth-time-journey-client.ts frontend/tests/birth-time-guide-agent.test.ts frontend/tests/birth-time-guide-route.test.ts
git commit -m "feat: add constrained birth time guide agent"
```
---
### Task 7: One-Question Chat UI, Draft Confirmation, and Automatic Polling
**Files:**
- Create: `frontend/src/components/birth-time-guide-turn.tsx`
- Create: `frontend/src/components/birth-time-evidence-draft-card.tsx`
- Modify: `frontend/src/components/birth-time-rectification.tsx`
- Modify: `frontend/src/components/birth-time-candidate-result.tsx`
- Modify: `frontend/src/app/page.tsx`
- Modify: `frontend/src/app/globals.css`
- Modify: `frontend/tests/birth-time-rectification-contract.test.ts`
- Create: `frontend/tests/birth-time-guide-flow.test.ts`
**Interfaces:**
- Consumes: parsed `JourneyClientResponse.nextAction`, guide prompt/draft APIs, `confirmBirthTimeEvidenceDraft`, `skipBirthTimeEvidenceQuestion`, and `pollBirthTimeScoring`.
- Produces: one-question composer, review card, progress display, score-pending state, terminal low/medium/high result actions.
- [ ] **Step 1: Add failing UI-flow contract tests**
```ts
test("guided rectification renders one question and a natural-language composer", () => {
assert.match(turnSource, /journey\.nextAction\.kind === "ask_baseline_evidence"/);
assert.match(turnSource, /说出大概年份也可以/);
assert.doesNotMatch(rectificationSource, /questions\.slice\(0, 3\)/);
});
test("draft review is explicit and scoring starts from confirmation", () => {
assert.match(draftSource, /确认并用于校正/);
assert.match(pageSource, /confirmBirthTimeEvidenceDraft/);
assert.doesNotMatch(turnSource, /比较候选时间/);
});
test("score_pending polls automatically and resume renders the persisted action", () => {
assert.match(pageSource, /pollBirthTimeScoring/);
assert.match(pageSource, /nextAction\.kind === "score_pending"/);
assert.match(pageSource, /resumeBirthTimeJourney/);
});
```
- [ ] **Step 2: Run and verify RED**
Run: `cd frontend && node --test tests/birth-time-rectification-contract.test.ts tests/birth-time-guide-flow.test.ts`
Expected: FAIL because guided components and handlers do not exist.
- [ ] **Step 3: Implement focused Client Components**
`BirthTimeGuideTurn` owns the one-question message input and skip action. `BirthTimeEvidenceDraftCard` owns editable domain-locked date/precision fields and the explicit confirm action. Keep candidate rendering in `BirthTimeCandidateResult`; do not put server transitions back into `page.tsx`.
- [ ] **Step 4: Wire page state and automatic polling**
When `nextAction` changes to an ask action, request Agent copy with a deterministic fallback already visible. When it changes to `score_pending`, start one bounded poll loop, cancel it on unmount/case/version change, and replace the whole Journey response on completion. Network failure leaves the persisted retry action visible.
- [ ] **Step 5: Preserve legacy rendering only behind normalized responses**
Remove the fixed three-question presentation from the active path. Existing legacy questionnaire fields may remain parsed for audit/migration, but `BirthTimeRectification` renders from `nextAction` only.
- [ ] **Step 6: Add responsive styles using existing tokens**
Use the existing card, type, color, spacing, focus, and 44px target tokens. Keep Chinese phrases such as “候选时间”, “当前排盘使用时间”, “关键经历”, and “确认并用于校正” phrase-safe at 390px.
- [ ] **Step 7: Run focused tests, typecheck, and lint**
Run: `cd frontend && node --test tests/birth-time-rectification-contract.test.ts tests/birth-time-guide-flow.test.ts && npx tsc --noEmit && npm run lint -- src/components/birth-time-guide-turn.tsx src/components/birth-time-evidence-draft-card.tsx src/components/birth-time-rectification.tsx src/app/page.tsx`
Expected: tests, typecheck, and targeted lint pass.
- [ ] **Step 8: Commit the UI**
```bash
git add frontend/src/components/birth-time-guide-turn.tsx frontend/src/components/birth-time-evidence-draft-card.tsx frontend/src/components/birth-time-rectification.tsx frontend/src/components/birth-time-candidate-result.tsx frontend/src/app/page.tsx frontend/src/app/globals.css frontend/tests/birth-time-rectification-contract.test.ts frontend/tests/birth-time-guide-flow.test.ts
git commit -m "feat: guide birth time evidence one question at a time"
```
---
### Task 8: Complete Verification Suite, Real Flow QA, and Accuracy Boundary
**Files:**
- Create: `frontend/tests/birth-time-agent-flow-e2e.test.ts`
- Create: `frontend/src/lib/birth-time-journey-telemetry.ts`
- Create: `frontend/tests/birth-time-journey-telemetry.test.ts`
- Modify: `frontend/src/app/api/birth-time-journey/route.ts`
- Modify: `frontend/src/app/api/birth-time-guide/route.ts`
- Modify: `frontend/DESIGN.md`
- Modify: `docs/superpowers/specs/2026-07-18-agent-guided-birth-time-rectification-design.md` only if implementation reveals a corrected contract; otherwise leave the committed spec unchanged.
**Interfaces:**
- Consumes the complete feature.
- Produces a reusable regression test set and manual QA evidence for baseline, adaptive, low, medium, high, failure, and resume branches.
- [ ] **Step 1: Add a fake-Agent/fake-engine full-flow test**
```ts
test("agent-guided journey cannot dead-end or apply without high confirmation", async () => {
let turn = await harness.assess(approximateAssessment);
for (const event of baselineEvents) {
turn = await harness.draftAndConfirm(turn, event);
assert.ok(turn.nextAction);
}
turn = await harness.pollUntilSettled(turn);
while (turn.nextAction.kind === "ask_adaptive_evidence") {
turn = await harness.skip(turn);
assert.ok(turn.nextAction);
}
assert.ok(["present_low_result", "present_medium_result", "request_candidate_confirmation"].includes(turn.nextAction.kind));
assert.equal(harness.profile.activeBirthTime, null);
});
```
- [ ] **Step 2: Run all frontend journey tests**
Run: `cd frontend && node --test tests/birth-time-*.test.ts`
Expected: all birth-time tests pass with no skipped tests.
- [ ] **Step 3: Run Python scoring and API tests**
Run: `.venv/bin/python -m pytest -q tests/test_active_rectification_questions.py tests/test_active_rectification_events.py tests/test_active_rectification_api.py tests/test_birth_time_journey_contract.py`
Expected: all selected Python tests pass.
- [ ] **Step 4: Run full frontend verification**
Run: `cd frontend && npm test && npx tsc --noEmit && npm run lint && npm run build`
Expected: full tests, typecheck, lint, and production build pass.
- [ ] **Step 5: Run manual browser scenarios**
Verify at desktop and 390px mobile:
1. baseline question → natural-language draft → edit → confirm → next question;
2. third baseline evidence → automatic calculation → adaptive question;
3. three low adaptive rounds → terminal saved range;
4. medium result → save only, no minute application;
5. high result → explicit representative-time confirmation → active profile time;
6. refresh on ask, draft, score-pending, retry, and confirmation states;
7. Agent unavailable fallback and scoring failure retry;
8. duplicate confirm does not duplicate evidence.
- [ ] **Step 6: Run security and code review**
Confirm the guide route has no billing/candidate/apply tool, job handles are owner-scoped and unguessable, raw event prose is not sent to the scorer or analytics, and low/medium confirmation attempts return conflict responses.
- [ ] **Step 7: Add privacy-safe structured journey metrics**
```ts
export type JourneyMetric =
| "turn_advanced"
| "draft_corrected"
| "journey_paused"
| "scoring_failed"
| "scoring_recovered"
| "illegal_snapshot";
export function journeyMetric(name: JourneyMetric, labels: {
phase: "baseline" | "adaptive" | "result";
confidence?: "low" | "medium" | "high";
}) {
console.info("[birth-time-journey]", JSON.stringify({ name, ...labels }));
}
```
Tests must prove the metric API has no field for raw message, event date, birth date, coordinates, case ID, or user ID. Route calls record state transitions and failures only.
- [ ] **Step 8: Record the accuracy boundary in `frontend/DESIGN.md`**
Document that the Agent controls wording only; confidence is a versioned internal deterministic gate and remains below external-oracle/real-case proof.
- [ ] **Step 9: Commit the verification set**
```bash
git add frontend/tests/birth-time-agent-flow-e2e.test.ts frontend/src/lib/birth-time-journey-telemetry.ts frontend/tests/birth-time-journey-telemetry.test.ts frontend/src/app/api/birth-time-journey/route.ts frontend/src/app/api/birth-time-guide/route.ts frontend/DESIGN.md
git commit -m "test: verify agent guided birth time journey"
```
## Execution Order and Subagent Ownership
1. Tasks 1 and 2 may run in parallel because they own separate new modules; coordinate the shared adapter/service type before merging.
2. Task 3 follows Task 2.
3. Task 4 follows Tasks 13.
4. Task 5 follows Task 4.
5. Task 6 may start after Task 2 but must integrate only after Task 4.
6. Task 7 follows Tasks 46.
7. Task 8 runs only after all implementation tasks pass their focused tests.
Each executor must state owned files, preserve other agents edits, capture RED and GREEN output, and hand back changed-file and test evidence. A separate reviewer checks spec compliance and code quality before the next dependent task begins.