Dated family evidence now moves candidates via D12 and kin houses, career receipts expose both D1-10 and D10, and appearance/marks may be asked as auxiliary first-house scores. New cases bind Skill 10.0.4. Co-authored-by: Cursor <cursoragent@cursor.com>
458 lines
17 KiB
TypeScript
458 lines
17 KiB
TypeScript
/**
|
|
* Eight-method follow-up routing for birth-time rectification.
|
|
*
|
|
* Web used to round-robin SQL missing_evidence_categories (relocation /
|
|
* health / finance). Local skill asks by method layer. This plan is the
|
|
* server's next question. It still only produces candidates, never a
|
|
* confirmed unique minute.
|
|
*
|
|
* Policy map:
|
|
* 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
|
|
* 4. Relatives — confirmed family evidence (D12 + 六亲 houses)
|
|
* 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 — folded into method 3 (D10 + D1 10th)
|
|
* 8. Horary — unsupported; skip
|
|
*/
|
|
|
|
import type { SessionOutcomeKind } from "./confirmation-gate.ts";
|
|
import type {
|
|
NakshatraBoundary,
|
|
OosBlindPrompt,
|
|
PrecisionStageId,
|
|
} from "./refinement-packet";
|
|
import type { InternalVargaObservation } from "./varga-observations";
|
|
|
|
export const METHOD_FOLLOWUP_IDS = [
|
|
"dasha_events",
|
|
"d9_relationship",
|
|
"d10_career",
|
|
"relatives",
|
|
"appearance",
|
|
"marks",
|
|
"horary",
|
|
] as const;
|
|
|
|
export type MethodFollowupId = (typeof METHOD_FOLLOWUP_IDS)[number];
|
|
|
|
export type MethodCoverageStatus = "covered" | "uncovered" | "skipped_by_policy";
|
|
|
|
export type MethodCoverage = Readonly<{
|
|
method_id: MethodFollowupId;
|
|
status: MethodCoverageStatus;
|
|
}>;
|
|
|
|
export type MethodFollowup = Readonly<{
|
|
method_id: "dasha_events" | "d9_relationship" | "d10_career" | "relatives" | "appearance" | "marks" | "active_focus" | "nakshatra_boundary" | "oos_blind";
|
|
intent: string;
|
|
ask_theme: "dated_event" | "relationship_style" | "career_style" | "family_event" | "appearance" | "marks" | "active_focus" | "nakshatra_trait" | "oos_blind";
|
|
domain: string | null;
|
|
kind_hint: string | null;
|
|
user_prompt_hint: string;
|
|
must_not_label: true;
|
|
source: "active_focus" | "method_coverage" | "varga_observation" | "precision_stage" | "nakshatra_boundary" | "oos_blind";
|
|
}>;
|
|
|
|
export type MethodFollowupPlan = Readonly<{
|
|
methods: readonly MethodCoverage[];
|
|
next_followup: MethodFollowup | null;
|
|
deferred_followup: MethodFollowup | null;
|
|
session_outcome: SessionOutcomeKind;
|
|
stop_domain_rotation: true;
|
|
do_not_poll: readonly ["horary"];
|
|
not_in_rotation: readonly ["relocation", "finance", "health"];
|
|
}>;
|
|
|
|
export type MethodFollowupEvidence = Readonly<{
|
|
status: string;
|
|
domain: string;
|
|
datePrecision: string;
|
|
occurredFrom: string | null;
|
|
occurredTo: string | null;
|
|
}>;
|
|
|
|
export type MethodFollowupFocus = Readonly<{
|
|
intent: string;
|
|
targetDomain: string | null;
|
|
targetKind: string | null;
|
|
}>;
|
|
|
|
const DO_NOT_POLL = ["horary"] as const;
|
|
const NOT_IN_ROTATION = ["relocation", "finance", "health"] as const;
|
|
|
|
function isConfirmedDated(item: MethodFollowupEvidence): boolean {
|
|
return item.status === "confirmed"
|
|
&& item.datePrecision !== "unknown"
|
|
&& Boolean(item.occurredFrom || item.occurredTo);
|
|
}
|
|
|
|
function hasConfirmedDomain(evidence: readonly MethodFollowupEvidence[], domain: string): boolean {
|
|
return evidence.some((item) => item.status === "confirmed" && item.domain === domain);
|
|
}
|
|
|
|
function declinedDomains(
|
|
topics: readonly Readonly<Record<string, unknown>>[],
|
|
): Set<string> {
|
|
const domains = new Set<string>();
|
|
for (const topic of topics) {
|
|
const domain = typeof topic.target_domain === "string"
|
|
? topic.target_domain
|
|
: typeof topic.targetDomain === "string"
|
|
? topic.targetDomain
|
|
: null;
|
|
if (domain) domains.add(domain);
|
|
}
|
|
return domains;
|
|
}
|
|
|
|
function coverage(
|
|
methodId: MethodFollowupId,
|
|
status: MethodCoverageStatus,
|
|
): MethodCoverage {
|
|
return { method_id: methodId, status };
|
|
}
|
|
|
|
function followup(
|
|
input: Omit<MethodFollowup, "must_not_label">,
|
|
): MethodFollowup {
|
|
return { ...input, must_not_label: true };
|
|
}
|
|
|
|
export type NextUserActionId =
|
|
| "adopt_representative"
|
|
| "score_now"
|
|
| "record_stated_events"
|
|
| "ask_method_followup"
|
|
| "explain_current_window"
|
|
| "start_consultation";
|
|
|
|
export type NextUserAction = Readonly<{
|
|
id: NextUserActionId;
|
|
user_meaning: string;
|
|
on_user_stop: {
|
|
id: Exclude<NextUserActionId, "ask_method_followup">;
|
|
user_meaning: string;
|
|
};
|
|
}>;
|
|
|
|
function action(
|
|
id: NextUserAction["on_user_stop"]["id"],
|
|
user_meaning: string,
|
|
): NextUserAction["on_user_stop"] {
|
|
return { id, user_meaning };
|
|
}
|
|
|
|
export function conversationalSessionOutcome(input: {
|
|
selectionAllowed: boolean;
|
|
confirmationAllowed: boolean;
|
|
nextFollowup: MethodFollowup | null;
|
|
}): SessionOutcomeKind {
|
|
if (input.confirmationAllowed) return "awaiting_confirmation";
|
|
if (input.selectionAllowed && !input.nextFollowup) return "adopt_representative";
|
|
return "collect_evidence";
|
|
}
|
|
|
|
export function buildNextUserAction(input: {
|
|
scorableCount: number;
|
|
evidenceCount: number;
|
|
hasLatestResult: boolean;
|
|
selectionAllowed: boolean;
|
|
sessionOutcome: SessionOutcomeKind;
|
|
nextFollowup: MethodFollowup | null;
|
|
workingTime: string | null;
|
|
accepted?: boolean;
|
|
}): NextUserAction {
|
|
const working = input.workingTime
|
|
? `当前排盘时间是 ${input.workingTime}`
|
|
: "当前还没有可采用的校正时间";
|
|
if (input.accepted) {
|
|
const consult = action(
|
|
"start_consultation",
|
|
"代表性时间已写入当前排盘。请用户用这个时间看盘;不要声称已确认唯一分钟。盘外对照问句由界面展示,本轮不要再当必须补证据。",
|
|
);
|
|
return { id: consult.id, user_meaning: consult.user_meaning, on_user_stop: consult };
|
|
}
|
|
const adopt = action(
|
|
"adopt_representative",
|
|
"本轮已有代表性候选时间。说明还不能确认唯一分钟,请用户采用下方时间卡片;采用后才用该时间看盘。不要只说记下了以后再说。",
|
|
);
|
|
if (input.sessionOutcome === "awaiting_confirmation") {
|
|
return { id: adopt.id, user_meaning: adopt.user_meaning, on_user_stop: adopt };
|
|
}
|
|
if (
|
|
input.sessionOutcome === "adopt_representative"
|
|
|| (input.selectionAllowed && !input.nextFollowup)
|
|
) {
|
|
return { id: adopt.id, user_meaning: adopt.user_meaning, on_user_stop: adopt };
|
|
}
|
|
const scoreNow = action(
|
|
"score_now",
|
|
"已有可评分事件但还没有候选结果。本轮必须比较候选,不要只口头确认事件。",
|
|
);
|
|
if (input.scorableCount > 0 && !input.hasLatestResult) {
|
|
return { id: scoreNow.id, user_meaning: scoreNow.user_meaning, on_user_stop: scoreNow };
|
|
}
|
|
const record = action(
|
|
"record_stated_events",
|
|
"账本里还没有带日期事件。若用户已经说过带日期的经历,本轮必须用 batch 写入后再比较;不要只在口头上复述。",
|
|
);
|
|
if (input.evidenceCount === 0 && input.scorableCount === 0) {
|
|
return { id: record.id, user_meaning: record.user_meaning, on_user_stop: record };
|
|
}
|
|
const explain = action(
|
|
"explain_current_window",
|
|
`${working}。现有事件还不够给出可采用的代表性时间。用户说没有更多时,说明缺什么、可以以后再补,或先用当前填报时间去咨询看盘;不要只说会话会保留。`,
|
|
);
|
|
if (input.nextFollowup) {
|
|
return {
|
|
id: "ask_method_followup",
|
|
user_meaning: input.nextFollowup.user_prompt_hint,
|
|
on_user_stop: input.selectionAllowed ? adopt : explain,
|
|
};
|
|
}
|
|
return { id: explain.id, user_meaning: explain.user_meaning, on_user_stop: explain };
|
|
}
|
|
|
|
export function buildMethodFollowupPlan(input: {
|
|
evidence: readonly MethodFollowupEvidence[];
|
|
activeFocus?: MethodFollowupFocus | null;
|
|
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
|
observations?: readonly InternalVargaObservation[];
|
|
sessionOutcome?: SessionOutcomeKind;
|
|
precisionStage?: PrecisionStageId | null;
|
|
nakshatraBoundary?: NakshatraBoundary | null;
|
|
oosBlindPrompts?: readonly OosBlindPrompt[];
|
|
accepted?: boolean;
|
|
}): MethodFollowupPlan {
|
|
const declined = declinedDomains(input.declinedTopics ?? []);
|
|
const dashaCovered = input.evidence.some(isConfirmedDated);
|
|
const relationshipCovered = hasConfirmedDomain(input.evidence, "relationship");
|
|
const careerCovered = hasConfirmedDomain(input.evidence, "career");
|
|
const familyCovered = hasConfirmedDomain(input.evidence, "family");
|
|
|
|
const appearanceCovered = hasConfirmedDomain(input.evidence, "appearance");
|
|
const marksCovered = hasConfirmedDomain(input.evidence, "marks");
|
|
|
|
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("appearance", appearanceCovered ? "covered" : "uncovered"),
|
|
coverage("marks", marksCovered ? "covered" : "uncovered"),
|
|
coverage("horary", "skipped_by_policy"),
|
|
];
|
|
|
|
const sessionOutcome = input.sessionOutcome ?? "collect_evidence";
|
|
const focus = input.activeFocus ?? null;
|
|
if (focus) {
|
|
return {
|
|
methods,
|
|
next_followup: followup({
|
|
method_id: "active_focus",
|
|
intent: focus.intent || "active_focus",
|
|
ask_theme: "active_focus",
|
|
domain: focus.targetDomain,
|
|
kind_hint: focus.targetKind,
|
|
user_prompt_hint: "先承接当前服务器焦点,不要另开领域清单。",
|
|
source: "active_focus",
|
|
}),
|
|
deferred_followup: null,
|
|
session_outcome: sessionOutcome,
|
|
stop_domain_rotation: true,
|
|
do_not_poll: DO_NOT_POLL,
|
|
not_in_rotation: NOT_IN_ROTATION,
|
|
};
|
|
}
|
|
|
|
if (input.accepted) {
|
|
const oos = oosFollowup(input.oosBlindPrompts);
|
|
return {
|
|
methods,
|
|
next_followup: null,
|
|
deferred_followup: oos,
|
|
session_outcome: sessionOutcome,
|
|
stop_domain_rotation: true,
|
|
do_not_poll: DO_NOT_POLL,
|
|
not_in_rotation: NOT_IN_ROTATION,
|
|
};
|
|
}
|
|
|
|
let next: MethodFollowup | null = null;
|
|
const stage = input.precisionStage ?? null;
|
|
if (!dashaCovered) {
|
|
next = followup({
|
|
method_id: "dasha_events",
|
|
intent: "collect_method_evidence",
|
|
ask_theme: "dated_event",
|
|
domain: null,
|
|
kind_hint: null,
|
|
user_prompt_hint: "可以先从最容易想起的一件带大概时间的经历开始。",
|
|
source: "method_coverage",
|
|
});
|
|
} else if (stage === "lagna_frame") {
|
|
next = followup({
|
|
method_id: "dasha_events",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "dated_event",
|
|
domain: null,
|
|
kind_hint: null,
|
|
user_prompt_hint: "窗口里本命上升还可能落在两段。请再补一件记得大概时间的经历,用来分开这两段;不要用性格或类型标签来选。",
|
|
source: "precision_stage",
|
|
});
|
|
} else if (stage === "d9_refine" && !declined.has("relationship")) {
|
|
next = followup({
|
|
method_id: "d9_relationship",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "relationship_style",
|
|
domain: "relationship",
|
|
kind_hint: "relationship_change",
|
|
user_prompt_hint: "关系盘仍会换升。可以再补一件记得大概时间的感情或关系变化;不要描述星座或类型标签。",
|
|
source: "precision_stage",
|
|
});
|
|
} else if (stage === "d10_refine" && !declined.has("career")) {
|
|
next = followup({
|
|
method_id: "d10_career",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "career_style",
|
|
domain: "career",
|
|
kind_hint: "career_change",
|
|
user_prompt_hint: "事业盘仍会换升。可以再补一件记得大概时间的工作变化;同一件事会同时对照本命第 10 宫和 D10,不要描述类型标签。",
|
|
source: "precision_stage",
|
|
});
|
|
} else if (stage === "theme_refine" && !declined.has("family")) {
|
|
next = followup({
|
|
method_id: "relatives",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "family_event",
|
|
domain: "family",
|
|
kind_hint: "family_event",
|
|
user_prompt_hint: "若还想继续收窄,可以再补一件记得大概时间的家人变化;也可以先采用代表性时间。",
|
|
source: "precision_stage",
|
|
});
|
|
} else if (!relationshipCovered && !declined.has("relationship")) {
|
|
next = followup({
|
|
method_id: "d9_relationship",
|
|
intent: "collect_method_evidence",
|
|
ask_theme: "relationship_style",
|
|
domain: "relationship",
|
|
kind_hint: "relationship_start",
|
|
user_prompt_hint: "可以先说一段记得大概时间的感情或关系变化,不必描述对象星座或类型标签。",
|
|
source: "method_coverage",
|
|
});
|
|
} else if (!careerCovered && !declined.has("career")) {
|
|
next = followup({
|
|
method_id: "d10_career",
|
|
intent: "collect_method_evidence",
|
|
ask_theme: "career_style",
|
|
domain: "career",
|
|
kind_hint: "career_entry",
|
|
user_prompt_hint: "可以先说一段记得大概时间的工作或事业变化。同一件事会同时对照本命第 10 宫和 D10。",
|
|
source: "method_coverage",
|
|
});
|
|
} else if (!familyCovered && !declined.has("family")) {
|
|
next = followup({
|
|
method_id: "relatives",
|
|
intent: "collect_method_evidence",
|
|
ask_theme: "family_event",
|
|
domain: "family",
|
|
kind_hint: "family_event",
|
|
user_prompt_hint: "可以先说一段记得大概时间的家人相关变化。",
|
|
source: "method_coverage",
|
|
});
|
|
} 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");
|
|
const d12 = input.observations?.find((item) => item.layer === "d12");
|
|
if (d9?.candidates_differ && !declined.has("relationship")) {
|
|
next = followup({
|
|
method_id: "d9_relationship",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "relationship_style",
|
|
domain: "relationship",
|
|
kind_hint: "relationship_change",
|
|
user_prompt_hint: "当前候选在关系主题上仍分不开,可以再补一件记得大概时间的感情或关系变化;不要描述星座或类型标签。",
|
|
source: "varga_observation",
|
|
});
|
|
} else if (d10?.candidates_differ && !declined.has("career")) {
|
|
next = followup({
|
|
method_id: "d10_career",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "career_style",
|
|
domain: "career",
|
|
kind_hint: "career_change",
|
|
user_prompt_hint: "当前候选在事业主题上仍分不开,可以再补一件记得大概时间的工作变化;不要描述类型标签。同一件事会同时对照本命第 10 宫和 D10。",
|
|
source: "varga_observation",
|
|
});
|
|
} else if (d12?.candidates_differ && !declined.has("family")) {
|
|
next = followup({
|
|
method_id: "relatives",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "family_event",
|
|
domain: "family",
|
|
kind_hint: "family_event",
|
|
user_prompt_hint: "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。",
|
|
source: "varga_observation",
|
|
});
|
|
} else if (input.nakshatraBoundary?.near_boundary) {
|
|
next = followup({
|
|
method_id: "nakshatra_boundary",
|
|
intent: "distinguish_candidates",
|
|
ask_theme: "nakshatra_trait",
|
|
domain: null,
|
|
kind_hint: null,
|
|
user_prompt_hint: input.nakshatraBoundary.user_meaning
|
|
?? "升点靠近两段日常节奏的交界。哪一组更像你近年的处事方式?这只用来偏置时间窗,不能确认唯一分钟。",
|
|
source: "nakshatra_boundary",
|
|
});
|
|
}
|
|
}
|
|
|
|
const deferAdoption = sessionOutcome === "adopt_representative";
|
|
return {
|
|
methods,
|
|
next_followup: deferAdoption ? null : next,
|
|
deferred_followup: deferAdoption ? next : null,
|
|
session_outcome: sessionOutcome,
|
|
stop_domain_rotation: true,
|
|
do_not_poll: DO_NOT_POLL,
|
|
not_in_rotation: NOT_IN_ROTATION,
|
|
};
|
|
}
|
|
|
|
function oosFollowup(prompts: readonly OosBlindPrompt[] | undefined): MethodFollowup | null {
|
|
const prompt = prompts?.[0];
|
|
if (!prompt) return null;
|
|
return followup({
|
|
method_id: "oos_blind",
|
|
intent: "out_of_sample_check",
|
|
ask_theme: "oos_blind",
|
|
domain: prompt.domain,
|
|
kind_hint: null,
|
|
user_prompt_hint: prompt.user_meaning,
|
|
source: "oos_blind",
|
|
});
|
|
}
|