|
|
|
@@ -6,23 +6,21 @@
|
|
|
|
|
* server's next question. It still only produces candidates, never a
|
|
|
|
|
* confirmed unique minute.
|
|
|
|
|
*
|
|
|
|
|
* Policy map:
|
|
|
|
|
* Policy map (classic eight):
|
|
|
|
|
* 1. Dasha + dated events — any confirmed dated event
|
|
|
|
|
* 2. D9 relationship — confirmed relationship evidence; no sign labels
|
|
|
|
|
* 3. D10 career — confirmed career evidence; same event also scores D1 10th house
|
|
|
|
|
* 3. D10 career — confirmed dated career evidence; same event also scores D1 10th house
|
|
|
|
|
* 4. Relatives — confirmed family evidence (D12 + D7 + D3)
|
|
|
|
|
* 5. Finance — confirmed dated finance evidence (D2 + D11)
|
|
|
|
|
* 6. Health — confirmed dated health_pressure evidence (D30)
|
|
|
|
|
* 7. Appearance / constitution — ask; dated answers are auxiliary 1st-house scores
|
|
|
|
|
* 8. Birthmarks / scars — ask; dated answers are auxiliary 1st-house scores
|
|
|
|
|
* 9. Occupation / 10th house — folded into method 3 (D10 + D1 10th)
|
|
|
|
|
* 10. Horary — unsupported; skip
|
|
|
|
|
* 5. Appearance / constitution — ask; dated answers are auxiliary 1st-house scores
|
|
|
|
|
* 6. Birthmarks / scars — ask; dated answers are auxiliary 1st-house scores
|
|
|
|
|
* 7. Occupation / 10th house — separate from dated career events; no type labels
|
|
|
|
|
* 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 are asked via this plan, never via SQL missing_evidence_categories.
|
|
|
|
|
* Method coverage (relationship → career → family → finance → health) finishes
|
|
|
|
|
* before repeating a precision-stage ask on an already-covered domain.
|
|
|
|
|
* Appearance and marks are auxiliary and do not block offering time cards.
|
|
|
|
|
* Finance/health score if volunteered; they are not method-layer rotation.
|
|
|
|
|
* Method coverage finishes before repeating a precision-stage ask.
|
|
|
|
|
* Appearance, marks and Horary do not block offering time cards.
|
|
|
|
|
* Occupation does block cards, even if the current question is appearance.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { sessionOutcomeFromGate, type SessionOutcomeKind } from "./confirmation-gate.ts";
|
|
|
|
@@ -38,10 +36,9 @@ export const METHOD_FOLLOWUP_IDS = [
|
|
|
|
|
"d9_relationship",
|
|
|
|
|
"d10_career",
|
|
|
|
|
"relatives",
|
|
|
|
|
"d2_finance",
|
|
|
|
|
"d30_health",
|
|
|
|
|
"appearance",
|
|
|
|
|
"marks",
|
|
|
|
|
"occupation",
|
|
|
|
|
"horary",
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
@@ -55,9 +52,9 @@ export type MethodCoverage = Readonly<{
|
|
|
|
|
}>;
|
|
|
|
|
|
|
|
|
|
export type MethodFollowup = Readonly<{
|
|
|
|
|
method_id: "dasha_events" | "d9_relationship" | "d10_career" | "d4_home" | "d5_education" | "relatives" | "d2_finance" | "d30_health" | "appearance" | "marks" | "active_focus" | "nakshatra_boundary" | "oos_blind";
|
|
|
|
|
method_id: "dasha_events" | "d9_relationship" | "d10_career" | "d4_home" | "d5_education" | "relatives" | "d2_finance" | "d30_health" | "appearance" | "marks" | "occupation" | "horary" | "active_focus" | "nakshatra_boundary" | "oos_blind";
|
|
|
|
|
intent: string;
|
|
|
|
|
ask_theme: "dated_event" | "relationship_style" | "career_style" | "home_change" | "education_style" | "family_event" | "finance_change" | "health_pressure" | "appearance" | "marks" | "active_focus" | "nakshatra_trait" | "oos_blind";
|
|
|
|
|
ask_theme: "dated_event" | "relationship_style" | "career_style" | "home_change" | "education_style" | "family_event" | "finance_change" | "health_pressure" | "appearance" | "marks" | "occupation" | "horary" | "active_focus" | "nakshatra_trait" | "oos_blind";
|
|
|
|
|
domain: string | null;
|
|
|
|
|
kind_hint: string | null;
|
|
|
|
|
user_prompt_hint: string;
|
|
|
|
@@ -71,7 +68,7 @@ export type MethodFollowupPlan = Readonly<{
|
|
|
|
|
deferred_followup: MethodFollowup | null;
|
|
|
|
|
session_outcome: SessionOutcomeKind;
|
|
|
|
|
stop_domain_rotation: true;
|
|
|
|
|
do_not_poll: readonly ["horary"];
|
|
|
|
|
do_not_poll: readonly [];
|
|
|
|
|
not_in_rotation: readonly ["relocation"];
|
|
|
|
|
}>;
|
|
|
|
|
|
|
|
|
@@ -89,8 +86,15 @@ export type MethodFollowupFocus = Readonly<{
|
|
|
|
|
targetKind: string | null;
|
|
|
|
|
}>;
|
|
|
|
|
|
|
|
|
|
const DO_NOT_POLL = ["horary"] as const;
|
|
|
|
|
const DO_NOT_POLL = [] as const;
|
|
|
|
|
const NOT_IN_ROTATION = ["relocation"] as const;
|
|
|
|
|
const BLOCKING_COVERAGE_IDS = new Set<MethodFollowupId>([
|
|
|
|
|
"dasha_events",
|
|
|
|
|
"d9_relationship",
|
|
|
|
|
"d10_career",
|
|
|
|
|
"relatives",
|
|
|
|
|
"occupation",
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
function isConfirmedDated(item: MethodFollowupEvidence): boolean {
|
|
|
|
|
return item.status === "confirmed"
|
|
|
|
@@ -166,6 +170,7 @@ const USER_STOP_PATTERN = /暂时想不到了|没有更多|先这样/;
|
|
|
|
|
const NON_BLOCKING_OFFER_METHODS = new Set<MethodFollowup["method_id"]>([
|
|
|
|
|
"appearance",
|
|
|
|
|
"marks",
|
|
|
|
|
"horary",
|
|
|
|
|
"oos_blind",
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
@@ -182,7 +187,13 @@ export function latestUserStoppedCollecting(
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isOfferBlockingFollowup(followup: MethodFollowup | null): boolean {
|
|
|
|
|
export function isOfferBlockingFollowup(
|
|
|
|
|
followup: MethodFollowup | null,
|
|
|
|
|
methods?: readonly MethodCoverage[],
|
|
|
|
|
): boolean {
|
|
|
|
|
if (methods?.some((item) => BLOCKING_COVERAGE_IDS.has(item.method_id) && item.status === "uncovered")) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (!followup) return false;
|
|
|
|
|
return !NON_BLOCKING_OFFER_METHODS.has(followup.method_id);
|
|
|
|
|
}
|
|
|
|
@@ -192,13 +203,14 @@ export function conversationalSessionOutcome(input: {
|
|
|
|
|
proposeAllowed: boolean;
|
|
|
|
|
confirmationAllowed: boolean;
|
|
|
|
|
nextFollowup: MethodFollowup | null;
|
|
|
|
|
methods?: readonly MethodCoverage[];
|
|
|
|
|
userStopped?: boolean;
|
|
|
|
|
}): SessionOutcomeKind {
|
|
|
|
|
return sessionOutcomeFromGate({
|
|
|
|
|
selectionAllowed: input.selectionAllowed,
|
|
|
|
|
proposeAllowed: input.proposeAllowed,
|
|
|
|
|
confirmationAllowed: input.confirmationAllowed,
|
|
|
|
|
interviewOpen: isOfferBlockingFollowup(input.nextFollowup),
|
|
|
|
|
interviewOpen: isOfferBlockingFollowup(input.nextFollowup, input.methods),
|
|
|
|
|
userStopped: input.userStopped,
|
|
|
|
|
}).kind;
|
|
|
|
|
}
|
|
|
|
@@ -283,17 +295,23 @@ export function buildMethodFollowupPlan(input: {
|
|
|
|
|
const healthCovered = hasConfirmedHealth(input.evidence);
|
|
|
|
|
const appearanceCovered = hasConfirmedDomain(input.evidence, "appearance");
|
|
|
|
|
const marksCovered = hasConfirmedDomain(input.evidence, "marks");
|
|
|
|
|
const occupationCovered = hasConfirmedDomain(input.evidence, "occupation") || declined.has("occupation");
|
|
|
|
|
const horaryGiven = hasConfirmedDomain(input.evidence, "horary");
|
|
|
|
|
const horaryStatus: MethodCoverageStatus = horaryGiven
|
|
|
|
|
? "covered"
|
|
|
|
|
: declined.has("horary")
|
|
|
|
|
? "skipped_by_policy"
|
|
|
|
|
: "uncovered";
|
|
|
|
|
|
|
|
|
|
const methods: MethodCoverage[] = [
|
|
|
|
|
coverage("dasha_events", dashaCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("d9_relationship", relationshipCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("d10_career", careerCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("relatives", familyCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("d2_finance", financeCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("d30_health", healthCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("appearance", appearanceCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("marks", marksCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("horary", "skipped_by_policy"),
|
|
|
|
|
coverage("d9_relationship", relationshipCovered || declined.has("relationship") ? "covered" : "uncovered"),
|
|
|
|
|
coverage("d10_career", careerCovered || declined.has("career") ? "covered" : "uncovered"),
|
|
|
|
|
coverage("relatives", familyCovered || declined.has("family") ? "covered" : "uncovered"),
|
|
|
|
|
coverage("appearance", appearanceCovered || declined.has("appearance") ? "covered" : "uncovered"),
|
|
|
|
|
coverage("marks", marksCovered || declined.has("marks") ? "covered" : "uncovered"),
|
|
|
|
|
coverage("occupation", occupationCovered ? "covered" : "uncovered"),
|
|
|
|
|
coverage("horary", horaryStatus),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const sessionOutcome = input.sessionOutcome ?? "collect_evidence";
|
|
|
|
@@ -383,24 +401,44 @@ export function buildMethodFollowupPlan(input: {
|
|
|
|
|
user_prompt_hint: "可以先说一段记得大概时间的家人相关变化。同一件事会对照 D12 父母盘、D7 子女盘和 D3 兄弟盘。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (!financeCovered && !declined.has("finance")) {
|
|
|
|
|
} else if (!appearanceCovered && !declined.has("appearance")) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "d2_finance",
|
|
|
|
|
method_id: "appearance",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "finance_change",
|
|
|
|
|
domain: "finance",
|
|
|
|
|
kind_hint: "finance_change",
|
|
|
|
|
user_prompt_hint: "可以先说一段记得大概时间的收入、资产或财务变化。同一件事会对照 D2 和 D11。",
|
|
|
|
|
ask_theme: "appearance",
|
|
|
|
|
domain: "appearance",
|
|
|
|
|
kind_hint: "appearance_note",
|
|
|
|
|
user_prompt_hint: "外貌或体质有没有比较稳定的特点?如果记得某次明显变化的大概时间,也可以说。这只作辅助对照,不会当成主评分。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (!healthCovered && !declinedHealth(declined)) {
|
|
|
|
|
} else if (!marksCovered && !declined.has("marks")) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "d30_health",
|
|
|
|
|
method_id: "marks",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "health_pressure",
|
|
|
|
|
domain: "health_pressure",
|
|
|
|
|
kind_hint: "self_health_event",
|
|
|
|
|
user_prompt_hint: "可以先说一段记得大概时间的健康、事故或压力变化。这只对照健康压力主题盘,不是医学判断。",
|
|
|
|
|
ask_theme: "marks",
|
|
|
|
|
domain: "marks",
|
|
|
|
|
kind_hint: "birthmark_or_scar",
|
|
|
|
|
user_prompt_hint: "有没有胎记,或记得大概时间的疤痕、受伤?有日期的会进一宫辅助对照,不会当成主评分。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (!occupationCovered) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "occupation",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "occupation",
|
|
|
|
|
domain: "occupation",
|
|
|
|
|
kind_hint: "occupation_note",
|
|
|
|
|
user_prompt_hint: "你长期做什么工作?对照本命第 10 宫和 D10 这条线怎么应验就可以,不要贴事业类型标签。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (horaryStatus === "uncovered") {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "horary",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "horary",
|
|
|
|
|
domain: "horary",
|
|
|
|
|
kind_hint: "horary_query",
|
|
|
|
|
user_prompt_hint: "有没有第一次认真问起这件事的时间?有的话可以按那个时间观察占问盘;没有也不挡给出时间卡。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (stage === "d9_refine" && !declined.has("relationship")) {
|
|
|
|
@@ -443,26 +481,6 @@ export function buildMethodFollowupPlan(input: {
|
|
|
|
|
user_prompt_hint: "成就盘或学业盘仍会换升。可以再补一件记得大概时间的学业、考试或被委以责任的变化;同一件事会对照本命五宫、D5 和 D24,不要贴类型标签。",
|
|
|
|
|
source: "precision_stage",
|
|
|
|
|
});
|
|
|
|
|
} else if (!appearanceCovered && !declined.has("appearance")) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "appearance",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "appearance",
|
|
|
|
|
domain: "appearance",
|
|
|
|
|
kind_hint: "appearance_note",
|
|
|
|
|
user_prompt_hint: "外貌或体质有没有比较稳定的特点?如果记得某次明显变化的大概时间,也可以说。这只作辅助对照,不会当成主评分。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else if (!marksCovered && !declined.has("marks")) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "marks",
|
|
|
|
|
intent: "collect_method_evidence",
|
|
|
|
|
ask_theme: "marks",
|
|
|
|
|
domain: "marks",
|
|
|
|
|
kind_hint: "birthmark_or_scar",
|
|
|
|
|
user_prompt_hint: "有没有胎记,或记得大概时间的疤痕、受伤?有日期的会进一宫辅助对照,不会当成主评分。",
|
|
|
|
|
source: "method_coverage",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const d9 = input.observations?.find((item) => item.layer === "d9");
|
|
|
|
|
const d10 = input.observations?.find((item) => item.layer === "d10");
|
|
|
|
@@ -522,7 +540,7 @@ export function buildMethodFollowupPlan(input: {
|
|
|
|
|
user_prompt_hint: "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。同一件事会对照 D12、D7 和 D3。",
|
|
|
|
|
source: "varga_observation",
|
|
|
|
|
});
|
|
|
|
|
} else if (d11?.candidates_differ && !declined.has("finance")) {
|
|
|
|
|
} else if (d11?.candidates_differ && financeCovered && !declined.has("finance")) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "d2_finance",
|
|
|
|
|
intent: "distinguish_candidates",
|
|
|
|
@@ -532,7 +550,7 @@ export function buildMethodFollowupPlan(input: {
|
|
|
|
|
user_prompt_hint: "当前候选在财务主题上仍分不开,可以再补一件记得大概时间的收入、资产或财务变化。同一件事会对照 D2 和 D11。",
|
|
|
|
|
source: "varga_observation",
|
|
|
|
|
});
|
|
|
|
|
} else if (d30?.candidates_differ && !declinedHealth(declined)) {
|
|
|
|
|
} else if (d30?.candidates_differ && healthCovered && !declinedHealth(declined)) {
|
|
|
|
|
next = followup({
|
|
|
|
|
method_id: "d30_health",
|
|
|
|
|
intent: "distinguish_candidates",
|
|
|
|
|