From a93a3cc1a92d52322b00f5faa324c52004a8839b Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 20 Aug 2026 09:11:54 +0800 Subject: [PATCH] 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 --- docs/BUG_HISTORY.md | 16 ++ .../components/rectification-agentic-chat.tsx | 34 ++++- .../rectification-agentic/v9/case-status.ts | 2 +- .../v9/method-followup.ts | 57 ++++++-- .../v9/public-receipt.ts | 2 + .../v9/refinement-packet.ts | 10 +- .../v9/varga-observations.ts | 31 +++- .../src/lib/rectification-candidate-result.ts | 98 ++++++++++++- .../src/lib/rectification-varga-sentence.ts | 2 + frontend/src/mastra/rectification-v9-tools.ts | 6 + ...20020000_rectification_d5_d7_precision.sql | 105 ++++++++++++++ .../rectification-candidate-result.test.ts | 50 +++++++ .../rectification-confirmation-gate.test.ts | 2 +- .../tests/rectification-eight-method.test.ts | 42 +++++- .../tests/rectification-ingest-p0.test.ts | 16 +- frontend/tests/rectification-v9-agent.test.ts | 4 +- .../tests/rectification-v9-contracts.test.ts | 4 +- .../rectification-v9-entry-routing.test.ts | 6 +- frontend/tests/skill-registry.test.ts | 4 +- scripts/active_rectification_event_engine.py | 10 +- scripts/rectification/decision_policy.py | 111 +++++++++++++- scripts/rectification/refinement_packet.py | 27 +++- scripts/rectification/scoring_service.py | 8 +- scripts/rectification_technique_contract.py | 2 +- .../jyotish-birth-time-rectification/SKILL.md | 4 +- .../references/candidate-comparison.md | 1 + .../references/conversation-strategy.md | 2 +- .../references/evidence-model.md | 2 +- .../references/technique-routing.md | 8 +- .../versions/10.0.5/SKILL.md | 137 ++++++++++++++++++ .../10.0.5/references/candidate-comparison.md | 58 ++++++++ .../references/conversation-strategy.md | 105 ++++++++++++++ .../10.0.5/references/evidence-model.md | 116 +++++++++++++++ .../10.0.5/references/technique-routing.md | 49 +++++++ .../references/truth-consent-boundaries.md | 43 ++++++ skills/skill-package-registry.json | 8 + ...rectification_family_appearance_scoring.py | 10 +- tests/test_rectification_refinement_packet.py | 65 ++++++++- .../test_rectification_technique_contract.py | 2 +- tests/test_rectification_v5_services.py | 2 +- 40 files changed, 1195 insertions(+), 66 deletions(-) create mode 100644 frontend/supabase/migrations/20260820020000_rectification_d5_d7_precision.sql create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/SKILL.md create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/references/candidate-comparison.md create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/references/conversation-strategy.md create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/references/evidence-model.md create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/references/technique-routing.md create mode 100644 skills/jyotish-birth-time-rectification/versions/10.0.5/references/truth-consent-boundaries.md diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b659a01a..55f2efb5 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4762,3 +4762,19 @@ - 相关记录:BUG-291 - 复发自:无 - 修复版本:待提交 + +## BUG-317 | D4 精度阶段混问家人;D5/D7 不算分;采用后不重算也不给技法审计 + +- 状态:resolved +- 首次发现:2026-08-20 +- 最近更新:2026-08-20 +- 影响面:生时纠正精度阶段、家人/教育计分、采用后本命表、Skill `jyotish-birth-time-rectification@10.0.5` +- 用户现象:居所盘换升时却被问家人。子女/伴侣细节和学业成就问了也难改候选。采用某个分钟后,宫位表仍停在代表性时间,也看不到本轮技法执行/阻塞。 +- 触发条件:窗口扫描出现 D4 换升;写入家人或教育事件;或采用非代表性候选。 +- 根因:`theme_refine` 把 D4 与家人混成一问。引擎分盘不含 D5/D7,家人只计 D12。采用后界面不按采用分钟切宫位表,也不投影执行账本。 +- 修复:精度阶段改为本命上升 → D9 → D10 → D4 居所 → D5 成就。家人走 D12+D7 方法覆盖,不混进 D4。教育事件同时公开 D24 与 D5。采用后按该分钟重算本命宫位,并折叠展示技法审计;KP / VedAstro / 唯一分钟确认保持 blocked。新 Case 绑定 Skill 10.0.5。已有 10.0.4 Case 保持原绑定。`confirmation_allowed` 仍为 false。 +- 验证:`tests/test_rectification_refinement_packet.py`(D4 不再是 theme_refine;按候选分钟重算宫位;技法审计含 blocked 唯一分钟);`tests/test_rectification_family_appearance_scoring.py`(D7/D5 进入可用层);`frontend/tests/rectification-eight-method.test.ts`(d4 问搬家、d5 问学业);`frontend/tests/rectification-candidate-result.test.ts`(采用分钟切换宫位表)。 +- 防复发:D4 精度阶段不得再问家人。家人计分必须含 D7。采用后宫位表必须跟采用分钟走。技法审计不得把未执行层写成已执行,也不得声称唯一分钟。 +- 相关记录:BUG-316 +- 复发自:无 +- 修复版本:待提交 diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 7aefd018..a345ed0e 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -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 && (
+ {candidateResult.natalRecast && ( +

{candidateResult.natalRecast.user_meaning}

+ )} + {savedStatus === "accepted" && candidateResult.techniqueAudit.length > 0 && ( + + )}
)} {candidateResult && } diff --git a/frontend/src/lib/rectification-agentic/v9/case-status.ts b/frontend/src/lib/rectification-agentic/v9/case-status.ts index c1b8e04b..36b01cd6 100644 --- a/frontend/src/lib/rectification-agentic/v9/case-status.ts +++ b/frontend/src/lib/rectification-agentic/v9/case-status.ts @@ -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"; diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index f7924339..554f1195 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -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) { diff --git a/frontend/src/lib/rectification-agentic/v9/public-receipt.ts b/frontend/src/lib/rectification-agentic/v9/public-receipt.ts index 12a2f448..73659a4e 100644 --- a/frontend/src/lib/rectification-agentic/v9/public-receipt.ts +++ b/frontend/src/lib/rectification-agentic/v9/public-receipt.ts @@ -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", diff --git a/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts b/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts index 70e8c95f..8da17140 100644 --- a/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts +++ b/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts @@ -13,10 +13,12 @@ const LAYER_LABEL: Record = { 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" ) diff --git a/frontend/src/lib/rectification-agentic/v9/varga-observations.ts b/frontend/src/lib/rectification-agentic/v9/varga-observations.ts index d0529734..76e927ae 100644 --- a/frontend/src/lib/rectification-agentic/v9/varga-observations.ts +++ b/frontend/src/lib/rectification-agentic/v9/varga-observations.ts @@ -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> | 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, diff --git a/frontend/src/lib/rectification-candidate-result.ts b/frontend/src/lib/rectification-candidate-result.ts index c1dab020..35076310 100644 --- a/frontend/src/lib/rectification-candidate-result.ts +++ b/frontend/src/lib/rectification-candidate-result.ts @@ -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>; + 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): 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, + receipt: Record | null, +): Record { + const raw = record(snapshot.houseTablesByTime) + ?? record(snapshot.house_tables_by_time) + ?? record(receipt?.house_tables_by_time); + const tables: Record = {}; + 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, + receipt: Record | 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 | null): string | null { + if (!table) return null; + return `本命宫位已按 ${table.time} 重算(上升 ${table.lagna})。下面是本轮实际执行的技法,不能当作唯一分钟确认。`; +} + +export function workingRectificationHouseTable( + result: Pick, +): RectificationHouseTable | null { + const wanted = result.selectedTime ?? result.representativeTime; + if (wanted && result.houseTablesByTime[wanted]) return result.houseTablesByTime[wanted]; + return result.houseTable; +} + +function techniqueAuditFromSnapshot( + snapshot: Record, + receipt: Record | null, +): readonly TechniqueAuditRow[] { + return normalizeTechniqueAuditRows(snapshot.techniqueAudit) + ?? normalizeTechniqueAuditRows(snapshot.technique_audit_table) + ?? normalizeTechniqueAuditRows(receipt?.technique_audit_table) + ?? []; } function receiptFromSnapshot(snapshot: Record): Record | 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, diff --git a/frontend/src/lib/rectification-varga-sentence.ts b/frontend/src/lib/rectification-varga-sentence.ts index 08b5d4ea..cc6df527 100644 --- a/frontend/src/lib/rectification-varga-sentence.ts +++ b/frontend/src/lib/rectification-varga-sentence.ts @@ -4,6 +4,8 @@ export const PUBLIC_RECTIFICATION_METHOD_LABELS: Readonly '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; diff --git a/frontend/tests/rectification-candidate-result.test.ts b/frontend/tests/rectification-candidate-result.test.ts index 458677c6..03dc18b8 100644 --- a/frontend/tests/rectification-candidate-result.test.ts +++ b/frontend/tests/rectification-candidate-result.test.ts @@ -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"); +}); + diff --git a/frontend/tests/rectification-confirmation-gate.test.ts b/frontend/tests/rectification-confirmation-gate.test.ts index d8d70409..04f5aa11 100644 --- a/frontend/tests/rectification-confirmation-gate.test.ts +++ b/frontend/tests/rectification-confirmation-gate.test.ts @@ -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, diff --git a/frontend/tests/rectification-eight-method.test.ts b/frontend/tests/rectification-eight-method.test.ts index 1d7f0db2..e092d8dd 100644 --- a/frontend/tests/rectification-eight-method.test.ts +++ b/frontend/tests/rectification-eight-method.test.ts @@ -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: [{ diff --git a/frontend/tests/rectification-ingest-p0.test.ts b/frontend/tests/rectification-ingest-p0.test.ts index 448ee19d..f6f50c34 100644 --- a/frontend/tests/rectification-ingest-p0.test.ts +++ b/frontend/tests/rectification-ingest-p0.test.ts @@ -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'/); +}); diff --git a/frontend/tests/rectification-v9-agent.test.ts b/frontend/tests/rectification-v9-agent.test.ts index d5cd6058..7af45d1b 100644 --- a/frontend/tests/rectification-v9-agent.test.ts +++ b/frontend/tests/rectification-v9-agent.test.ts @@ -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", () => { diff --git a/frontend/tests/rectification-v9-contracts.test.ts b/frontend/tests/rectification-v9-contracts.test.ts index 79a180be..72cc6dec 100644 --- a/frontend/tests/rectification-v9-contracts.test.ts +++ b/frontend/tests/rectification-v9-contracts.test.ts @@ -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`); diff --git a/frontend/tests/rectification-v9-entry-routing.test.ts b/frontend/tests/rectification-v9-entry-routing.test.ts index d2cc4485..b7c1f2f9 100644 --- a/frontend/tests/rectification-v9-entry-routing.test.ts +++ b/frontend/tests/rectification-v9-entry-routing.test.ts @@ -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); diff --git a/frontend/tests/skill-registry.test.ts b/frontend/tests/skill-registry.test.ts index 260d4e0f..fa816bb4 100644 --- a/frontend/tests/skill-registry.test.ts +++ b/frontend/tests/skill-registry.test.ts @@ -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", diff --git a/scripts/active_rectification_event_engine.py b/scripts/active_rectification_event_engine.py index ac1cc5b9..f3d206eb 100644 --- a/scripts/active_rectification_event_engine.py +++ b/scripts/active_rectification_event_engine.py @@ -47,14 +47,14 @@ INPUT_CONTRACT_VERSION: Final = "rectification-candidate-input-v2" DomainConfig = tuple[tuple[str, ...], tuple[int, ...]] DOMAIN_CONFIG: Final[dict[EventDomain, DomainConfig]] = { - "education": (("D24",), (4, 5, 9)), + "education": (("D24", "D5"), (4, 5, 9)), "relocation": (("D4",), (4, 12)), "relationship": (("D9",), (7,)), "career": (("D10",), (10,)), "finance": (("D2", "D11"), (2, 11)), "health_pressure": (("D30",), (6, 8, 12)), - # 六亲: D12 parents plus D1 houses 3/4/5/9 (siblings, mother/home, children, father). - "family": (("D12",), (3, 4, 5, 9)), + # 六亲: D12 parents, D7 children/spouse detail, plus D1 houses 3/4/5/9. + "family": (("D12", "D7"), (3, 4, 5, 9)), # Dated appearance/marks: D1 lagna / 1st house only. Not a primary formula. "appearance": ((), (1,)), } @@ -369,12 +369,12 @@ def build_candidate_static_context( charts = varga.calc_all_vargas( planet_longitudes, ascendant_longitude, - divisions=[2, 4, 9, 10, 12, 24, 30], + divisions=[2, 4, 5, 7, 9, 10, 12, 24, 30], ) d11_chart = _d11_chart(planet_longitudes, ascendant_longitude) varga_charts = { prefix: d11_chart if prefix == "D11" else _varga_chart(charts, prefix) - for prefix in ("D2", "D4", "D9", "D10", "D11", "D12", "D24", "D30") + for prefix in ("D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30") } available_layers = ["D1"] blocked_layers = ["KP_cusps"] diff --git a/scripts/rectification/decision_policy.py b/scripts/rectification/decision_policy.py index 6cf51ca4..85bd7e14 100644 --- a/scripts/rectification/decision_policy.py +++ b/scripts/rectification/decision_policy.py @@ -40,6 +40,105 @@ def _decimal(value: Any, default: str = "0") -> Decimal: return Decimal(default) +_AUDIT_LABELS = { + "d1-rashi": ("D1 本命盘", "本轮已按该分钟重算本命宫位。"), + "d2-hora": ("D2 财帛分盘", "本轮已对照财帛主题。"), + "d4-chaturthamsha": ("D4 迁移分盘", "本轮已对照居所或迁移。"), + "d5-panchamsha": ("D5 成就分盘", "本轮已对照学业或被委以责任的变化。"), + "d7-saptamsha": ("D7 子女分盘", "本轮已对照子女或伴侣细节。"), + "d9-navamsa": ("D9 婚姻分盘", "本轮已对照关系主题,未给类型标签。"), + "d10-dashamsa": ("D10 事业分盘", "本轮已对照事业主题,未给类型标签。"), + "d11-labhamsha": ("D11 收益分盘", "本轮已对照收益主题。"), + "d12-dwadashamsha": ("D12 父母分盘", "本轮已对照家人主题。"), + "d24-chaturvimshamsha": ("D24 教育分盘", "本轮已对照学业主题。"), + "d30-trimshamsha": ("D30 健康压力分盘", "本轮已对照健康压力主题。"), + "vimshottari-dasha": ("Vimshottari", "本轮已对照主限。"), + "narayana-dasha": ("Narayana", "本轮已对照分盘大运。"), + "gochara": ("Gochara", "本轮已做受控行运辅助对照。"), + "ashtakavarga": ("Ashtakavarga", "本轮已做 Ashtakavarga 辅助对照。"), + "shadbala": ("Shadbala", "本轮已做已核验的 Shadbala 分量辅助对照。"), + "arudha-pada": ("Arudha Pada", "本轮已做 Arudha 辅助对照。"), + "functional-benefic-malefic": ("功能吉凶星", "本轮已叠加本命功能吉凶星。"), +} + + +def natal_recast_copy(time: str, lagna: str) -> dict[str, Any]: + return { + "time": time[:5], + "lagna": lagna, + "user_meaning": ( + f"本命宫位已按 {time[:5]} 重算(上升 {lagna})。" + "下面是本轮实际执行的技法,不能当作唯一分钟确认。" + ), + "unique_minute_claim": False, + "confirmation_allowed": False, + } + + +def _executed_public_methods(built: dict[str, Any]) -> list[str]: + methods: set[str] = set() + for contributions in (built.get("matrix") or {}).values(): + if not isinstance(contributions, dict): + continue + for cell in contributions.values(): + if not isinstance(cell, dict): + continue + for layer in cell.get("technique_layers") or []: + if layer in _AUDIT_LABELS: + methods.add(str(layer)) + for rule in cell.get("rule_ids") or []: + text = str(rule) + if text.startswith("vim_"): + methods.add("vimshottari-dasha") + elif text.startswith("narayana_"): + methods.add("narayana-dasha") + elif "functional_benefic" in text or "functional_malefic" in text: + methods.add("functional-benefic-malefic") + elif text.startswith("gochara") or "controlled_transit" in text: + methods.add("gochara") + elif "ashtakavarga" in text: + methods.add("ashtakavarga") + elif "shadbala" in text: + methods.add("shadbala") + elif "arudha" in text: + methods.add("arudha-pada") + return [key for key in _AUDIT_LABELS if key in methods] + + +def build_technique_audit( + built: dict[str, Any], + *, + house_table: dict[str, Any] | None, +) -> list[dict[str, str]]: + executed = set(_executed_public_methods(built)) + if house_table: + executed.add("d1-rashi") + rows: list[dict[str, str]] = [] + for method in _AUDIT_LABELS: + if method not in executed: + continue + label, note = _AUDIT_LABELS[method] + rows.append({"technique": label, "status": "executed", "note": note}) + rows.append({ + "technique": "KP 宫头", + "status": "blocked", + "note": "KP 宫头本轮未计算。", + }) + rows.extend(( + { + "technique": "VedAstro 分钟级校验", + "status": "blocked", + "note": "官方分钟级校验尚未评估。", + }, + { + "technique": "唯一分钟确认", + "status": "blocked", + "note": "采用不等于确认唯一分钟。", + }, + )) + return rows + + def _quantized_score(row: CandidateScoreRow) -> Decimal: return _decimal(row.get("score")).quantize(SCORE_QUANTUM, rounding=ROUND_HALF_UP) @@ -263,12 +362,22 @@ def build_decision_receipt( "exact_confirmation": exact_confirmation, }, } - house_table = compact_house_table_from_contexts( + house_tables_by_time: dict[str, dict[str, Any]] = {} + for decision in candidate_decisions: + table = compact_house_table_from_contexts(built.get("static_contexts"), decision.get("time")) + if table: + house_tables_by_time[table["time"]] = table + house_table = house_tables_by_time.get(representative["time"] if representative else "") or compact_house_table_from_contexts( built.get("static_contexts"), representative["time"] if representative else None, ) if house_table: receipt["house_table"] = house_table + recast = natal_recast_copy(house_table["time"], house_table["lagna"]) + receipt["natal_recast"] = recast + if house_tables_by_time: + receipt["house_tables_by_time"] = house_tables_by_time + receipt["technique_audit_table"] = build_technique_audit(built, house_table=house_table) receipt.update({ "window_scan": packet["window_scan"], "event_dasha_ledger": packet["event_dasha_ledger"], diff --git a/scripts/rectification/refinement_packet.py b/scripts/rectification/refinement_packet.py index e708a076..ce141dc3 100644 --- a/scripts/rectification/refinement_packet.py +++ b/scripts/rectification/refinement_packet.py @@ -113,11 +113,19 @@ def _split_track_points(rule_ids: Sequence[str], points: float) -> tuple[float, return points * vim / total, points * narayana / total -_LAYER_LABEL = {"d1": "本命上升", "d9": "D9", "d10": "D10", "d4": "D4", "d12": "D12"} +_LAYER_LABEL = { + "d1": "本命上升", + "d9": "D9", + "d10": "D10", + "d4": "D4", + "d5": "D5", + "d7": "D7", + "d12": "D12", +} def window_scan(built: dict[str, Any]) -> dict[str, Any]: - """D1/D9/D10/D4 diversity plus change minutes. Indices only; never sign names.""" + """D1/D9/D10/D4/D5/D7/D12 diversity plus change minutes. Indices only; never sign names.""" counts: dict[str, set[int]] = {layer: set() for layer in _LAYER_LABEL} transitions: list[dict[str, Any]] = [] previous: dict[str, int | None] | None = None @@ -128,6 +136,8 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]: "d9": vargas.get("D9") if isinstance(vargas.get("D9"), int) else None, "d10": vargas.get("D10") if isinstance(vargas.get("D10"), int) else None, "d4": vargas.get("D4") if isinstance(vargas.get("D4"), int) else None, + "d5": vargas.get("D5") if isinstance(vargas.get("D5"), int) else None, + "d7": vargas.get("D7") if isinstance(vargas.get("D7"), int) else None, "d12": vargas.get("D12") if isinstance(vargas.get("D12"), int) else None, } for layer, bucket in counts.items(): @@ -154,11 +164,15 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]: "d9_lagna_count": len(counts["d9"]), "d10_lagna_count": len(counts["d10"]), "d4_lagna_count": len(counts["d4"]), + "d5_lagna_count": len(counts["d5"]), + "d7_lagna_count": len(counts["d7"]), "d12_lagna_count": len(counts["d12"]), "d1_candidates_differ": len(counts["d1"]) > 1, "d9_candidates_differ": len(counts["d9"]) > 1, "d10_candidates_differ": len(counts["d10"]) > 1, "d4_candidates_differ": len(counts["d4"]) > 1, + "d5_candidates_differ": len(counts["d5"]) > 1, + "d7_candidates_differ": len(counts["d7"]) > 1, "d12_candidates_differ": len(counts["d12"]) > 1, "transitions": transitions, } @@ -360,14 +374,17 @@ def precision_stage(scan: dict[str, Any], event_count: int) -> dict[str, Any]: current = "d10_refine" meaning = "关系盘已较稳,事业盘仍会换升。可再补一件记得时间的工作变化。" elif scan.get("d4_candidates_differ"): - current = "theme_refine" - meaning = "核心分盘已较稳。若还想收窄,可再补一件记得时间的家人或住处变化;也可以先采用代表性时间。" + current = "d4_refine" + meaning = "事业盘已较稳,居所盘仍会换升。可再补一件记得时间的搬家或住处变化。" + elif scan.get("d5_candidates_differ"): + current = "d5_refine" + meaning = "居所盘已较稳,成就盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化;不要贴类型标签。" else: current = "ready_to_adopt" meaning = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。" return { "current": current, - "can_stop": current in {"d9_refine", "d10_refine", "theme_refine", "ready_to_adopt"}, + "can_stop": current in {"d9_refine", "d10_refine", "d4_refine", "d5_refine", "theme_refine", "ready_to_adopt"}, "user_meaning": meaning, "unique_minute_claim": False, } diff --git a/scripts/rectification/scoring_service.py b/scripts/rectification/scoring_service.py index c2682a6d..f88082ae 100644 --- a/scripts/rectification/scoring_service.py +++ b/scripts/rectification/scoring_service.py @@ -12,7 +12,7 @@ from scripts.active_rectification_event_engine import compute_candidate_static_c from scripts.active_rectification_events import CandidateScoreRow from scripts.rectification.contracts import LifeEvent, RectificationRequest, is_scoreable_event -ALGORITHM_VERSION = "rectification-v5-matrix-scoring-3" +ALGORITHM_VERSION = "rectification-v5-matrix-scoring-4" INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4" PRECISION_WEIGHTS = { "day": 1.0, @@ -184,7 +184,11 @@ def public_technique_layers(domain: str, rule_ids: Sequence[str]) -> list[str]: if domain == "career": layers.update({"d1-rashi", "d10-dashamsa"}) elif domain == "family": - layers.update({"d1-rashi", "d12-dwadashamsha"}) + layers.update({"d1-rashi", "d12-dwadashamsha", "d7-saptamsha"}) + elif domain == "education": + layers.update({"d1-rashi", "d24-chaturvimshamsha", "d5-panchamsha"}) + elif domain == "relocation": + layers.update({"d1-rashi", "d4-chaturthamsha"}) elif domain in {"appearance", "marks"}: layers.add("d1-rashi") return sorted(layers) diff --git a/scripts/rectification_technique_contract.py b/scripts/rectification_technique_contract.py index df5c8177..1151704c 100644 --- a/scripts/rectification_technique_contract.py +++ b/scripts/rectification_technique_contract.py @@ -80,7 +80,7 @@ def build_rectification_technique_contract( return { "schema_version": 2, "calculation_status": "not_started" if event_count == 0 else "evaluated", - "used_divisional_charts": ["D2", "D4", "D9", "D10", "D11", "D12", "D24", "D30"], + "used_divisional_charts": ["D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"], "used_arudha": ["A7", "UL", "A10"], "dasha_tracks": ["vimshottari_md_ad_pd", "narayana_md_ad"], "missing_layers": reported_missing_layers, diff --git a/skills/jyotish-birth-time-rectification/SKILL.md b/skills/jyotish-birth-time-rectification/SKILL.md index 44e5b510..de9fef18 100644 --- a/skills/jyotish-birth-time-rectification/SKILL.md +++ b/skills/jyotish-birth-time-rectification/SKILL.md @@ -1,6 +1,6 @@ --- name: jyotish-birth-time-rectification -version: 10.0.4 +version: 10.0.5 description: "生时校正专用 Skill(V10)。以服务器权威 Case、ConversationFocus 与 CaseConversationSummary 驱动低负担访谈;批量证据逐项判定,candidate / accepted / confirmed 严格分离,全部计算与持久化只走服务端工具。触发词:生时校正、出生时间校正、校正出生时间、rectification、birth time correction。" --- @@ -126,6 +126,8 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv - 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。 - 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。 - 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分,也不得给用户贴 D9/D10 星座或类型标签。有日期的外貌/疤痕只作上升/一宫辅助对照。`internal_observations` 只用于选择下一问主题。 +- 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5 成就收窄;家人走 D12/D7 方法覆盖,不得混进 D4。 +- 采用后本命按采用分钟重算并折叠展示技法审计;不得声称唯一分钟,也不自动进入咨询 Agent。 - 采用候选后只需自然说明 accepted 与 confirmed 边界;不强制下一问,不主动关闭 Case,Session 会保留并可日后继续。 - 不再有固定 10–15 个事件、固定 80%/60% 匹配率、外貌/体型/疤痕主评分、固定 A/B/C/D 问卷、D9/D10 类型表贴标签,或“稳定确定到精确分钟”的承诺。 - 无法验证时如实降级并说明受限,不得把内部一致性伪装成全球顶级精度。 diff --git a/skills/jyotish-birth-time-rectification/references/candidate-comparison.md b/skills/jyotish-birth-time-rectification/references/candidate-comparison.md index 84ea1e3e..9314c8d4 100644 --- a/skills/jyotish-birth-time-rectification/references/candidate-comparison.md +++ b/skills/jyotish-birth-time-rectification/references/candidate-comparison.md @@ -53,5 +53,6 @@ ## 6. 保存边界 - accepted 写入 `active_birth_time`,保留 `reported_birth_time` 原填报,不写兼容 `birth_time`。 +- 采用后界面按采用分钟重算本命宫位表,并折叠展示本轮技法审计。这不是唯一分钟确认,也不自动进入咨询 Agent。 - confirmed 同样保留原填报;不自动写入,需要用户明确同意。 - 失败、空流、Skill 未加载或未完成必要工具链时不保存、不扣费。 diff --git a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md index b3eb39c9..d64ea02d 100644 --- a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md +++ b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md @@ -73,7 +73,7 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服 追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级: 1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。 -2. `method_followup_plan.next_followup` 指定的下一方法层(有日期事件 → 关系 → 事业 → 家人 → 外貌/体质 → 胎记/疤痕)。占星占问不追问。外貌/疤痕可以问,但不得当主评分。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。仍有 `next_followup` 时即使 `selection_allowed` 也继续问。 +2. `method_followup_plan.next_followup` 指定的下一方法层。精度阶段优先于方法覆盖:有日期事件 → 关系 → 事业 → 居所(D4)→ 学业/成就(D5)→ 家人(D12/D7)→ 外貌/体质 → 胎记/疤痕。占星占问不追问。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。外貌/疤痕可以问,但不得当主评分。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。仍有 `next_followup` 时即使 `selection_allowed` 也继续问。 3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察只用于选题,不得说成用户星座或类型标签。 4. pending revision 的一个关键歧义。 5. 已有证据的必要稳定性补强。 diff --git a/skills/jyotish-birth-time-rectification/references/evidence-model.md b/skills/jyotish-birth-time-rectification/references/evidence-model.md index d0c00125..c3b9054d 100644 --- a/skills/jyotish-birth-time-rectification/references/evidence-model.md +++ b/skills/jyotish-birth-time-rectification/references/evidence-model.md @@ -111,6 +111,6 @@ draft / pending_confirmation -> rejected (用户否认,保留只读历史) ## 7. 评分输入边界 - 只有 `confirmed` 证据进入评分账本;`draft` 与 `pending_confirmation` 都不参与评分。 -- `family_event` 进入评分(D12 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。 +- `family_event` 进入评分(D12 + D7 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。 - `appearance_note` / `birthmark_or_scar`:无日期只覆盖访谈;有日期才进上升/一宫辅助评分,不得当主公式。 - 证据变化才触发重算;相同证据指纹复用缓存,不重复评分。 diff --git a/skills/jyotish-birth-time-rectification/references/technique-routing.md b/skills/jyotish-birth-time-rectification/references/technique-routing.md index 9736edaf..189f0319 100644 --- a/skills/jyotish-birth-time-rectification/references/technique-routing.md +++ b/skills/jyotish-birth-time-rectification/references/technique-routing.md @@ -26,11 +26,13 @@ - 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。 - 财富:`D2 / D11`。 - 婚恋:`D9 + UL`(UL 为 Upapada Lagna,服务器可用时)。 -- 六亲/家人:`D12` 加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。 +- 六亲/家人:`D12` 加 `D7`(子女/伴侣细节)加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。 - 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分,也不得发明星座或类型标签。无日期的回答只覆盖访谈,不进主公式。 - 健康:D1 + 必要时 D30(后置)。 -- 迁居/教育:D4 / D24。 -- D9/D10 类型表只作内部观察,不得给用户贴标签。`internal_observations.ask_theme` 只决定下一问是关系、事业还是家人经历,不得说出星座、配偶类型或事业特质。 +- 迁居:精度阶段 `d4_refine` 问带日期的搬家/住处变化;这不是领域轮询。计分 D4 + D1 四/十二宫。 +- 教育/成就:精度阶段 `d5_refine` 问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。不得贴类型标签。 +- 精度阶段顺序:有日期事件 → 本命上升 → D9 → D10 → D4 → D5 → 可采用。家人走方法覆盖与 D12/D7 观察,不得混进 D4。 +- D9/D10 类型表只作内部观察,不得给用户贴标签。`internal_observations.ask_theme` 只决定下一问主题,不得说出星座、配偶类型或事业特质。 ## 4. 受限技法边界 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/SKILL.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/SKILL.md new file mode 100644 index 00000000..de9fef18 --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/SKILL.md @@ -0,0 +1,137 @@ +--- +name: jyotish-birth-time-rectification +version: 10.0.5 +description: "生时校正专用 Skill(V10)。以服务器权威 Case、ConversationFocus 与 CaseConversationSummary 驱动低负担访谈;批量证据逐项判定,candidate / accepted / confirmed 严格分离,全部计算与持久化只走服务端工具。触发词:生时校正、出生时间校正、校正出生时间、rectification、birth time correction。" +--- + +# Jyotish 生时校正(V10) + +## 1. 触发条件与方法学归属 + +本 Skill 只服务 `agentic_rectification_cases` 绑定的生时校正会话: + +- 服务端 Case 存在且 `skill_name = 'jyotish-birth-time-rectification'`。 +- 用户话题是出生时间 / 出生分钟 / 事件发生时间能否定位到某几分钟,而不是普通解盘或推运。 +- 普通咨询、推运、合盘、补救问题交给 `jyotish-vedic-astrology`,不要在这里处理。 + +生时校正的方法学、访谈策略、证据边界与候选表达规则只定义在本 Skill 及其 references。system prompt 只保留安全、权限、隐私、工具和运行边界,不得复制、压缩或另写一套校时方法学,也不得用 system prompt 覆盖本版本政策。 + +## 2. 必须先读与服务器权威 + +进入任何一轮实质工作前读取(服务器会随 Dossier 提供投影,缺文件时以服务器 Dossier 为准): + +1. `references/evidence-model.md`:证据种类、日期精度、原文引用、修订链、服务器持有 ID。 +2. `references/conversation-strategy.md`:OpeningPolicy、ConversationFocus、长会话记忆、批量证据与追问策略。 +3. `references/candidate-comparison.md`:candidate / accepted / confirmed 三层语义与表达边界。 +4. `references/technique-routing.md`:技法按主题调用,D9/D10 核心,不一次性调用所有分盘。 +5. `references/truth-consent-boundaries.md`:真实性、同意与选择政策。 + +服务器是下列信息的唯一权威:Skill 绑定版本、Case/Session 身份与状态、`ConversationFocus`、`CaseConversationSummary`、evidence/focus ID、事件状态与修订链、候选范围与评分、采用/确认权限、工具执行、持久化和计费。Agent 只能解释服务器投影并选择自然表达,不得从对话文本、上一条 assistant 消息或 recent turns 重建权威状态。 + +每次 attempt 必须先完成真实 Skill 绑定和 Case 加载,之后才能执行 action。失败或重试 attempt 的部分文本、工具结果与推断不得当作已提交事实;只依据服务器提交成功的 attempt 与 receipt。 + +## 3. Case 状态与只读边界 + +服务器 Dossier 会给出当前 `status`。按表行动: + +| status | 允许动作 | +|---|---| +| `draft` / `collecting_evidence` | 继续收集/修订带日期事件;可读取诊断。`next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间,**不得**同时追问;仍有 `next_followup` 时继续收集,**不得**提供候选。`selection_allowed` 不够作为出示卡片的理由 | +| `candidate_ready` | 可比较候选、说明当前边界;仍可继续补证据 | +| `candidate_accepted` | 已采用候选,但**不等于**唯一分钟确认;可继续补证据或进入确认门 | +| `needs_rebaseline` | 出生资料基线已变化,候选失效;只允许重新收集/修订事件,禁止引用旧候选 | +| `paused` | 可继续访谈;不要声称结束 | +| `confirmed` / `closed` / `abandoned` / `superseded` | terminal Case,只读历史;不得追加/修订/确认证据,不得采用/确认候选,不得关闭第二次 | + +- terminal Case 的只读限制由服务器强制;Agent 不得用换工具、换措辞、重试或旧 focus 绕过。用户要继续校正时,说明需要走显式新建 Case 的入口。 +- 同一用户可以保留多个可恢复 Case;首页显式新建与历史 Session 精确恢复是两条不同入口,不得因存在旧 Case 强制回到旧 Session。 +- 历史 Session 必须恢复对应的精确 Case/Session;不得把另一个 resumable Case 的上下文混入当前会话。 + +## 4. OpeningPolicy + +服务端首次只提供 opening brief:Case 状态、出生时间不确定类型、已有证据摘要、当前可询问范围。Agent 根据 brief 自然开场,不得固定复述身份、完整流程、领域清单或要求用户先准备一套材料。 + +开场必须满足: + +- 降低回忆负担:从用户最容易想起的一件经历或当前最自然的入口开始,不要求列出固定数量事件。 +- 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。 +- 不要求一次说完:明确或自然体现可以分多轮补充、修正或换方向。 +- 至多一个主问题:开场可以没有问题;有问题时只问一个最容易回答、最有信息增益的问题。 +- 不机械复述 opening brief,不泄露服务器字段、内部状态对象或出生资料明文。 + +## 5. ConversationFocus 与意图承接 + +`ConversationFocus` 是服务器持久化的当前对话目标,至少包含 `id`(即 `focusId`)、`questionId`、`intent`、`targetEvidenceId`、目标领域/类型、预期回答结构、状态与时间。Agent 可做意图分类,但服务器必须验证目标仍为 `active`。 + +- “是的 / 不是 / 大概那年 / 后来改了 / 不记得 / 不想回答 / 换个方向”等承接、拒答、确认和修订,必须依赖服务器给出的 active focus。 +- 拒绝、跳过、解决或修订既有目标时,工具调用必须引用服务器提供的 `focusId`;涉及既有证据时还必须引用对应 `evidenceId`。用户对已有 pending 说“对/是”时,`rectification-confirm-evidence` 可以省略 `focusId`,尤其当 active focus 是无 `target_evidence_id` 的 opening focus 时,不得用它烧掉后续事件确认。 +- 不得从 assistant 上一句倒推拒答目标,不得仅靠 pending revision 或中文正则构造 active focus,也不得把脱离上下文的承接词保存成新事件。 +- 没有 active focus、focus 已 resolved/declined/skipped/superseded、或当前表达可能指向多个目标时,只做一句简短澄清;不得猜测或写 evidence。 +- 当前轮用户主动、明确、无歧义地提出全新事件时,可按新事件处理;若需要后续问题,由服务器建立新的 focus。 +- 用户已拒绝或跳过的目标不得换词重问;只有用户主动重开该主题或服务器建立新的有效 focus 才可继续。 + +## 6. CaseConversationSummary 与长会话记忆 + +`CaseConversationSummary` 是长会话的权威记忆,至少投影:confirmed evidence summary、pending revisions、active focus、declined/skipped topics、candidate divergence summary、missing evidence categories、`method_followup_plan`、last result policy。 + +- 选择下一动作、识别已确认事实、避免重复追问、理解候选差异与结果政策时,优先依据服务器提供的 `CaseConversationSummary` 与 `method_followup_plan`。 +- 不要按 `missing_evidence_categories` 轮询迁居 / 健康 / 财务。下一问只跟 `method_followup_plan.next_followup`。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,本轮零追问;`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。 +- recent turns 只是有界的原文引用窗口,用于核对当前措辞、quote 和局部承接;不得把 recent turns 当作唯一记忆,也不得用截断历史覆盖 summary。 +- summary 与 recent turns 看似冲突时,不自行裁决或默默改写事实:以服务器状态为准;需要用户确认时围绕 active focus 只澄清一个关键点。 +- 超过长会话窗口后仍不得忘记已确认证据、pending revision、拒答主题或 active focus。 + +## 7. 批量证据与日期真实性 + +一次用户消息可包含多件事件。优先使用服务器提供的批量 proposal/confirmation 服务,并遵守逐项原子语义: + +- 每件事件独立保留用户原话 `quote`、`kind`、`domain` 和真实 `date precision`;不得合并、拆错主体或要求用户逐条重发。 +- 服务器逐项返回 `accepted` / `needs_clarification` / `rejected`;Agent 按每项结果分别处理,不得让一条模糊或拒绝项阻塞同批清晰项。 +- 清晰且 quote grounding 通过的新事件必须走批量服务写入;不要对同一句用户消息里的多件事件逐条 propose+confirm。`rectification-confirm-evidence` 只用于用户对已有 pending 明确说“对/是”。 +- 证据有效写入后,服务器会按当前账本重算候选。不要等用户说“没有更多了”才 compare;同一证据指纹不要再 compare。不要调用新的扫描工具。 +- 批量结果中的 evidence item `accepted` 只是该项被服务接纳处理,不等于候选 `accepted`;清晰项在批量路径上可由服务器直接 `confirmed`。 +- 复述任何事件日期必须使用服务器 `display_date_label`。日级不得说成“年份已确定为 YYYY”。用户确认“是/对”不得改 `date_precision`。 +- `needs_clarification` 不得猜补日期、主体、事件身份、主动/被动、原因或人物关系;`rejected` 不得伪装成已记录。 +- 修订必须生成 superseding revision,引用 active `focusId` 与目标 `evidenceId`,不得覆盖历史;pending revision 不自动确认。 +- 日期精度真实保留:`year` / `month` / `quarter` / `day` / `range` / `unknown` 按用户原话保存,范围不得取中点,只有服务器目标已明确年份时才可把用户补充的月份/季度并入修订。 +- 批量服务与单项工具都必须依赖服务器幂等键;重试不得重复创建或确认 evidence。Agent 不自行生成 evidence/focus ID。 + +## 8. 可调用工具与输入边界 + +只调用服务器提供的 `rectification-*` 工具,包括 read-case、set/resolve-focus、批量 evidence、单项 proposal/confirmation/revision、candidate comparison/offer/accept/confirm 与 close-case。工具 input 只含服务端合同要求的最小引用(如 caseId、focusId、evidenceId、quote、proposedKind),**绝不**传: + +- userId、出生日期/时间/地点/时区、candidate range、完整 events 数组、分数与阈值、confirmationAllowed/selectionAllowed、profile 写入目标。 + +工具结果只读取;事实、ID、评分、范围、状态、持久化、幂等与权限一律以服务器为准。工具执行对用户保持静默:不得叙述读取 Skill、Case 已加载、调用工具、建立草稿、读取诊断或呈现快照,也不得自行生成“本轮做了什么”“执行步骤”“使用技法”或 Activity 状态文案;运行状态和实际方法 receipt 只由服务器公开凭证展示。 + +## 9. candidate / accepted / confirmed 语言边界 + +- `candidate`:引擎对当前证据的归一化比较结果,称“当前候选 / 相对支持度”,**不得**称概率、置信度或确定性。 +- `accepted`:用户明确选择的当前排盘时间,称“校正采用时间”,**不得**称“已确认唯一出生时间”。 +- `confirmed`:通过服务器确认门且用户明确同意,称“已确认校正时间”。 +- `session_outcome=adopt_representative` / `next_user_action.id=adopt_representative`:本轮**有结果**,结果是采用代表性时间作当前排盘。正文必须说还不能确认唯一分钟。不要调用 confirm。只有这时才调用 `rectification-offer-candidates`。`collecting_evidence` 且仍有 `next_followup` 时不得 offer/accept。 +- 确认门以 `latest_result.confirmation_gate` 为准。任一 blocker 未通过时只能说还不能确认;用户仍可 accepted 代表性候选。 +- `vedastro_minute_sensitive` 为 `not_evaluated` 表示尚未跑通,不等于 fail,但缺它不能写 confirmed。 +- `public_aa_holdout` 为 `not_ready` 时不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。 +- 未达到唯一分钟确认门时,任何“就用 HH:MM”都只能进入 accepted;只有 `confirmation_allowed=true` 且用户同意才可写 confirmed。 +- 若不可分 blocker 为 `blocked`、宽度大于 5、top `tied_minute_count` > 1,或 `confirmation_allowed=false`,正文必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。 +- 分钟窗口扫描只在服务端;结果进入候选卡 / 平台语言。不得把「几件事件」说成已确定到 ±5 分钟。 +- 候选卡负责候选时间、排名、相对支持度、采用动作和选中状态;正文只解释当前意义与不确定性,不重复候选表、编号菜单或卡片数字。 +- 不得在同一回复中一边要求继续补证据、一边提供采用候选。 +- 不得伪造出生分钟、分数、权重、事件 ID、分盘事实或确认门结果。 + +## 10. 输出与停止条件 + +- 简体中文,自然对话;不固定以“收到 / 已记录”开头,不机械复读,不擅自解释事件的“人生意义”,不推断用户未陈述的动机、心理或因果关系。 +- 每轮最多一个主要问题;完整回复可以零问题,不为了延续对话强行追问,不生成三条推荐问题。 +- 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。 +- 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。 +- 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分,也不得给用户贴 D9/D10 星座或类型标签。有日期的外貌/疤痕只作上升/一宫辅助对照。`internal_observations` 只用于选择下一问主题。 +- 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5 成就收窄;家人走 D12/D7 方法覆盖,不得混进 D4。 +- 采用后本命按采用分钟重算并折叠展示技法审计;不得声称唯一分钟,也不自动进入咨询 Agent。 +- 采用候选后只需自然说明 accepted 与 confirmed 边界;不强制下一问,不主动关闭 Case,Session 会保留并可日后继续。 +- 不再有固定 10–15 个事件、固定 80%/60% 匹配率、外貌/体型/疤痕主评分、固定 A/B/C/D 问卷、D9/D10 类型表贴标签,或“稳定确定到精确分钟”的承诺。 +- 无法验证时如实降级并说明受限,不得把内部一致性伪装成全球顶级精度。 + +## 11. 上游同步边界 + +方法源只在本 Skill 与 references。不得把本 Skill 内容反向写回 `yinduzhanxing` 上游快照,也不得在同步时自动覆盖商业 Skill。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/references/candidate-comparison.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/candidate-comparison.md new file mode 100644 index 00000000..9314c8d4 --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/candidate-comparison.md @@ -0,0 +1,58 @@ +# Candidate Comparison(V9) + +候选比较是服务器计算产物,Agent 只负责解释与引导,不负责产生候选、分数或范围。 + +## 1. 三层语义 + +| 层 | 含义 | 表达 | +|---|---|---| +| `candidate` | 引擎对当前证据的归一化比较结果 | “当前候选”“相对支持度” | +| `accepted` | 用户明确选择的当前排盘时间 | “校正采用时间” | +| `confirmed` | 通过服务器确认门且用户明确同意 | “已确认校正时间” | + +- `candidate_accepted` 不是“唯一出生分钟已确认”,默认仍可继续补充证据。 +- accepted 后用户仍可在同一批有效候选中改选(幂等 RPC 支持)。 +- confirmed 只能由服务器确认门 + 用户明确同意触发,同时写 `completed_at`。 + +## 2. 何时提供候选 + +- 只有本轮完成 `rectification-offer-candidates` 且返回 `selection_allowed=true` 时,界面才展示候选卡。 +- `selection_allowed` 只表示可以采用代表性时间,**不是**本轮必须出示卡片。仍有 `method_followup_plan.next_followup` 时继续收集,不得 offer。 +- `next_user_action.id=adopt_representative`,或用户停止且 `on_user_stop` 为 adopt 时,本轮才 offer/accept。这是采用代表性时间,不是 confirmed。 +- 继续收集证据时不得边追问边提供采用。 +- 候选卡内容来自持久化 Candidate Snapshot(`agentic_rectification_results`),不是 Agent 文本解析。 +- 候选卡拥有时间、排名、相对支持度、采用动作与选中状态;Agent 正文不得重复表格、编号菜单或选择提示。 + +## 3. 表达边界 + +- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。 +- 不暴露原始分数、内部权重、贡献矩阵、技术层名称、隐藏分钟证据或第二候选簇。 +- 候选范围必须说明“待核对边界”,不得表述为已确认出生分钟。 +- 外部验证状态按服务器字面读取:`not_evaluated` 表示未调用(入口门未就绪),不是“调用了但失败”。 + +## 4. 证据变化与重算 + +- 证据有效变化时由服务器重算候选;Agent 不必等用户说“没有更多了”才 compare。 +- 相同 evidence 指纹 + 引擎版本复用缓存;不要对同一指纹再 compare。 +- 分钟窗口扫描只在服务端,结果进入候选卡 / 不可分平台语言。不得把若干事件说成已确定到 ±5 分钟。 +- 普通澄清轮若不改变账本指纹,不重复播报。 +- 出生资料基线变化 → `needs_rebaseline`,旧候选失效;不得静默继续用旧结果。 +- `needs_rebaseline` 下不引用旧候选、不提供采用。 + +## 5. 不可分平台与确认门(必须说出来) + +服务器 `latest_result` 含 `confirmation_gate`、`indistinguishable_width_minutes`、`confirmation_allowed`、`selection_allowed` 与 `margin_percent`(若有)。`confirmation_gate` 是确认门权威,不是让 Agent 另算一分钟。 + +- 宽度大于 `maxConfirmationWidthMinutes`(5),或 top 候选 `tied_minute_count` > 1,或 `confirmation_allowed=false` 时:正文必须说这是**一段不可分区间**,必须把代表分钟说成**代表性候选**,不得说已定位到唯一分钟,也不得学本地扫分钟后的 1 分钟尖峰。 +- `vedastro_minute_sensitive` 为 `not_evaluated` 表示官方分钟敏感校验尚未跑通,不是 fail;缺它不能写 confirmed。 +- `public_aa_holdout` 为 `not_ready` 时不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。 +- 用户仍可 accepted 代表性候选;accepted ≠ confirmed。`session_outcome=adopt_representative` 时正文必须说还不能确认唯一分钟。 +- `confirmation_allowed=true` 才允许进入唯一分钟确认门;平台结果禁止把 `confirmation_allowed` 说成已确认。 +- 候选卡仍可展示代表性时间;Agent 不得把该时间写成“已校正到 HH:MM”。 + +## 6. 保存边界 + +- accepted 写入 `active_birth_time`,保留 `reported_birth_time` 原填报,不写兼容 `birth_time`。 +- 采用后界面按采用分钟重算本命宫位表,并折叠展示本轮技法审计。这不是唯一分钟确认,也不自动进入咨询 Agent。 +- confirmed 同样保留原填报;不自动写入,需要用户明确同意。 +- 失败、空流、Skill 未加载或未完成必要工具链时不保存、不扣费。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/conversation-strategy.md new file mode 100644 index 00000000..d64ea02d --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/conversation-strategy.md @@ -0,0 +1,105 @@ +# Conversation Strategy(V10) + +生时校正访谈是自然对话,不是问卷。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、自然表达和选择一个有信息增益的下一步。 + +## 1. 每轮上下文优先级 + +每轮先按以下优先级理解会话: + +1. 当前 Case 的服务器状态与读写权限。 +2. `CaseConversationSummary`:confirmed evidence、pending revisions、active focus、declined/skipped topics、candidate divergence、`method_followup_plan`、last result policy。不要把 `missing_evidence_categories` 当下一问。 +3. 当前用户消息。 +4. recent turns:只作为有界原文引用窗口,辅助 quote grounding 和局部措辞理解。 + +recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什么”的倒推、正则匹配或被截断的聊天记录重建 Case 状态。summary 与局部文本不一致时,以服务器状态为准;若用户意图仍不唯一,只澄清一个关键点。 + +## 2. OpeningPolicy + +首次开场只使用服务器 opening brief 中的 Case 状态、出生时间不确定类型、已有证据摘要与当前可询问范围,并自然满足: + +- 降低回忆负担:从最容易想起的一件经历或用户当前话题切入,不索要固定清单。 +- 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。 +- 不要求一次说完,允许分多轮补充、修正、暂停或换方向。 +- 至多一个主问题;开场可以零问题。 +- 不固定复述身份、流程、领域列表、证据数量要求或 opening brief 原文。 + +示例方向(不是固定话术):“可以先从你最容易想起的一件经历开始,大概年份也可以,不需要一次说完。哪件事你现在最容易确定?” + +## 3. 一轮的基本形态 + +1. 先判断用户意图:新事件、批量事件、补日期、修正旧事实、回答上一问、确认/否认、询问进度或原因、拒答/换方向、查看或采用候选。 +2. 先读取服务器 Case、summary 与 active focus;静默完成必要的工具调用后再输出答案。正文不叙述内部执行步骤,也不生成 Activity/技法凭证文案。 +3. 自然回应本轮内容,不固定以“收到 / 已记录”开头,不机械复读,不擅自解释事件的“人生意义”。 +4. 清晰项先处理;若仍需追问,只保留一个最有信息增益的主问题。完整回复可以没有问题。 +5. 不允许在同一回复中既要求补证据、又提供采用候选;不生成三条推荐问题。 +6. `next_user_action.id=adopt_representative` 时本轮只解释结果并邀请采用,零追问(除非有 active focus)。仍有 `next_followup` 时不得出示采用卡。 + +## 4. ConversationFocus + +active `ConversationFocus` 是承接型意图的唯一目标来源。它由服务器持久化并提供 `focusId`、目标 `evidenceId`(如有)、intent、预期回答结构和状态。 + +- “是的 / 不是 / 对 / 不对 / 大概那年 / 后来改了 / 不记得 / 不想回答 / 换个方向”只有在存在唯一 active focus 时才能解释为回答、拒答、确认或修订。 +- 拒绝、跳过、解决 focus 时,工具调用必须引用 active `focusId`;修订既有 evidence 时同时引用目标 `evidenceId`。用户对已有 pending 说“对/是”时,确认工具可以省略 `focusId`;opening focus(无 `target_evidence_id`)不得因第一条确认被 resolve。 +- 无 active focus、focus 已非 active、目标已被 supersede、或一句话可能指向多个问题时,简短问清“你指的是哪一件/哪一个时间点”;不得猜测,不调用 evidence 写工具。 +- 脱离 active focus 的“是的 / 不是”不是新事件。不得从 assistant 上一句倒推目标,不得只用 pending revision 构造 `active_followup`。 +- 当前消息若主动、明确陈述全新事件,可独立进入 evidence 流程;需要追问时由服务器建立新 focus。 +- 服务器验证 focus 已失效时,停止该动作并基于最新 summary 重新回应,不沿用旧目标。 + +## 5. 自然叙述与批量 evidence + +用户一段话中可以包含多件事件。应优先走服务器批量服务: + +- 每件事件分别保留原话 `quote`、`kind`、`domain`、主体和日期精度,不合并,不要求逐条重发。 +- 服务器对每项独立返回 `accepted`、`needs_clarification` 或 `rejected`。一项失败不改变其他项结果。 +- 新事件优先走批量服务;一句里两件及以上事件时只允许批量。清晰项在批量路径上可由服务器直接 `confirmed`,不要再逐条 propose+confirm。不要让模糊项阻塞清晰项。 +- 多个模糊项同时存在时,只选择信息增益最高的一项追问一个关键点,其余维持待澄清,不连续抛出问题清单。 +- `needs_clarification` 只问缺失的关键事实;不猜日期、主体、事件身份、动机、因果、主动/被动或人物关系。 +- `rejected` 如需解释,只说明用户可理解的边界,不伪装成已记录。 +- 批量 evidence item 的 `accepted` 是服务处理结果,不是候选采用状态;清晰项的最终 `status` 以服务器返回为准,批量路径上可以为 `confirmed`。 +- 询问进度/原因、拒答、查看结果、采用候选,以及无唯一 active focus 的承接词,都不是新事件。 + +## 6. 确认、修订、拒答与换方向 + +- 确认既有事实:必须有对应 `evidenceId`;确认词本身不创建新 evidence。无匹配 pending-target 的 focus 时可省略 `focusId`。 +- 修订既有事实:必须有 active `focusId` 和目标 `evidenceId`,生成 superseding revision,不覆盖历史;pending revision 不自动确认。 +- 用户明确“不知道 / 记不清”:将 active focus 解决为相应状态,不诱导猜测。 +- 用户明确“不想回答 / 换个方向”:decline/skip active focus;不得换词重开同一目标。 +- 用户主动重新打开曾拒绝主题时,可让服务器建立新 focus;否则 declined/skipped topics 以 `CaseConversationSummary` 为准。 +- 用户说“目前没有 / 没有更多事件”时,停止轮换证据领域;不要求结束、暂停或保存进度。 +- 若没有其他具备信息增益的问题,可以直接说明当前边界或自然结束本轮。 + +## 7. 追问策略 + +追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级: + +1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。 +2. `method_followup_plan.next_followup` 指定的下一方法层。精度阶段优先于方法覆盖:有日期事件 → 关系 → 事业 → 居所(D4)→ 学业/成就(D5)→ 家人(D12/D7)→ 外貌/体质 → 胎记/疤痕。占星占问不追问。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。外貌/疤痕可以问,但不得当主评分。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。仍有 `next_followup` 时即使 `selection_allowed` 也继续问。 +3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察只用于选题,不得说成用户星座或类型标签。 +4. pending revision 的一个关键歧义。 +5. 已有证据的必要稳定性补强。 + +不要按 `missing_evidence_categories` 轮询迁居 / 健康 / 财务。`stop_domain_rotation=true` 时停止领域清单。一轮最多一个主要问题。用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,直接说明目的、当前状态和边界,不绕开问题继续索取证据。 + +## 8. 日期精度 + +- `year`:只说年份;复述用 `display_date_label`(如 `2024年`)。 +- `month`:明确到月份;复述如 `2024-05`。 +- `quarter`:明确到季度。 +- `day`:明确到日期;复述必须是 `YYYY-MM-DD`,禁止说成“年份已确定为 YYYY”。 +- `range`:只有范围,不得擅自取中点当事实;复述用 `from–to`。 +- `unknown`:日期不明;可保留背景,但不得当作高权重校正证据。 +- 用户确认“是 / 对”不得改 `date_precision`。 +- 用户只补月份/季度时,只有 active focus 与目标 evidence 已由服务器明确年份,才可合并为 revision;不得猜年份。 +- “大概 3 月”仍按用户真实表达保存,不升级成某一天。 + +## 9. 候选输出与终态 + +- 候选卡负责呈现时间、排名、相对支持度、采用动作与选中状态。 +- 正文只解释“这些候选当前意味着什么”和“不确定性在哪里”,不重复候选表、编号菜单或候选卡数字。 +- `relative_support` 不是概率,不能写“准确率 70%”。 +- candidate、accepted、confirmed 严格分离;accepted 不是 confirmed。 +- `next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间;正文必须说还不能确认唯一分钟。仍有 `next_followup` 时不得出示采用卡。 +- 确认门以 `confirmation_gate` 为准。`not_evaluated` 不是 fail;holdout `not_ready` 时不得声称精确分钟或发布准确率。 +- 若 `indistinguishable_width_minutes` > 5 或 top `tied_minute_count` > 1,或 `confirmation_allowed=false`,必须说不可分区间 / 代表性候选,不得说已定位到唯一分钟。accepted ≠ confirmed。 +- accepted 后自然说明它不是唯一分钟确认即可;不强制追问,不要求用户结束、暂停或保存进度。 +- terminal Case(confirmed / closed / abandoned / superseded)只读:不得新增/修订/确认 evidence,不得采用/确认候选;若用户要继续,指向显式新建 Case。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/references/evidence-model.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/evidence-model.md new file mode 100644 index 00000000..c3b9054d --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/evidence-model.md @@ -0,0 +1,116 @@ +# Evidence Model(V9) + +证据是生时校正的唯一事实账本。本文件定义证据如何进入、校验、修订与关闭。服务器是证据账本的唯一写入者;Agent 只能提出 proposal。 + +## 1. 证据最小单元 + +一条证据(`agentic_rectification_evidence` 一行)至少包含: + +- `case_id`:所属 Case,由服务器生成。 +- `source_turn_id`:用户消息所在轮次;`source_message_id` 可选。 +- `user_quote`:用户原话的规范化子串。 +- `subject`:主体(`self` 或亲属关系;家庭事件必须显式 `related_person`)。 +- `event_kind`:语义种类(见 §2),不再只保留粗领域。 +- `domain`:评分/路由领域。 +- `occurred_from` / `occurred_to`:真实日期边界,可空。 +- `date_precision`:`year | month | quarter | day | range | unknown`。 +- `summary`:服务器从已验证引用中生成的安全摘要。 +- `status`:`draft | pending_confirmation | confirmed | superseded | rejected`。 +- `supersedes_evidence_id`:修订链指针。 + +## 2. 事件种类(event_kind) + +```text +education_start +education_completion +education_interruption +education_change +education_milestone +career_entry +career_change +promotion +career_pressure +career_exit +business_start +relationship_start +relationship_commitment +relationship_separation +relationship_end +relationship_change +relocation +foreign_move +return +home_change +finance_gain +finance_loss +income_change +asset_change +finance_change +self_health_event +pressure_period +family_event +appearance_note +birthmark_or_scar +other +``` + +语义不折叠:`career_entry / career_pressure / career_exit` 不同;`relationship_start / relationship_commitment / relationship_separation` 不同;不得把“开始关系”与“关系变化”混成同一事件。`education_milestone`、`relationship_end`、`return`、`home_change`、`health_pressure` 等与 TypeScript `EVIDENCE_KINDS` / `EVIDENCE_DOMAINS` 对齐,不得再因枚举缺口导致写入失败。 + +领域(`domain`): + +```text +education +career +relationship +relocation +finance +health +health_pressure +family +appearance +marks +other +``` + +## 3. 日期精度 + +- 用户只给年份 → `date_precision = 'year'`,`occurred_from = YYYY-01-01`(边界),不得诱导编造月份。 +- 用户给年月 → `month`;给季度 → `quarter`;给年月日 → `day`;给区间 → `range`。 +- 相对表达(“刚毕业那年”)必须由服务器结合权威当前时间解析,Agent 不得自行假设年份。 +- 跨午夜、未知时间不伪造具体分钟;`unknown` 精度允许保留。 +- 服务器投影只读字段 `display_date_label`:日级用 `YYYY-MM-DD`,月级用 `YYYY-MM`,年级用 `YYYY年`,range 用 `from–to`。复述必须用该标签;禁止把日级格式化成“年份已确定为 YYYY”。用户确认“是/对”不得改 `date_precision`。更粗的修订若 quote 并没有更粗的日期表达,服务器拒绝 `precision_downgrade`。 + +## 4. 原文引用(quote grounding) + +- `user_quote` 必须能在对应 `source_turn.user_message` 中找到规范化匹配(去空白、去标点后子串命中)。 +- 服务器确认路径必须校验:引用来自本轮用户消息、kind 属于枚举、日期与原文一致。 +- 模型不得凭空补充月份、日期、原因、主动/被动、人物关系。 + +## 5. 修订链(append-only) + +- 事实变化 = 新增 superseding row,旧行标记 `superseded`,永不覆盖/删除。 +- 合法修订:日期更正、日期补全(如“2016 年 + 9 月”合并为 `2016-09`)、事件重分类(同身份)。 +- 非法修订:跨事件覆盖既有 ID(如把“大学入学”改成“搬家”);服务器拒绝并降级为新的 pending proposal。 +- 证据 ID 只能由服务器生成;模型不得提供或覆盖。 + +## 6. 状态迁移 + +```text +draft -> confirmed (当前轮明确事件:proposal 通过原文绑定后,同轮走服务器确认路径) +draft -> pending_confirmation (事实模糊、冲突或需要用户补充) +pending_confirmation -> confirmed (用户明确确认 + 服务器确认路径) +pending_confirmation -> superseded(用户更正,产生修订) +confirmed -> superseded (后续修订使旧事实失效) +draft / pending_confirmation -> rejected (用户否认,保留只读历史) +``` + +- Agent 只能先产生 `draft`;`confirmed` 只能由服务器确认路径产生。服务器确认路径不等于必须额外等待一轮用户回复。 +- 终态 Case(confirmed/closed/abandoned/superseded)禁止新增或修订证据。 +- 同一请求重放不得重复写证据(幂等键 = case + source_turn + quote + kind + summary)。 + +## 7. 评分输入边界 + +- 只有 `confirmed` 证据进入评分账本;`draft` 与 `pending_confirmation` 都不参与评分。 +- `family_event` 进入评分(D12 + D7 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。 +- `appearance_note` / `birthmark_or_scar`:无日期只覆盖访谈;有日期才进上升/一宫辅助评分,不得当主公式。 +- 证据变化才触发重算;相同证据指纹复用缓存,不重复评分。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/references/technique-routing.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/technique-routing.md new file mode 100644 index 00000000..189f0319 --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/technique-routing.md @@ -0,0 +1,49 @@ +# Technique Routing(V9) + +生时校正是“有日期事件 + Dasha 为主要证据”的校准任务,分盘按主题调用,不一次性调用所有分盘。所有计算只能通过服务端工具;本文件只决定读哪些技法证据,不复制任何引擎实现。 + +## 1. 主证据 + +- 有明确日期(年月级或更精确)的人生事件 + 对应 Dasha 边界是主要证据。 +- 事件原文是用户原话;日期精度按用户真实提供保留。 +- 不把“支持某技法”误当作已完成独立验证;内部一致性不得伪装成全球顶级精度。 + +## 2. 分盘调用层级 + +| 层级 | 分盘 | 用途 | +|---|---|---| +| 核心 | D1(本命) | 全局框架 | +| 核心辅助 | D9、D10 | 关系与事业的主要主题 | +| 主题 | D2/D11(财富)、D7(子女/伴侣细节)、D12(父母)、D24(教育)、D4(居所/不动产) | 按主题补充 | +| 后置 | D30 | 只在健康/意外等强信号时后置调用 | +| 仅参考 | D60 | 只作参考,不驱动结论 | + +- 同一轮最多调用 2–3 个相关分盘;D9/D10 之外的分盘必须由当前主题驱动。 +- 未执行、不可用或仅供参考的技法不得显示为已执行。 + +## 3. 按问题域强制调取 + +- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。 +- 财富:`D2 / D11`。 +- 婚恋:`D9 + UL`(UL 为 Upapada Lagna,服务器可用时)。 +- 六亲/家人:`D12` 加 `D7`(子女/伴侣细节)加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。 +- 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分,也不得发明星座或类型标签。无日期的回答只覆盖访谈,不进主公式。 +- 健康:D1 + 必要时 D30(后置)。 +- 迁居:精度阶段 `d4_refine` 问带日期的搬家/住处变化;这不是领域轮询。计分 D4 + D1 四/十二宫。 +- 教育/成就:精度阶段 `d5_refine` 问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。不得贴类型标签。 +- 精度阶段顺序:有日期事件 → 本命上升 → D9 → D10 → D4 → D5 → 可采用。家人走方法覆盖与 D12/D7 观察,不得混进 D4。 +- D9/D10 类型表只作内部观察,不得给用户贴标签。`internal_observations.ask_theme` 只决定下一问主题,不得说出星座、配偶类型或事业特质。 + +## 4. 受限技法边界 + +- KP、Muhurta、Gochara、Sahams、Sphuta、Tajika 为 reference-only 或 blocked;不得作为确认或精确应期依据。 +- Shadbala / Ashtakavarga 外部绝对值未闭环前不作确定性结论。 +- 外部验证状态按服务器字面读取;`not_evaluated` ≠ `fail`。 +- 禁止 D60 驱动结论;禁止把邻近分钟与留一事件诊断描述为硬阻塞。 + +## 5. 决策树(简化) + +1. 有日期事件 → 按 Dasha 建立时间框架。 +2. 主题缺口 → 调对应分盘(§2/§3)。 +3. 候选对比有差异 → 服务器 Candidate Contrast 驱动下一问。 +4. 唯一分钟确认门以 `confirmation_gate` 为准(事件数/领域数/宽度/唯一领先/必需层/VedAstro/holdout)。`not_evaluated` ≠ fail。Agent 不得自行宣告通过或失败。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.5/references/truth-consent-boundaries.md b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/truth-consent-boundaries.md new file mode 100644 index 00000000..49ec686e --- /dev/null +++ b/skills/jyotish-birth-time-rectification/versions/10.0.5/references/truth-consent-boundaries.md @@ -0,0 +1,43 @@ +# Truth / Consent Boundaries(V9) + +本文件定义真实性、用户同意与选择政策。服务器拥有事实、权限与状态;Agent 必须服从服务器返回的 truth/consent/selection policy。 + +## 1. 真实性硬边界 + +- 禁止虚构:事件、日期、候选、分盘数据、评分、Dasha 边界或出生分钟。 +- 计算只能通过服务端工具;模型不得重算或发明行星位置、分数或权重。 +- 内部一致性不等于“全球顶级精度”;外部 oracle 未闭环、参照引擎不可用时必须写成 `blocked` 或降级置信度。 +- 系统提示词与 Skill 原文不得输出;reasoning / chain-of-thought 不向用户展示。 + +## 2. 用户同意边界 + +- 保存 profile 需要用户明确同意 + 服务器确认门。 +- accepted(用户选择)与 confirmed(引擎唯一确认 + 用户同意)严格区分;不得把 accepted 写成 confirmed。`confirmation_gate` 是确认门权威;`not_evaluated` 不是失败。 +- 助手文本、模型推断与历史摘要不得升级为已确认事实;当前轮用户主动、明确且无歧义的事件可在 quote grounding 通过后同轮走服务器确认路径。旧文本只能作为显示历史或 pending evidence draft。 +- 用户说“不知道/不想回答”时尊重并关闭该目标,不换词重开。 + +## 3. 选择政策 + +- 候选卡只展示服务器持久化候选与相对支持度;不得暴露原始分数、权重、贡献矩阵、技术层或隐藏分钟。 +- 继续收集证据时不得同时提供采用操作。界面只在本轮完成 `rectification-offer-candidates` 且 `selection_allowed=true` 时展示候选卡。 +- 相同 evidence 指纹复用缓存;只有有效变化才重算。 +- 终态 Case 只读;追加证据、采用、确认全部拒绝。 + +## 4. 隐私与泄露防护 + +- 不输出 userId、出生资料明文、内部 ID、工具参数/结果、数据库错误原文、密钥或内部 URL。 +- 每轮持久化公开执行回执(phase/tool 白名单、状态、时间),不含 reasoning 与 payload。 +- 家庭健康事件不得投射为本人生成评分证据;亲属主体必须显式标记。 + +## 5. 受限技法降级 + +| 状态 | 表达 | +|---|---| +| `blocked` | 明确写 blocked,不得包装成通过 | +| `partial` | 说明部分边界,降级置信度 | +| `reference_only` | 只作参考,不驱动结论 | +| `not_evaluated`(外部验证) | 未调用,不等于失败 | + +## 6. 功能吉凶层(高严谨模式) + +进入高严谨模式(事业/财富/婚恋/应期/技法可靠性)时,除自然吉凶星外必须叠加当前 Lagna 下的 Functional Benefic/Malefic 判定;自然与功能属性冲突时必须说明冲突来源并降级或标记 blocked。未完成该判定不得声称高严谨解读完成。 diff --git a/skills/skill-package-registry.json b/skills/skill-package-registry.json index fafba927..fd065d87 100644 --- a/skills/skill-package-registry.json +++ b/skills/skill-package-registry.json @@ -47,6 +47,14 @@ "sha256": "1a032b1af54d8ca593ad37da8dee116668b71524d37c7b77ec0e3e44f8c7624c", "sourceCommit": null, "packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.4", + "status": "deprecated" + }, + { + "name": "jyotish-birth-time-rectification", + "version": "10.0.5", + "sha256": "47cd353b5f7a08695007ce52d77560584300a1097ed97248f3a81372eeaca9c6", + "sourceCommit": null, + "packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.5", "status": "active" }, { diff --git a/tests/test_rectification_family_appearance_scoring.py b/tests/test_rectification_family_appearance_scoring.py index 63c69e79..0a879005 100644 --- a/tests/test_rectification_family_appearance_scoring.py +++ b/tests/test_rectification_family_appearance_scoring.py @@ -20,7 +20,7 @@ from scripts.rectification.scoring_service import public_technique_layers def test_family_event_is_scoreable_with_d12_and_kin_houses() -> None: prefixes, houses = DOMAIN_CONFIG["family"] - assert prefixes == ("D12",) + assert prefixes == ("D12", "D7") assert houses == (3, 4, 5, 9) event = { "id": "00000000-0000-4000-8000-000000000001", @@ -158,11 +158,14 @@ def test_family_event_engine_rows_include_d12_and_are_not_skipped() -> None: } contexts = compute_candidate_static_contexts(request) assert all("D12" in context["feature"]["available_layers"] for context in contexts) + assert all("D7" in context["feature"]["available_layers"] for context in contexts) assert all(context["varga_charts"]["D12"] is not None for context in contexts) + assert all(context["varga_charts"]["D7"] is not None for context in contexts) rows = compute_event_candidate_rows(request, static_contexts=contexts) assert [row["time"] for row in rows] == ["05:13", "05:14"] for row in rows: assert "D12" not in row["missing_layers"] + assert "D7" not in row["missing_layers"] assert row["evidence"], "family events must enter scoring instead of being skipped" assert row["evidence"][0]["domain"] == "family" assert "event_kind:family_event" in row["evidence"][0]["rule_ids"] @@ -181,4 +184,9 @@ def test_family_event_engine_rows_include_d12_and_are_not_skipped() -> None: career_rules = [rule for row in career_rows for item in row["evidence"] for rule in item["rule_ids"]] layers = public_technique_layers("career", career_rules) assert "d1-rashi" in layers and "d10-dashamsa" in layers + family_layers = public_technique_layers("family", ["vim_md_domain_house", "vim_md_domain_varga"]) + assert "d12-dwadashamsha" in family_layers and "d7-saptamsha" in family_layers + education_layers = public_technique_layers("education", ["vim_md_domain_house"]) + assert "d5-panchamsha" in education_layers and "d24-chaturvimshamsha" in education_layers assert all("D10" in context["feature"]["available_layers"] for context in contexts) + assert all("D5" in context["feature"]["available_layers"] for context in contexts) diff --git a/tests/test_rectification_refinement_packet.py b/tests/test_rectification_refinement_packet.py index e835b02f..a253ae7b 100644 --- a/tests/test_rectification_refinement_packet.py +++ b/tests/test_rectification_refinement_packet.py @@ -12,7 +12,7 @@ from scripts.rectification.refinement_packet import ( ) -def feature(time: str, *, d1: int | None = None, d9: int | None = None, d10: int | None = None, d4: int | None = None, degree: float | None = None) -> dict: +def feature(time: str, *, d1: int | None = None, d9: int | None = None, d10: int | None = None, d4: int | None = None, d5: int | None = None, d7: int | None = None, d12: int | None = None, degree: float | None = None) -> dict: vargas = {} if d9 is not None: vargas["D9"] = d9 @@ -20,6 +20,12 @@ def feature(time: str, *, d1: int | None = None, d9: int | None = None, d10: int vargas["D10"] = d10 if d4 is not None: vargas["D4"] = d4 + if d5 is not None: + vargas["D5"] = d5 + if d7 is not None: + vargas["D7"] = d7 + if d12 is not None: + vargas["D12"] = d12 row: dict = {"time": time, "varga_ascendants": vargas} if d1 is not None: row["ascendant_sign_index"] = d1 @@ -87,7 +93,11 @@ class RefinementPacketTest(unittest.TestCase): self.assertEqual(precision_stage({"d1_candidates_differ": True}, 2)["current"], "lagna_frame") self.assertEqual(precision_stage({"d9_candidates_differ": True}, 2)["current"], "d9_refine") self.assertEqual(precision_stage({"d10_candidates_differ": True}, 2)["current"], "d10_refine") - self.assertEqual(precision_stage({"d4_candidates_differ": True}, 2)["current"], "theme_refine") + self.assertEqual(precision_stage({"d4_candidates_differ": True}, 2)["current"], "d4_refine") + self.assertEqual(precision_stage({"d5_candidates_differ": True}, 2)["current"], "d5_refine") + self.assertNotEqual(precision_stage({"d4_candidates_differ": True}, 2)["current"], "theme_refine") + self.assertIn("搬家", precision_stage({"d4_candidates_differ": True}, 2)["user_meaning"]) + self.assertNotIn("家人", precision_stage({"d4_candidates_differ": True}, 2)["user_meaning"]) ready = precision_stage({}, 3) self.assertEqual(ready["current"], "ready_to_adopt") self.assertFalse(ready["unique_minute_claim"]) @@ -170,6 +180,57 @@ class RefinementPacketTest(unittest.TestCase): self.assertIn("vimshottari_narayana_conflict", receipt["reasons"]) self.assertEqual(receipt["dasha_agreement"]["status"], "conflict") self.assertEqual(receipt["event_dasha_ledger"][0]["match_label"], "强相关") + self.assertTrue(any(row["technique"] == "唯一分钟确认" and row["status"] == "blocked" for row in receipt["technique_audit_table"])) + self.assertTrue(any(row["technique"] == "VedAstro 分钟级校验" and row["status"] == "blocked" for row in receipt["technique_audit_table"])) + self.assertFalse(receipt["unique_minute_claim"]) + + def test_receipt_recasts_house_tables_per_candidate_and_blocks_unique_minute(self): + request = request_events() + rows = [ + {"time": "05:13", "score": 20, "evidence": [], "missing_layers": []}, + {"time": "05:14", "score": 8, "evidence": [], "missing_layers": []}, + ] + decisions = build_candidate_decisions(rows, result_id="00000000-0000-4000-8000-000000000099") + chart_a = {"ascendant": {"sign": "Taurus", "lon": 40.0}, "planets": {"Sun": {"sign": "Aries", "house": 12}}} + chart_b = {"ascendant": {"sign": "Gemini", "lon": 70.0}, "planets": {"Sun": {"sign": "Aries", "house": 11}}} + built = { + "missing_layers": [], + "static_contexts": [ + {**feature("05:13", d1=1, d9=1, d10=4), "chart": chart_a}, + {**feature("05:14", d1=2, d9=1, d10=4), "chart": chart_b}, + ], + "matrix": { + request["events"][0]["id"]: { + "05:13": {"points": 8, "rule_ids": ["vim_md_domain_house"], "technique_layers": ["d1-rashi", "d10-dashamsa"]}, + "05:14": {"points": 1, "rule_ids": ["vim_ad_domain_house"], "technique_layers": ["d1-rashi", "d10-dashamsa"]}, + }, + request["events"][1]["id"]: { + "05:13": {"points": 1, "rule_ids": ["narayana_ad_domain_house"], "technique_layers": ["d1-rashi", "d9-navamsa"]}, + "05:14": {"points": 9, "rule_ids": ["narayana_md_domain_house"], "technique_layers": ["d1-rashi", "d9-navamsa"]}, + }, + request["events"][2]["id"]: { + "05:13": {"points": 1, "rule_ids": ["vim_md_domain_varga"], "technique_layers": ["d1-rashi", "d5-panchamsha"]}, + "05:14": {"points": 1, "rule_ids": ["vim_md_domain_varga"], "technique_layers": ["d1-rashi", "d5-panchamsha"]}, + }, + }, + } + receipt = build_decision_receipt(request, decisions, built, { + "leave_one_event_out_retention_rate": 1, + "leave_one_domain_out_retention_rate": 1, + "date_sensitivity_retention_rate": 1, + "primary_secondary_margin_percent": 50, + }) + self.assertEqual(receipt["house_table"]["time"], "05:13") + self.assertEqual(receipt["house_table"]["lagna"], "金牛座") + self.assertEqual(receipt["house_tables_by_time"]["05:14"]["lagna"], "双子座") + self.assertEqual(receipt["natal_recast"]["time"], "05:13") + self.assertFalse(receipt["natal_recast"]["unique_minute_claim"]) + self.assertFalse(receipt["confirmation_allowed"]) + techniques = {row["technique"]: row["status"] for row in receipt["technique_audit_table"]} + self.assertEqual(techniques["D1 本命盘"], "executed") + self.assertEqual(techniques["D5 成就分盘"], "executed") + self.assertEqual(techniques["KP 宫头"], "blocked") + self.assertEqual(techniques["唯一分钟确认"], "blocked") if __name__ == "__main__": diff --git a/tests/test_rectification_technique_contract.py b/tests/test_rectification_technique_contract.py index 27cfc8b4..fcac77db 100644 --- a/tests/test_rectification_technique_contract.py +++ b/tests/test_rectification_technique_contract.py @@ -10,7 +10,7 @@ def test_zero_events_are_not_a_completed_rectification() -> None: def test_contract_discloses_used_and_missing_layers() -> None: contract = build_rectification_technique_contract(event_count=4, domain_count=3) - assert {"D2", "D4", "D9", "D10", "D11", "D12", "D24", "D30"} <= set(contract["used_divisional_charts"]) + assert {"D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"} <= set(contract["used_divisional_charts"]) assert contract["dasha_tracks"] == ["vimshottari_md_ad_pd", "narayana_md_ad"] assert contract["used_arudha"] == ["A7", "UL", "A10"] assert contract["missing_layers"] == ["shadbala_kala_dig_chesta_total"] diff --git a/tests/test_rectification_v5_services.py b/tests/test_rectification_v5_services.py index 149aff1f..a3ebcedb 100644 --- a/tests/test_rectification_v5_services.py +++ b/tests/test_rectification_v5_services.py @@ -154,7 +154,7 @@ class RectificationV5ServicesTest(unittest.TestCase): built = build_event_contribution_matrix(normalized, row_provider=rows) self.assertEqual(seen, ["family"]) self.assertEqual(built["matrix"][normalized["events"][0]["id"]]["05:13"]["technique_layers"], [ - "d1-rashi", "d12-dwadashamsha", "vim_md_domain_house", + "d1-rashi", "d12-dwadashamsha", "d7-saptamsha", "vim_md_domain_house", ]) def test_declared_date_precision_changes_real_contribution_weight(self):