fix(rectification): split D4/D5 stages and recast after adopt
Keep family on D12+D7 instead of mixing it into D4, score D5 on education, and show a natal recast plus technique audit after adopt without unique-minute claims. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,9 @@ import {
|
||||
} from "@/lib/rectification-activity-receipt";
|
||||
import {
|
||||
isRecommendedRectificationCandidate,
|
||||
natalRecastMeaning,
|
||||
parseRectificationCandidateResult,
|
||||
workingRectificationHouseTable,
|
||||
type RectificationCandidateResult,
|
||||
} from "@/lib/rectification-candidate-result";
|
||||
import { membershipHref } from "@/lib/membership";
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
} from "./chat-message-actions";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { RectificationHouseTableView } from "./rectification-house-table";
|
||||
import { TechniqueAuditDisclosure } from "./technique-audit-disclosure";
|
||||
import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
|
||||
@@ -565,7 +568,30 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
if (!response.ok || payload?.ok !== true || payload?.status !== "accepted") {
|
||||
throw new Error(payload?.error || payload?.message || "暂时无法采用该候选时间");
|
||||
}
|
||||
setCandidateResult((current) => current ? { ...current, selectedTime: payload.saved_time, selectionKind: "user_accepted" } : current);
|
||||
setCandidateResult((current) => {
|
||||
if (!current) return current;
|
||||
const selectedTime = typeof payload.saved_time === "string" ? payload.saved_time.slice(0, 5) : current.selectedTime;
|
||||
const next = {
|
||||
...current,
|
||||
selectedTime,
|
||||
selectionKind: "user_accepted" as const,
|
||||
};
|
||||
const houseTable = workingRectificationHouseTable(next);
|
||||
const recastMeaning = natalRecastMeaning(houseTable);
|
||||
return {
|
||||
...next,
|
||||
houseTable: houseTable ?? current.houseTable,
|
||||
natalRecast: houseTable && recastMeaning
|
||||
? {
|
||||
time: houseTable.time,
|
||||
lagna: houseTable.lagna,
|
||||
user_meaning: recastMeaning,
|
||||
unique_minute_claim: false as const,
|
||||
confirmation_allowed: false as const,
|
||||
}
|
||||
: current.natalRecast,
|
||||
};
|
||||
});
|
||||
setSavedTime(payload.saved_time);
|
||||
setSavedStatus("accepted");
|
||||
onSaved?.(payload.saved_time, "accepted");
|
||||
@@ -722,7 +748,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
})}
|
||||
{candidateResult?.houseTable && (
|
||||
<div className="rectification-snapshot">
|
||||
{candidateResult.natalRecast && (
|
||||
<p className="rectification-refinement__stage">{candidateResult.natalRecast.user_meaning}</p>
|
||||
)}
|
||||
<RectificationHouseTableView table={candidateResult.houseTable} />
|
||||
{savedStatus === "accepted" && candidateResult.techniqueAudit.length > 0 && (
|
||||
<TechniqueAuditDisclosure rows={candidateResult.techniqueAudit} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{candidateResult && <RectificationRefinementPanel result={candidateResult} />}
|
||||
|
||||
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
|
||||
export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.4";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.5";
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 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)
|
||||
* 4. Relatives — confirmed family evidence (D12 + D7 + 六亲 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)
|
||||
@@ -45,9 +45,9 @@ export type MethodCoverage = Readonly<{
|
||||
}>;
|
||||
|
||||
export type MethodFollowup = Readonly<{
|
||||
method_id: "dasha_events" | "d9_relationship" | "d10_career" | "relatives" | "appearance" | "marks" | "active_focus" | "nakshatra_boundary" | "oos_blind";
|
||||
method_id: "dasha_events" | "d9_relationship" | "d10_career" | "d4_home" | "d5_education" | "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";
|
||||
ask_theme: "dated_event" | "relationship_style" | "career_style" | "home_change" | "education_style" | "family_event" | "appearance" | "marks" | "active_focus" | "nakshatra_trait" | "oos_blind";
|
||||
domain: string | null;
|
||||
kind_hint: string | null;
|
||||
user_prompt_hint: string;
|
||||
@@ -322,14 +322,24 @@ export function buildMethodFollowupPlan(input: {
|
||||
user_prompt_hint: "事业盘仍会换升。可以再补一件记得大概时间的工作变化;同一件事会同时对照本命第 10 宫和 D10,不要描述类型标签。",
|
||||
source: "precision_stage",
|
||||
});
|
||||
} else if (stage === "theme_refine" && !declined.has("family")) {
|
||||
} else if ((stage === "d4_refine" || stage === "theme_refine") && !declined.has("relocation")) {
|
||||
next = followup({
|
||||
method_id: "relatives",
|
||||
method_id: "d4_home",
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: "family_event",
|
||||
domain: "family",
|
||||
kind_hint: "family_event",
|
||||
user_prompt_hint: "若还想继续收窄,可以再补一件记得大概时间的家人变化;也可以先采用代表性时间。",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
kind_hint: "home_change",
|
||||
user_prompt_hint: "居所盘仍会换升。可以再补一件记得大概时间的搬家或住处变化。",
|
||||
source: "precision_stage",
|
||||
});
|
||||
} else if (stage === "d5_refine" && !declined.has("education")) {
|
||||
next = followup({
|
||||
method_id: "d5_education",
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: "education_style",
|
||||
domain: "education",
|
||||
kind_hint: "education_milestone",
|
||||
user_prompt_hint: "成就盘仍会换升。可以再补一件记得大概时间的学业、考试或被委以责任的变化;同一件事会对照本命五宫和 D5,不要贴类型标签。",
|
||||
source: "precision_stage",
|
||||
});
|
||||
} else if (!relationshipCovered && !declined.has("relationship")) {
|
||||
@@ -359,7 +369,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
ask_theme: "family_event",
|
||||
domain: "family",
|
||||
kind_hint: "family_event",
|
||||
user_prompt_hint: "可以先说一段记得大概时间的家人相关变化。",
|
||||
user_prompt_hint: "可以先说一段记得大概时间的家人相关变化。同一件事会对照 D12 父母盘和 D7 子女盘。",
|
||||
source: "method_coverage",
|
||||
});
|
||||
} else if (!appearanceCovered && !declined.has("appearance")) {
|
||||
@@ -385,6 +395,9 @@ export function buildMethodFollowupPlan(input: {
|
||||
} else {
|
||||
const d9 = input.observations?.find((item) => item.layer === "d9");
|
||||
const d10 = input.observations?.find((item) => item.layer === "d10");
|
||||
const d4 = input.observations?.find((item) => item.layer === "d4");
|
||||
const d5 = input.observations?.find((item) => item.layer === "d5");
|
||||
const d7 = input.observations?.find((item) => item.layer === "d7");
|
||||
const d12 = input.observations?.find((item) => item.layer === "d12");
|
||||
if (d9?.candidates_differ && !declined.has("relationship")) {
|
||||
next = followup({
|
||||
@@ -406,14 +419,34 @@ export function buildMethodFollowupPlan(input: {
|
||||
user_prompt_hint: "当前候选在事业主题上仍分不开,可以再补一件记得大概时间的工作变化;不要描述类型标签。同一件事会同时对照本命第 10 宫和 D10。",
|
||||
source: "varga_observation",
|
||||
});
|
||||
} else if (d12?.candidates_differ && !declined.has("family")) {
|
||||
} else if (d4?.candidates_differ && !declined.has("relocation")) {
|
||||
next = followup({
|
||||
method_id: "d4_home",
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
kind_hint: "home_change",
|
||||
user_prompt_hint: "当前候选在居所主题上仍分不开,可以再补一件记得大概时间的搬家或住处变化。",
|
||||
source: "varga_observation",
|
||||
});
|
||||
} else if (d5?.candidates_differ && !declined.has("education")) {
|
||||
next = followup({
|
||||
method_id: "d5_education",
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: "education_style",
|
||||
domain: "education",
|
||||
kind_hint: "education_milestone",
|
||||
user_prompt_hint: "当前候选在学业或成就主题上仍分不开,可以再补一件记得大概时间的学业、考试或被委以责任的变化;不要贴类型标签。",
|
||||
source: "varga_observation",
|
||||
});
|
||||
} else if ((d12?.candidates_differ || d7?.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: "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。",
|
||||
user_prompt_hint: "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。同一件事会对照 D12 和 D7。",
|
||||
source: "varga_observation",
|
||||
});
|
||||
} else if (input.nakshatraBoundary?.near_boundary) {
|
||||
|
||||
@@ -53,6 +53,8 @@ export const PUBLIC_RECTIFICATION_METHODS = [
|
||||
"d1-rashi",
|
||||
"d2-hora",
|
||||
"d4-chaturthamsha",
|
||||
"d5-panchamsha",
|
||||
"d7-saptamsha",
|
||||
"d9-navamsa",
|
||||
"d10-dashamsa",
|
||||
"d11-labhamsha",
|
||||
|
||||
@@ -13,10 +13,12 @@ const LAYER_LABEL: Record<WindowScanLayer, string> = {
|
||||
d9: "D9",
|
||||
d10: "D10",
|
||||
d4: "D4",
|
||||
d5: "D5",
|
||||
d7: "D7",
|
||||
d12: "D12",
|
||||
};
|
||||
|
||||
export type WindowScanLayer = "d1" | "d9" | "d10" | "d4" | "d12";
|
||||
export type WindowScanLayer = "d1" | "d9" | "d10" | "d4" | "d5" | "d7" | "d12";
|
||||
|
||||
export type WindowScanTransition = Readonly<{
|
||||
layer: WindowScanLayer;
|
||||
@@ -68,6 +70,8 @@ export type PrecisionStageId =
|
||||
| "lagna_frame"
|
||||
| "d9_refine"
|
||||
| "d10_refine"
|
||||
| "d4_refine"
|
||||
| "d5_refine"
|
||||
| "theme_refine"
|
||||
| "ready_to_adopt";
|
||||
|
||||
@@ -113,7 +117,7 @@ export function parseWindowScanTransitions(value: unknown): readonly WindowScanT
|
||||
const at = asTime(row?.at);
|
||||
if (
|
||||
!row
|
||||
|| (layer !== "d1" && layer !== "d9" && layer !== "d10" && layer !== "d4" && layer !== "d12")
|
||||
|| (layer !== "d1" && layer !== "d9" && layer !== "d10" && layer !== "d4" && layer !== "d5" && layer !== "d7" && layer !== "d12")
|
||||
|| !at
|
||||
) continue;
|
||||
const key = `${layer}:${at}`;
|
||||
@@ -252,6 +256,8 @@ export function parsePrecisionStage(value: unknown): PrecisionStage | null {
|
||||
&& current !== "lagna_frame"
|
||||
&& current !== "d9_refine"
|
||||
&& current !== "d10_refine"
|
||||
&& current !== "d4_refine"
|
||||
&& current !== "d5_refine"
|
||||
&& current !== "theme_refine"
|
||||
&& current !== "ready_to_adopt"
|
||||
)
|
||||
|
||||
@@ -19,19 +19,23 @@ export type WindowScan = Readonly<{
|
||||
d9_lagna_count: number;
|
||||
d10_lagna_count: number;
|
||||
d4_lagna_count: number;
|
||||
d5_lagna_count: number;
|
||||
d7_lagna_count: number;
|
||||
d12_lagna_count: number;
|
||||
d1_candidates_differ: boolean;
|
||||
d9_candidates_differ: boolean;
|
||||
d10_candidates_differ: boolean;
|
||||
d4_candidates_differ: boolean;
|
||||
d5_candidates_differ: boolean;
|
||||
d7_candidates_differ: boolean;
|
||||
d12_candidates_differ: boolean;
|
||||
transitions: readonly WindowScanTransition[];
|
||||
}>;
|
||||
|
||||
export type InternalVargaObservation = Readonly<{
|
||||
layer: "d9" | "d10" | "d12";
|
||||
layer: "d9" | "d10" | "d4" | "d5" | "d7" | "d12";
|
||||
candidates_differ: boolean;
|
||||
ask_theme: "relationship_style" | "career_style" | "family_event" | null;
|
||||
ask_theme: "relationship_style" | "career_style" | "home_change" | "education_style" | "family_event" | null;
|
||||
}>;
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
@@ -65,11 +69,15 @@ export function parseWindowScan(value: unknown): WindowScan | null {
|
||||
if (d9Count === null || d10Count === null) return null;
|
||||
const d1Count = optionalCount(row.d1_lagna_count);
|
||||
const d4Count = optionalCount(row.d4_lagna_count);
|
||||
const d5Count = optionalCount(row.d5_lagna_count);
|
||||
const d7Count = optionalCount(row.d7_lagna_count);
|
||||
const d12Count = optionalCount(row.d12_lagna_count);
|
||||
const d9Differ = row.d9_candidates_differ === true || d9Count > 1;
|
||||
const d10Differ = row.d10_candidates_differ === true || d10Count > 1;
|
||||
const d1Differ = row.d1_candidates_differ === true || d1Count > 1;
|
||||
const d4Differ = row.d4_candidates_differ === true || d4Count > 1;
|
||||
const d5Differ = row.d5_candidates_differ === true || d5Count > 1;
|
||||
const d7Differ = row.d7_candidates_differ === true || d7Count > 1;
|
||||
const d12Differ = row.d12_candidates_differ === true || d12Count > 1;
|
||||
return {
|
||||
scanned: true,
|
||||
@@ -79,11 +87,15 @@ export function parseWindowScan(value: unknown): WindowScan | null {
|
||||
d9_lagna_count: d9Count,
|
||||
d10_lagna_count: d10Count,
|
||||
d4_lagna_count: d4Count,
|
||||
d5_lagna_count: d5Count,
|
||||
d7_lagna_count: d7Count,
|
||||
d12_lagna_count: d12Count,
|
||||
d1_candidates_differ: d1Differ,
|
||||
d9_candidates_differ: d9Differ,
|
||||
d10_candidates_differ: d10Differ,
|
||||
d4_candidates_differ: d4Differ,
|
||||
d5_candidates_differ: d5Differ,
|
||||
d7_candidates_differ: d7Differ,
|
||||
d12_candidates_differ: d12Differ,
|
||||
transitions: parseWindowScanTransitions(row.transitions),
|
||||
};
|
||||
@@ -110,6 +122,21 @@ export function internalObservationsFromWindowScan(
|
||||
candidates_differ: scan.d10_candidates_differ,
|
||||
ask_theme: scan.d10_candidates_differ ? "career_style" : null,
|
||||
},
|
||||
{
|
||||
layer: "d4",
|
||||
candidates_differ: scan.d4_candidates_differ,
|
||||
ask_theme: scan.d4_candidates_differ ? "home_change" : null,
|
||||
},
|
||||
{
|
||||
layer: "d5",
|
||||
candidates_differ: scan.d5_candidates_differ,
|
||||
ask_theme: scan.d5_candidates_differ ? "education_style" : null,
|
||||
},
|
||||
{
|
||||
layer: "d7",
|
||||
candidates_differ: scan.d7_candidates_differ,
|
||||
ask_theme: scan.d7_candidates_differ ? "family_event" : null,
|
||||
},
|
||||
{
|
||||
layer: "d12",
|
||||
candidates_differ: scan.d12_candidates_differ,
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
refinementFromDecisionReceipt,
|
||||
} from "./rectification-agentic/v9/refinement-packet";
|
||||
import { parseWindowScan } from "./rectification-agentic/v9/varga-observations";
|
||||
import type { TechniqueAuditRow } from "./consultation-agent-events";
|
||||
import { normalizeTechniqueAuditRows } from "./consultation-technique-audit";
|
||||
|
||||
export type RectificationCandidate = Readonly<{
|
||||
candidateId: string;
|
||||
@@ -33,6 +35,14 @@ export type RectificationHouseTable = Readonly<{
|
||||
houses: readonly RectificationHouseRow[];
|
||||
}>;
|
||||
|
||||
export type RectificationNatalRecast = Readonly<{
|
||||
time: string;
|
||||
lagna: string;
|
||||
user_meaning: string;
|
||||
unique_minute_claim: false;
|
||||
confirmation_allowed: false;
|
||||
}>;
|
||||
|
||||
export type RectificationCandidateResult = Readonly<{
|
||||
resultId: string;
|
||||
candidates: readonly RectificationCandidate[];
|
||||
@@ -43,6 +53,9 @@ export type RectificationCandidateResult = Readonly<{
|
||||
selectedTime: string | null;
|
||||
selectionKind: string | null;
|
||||
houseTable: RectificationHouseTable | null;
|
||||
houseTablesByTime: Readonly<Record<string, RectificationHouseTable>>;
|
||||
natalRecast: RectificationNatalRecast | null;
|
||||
techniqueAudit: readonly TechniqueAuditRow[];
|
||||
windowTransitions: readonly WindowScanTransition[];
|
||||
eventDashaLedger: readonly EventDashaLedgerRow[];
|
||||
dashaAgreement: DashaAgreement | null;
|
||||
@@ -102,11 +115,68 @@ export function parseRectificationHouseTable(value: unknown): RectificationHouse
|
||||
return { time: tableTime, lagna, houses };
|
||||
}
|
||||
|
||||
function houseTableFromSnapshot(snapshot: Record<string, unknown>): RectificationHouseTable | null {
|
||||
return parseRectificationHouseTable(snapshot.houseTable)
|
||||
?? parseRectificationHouseTable(snapshot.house_table)
|
||||
?? parseRectificationHouseTable(record(snapshot.decisionReceipt)?.house_table)
|
||||
?? parseRectificationHouseTable(record(snapshot.decision_receipt)?.house_table);
|
||||
function houseTablesByTimeFromSnapshot(
|
||||
snapshot: Record<string, unknown>,
|
||||
receipt: Record<string, unknown> | null,
|
||||
): Record<string, RectificationHouseTable> {
|
||||
const raw = record(snapshot.houseTablesByTime)
|
||||
?? record(snapshot.house_tables_by_time)
|
||||
?? record(receipt?.house_tables_by_time);
|
||||
const tables: Record<string, RectificationHouseTable> = {};
|
||||
if (!raw) return tables;
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const clock = time(key);
|
||||
const table = parseRectificationHouseTable(value);
|
||||
if (!clock || !table) continue;
|
||||
tables[clock] = table;
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
function natalRecastFromSnapshot(
|
||||
snapshot: Record<string, unknown>,
|
||||
receipt: Record<string, unknown> | null,
|
||||
table: RectificationHouseTable | null,
|
||||
): RectificationNatalRecast | null {
|
||||
const raw = record(snapshot.natalRecast) ?? record(snapshot.natal_recast) ?? record(receipt?.natal_recast);
|
||||
const recastTime = table?.time ?? time(raw?.time);
|
||||
const lagna = table?.lagna || (typeof raw?.lagna === "string" ? raw.lagna.trim() : "") || "";
|
||||
if (!recastTime || !lagna || deniedHouseTable.test(lagna)) return null;
|
||||
const meaning = natalRecastMeaning({ time: recastTime, lagna })
|
||||
?? (typeof raw?.user_meaning === "string" && raw.user_meaning.trim() && !deniedHouseTable.test(raw.user_meaning)
|
||||
? raw.user_meaning.trim().slice(0, 220)
|
||||
: null);
|
||||
if (!meaning) return null;
|
||||
return {
|
||||
time: recastTime,
|
||||
lagna,
|
||||
user_meaning: meaning,
|
||||
unique_minute_claim: false,
|
||||
confirmation_allowed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function natalRecastMeaning(table: Pick<RectificationHouseTable, "time" | "lagna"> | null): string | null {
|
||||
if (!table) return null;
|
||||
return `本命宫位已按 ${table.time} 重算(上升 ${table.lagna})。下面是本轮实际执行的技法,不能当作唯一分钟确认。`;
|
||||
}
|
||||
|
||||
export function workingRectificationHouseTable(
|
||||
result: Pick<RectificationCandidateResult, "selectedTime" | "representativeTime" | "houseTable" | "houseTablesByTime">,
|
||||
): RectificationHouseTable | null {
|
||||
const wanted = result.selectedTime ?? result.representativeTime;
|
||||
if (wanted && result.houseTablesByTime[wanted]) return result.houseTablesByTime[wanted];
|
||||
return result.houseTable;
|
||||
}
|
||||
|
||||
function techniqueAuditFromSnapshot(
|
||||
snapshot: Record<string, unknown>,
|
||||
receipt: Record<string, unknown> | null,
|
||||
): readonly TechniqueAuditRow[] {
|
||||
return normalizeTechniqueAuditRows(snapshot.techniqueAudit)
|
||||
?? normalizeTechniqueAuditRows(snapshot.technique_audit_table)
|
||||
?? normalizeTechniqueAuditRows(receipt?.technique_audit_table)
|
||||
?? [];
|
||||
}
|
||||
|
||||
function receiptFromSnapshot(snapshot: Record<string, unknown>): Record<string, unknown> | null {
|
||||
@@ -139,6 +209,15 @@ export function parseRectificationCandidateResult(value: unknown): Rectification
|
||||
const receipt = receiptFromSnapshot(snapshot);
|
||||
const refinement = refinementFromDecisionReceipt(receipt);
|
||||
const windowScan = parseWindowScan(receipt?.window_scan) ?? parseWindowScan(snapshot.window_scan);
|
||||
const selectedTime = time(snapshot.selectedTime);
|
||||
const representativeTime = time(snapshot.representativeTime);
|
||||
const houseTablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt);
|
||||
const fallbackHouseTable = parseRectificationHouseTable(snapshot.houseTable)
|
||||
?? parseRectificationHouseTable(snapshot.house_table)
|
||||
?? parseRectificationHouseTable(receipt?.house_table);
|
||||
const houseTable = (selectedTime && houseTablesByTime[selectedTime])
|
||||
?? (representativeTime && houseTablesByTime[representativeTime])
|
||||
?? fallbackHouseTable;
|
||||
|
||||
return {
|
||||
resultId: snapshot.resultId,
|
||||
@@ -148,10 +227,13 @@ export function parseRectificationCandidateResult(value: unknown): Rectification
|
||||
: "low",
|
||||
selectionAllowed: snapshot.selectionAllowed === true,
|
||||
confirmationAllowed: snapshot.confirmationAllowed === true,
|
||||
representativeTime: time(snapshot.representativeTime),
|
||||
selectedTime: time(snapshot.selectedTime),
|
||||
representativeTime,
|
||||
selectedTime,
|
||||
selectionKind: text(snapshot.selectionKind),
|
||||
houseTable: houseTableFromSnapshot(snapshot),
|
||||
houseTable,
|
||||
houseTablesByTime,
|
||||
natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable),
|
||||
techniqueAudit: techniqueAuditFromSnapshot(snapshot, receipt),
|
||||
windowTransitions: windowScan?.transitions ?? parseWindowScanTransitions(snapshot.window_scan),
|
||||
eventDashaLedger: refinement.event_dasha_ledger,
|
||||
dashaAgreement: refinement.dasha_agreement,
|
||||
|
||||
@@ -4,6 +4,8 @@ export const PUBLIC_RECTIFICATION_METHOD_LABELS: Readonly<Record<PublicRectifica
|
||||
"d1-rashi": "D1 本命盘",
|
||||
"d2-hora": "D2 财帛分盘",
|
||||
"d4-chaturthamsha": "D4 迁移分盘",
|
||||
"d5-panchamsha": "D5 成就分盘",
|
||||
"d7-saptamsha": "D7 子女分盘",
|
||||
"d9-navamsa": "D9 婚姻分盘",
|
||||
"d10-dashamsa": "D10 事业分盘",
|
||||
"d11-labhamsha": "D11 收益分盘",
|
||||
|
||||
@@ -298,6 +298,12 @@ export function latestResultToolProjection(
|
||||
oos_blind_prompts: refinement.oos_blind_prompts,
|
||||
unique_minute_claim: false,
|
||||
...(houseTable ? { house_table: houseTable } : {}),
|
||||
...(typeof latest.decisionReceipt?.natal_recast === "object" && latest.decisionReceipt.natal_recast
|
||||
? { natal_recast: latest.decisionReceipt.natal_recast }
|
||||
: {}),
|
||||
...(Array.isArray(latest.decisionReceipt?.technique_audit_table)
|
||||
? { technique_audit_table: latest.decisionReceipt.technique_audit_table }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
begin;
|
||||
|
||||
create or replace function public.insert_agentic_rectification_tool_receipt(
|
||||
p_user_id uuid,
|
||||
p_case_id uuid,
|
||||
p_turn_id uuid,
|
||||
p_tool_name text,
|
||||
p_public_phase text,
|
||||
p_status text,
|
||||
p_input_fingerprint text,
|
||||
p_result_fingerprint text,
|
||||
p_engine_version text,
|
||||
p_safe_error_code text,
|
||||
p_executed_methods jsonb,
|
||||
p_attempt_id uuid
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_attempt public.agentic_rectification_run_attempts%rowtype;
|
||||
v_receipt_id uuid;
|
||||
begin
|
||||
if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
|
||||
or p_tool_name not in (
|
||||
'rectification-read-case', 'rectification-set-focus',
|
||||
'rectification-resolve-focus', 'rectification-record-evidence-batch',
|
||||
'rectification-propose-evidence', 'rectification-confirm-evidence',
|
||||
'rectification-revise-evidence', 'rectification-compare-candidates',
|
||||
'rectification-read-diagnostics', 'rectification-offer-candidates',
|
||||
'rectification-accept-candidate', 'rectification-confirm-birth-time',
|
||||
'rectification-close-case'
|
||||
)
|
||||
or p_public_phase not in (
|
||||
'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
|
||||
'intent.classified', 'evidence.proposed', 'evidence.confirmed',
|
||||
'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
|
||||
'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
|
||||
'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
|
||||
)
|
||||
or p_status not in ('started', 'completed', 'failed', 'skipped')
|
||||
or p_executed_methods is null or jsonb_typeof(p_executed_methods) <> 'array'
|
||||
or exists (
|
||||
select 1 from jsonb_array_elements_text(p_executed_methods) method(value)
|
||||
where method.value not in (
|
||||
'd1-rashi', 'd2-hora', 'd4-chaturthamsha', 'd5-panchamsha',
|
||||
'd7-saptamsha', 'd9-navamsa',
|
||||
'd10-dashamsa', 'd11-labhamsha', 'd12-dwadashamsha',
|
||||
'd24-chaturvimshamsha',
|
||||
'd30-trimshamsha', 'vimshottari-dasha', 'narayana-dasha',
|
||||
'gochara', 'ashtakavarga', 'shadbala', 'arudha-pada',
|
||||
'functional-benefic-malefic'
|
||||
)
|
||||
) then
|
||||
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from public.agentic_rectification_cases
|
||||
where id = p_case_id and user_id = p_user_id
|
||||
) then
|
||||
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from public.agentic_rectification_turns
|
||||
where id = p_turn_id and case_id = p_case_id
|
||||
) then
|
||||
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
select * into v_attempt
|
||||
from public.agentic_rectification_run_attempts
|
||||
where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
|
||||
for update;
|
||||
if not found then
|
||||
raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
if v_attempt.status <> 'started' then
|
||||
raise exception 'agentic_rectification_attempt_not_started' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
insert into public.agentic_rectification_tool_receipts (
|
||||
case_id, turn_id, attempt_id, tool_name, public_phase, status,
|
||||
input_fingerprint, result_fingerprint, engine_version, safe_error_code,
|
||||
executed_methods, completed_at
|
||||
) values (
|
||||
p_case_id, p_turn_id, p_attempt_id, p_tool_name, p_public_phase, p_status,
|
||||
p_input_fingerprint, p_result_fingerprint, p_engine_version, p_safe_error_code,
|
||||
p_executed_methods,
|
||||
case when p_status = 'completed' then pg_catalog.now() else null end
|
||||
) returning id into v_receipt_id;
|
||||
|
||||
return jsonb_build_object('receipt_id', v_receipt_id, 'attempt_id', p_attempt_id);
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.insert_agentic_rectification_tool_receipt(
|
||||
uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb, uuid
|
||||
) from public, anon, authenticated;
|
||||
grant execute on function public.insert_agentic_rectification_tool_receipt(
|
||||
uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb, uuid
|
||||
) to service_role;
|
||||
|
||||
commit;
|
||||
@@ -153,3 +153,53 @@ test("reads dasha ledger and change minutes without scores or D9 labels", () =>
|
||||
assert.doesNotMatch(result?.eventDashaLedger[0]?.user_meaning ?? "", /points/);
|
||||
});
|
||||
|
||||
function twelveHouses(sign: string, occupant?: string) {
|
||||
return Array.from({ length: 12 }, (_, index) => ({
|
||||
house: index + 1,
|
||||
sign,
|
||||
occupants: index === 0 && occupant ? [occupant] : [],
|
||||
}));
|
||||
}
|
||||
|
||||
test("adopted minute recasts the house table and keeps technique audit collapsed-safe", () => {
|
||||
const result = parseRectificationCandidateResult({
|
||||
...camelCaseSnapshot,
|
||||
representativeTime: "05:07",
|
||||
selectedTime: "05:08",
|
||||
decisionReceipt: {
|
||||
house_table: { time: "05:07", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
|
||||
house_tables_by_time: {
|
||||
"05:07": { time: "05:07", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
|
||||
"05:08": { time: "05:08", lagna: "双子座", houses: twelveHouses("双子座", "月亮") },
|
||||
},
|
||||
natal_recast: {
|
||||
time: "05:07",
|
||||
lagna: "金牛座",
|
||||
user_meaning: "本命宫位已按 05:07 重算(上升 金牛座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
|
||||
unique_minute_claim: false,
|
||||
confirmation_allowed: false,
|
||||
},
|
||||
technique_audit_table: [
|
||||
{ technique: "D1 本命盘", status: "executed", note: "本轮已按该分钟重算本命宫位。" },
|
||||
{ technique: "KP 宫头", status: "blocked", note: "KP 宫头本轮未计算。" },
|
||||
{ technique: "唯一分钟确认", status: "blocked", note: "采用不等于确认唯一分钟。" },
|
||||
],
|
||||
precision_stage: {
|
||||
current: "d4_refine",
|
||||
can_stop: true,
|
||||
user_meaning: "居所盘仍会换升。",
|
||||
unique_minute_claim: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(result?.houseTable?.time, "05:08");
|
||||
assert.equal(result?.houseTable?.lagna, "双子座");
|
||||
assert.equal(result?.houseTablesByTime["05:08"]?.lagna, "双子座");
|
||||
assert.equal(result?.natalRecast?.time, "05:08");
|
||||
assert.equal(result?.natalRecast?.lagna, "双子座");
|
||||
assert.equal(result?.natalRecast?.unique_minute_claim, false);
|
||||
assert.equal(result?.techniqueAudit.length, 3);
|
||||
assert.equal(result?.techniqueAudit[2]?.status, "blocked");
|
||||
assert.equal(result?.precisionStage?.current, "d4_refine");
|
||||
});
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(agentSource, /session_outcome=adopt_representative/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -255,6 +255,9 @@ test("D9 differ becomes an internal ask theme without sign labels", () => {
|
||||
assert.deepEqual(observations, [
|
||||
{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" },
|
||||
{ layer: "d10", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d4", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d5", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d7", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d12", candidates_differ: false, ask_theme: null },
|
||||
]);
|
||||
assert.doesNotMatch(JSON.stringify({ scan, observations }), FORBIDDEN_LABELS);
|
||||
@@ -501,9 +504,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 13 and new cases bind 10.0.4", () => {
|
||||
test("public tool surface stays at 13 and new cases bind 10.0.5", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -569,6 +572,41 @@ test("precision stage lagna_frame asks another dated event instead of rotating d
|
||||
assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS);
|
||||
});
|
||||
|
||||
test("precision stage d4 asks home change not family, and d5 asks education", () => {
|
||||
const d4 = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
],
|
||||
precisionStage: "d4_refine",
|
||||
});
|
||||
assert.equal(d4.next_followup?.source, "precision_stage");
|
||||
assert.equal(d4.next_followup?.method_id, "d4_home");
|
||||
assert.equal(d4.next_followup?.domain, "relocation");
|
||||
assert.equal(d4.next_followup?.ask_theme, "home_change");
|
||||
assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/);
|
||||
const legacyTheme = buildMethodFollowupPlan({
|
||||
evidence: d4.next_followup ? [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
] : [],
|
||||
precisionStage: "theme_refine",
|
||||
});
|
||||
assert.equal(legacyTheme.next_followup?.ask_theme, "home_change");
|
||||
const d5 = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
],
|
||||
precisionStage: "d5_refine",
|
||||
});
|
||||
assert.equal(d5.next_followup?.method_id, "d5_education");
|
||||
assert.equal(d5.next_followup?.domain, "education");
|
||||
assert.doesNotMatch(JSON.stringify(d4), FORBIDDEN_LABELS);
|
||||
});
|
||||
|
||||
test("accepted representative time hands off to consultation", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
|
||||
@@ -202,10 +202,20 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||||
});
|
||||
|
||||
test("new-case skill identity is 10.0.4 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.match(skill, /^version: 10\.0\.4$/m);
|
||||
test("new-case skill identity is 10.0.5 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
assert.match(skill, /^version: 10\.0\.5$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /当前轮新事件一律走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
});
|
||||
|
||||
test("receipt allowlist includes D5 and D7 public methods", () => {
|
||||
const d5d7Migration = readFileSync(
|
||||
new URL("../supabase/migrations/20260820020000_rectification_d5_d7_precision.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(d5d7Migration, /'d5-panchamsha'/);
|
||||
assert.match(d5d7Migration, /'d7-saptamsha'/);
|
||||
assert.match(d5d7Migration, /'d12-dwadashamsha'/);
|
||||
});
|
||||
|
||||
@@ -76,11 +76,11 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.4"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.5"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
|
||||
@@ -92,9 +92,9 @@ test("terminal transitions are one-way and evidence writes stop at terminal", ()
|
||||
|
||||
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.4$/m);
|
||||
assert.match(skill, /^version: 10\.0\.5$/m);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
assert.ok(content.length > 0, `${reference} must be non-empty`);
|
||||
|
||||
@@ -209,7 +209,7 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
session_id: SESSION_ID,
|
||||
status: "draft",
|
||||
should_start_opening: true,
|
||||
skill_version: "10.0.4",
|
||||
skill_version: "10.0.5",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -247,11 +247,11 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
});
|
||||
assert.equal(response.disposition, "created");
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
assert.equal(response.skillVersion, "10.0.4");
|
||||
assert.equal(response.skillVersion, "10.0.5");
|
||||
const openCall = accounting.calls.find((call) => call.fn === "open_agentic_rectification_case_v2");
|
||||
assert.ok(openCall);
|
||||
assert.equal(openCall.args.p_skill_name, "jyotish-birth-time-rectification");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.4");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.5");
|
||||
assert.equal(openCall.args.p_user_id, "user-1");
|
||||
// The server derives the baseline; the request never carries it from the browser.
|
||||
assert.equal("birth_date" in openCall.args, false);
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.4",
|
||||
sha256: "1a032b1af54d8ca593ad37da8dee116668b71524d37c7b77ec0e3e44f8c7624c",
|
||||
version: "10.0.5",
|
||||
sha256: "47cd353b5f7a08695007ce52d77560584300a1097ed97248f3a81372eeaca9c6",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
|
||||
Reference in New Issue
Block a user