fix: isolate dynamic rectification persistence
This commit is contained in:
@@ -117,6 +117,9 @@ export async function POST(request: Request) {
|
||||
case "poll_scoring":
|
||||
{
|
||||
const before = await service.resume(user.id, parsed.data.caseId);
|
||||
if (before.journeyProtocol === "dynamic-choice-v2") {
|
||||
throw new GuidedJourneyLegacyMutationError(parsed.data.caseId);
|
||||
}
|
||||
const response = await service.pollScoringJob(user.id, parsed.data.caseId, parsed.data.jobId);
|
||||
recordScoringJourneyMetric(before, response);
|
||||
return NextResponse.json(response);
|
||||
|
||||
@@ -54,7 +54,10 @@ export function createSupabaseGuidedCandidateStore(
|
||||
const { error } = await supabase.rpc(functionName, args);
|
||||
if (error) {
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (current?.processedActionIds?.includes(command.actionId.toLowerCase())) return current;
|
||||
if (
|
||||
current?.journeyProtocol === "legacy-guided-v1"
|
||||
&& current.processedActionIds?.includes(command.actionId.toLowerCase())
|
||||
) return current;
|
||||
if (isDomainError(error, "stale_guided_candidate_turn")) {
|
||||
throw new StaleJourneyTurnError(
|
||||
value.id,
|
||||
@@ -68,7 +71,9 @@ export function createSupabaseGuidedCandidateStore(
|
||||
throw new BirthTimeJourneyStoreError("update_case");
|
||||
}
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (!current) throw new BirthTimeJourneyStoreError("load_case");
|
||||
if (!current || current.journeyProtocol !== "legacy-guided-v1") {
|
||||
throw new BirthTimeJourneyStoreError("load_case");
|
||||
}
|
||||
return current;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -162,6 +162,10 @@ export async function loadStoredRectificationCase(
|
||||
: null;
|
||||
if (dynamicTurn && !dynamicTurn.success) throw new BirthTimeJourneyStoreError("load_case");
|
||||
const dynamicTurnState = dynamicTurn?.data ?? null;
|
||||
if (
|
||||
dynamicTurnState !== null
|
||||
&& dynamicTurnState.turnVersion !== parsed.turn_version
|
||||
) throw new BirthTimeJourneyStoreError("load_case");
|
||||
const evidenceDraft = parseEvidenceDraft(parsed.evidence_draft);
|
||||
let dynamicPrivate = null;
|
||||
if (parsed.journey_protocol === "dynamic-choice-v2") {
|
||||
|
||||
@@ -53,8 +53,6 @@ export async function saveDynamicAssessment(
|
||||
p_private_state: initial.privateState,
|
||||
p_profile: {
|
||||
reportedBirthTime: details.reportedTime,
|
||||
activeBirthTime: value.snapshot.activeTime,
|
||||
birthTime: value.snapshot.activeTime,
|
||||
birthTimeSource: value.assessment.source,
|
||||
birthTimePeriod: details.period,
|
||||
birthTimeClue: details.clue,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { DynamicJourneyTurnState } from "./birth-time-journey-turn-protocol.ts";
|
||||
import type {
|
||||
DynamicStoredRectificationCase,
|
||||
LegacyStoredRectificationCase,
|
||||
StoredRectificationCase,
|
||||
} from "./birth-time-journey-service.ts";
|
||||
import {
|
||||
@@ -119,9 +120,8 @@ export function createDynamicTurnPersistence(
|
||||
},
|
||||
|
||||
async upgradeLegacyActiveCase(
|
||||
value: StoredRectificationCase,
|
||||
value: LegacyStoredRectificationCase,
|
||||
): Promise<StoredRectificationCase> {
|
||||
if (value.journeyProtocol === "dynamic-choice-v2") return value;
|
||||
if (isTerminalLegacyCase(value)) return value;
|
||||
const upgraded = prepareLegacyDynamicUpgrade(value, asOfDate());
|
||||
const result = await client.rpc("upgrade_birth_time_legacy_case", {
|
||||
|
||||
@@ -221,6 +221,7 @@ export function prepareLegacyDynamicUpgrade(
|
||||
journeyProtocol: "dynamic-choice-v2",
|
||||
turnVersion: value.turnVersion ?? 0,
|
||||
processedActionIds: value.processedActionIds ?? [],
|
||||
persistedProgress: value.persistedProgress ?? { adaptiveRound: 0, askedDomains: [] },
|
||||
turnState: null,
|
||||
evidenceDraft: null,
|
||||
dynamicTurnState,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
} from "./birth-time-journey-turn.ts";
|
||||
import type { PersistedJourneyProgress } from "./birth-time-journey-turn-persistence.ts";
|
||||
import type {
|
||||
DynamicStoredRectificationCase,
|
||||
DynamicVersionedJourneyResponse,
|
||||
JourneyResponseBase,
|
||||
StoredRectificationCase,
|
||||
VersionedJourneyResponse,
|
||||
@@ -132,6 +134,22 @@ export function storedJourneyResponse(
|
||||
};
|
||||
}
|
||||
|
||||
export function storedDynamicJourneyResponse(
|
||||
stored: DynamicStoredRectificationCase,
|
||||
): DynamicVersionedJourneyResponse {
|
||||
return {
|
||||
caseId: stored.id,
|
||||
snapshot: stored.snapshot,
|
||||
questionnaire: stored.questionnaire,
|
||||
scoring: stored.scoring ?? null,
|
||||
answers: stored.answers,
|
||||
lifeEvents: stored.lifeEvents ?? [],
|
||||
candidateResult: stored.candidateResult ?? null,
|
||||
evidenceDraft: null,
|
||||
...stored.dynamicTurnState,
|
||||
};
|
||||
}
|
||||
|
||||
export function persistedJourneyResponse(
|
||||
stored: StoredRectificationCase,
|
||||
): VersionedJourneyResponse {
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import { assessBirthTime, withRectificationScoring, type BirthTimeAssessment, type JourneySnapshot, type RectificationScoring } from "./birth-time-journey.ts";
|
||||
import { createJourneyTurnActions } from "./birth-time-journey-actions.ts";
|
||||
import {
|
||||
assertLegacyJourneyMutation,
|
||||
createBirthTimeEvidenceActions,
|
||||
} from "./birth-time-evidence-service.ts";
|
||||
import { assertLegacyJourneyMutation, createBirthTimeEvidenceActions } from "./birth-time-evidence-service.ts";
|
||||
import type { CandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import type { CandidateVargaSample } from "./birth-time-question-planner.ts";
|
||||
import { projectJourneyResponse, storedJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import { projectJourneyResponse, storedDynamicJourneyResponse, storedJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import type { JourneyTurnState } from "./birth-time-journey-turn.ts";
|
||||
import type { DynamicJourneyTurnState } from "./birth-time-journey-turn-protocol.ts";
|
||||
import type { ScoringJobClaim, ScoringJobIdentity, ScoringJobSpec } from "./birth-time-scoring-job.ts";
|
||||
import { createBirthTimeScoringService } from "./birth-time-scoring-service.ts";
|
||||
import { scanAssessment } from "./birth-time-journey-assessment.ts";
|
||||
import type { GuidedCandidateCommit } from "./birth-time-guided-candidate.ts";
|
||||
import { createGuidedCandidateActions } from "./birth-time-guided-candidate.ts";
|
||||
import { createGuidedDraftRevisionActions } from "./birth-time-guided-draft-revision.ts";
|
||||
import type {
|
||||
CandidateDifferenceBuild,
|
||||
DynamicChoiceScoringResult,
|
||||
ServerChoiceEvidence,
|
||||
} from "./birth-time-dynamic-choice-internal.ts";
|
||||
import type { CandidateDifferenceBuild, DynamicChoiceScoringResult, ServerChoiceEvidence } from "./birth-time-dynamic-choice-internal.ts";
|
||||
import type { TimeRange } from "./birth-time-dynamic-choice.ts";
|
||||
import type {
|
||||
DynamicStoredFields,
|
||||
LegacyStoredFields,
|
||||
} from "./birth-time-journey-stored-protocol.ts";
|
||||
import {
|
||||
RectificationCaseNotFoundError,
|
||||
RectificationQuestionsUnavailableError,
|
||||
} from "./birth-time-journey-errors.ts";
|
||||
import type { DynamicStoredFields, LegacyStoredFields } from "./birth-time-journey-stored-protocol.ts";
|
||||
import { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError } from "./birth-time-journey-errors.ts";
|
||||
|
||||
export { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError };
|
||||
|
||||
@@ -152,18 +140,18 @@ export type StoredRectificationCase =
|
||||
export interface BirthTimeJourneyStore {
|
||||
saveAssessment(value: PersistedJourneyAssessment): Promise<string>;
|
||||
loadCase(userId: string, caseId: string): Promise<StoredRectificationCase | null>;
|
||||
saveScoring(value: StoredRectificationCase): Promise<void>;
|
||||
saveTurn(value: StoredRectificationCase, expectedVersion: number, actionId: string): Promise<StoredRectificationCase>;
|
||||
saveScoring(value: LegacyStoredRectificationCase): Promise<void>;
|
||||
saveTurn(value: LegacyStoredRectificationCase, expectedVersion: number, actionId: string): Promise<StoredRectificationCase>;
|
||||
saveDynamicTurn(value: DynamicStoredRectificationCase, expectedVersion: number, actionId: string): Promise<DynamicStoredRectificationCase>;
|
||||
upgradeLegacyActiveCase(value: StoredRectificationCase): Promise<StoredRectificationCase>;
|
||||
createScoringJob(value: StoredRectificationCase, expectedVersion: number, actionId: string, job: ScoringJobSpec): Promise<StoredRectificationCase>;
|
||||
upgradeLegacyActiveCase(value: LegacyStoredRectificationCase): Promise<StoredRectificationCase>;
|
||||
createScoringJob(value: LegacyStoredRectificationCase, expectedVersion: number, actionId: string, job: ScoringJobSpec): Promise<StoredRectificationCase>;
|
||||
claimScoringJob(identity: ScoringJobIdentity): Promise<ScoringJobClaim>;
|
||||
completeScoringJob(value: StoredRectificationCase, expectedVersion: number, jobId: string, evidenceFingerprint: string): Promise<StoredRectificationCase>;
|
||||
failScoringJob(value: StoredRectificationCase, expectedVersion: number, jobId: string, evidenceFingerprint: string, failureCode: string): Promise<StoredRectificationCase>;
|
||||
saveCandidateResult(value: StoredRectificationCase): Promise<void>;
|
||||
saveCandidate(value: StoredRectificationCase): Promise<void>;
|
||||
confirmCandidate(value: StoredRectificationCase): Promise<void>;
|
||||
commitGuidedCandidate(value: StoredRectificationCase, command: GuidedCandidateCommit): Promise<StoredRectificationCase>;
|
||||
completeScoringJob(value: LegacyStoredRectificationCase, expectedVersion: number, jobId: string, evidenceFingerprint: string): Promise<StoredRectificationCase>;
|
||||
failScoringJob(value: LegacyStoredRectificationCase, expectedVersion: number, jobId: string, evidenceFingerprint: string, failureCode: string): Promise<StoredRectificationCase>;
|
||||
saveCandidateResult(value: LegacyStoredRectificationCase): Promise<void>;
|
||||
saveCandidate(value: LegacyStoredRectificationCase): Promise<void>;
|
||||
confirmCandidate(value: LegacyStoredRectificationCase): Promise<void>;
|
||||
commitGuidedCandidate(value: LegacyStoredRectificationCase, command: GuidedCandidateCommit): Promise<StoredRectificationCase>;
|
||||
}
|
||||
|
||||
export type BirthTimeJourneyPorts = {
|
||||
@@ -182,11 +170,15 @@ export type JourneyResponseBase = {
|
||||
readonly candidateResult: CandidateResult | null;
|
||||
};
|
||||
|
||||
export type VersionedJourneyResponse = JourneyResponseBase & JourneyTurnState;
|
||||
export type VersionedJourneyResponse = JourneyResponseBase & JourneyTurnState
|
||||
& { readonly journeyProtocol?: undefined };
|
||||
|
||||
export type DynamicVersionedJourneyResponse = JourneyResponseBase & DynamicJourneyTurnState
|
||||
& { readonly evidenceDraft: null };
|
||||
|
||||
export type LegacyJourneyResponse = JourneyResponseBase & { readonly turnVersion?: undefined; readonly nextAction?: undefined; readonly progress?: undefined; readonly permissions?: undefined; readonly evidenceDraft?: undefined; };
|
||||
|
||||
export type JourneyResponse = LegacyJourneyResponse | VersionedJourneyResponse;
|
||||
export type JourneyResponse = LegacyJourneyResponse | VersionedJourneyResponse | DynamicVersionedJourneyResponse;
|
||||
|
||||
export function createBirthTimeJourneyService(ports: BirthTimeJourneyPorts) {
|
||||
const evidenceActions = createBirthTimeEvidenceActions(ports);
|
||||
@@ -217,9 +209,12 @@ export function createBirthTimeJourneyService(ports: BirthTimeJourneyPorts) {
|
||||
}, 0);
|
||||
},
|
||||
|
||||
async resume(userId: string, caseId: string): Promise<VersionedJourneyResponse> {
|
||||
async resume(userId: string, caseId: string): Promise<VersionedJourneyResponse | DynamicVersionedJourneyResponse> {
|
||||
const stored = await ports.store.loadCase(userId, caseId);
|
||||
if (!stored) throw new RectificationCaseNotFoundError(caseId);
|
||||
if (stored.journeyProtocol === "dynamic-choice-v2") {
|
||||
return storedDynamicJourneyResponse(stored);
|
||||
}
|
||||
const completedLegacyQuestionnaire = stored.snapshot.input === "rectification_questions"
|
||||
&& stored.scoring?.nextRound === null
|
||||
&& stored.scoring.nextRoundQuestions.length === 0
|
||||
@@ -245,7 +240,7 @@ export function createBirthTimeJourneyService(ports: BirthTimeJourneyPorts) {
|
||||
const answers = { ...stored.answers, [questionId]: answer };
|
||||
const scoring = await ports.engine.score({ questionnaire: stored.questionnaire, answers });
|
||||
const snapshot = withRectificationScoring(stored.snapshot, scoring);
|
||||
const updated = { ...stored, answers, scoring, snapshot } satisfies StoredRectificationCase;
|
||||
const updated = { ...stored, answers, scoring, snapshot } satisfies LegacyStoredRectificationCase;
|
||||
await ports.store.saveScoring(updated);
|
||||
return projectJourneyResponse({
|
||||
caseId,
|
||||
|
||||
@@ -54,7 +54,7 @@ export function createSupabaseBirthTimeJourneyStore(
|
||||
...guidedCandidates,
|
||||
|
||||
async saveScoring(value) {
|
||||
const { error } = await supabase
|
||||
const { data, error } = await supabase
|
||||
.from("birth_time_rectification_cases")
|
||||
.update({
|
||||
status: caseStatus(value.snapshot),
|
||||
@@ -64,8 +64,11 @@ export function createSupabaseBirthTimeJourneyStore(
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", value.id)
|
||||
.eq("user_id", value.userId);
|
||||
if (error) throw new BirthTimeJourneyStoreError("update_case");
|
||||
.eq("user_id", value.userId)
|
||||
.eq("journey_protocol", "legacy-guided-v1")
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
if (error || !data) throw new BirthTimeJourneyStoreError("update_case");
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
@@ -77,7 +80,7 @@ export function createSupabaseBirthTimeJourneyStore(
|
||||
|
||||
async saveCandidateResult(value) {
|
||||
const winner = value.candidateResult?.winningSegment ?? null;
|
||||
const { error } = await supabase
|
||||
const { data, error } = await supabase
|
||||
.from("birth_time_rectification_cases")
|
||||
.update({
|
||||
status: caseStatus(value.snapshot),
|
||||
@@ -91,8 +94,11 @@ export function createSupabaseBirthTimeJourneyStore(
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", value.id)
|
||||
.eq("user_id", value.userId);
|
||||
if (error) throw new BirthTimeJourneyStoreError("update_case");
|
||||
.eq("user_id", value.userId)
|
||||
.eq("journey_protocol", "legacy-guided-v1")
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
if (error || !data) throw new BirthTimeJourneyStoreError("update_case");
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
@@ -106,13 +112,16 @@ export function createSupabaseBirthTimeJourneyStore(
|
||||
},
|
||||
|
||||
async saveCandidate(value) {
|
||||
const { error } = await supabase
|
||||
const { data, error } = await supabase
|
||||
.from("birth_time_rectification_cases")
|
||||
.update({ candidate_saved_at: new Date().toISOString(), updated_at: new Date().toISOString() })
|
||||
.eq("id", value.id)
|
||||
.eq("user_id", value.userId)
|
||||
.eq("candidate_result_id", value.candidateResult?.resultId ?? null);
|
||||
if (error) throw new BirthTimeJourneyStoreError("update_case");
|
||||
.eq("journey_protocol", "legacy-guided-v1")
|
||||
.eq("candidate_result_id", value.candidateResult?.resultId ?? null)
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
if (error || !data) throw new BirthTimeJourneyStoreError("update_case");
|
||||
},
|
||||
|
||||
async confirmCandidate(value) {
|
||||
|
||||
@@ -14,7 +14,7 @@ type LegacyProgress = {
|
||||
};
|
||||
|
||||
export type LegacyStoredFields = {
|
||||
readonly journeyProtocol?: "legacy-guided-v1";
|
||||
readonly journeyProtocol: "legacy-guided-v1";
|
||||
readonly turnVersion?: number;
|
||||
readonly turnState?: JourneyTurnState | null;
|
||||
readonly evidenceDraft?: EvidenceDraft | null;
|
||||
@@ -32,11 +32,11 @@ export type LegacyStoredFields = {
|
||||
export type DynamicStoredFields = {
|
||||
readonly journeyProtocol: "dynamic-choice-v2";
|
||||
readonly turnVersion: number;
|
||||
readonly turnState?: null;
|
||||
readonly turnState: null;
|
||||
readonly dynamicTurnState: DynamicJourneyTurnState;
|
||||
readonly evidenceDraft?: null;
|
||||
readonly evidenceDraft: null;
|
||||
readonly processedActionIds: readonly string[];
|
||||
readonly persistedProgress?: LegacyProgress;
|
||||
readonly persistedProgress: LegacyProgress;
|
||||
readonly candidateModel: Readonly<Record<string, unknown>> | null;
|
||||
readonly currentChoiceQuestion: PersistedDynamicChoiceQuestion | null;
|
||||
readonly choiceAnswers: readonly StoredChoiceAnswer[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { z } from "zod";
|
||||
import type { JourneySnapshot } from "./birth-time-journey.ts";
|
||||
import type { StoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import type { LegacyStoredRectificationCase, StoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import { evidenceDomains } from "./birth-time-question-planner.ts";
|
||||
import type { EvidenceDomain } from "./birth-time-question-planner.ts";
|
||||
import type { EvidenceDraft, JourneyTurnState } from "./birth-time-journey-turn.ts";
|
||||
@@ -61,7 +61,7 @@ export function createJourneyTurnPersistence(
|
||||
) {
|
||||
return {
|
||||
async saveTurn(
|
||||
value: StoredRectificationCase,
|
||||
value: LegacyStoredRectificationCase,
|
||||
expectedVersion: number,
|
||||
actionId: string,
|
||||
): Promise<StoredRectificationCase> {
|
||||
@@ -87,6 +87,7 @@ export function createJourneyTurnPersistence(
|
||||
})
|
||||
.eq("id", value.id)
|
||||
.eq("user_id", value.userId)
|
||||
.eq("journey_protocol", "legacy-guided-v1")
|
||||
.eq("turn_version", expectedVersion)
|
||||
.not("processed_action_ids", "cs", `{${parsedActionId}}`)
|
||||
.select("id")
|
||||
@@ -104,7 +105,10 @@ export function createJourneyTurnPersistence(
|
||||
} satisfies StoredRectificationCase;
|
||||
}
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (current?.processedActionIds?.includes(parsedActionId)) return current;
|
||||
if (
|
||||
current?.journeyProtocol === "legacy-guided-v1"
|
||||
&& current.processedActionIds?.includes(parsedActionId)
|
||||
) return current;
|
||||
throw new StaleJourneyTurnError(value.id, expectedVersion, current?.turnVersion ?? 0);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createScoringJobSpec } from "./birth-time-scoring-job.ts";
|
||||
import type {
|
||||
BirthTimeJourneyStore,
|
||||
LegacyStoredRectificationCase,
|
||||
StoredRectificationCase,
|
||||
} from "./birth-time-journey-service.ts";
|
||||
|
||||
@@ -11,7 +12,7 @@ export type JourneyTurnPersistencePorts = {
|
||||
|
||||
export async function persistGuidedJourneyTurn(
|
||||
ports: JourneyTurnPersistencePorts,
|
||||
value: StoredRectificationCase,
|
||||
value: LegacyStoredRectificationCase,
|
||||
expectedVersion: number,
|
||||
actionId: string,
|
||||
): Promise<StoredRectificationCase> {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { BirthTimeScoringJobError } from "./birth-time-scoring-job.ts";
|
||||
import type {
|
||||
BirthTimeJourneyStore,
|
||||
LegacyStoredRectificationCase,
|
||||
StoredRectificationCase,
|
||||
} from "./birth-time-journey-service.ts";
|
||||
import { BirthTimeJourneyStoreError } from "./birth-time-journey-turn-persistence.ts";
|
||||
@@ -48,9 +49,11 @@ async function requireReloaded(
|
||||
loadCase: (userId: string, caseId: string) => Promise<StoredRectificationCase | null>,
|
||||
userId: string,
|
||||
caseId: string,
|
||||
): Promise<StoredRectificationCase> {
|
||||
): Promise<LegacyStoredRectificationCase> {
|
||||
const stored = await loadCase(userId, caseId);
|
||||
if (!stored) throw new BirthTimeJourneyStoreError("load_case");
|
||||
if (!stored || stored.journeyProtocol !== "legacy-guided-v1") {
|
||||
throw new BirthTimeJourneyStoreError("load_case");
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
@@ -78,7 +81,10 @@ export function createSupabaseScoringJobStore(
|
||||
});
|
||||
if (error) {
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (current?.processedActionIds?.includes(actionId.toLowerCase())) return current;
|
||||
if (
|
||||
current?.journeyProtocol === "legacy-guided-v1"
|
||||
&& current.processedActionIds?.includes(actionId.toLowerCase())
|
||||
) return current;
|
||||
throw mapScoringRpcError(error.message, error.code);
|
||||
}
|
||||
return requireReloaded(loadCase, value.userId, value.id);
|
||||
|
||||
Reference in New Issue
Block a user