Web was round-robinning missing domains and waiting to score until the user said they had no more events. Server follow-up now uses the eight-method plan, rescored snapshots stay candidates, and D9/D10 observations never become user labels. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
7.8 KiB
TypeScript
231 lines
7.8 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 (occupation folded in)
|
|
* 4. Relatives — confirmed family evidence
|
|
* 5. Appearance / constitution — never poll
|
|
* 6. Birthmarks / scars — never poll
|
|
* 7. Occupation / 10th house — folded into method 3
|
|
* 8. Horary — unsupported; skip
|
|
*/
|
|
|
|
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" | "active_focus";
|
|
intent: string;
|
|
ask_theme: "dated_event" | "relationship_style" | "career_style" | "family_event" | "active_focus";
|
|
domain: string | null;
|
|
kind_hint: string | null;
|
|
user_prompt_hint: string;
|
|
must_not_label: true;
|
|
source: "active_focus" | "method_coverage" | "varga_observation";
|
|
}>;
|
|
|
|
export type MethodFollowupPlan = Readonly<{
|
|
methods: readonly MethodCoverage[];
|
|
next_followup: MethodFollowup | null;
|
|
stop_domain_rotation: true;
|
|
do_not_poll: readonly ["appearance", "marks", "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 = ["appearance", "marks", "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 function buildMethodFollowupPlan(input: {
|
|
evidence: readonly MethodFollowupEvidence[];
|
|
activeFocus?: MethodFollowupFocus | null;
|
|
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
|
observations?: readonly InternalVargaObservation[];
|
|
}): 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 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", "skipped_by_policy"),
|
|
coverage("marks", "skipped_by_policy"),
|
|
coverage("horary", "skipped_by_policy"),
|
|
];
|
|
|
|
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",
|
|
}),
|
|
stop_domain_rotation: true,
|
|
do_not_poll: DO_NOT_POLL,
|
|
not_in_rotation: NOT_IN_ROTATION,
|
|
};
|
|
}
|
|
|
|
let next: MethodFollowup | null = 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 (!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: "可以先说一段记得大概时间的工作或事业变化。",
|
|
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 {
|
|
const d9 = input.observations?.find((item) => item.layer === "d9");
|
|
const d10 = input.observations?.find((item) => item.layer === "d10");
|
|
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: "当前候选在事业主题上仍分不开,可以再补一件记得大概时间的工作变化;不要描述类型标签。",
|
|
source: "varga_observation",
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
methods,
|
|
next_followup: next,
|
|
stop_domain_rotation: true,
|
|
do_not_poll: DO_NOT_POLL,
|
|
not_in_rotation: NOT_IN_ROTATION,
|
|
};
|
|
}
|