fix(web): offer representative time after occupation notes cover
Dateless occupation_note stayed draft, so classic coverage never finished and offer-candidates stayed blocked. Confirm those notes, stop crowding dasha probes with encoded exam quality, and adopt once blocking methods are covered. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,8 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
datePrecision: string;
|
||||
occurredFrom: string | null;
|
||||
occurredTo: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[];
|
||||
conversationSummary: {
|
||||
activeFocus: {
|
||||
@@ -32,6 +34,7 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
};
|
||||
latestResult: {
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
selectionAllowed?: boolean;
|
||||
} | null;
|
||||
case: {
|
||||
acceptedTime: string | null;
|
||||
@@ -51,5 +54,7 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
oosBlindPrompts: refinement.oos_blind_prompts,
|
||||
eventProbes: refinement.discriminating_event_probes,
|
||||
accepted: Boolean(dossier.case.acceptedTime),
|
||||
selectionAllowed: dossier.latestResult?.selectionAllowed === true,
|
||||
proposeAllowed: dossier.latestResult?.decisionReceipt?.propose_allowed === true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,18 +14,21 @@
|
||||
* 4. Relatives — confirmed family evidence (D12 + D7 + D3)
|
||||
* 5. Appearance / constitution — skipped; never asked
|
||||
* 6. Birthmarks / scars — skipped; never asked
|
||||
* 7. Occupation / 10th house — separate from dated career events; D10 type table allowed
|
||||
* 7. Occupation / 10th house — separate from dated career events; D10 type table allowed.
|
||||
* A draft/confirmed occupation_note without a date still covers this layer.
|
||||
* 8. Horary — ask once for the first question time; recast if given; never blocks cards
|
||||
*
|
||||
* Relocation stays out of domain rotation and is only asked at d4_refine.
|
||||
* Finance/health score if volunteered; they are not method-layer rotation.
|
||||
* Method coverage finishes before repeating a precision-stage ask.
|
||||
* Appearance and marks are skipped_by_policy. Horary does not block offering
|
||||
* time cards. Occupation does block cards.
|
||||
* time cards. Occupation does block cards until a note exists.
|
||||
* Method coverage asks for dated events in natural language.
|
||||
* After the first dated event, remaining dasha conflict probes
|
||||
* (year/activation differences) are asked before more method rotation
|
||||
* and they block offering time cards so the window can be filtered.
|
||||
* Once blocking methods are covered and propose_allowed, offer a
|
||||
* representative time even if probes or horary remain.
|
||||
* A/B/C/D choice frames attach only when candidates already diverge
|
||||
* (event probes, precision stage, varga observation, nakshatra, or holdout).
|
||||
*/
|
||||
@@ -94,6 +97,8 @@ export type MethodFollowupEvidence = Readonly<{
|
||||
datePrecision: string;
|
||||
occurredFrom: string | null;
|
||||
occurredTo: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>;
|
||||
|
||||
export type MethodFollowupFocus = Readonly<{
|
||||
@@ -123,6 +128,37 @@ function hasConfirmedDomain(evidence: readonly MethodFollowupEvidence[], domain:
|
||||
return evidence.some((item) => item.status === "confirmed" && item.domain === domain);
|
||||
}
|
||||
|
||||
function evidenceYear(item: MethodFollowupEvidence): number | null {
|
||||
const raw = item.occurredFrom || item.occurredTo;
|
||||
if (!raw || raw.length < 4 || !/^\d{4}/.test(raw)) return null;
|
||||
const year = Number(raw.slice(0, 4));
|
||||
return year >= 1900 && year <= 2100 ? year : null;
|
||||
}
|
||||
|
||||
function hasDatedEvidenceInYear(
|
||||
evidence: readonly MethodFollowupEvidence[],
|
||||
domain: string,
|
||||
year: number,
|
||||
): boolean {
|
||||
return evidence.some((item) =>
|
||||
(item.status === "confirmed" || item.status === "draft" || item.status === "pending_confirmation")
|
||||
&& item.domain === domain
|
||||
&& evidenceYear(item) === year
|
||||
);
|
||||
}
|
||||
|
||||
function isOccupationNote(item: MethodFollowupEvidence): boolean {
|
||||
if (item.domain !== "occupation") return false;
|
||||
if (item.status !== "confirmed" && item.status !== "draft" && item.status !== "pending_confirmation") {
|
||||
return false;
|
||||
}
|
||||
return !item.eventKind || item.eventKind === "occupation_note";
|
||||
}
|
||||
|
||||
function blockingMethodsCovered(methods: readonly MethodCoverage[]): boolean {
|
||||
return !methods.some((item) => BLOCKING_COVERAGE_IDS.has(item.method_id) && item.status === "uncovered");
|
||||
}
|
||||
|
||||
function hasConfirmedHealth(evidence: readonly MethodFollowupEvidence[]): boolean {
|
||||
return hasConfirmedDomain(evidence, "health_pressure") || hasConfirmedDomain(evidence, "health");
|
||||
}
|
||||
@@ -198,7 +234,7 @@ function remainingReverseVerifyProbes(
|
||||
for (const probe of probes ?? []) {
|
||||
if (probe.source === "known_event_quality" || probe.role === "distinguish") continue;
|
||||
if (declined.has(probe.domain)) continue;
|
||||
if (hasConfirmedDomain(evidence, probe.domain)) continue;
|
||||
if (hasDatedEvidenceInYear(evidence, probe.domain, probe.year)) continue;
|
||||
if (probe.source === "dasha_boundary" || probe.source === "dasha_activation") {
|
||||
dasha.push(probe);
|
||||
} else {
|
||||
@@ -300,7 +336,7 @@ export function isOfferBlockingFollowup(
|
||||
return true;
|
||||
}
|
||||
if (!followup) return false;
|
||||
if (followup.source === "event_probe") return true;
|
||||
if (followup.source === "event_probe") return methods == null;
|
||||
if (followup.source !== "method_coverage") return false;
|
||||
return BLOCKING_COVERAGE_IDS.has(followup.method_id as MethodFollowupId);
|
||||
}
|
||||
@@ -429,7 +465,9 @@ export function buildMethodFollowupPlan(input: {
|
||||
const familyCovered = hasConfirmedDomain(input.evidence, "family");
|
||||
const financeCovered = hasConfirmedDomain(input.evidence, "finance");
|
||||
const healthCovered = hasConfirmedHealth(input.evidence);
|
||||
const occupationCovered = hasConfirmedDomain(input.evidence, "occupation") || declined.has("occupation");
|
||||
const occupationCovered = hasConfirmedDomain(input.evidence, "occupation")
|
||||
|| input.evidence.some(isOccupationNote)
|
||||
|| declined.has("occupation");
|
||||
const horaryGiven = hasConfirmedDomain(input.evidence, "horary");
|
||||
const horaryStatus: MethodCoverageStatus = horaryGiven
|
||||
? "covered"
|
||||
@@ -453,7 +491,24 @@ export function buildMethodFollowupPlan(input: {
|
||||
const keepAcceptedFocus = Boolean(
|
||||
focus && (focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check"),
|
||||
);
|
||||
if (focus && (!input.accepted || keepAcceptedFocus)) {
|
||||
const coverageComplete = blockingMethodsCovered(methods);
|
||||
const staleCollectFocus = Boolean(
|
||||
focus
|
||||
&& focus.intent === "collect_method_evidence"
|
||||
&& (
|
||||
(focus.targetDomain === "occupation" && occupationCovered)
|
||||
|| (focus.targetDomain === "relationship" && (relationshipCovered || declined.has("relationship")))
|
||||
|| (focus.targetDomain === "career" && (careerCovered || declined.has("career")))
|
||||
|| (focus.targetDomain === "family" && (familyCovered || declined.has("family")))
|
||||
|| (focus.targetDomain === "horary" && horaryStatus !== "uncovered")
|
||||
),
|
||||
);
|
||||
if (
|
||||
focus
|
||||
&& !staleCollectFocus
|
||||
&& (sessionOutcome !== "adopt_representative" || keepAcceptedFocus)
|
||||
&& (!input.accepted || keepAcceptedFocus)
|
||||
) {
|
||||
const existingChoice = parseAgentChoiceCopy(focus.expectedAnswerSchema ?? null);
|
||||
const reverseVerify = focus.intent === "reverse_verify";
|
||||
const keepChoice = reverseVerify || (Boolean(existingChoice) && (
|
||||
@@ -528,7 +583,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
),
|
||||
source: "method_coverage",
|
||||
});
|
||||
} else if (conflictProbe) {
|
||||
} else if (conflictProbe && !coverageComplete) {
|
||||
next = makeFollowup({
|
||||
method_id: PROBE_METHOD_ID[conflictProbe.domain],
|
||||
intent: "distinguish_candidates",
|
||||
@@ -807,9 +862,27 @@ export function buildMethodFollowupPlan(input: {
|
||||
}
|
||||
|
||||
export function projectRectificationChoiceCard(
|
||||
input: Parameters<typeof buildMethodFollowupPlan>[0],
|
||||
input: Parameters<typeof buildMethodFollowupPlan>[0] & {
|
||||
selectionAllowed?: boolean;
|
||||
proposeAllowed?: boolean;
|
||||
userStopped?: boolean;
|
||||
},
|
||||
): RectificationChoiceCard | null {
|
||||
const plan = buildMethodFollowupPlan(input);
|
||||
const sessionOutcome = conversationalSessionOutcome({
|
||||
selectionAllowed: input.selectionAllowed === true,
|
||||
proposeAllowed: input.proposeAllowed === true,
|
||||
confirmationAllowed: false,
|
||||
nextFollowup: plan.next_followup,
|
||||
methods: plan.methods,
|
||||
userStopped: input.userStopped,
|
||||
});
|
||||
if (
|
||||
!input.accepted
|
||||
&& (sessionOutcome === "adopt_representative" || sessionOutcome === "awaiting_confirmation")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const frame = plan.next_followup?.choice_frame ?? plan.deferred_followup?.choice_frame ?? null;
|
||||
if (!frame) return null;
|
||||
return mergeChoiceCard(frame, parseAgentChoiceCopy(input.activeFocus?.expectedAnswerSchema ?? null));
|
||||
|
||||
Reference in New Issue
Block a user