From 8010245981f0b29d9b4094f038eafa6c43fedd50 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 20 Aug 2026 21:42:47 +0800 Subject: [PATCH] feat(product): present consult and rectification in local skill form Default ayanamsa to Raman with true_pushya support, attach governed Raman packets, restore Path C questionnaires and eight-method verification copy, and keep unique-minute confirmation blocked. Co-authored-by: Cursor --- frontend/src/app/api/consult/route.ts | 4 +- .../src/lib/birth-time-journey-assessment.ts | 4 +- .../src/lib/birth-time-journey-service.ts | 2 +- .../src/lib/consultation-technique-audit.ts | 11 +- frontend/src/lib/global-birth-payloads.ts | 2 +- .../src/lib/personal-report-generation.ts | 2 +- .../v9/method-followup.ts | 133 ++++-- .../v9/refinement-packet.ts | 44 +- .../v9/skill-verification-report.ts | 163 +++++++ .../v9/varga-observations.ts | 25 +- frontend/src/mastra/agentic-rectification.ts | 6 +- frontend/src/mastra/consultation-tools.ts | 12 + frontend/src/mastra/consultation-workflow.ts | 16 + frontend/src/mastra/index.ts | 8 +- frontend/src/mastra/product-voice.ts | 32 +- frontend/src/mastra/rectification-v9-tools.ts | 16 + frontend/src/mastra/skill-binding.ts | 2 +- frontend/tests/consultation-context.test.ts | 2 + .../tests/consultation-entrypoint.test.ts | 2 +- .../consultation-technique-audit.test.ts | 9 +- .../tests/consultation-voice-contract.test.ts | 32 +- .../tests/global-birth-profile-routes.test.ts | 3 +- .../rectification-confirmation-gate.test.ts | 5 +- .../tests/rectification-eight-method.test.ts | 41 +- frontend/tests/rectification-v9-agent.test.ts | 8 +- .../tests/rectification-v9-contracts.test.ts | 12 +- frontend/tests/skill-registry.test.ts | 2 +- ...a_parameter_support_packet_2026_08_20.json | 58 +++ ...napha_batch_closure_packet_2026_08_20.json | 129 ++++++ ...ldren_batch_closure_packet_2026_08_20.json | 63 +++ ...ldren_batch_fixture_packet_2026_08_20.json | 31 ++ ...other_batch_closure_packet_2026_08_20.json | 77 ++++ ...other_batch_fixture_packet_2026_08_20.json | 37 ++ ..._core_batch_closure_packet_2026_08_20.json | 37 ++ ..._core_batch_fixture_packet_2026_08_20.json | 19 + ...pouse_batch_closure_packet_2026_08_20.json | 37 ++ ...pouse_batch_fixture_packet_2026_08_20.json | 19 + ..._core_batch_closure_packet_2026_08_20.json | 90 ++++ ..._core_batch_fixture_packet_2026_08_20.json | 78 ++++ ...ha_boundary_fixture_family_2026_08_20.json | 48 ++ ...sha_boundary_parity_packet_2026_08_20.json | 411 ++++++++++++++++++ ...re_subrules_fixture_packet_2026_08_20.json | 119 +++++ ...dasha_core_subrules_packet_2026_08_20.json | 65 +++ ...dasha_source_packet_family_2026_08_20.json | 60 +++ ...re_subrules_fixture_packet_2026_08_20.json | 46 ++ ...gment_core_subrules_packet_2026_08_20.json | 66 +++ ...udgment_methodology_packet_2026_08_20.json | 99 +++++ .../upstream/yinduzhanxing/import-plan.json | 45 +- scripts/active_rectification_event_engine.py | 3 +- scripts/active_rectification_questions.py | 4 +- scripts/ayanamsa_utils.py | 50 ++- scripts/daily_guidance_service.py | 2 +- scripts/domain_calculation_service.py | 18 +- scripts/jyotish_api_server.py | 86 ++-- scripts/jyotish_engine.py | 34 +- scripts/raman_support_observations.py | 314 +++++++++++++ scripts/rectification/decision_policy.py | 5 +- scripts/rectification/horary_observation.py | 2 +- scripts/rectification/kp_cusp_observation.py | 4 +- scripts/rectification/refinement_packet.py | 75 +++- scripts/rectification/scoring_service.py | 2 +- scripts/shadbala.py | 13 +- .../jyotish-birth-time-rectification/SKILL.md | 13 +- .../references/candidate-comparison.md | 6 +- .../references/conversation-strategy.md | 14 +- .../references/evidence-model.md | 2 +- .../references/technique-routing.md | 8 +- .../versions/10.0.10/SKILL.md | 13 +- .../references/candidate-comparison.md | 6 +- .../references/conversation-strategy.md | 14 +- .../10.0.10/references/evidence-model.md | 2 +- .../10.0.10/references/technique-routing.md | 8 +- skills/skill-package-registry.json | 2 +- tests/test_active_rectification_events.py | 2 +- tests/test_active_rectification_events_v4.py | 48 +- tests/test_api_server_security.py | 2 +- tests/test_ayanamsa_switching.py | 31 +- tests/test_raman_support_observations.py | 100 +++++ ...test_rectification_diagnostics_clusters.py | 7 +- tests/test_rectification_provenance.py | 2 +- tests/test_rectification_refinement_packet.py | 9 +- tests/test_rectification_v5_services.py | 4 +- tests/test_upstream_import_plan.py | 14 + 83 files changed, 2881 insertions(+), 270 deletions(-) create mode 100644 frontend/src/lib/rectification-agentic/v9/skill-verification-report.ts create mode 100755 references/oracle/raman_2026_08_20/raman_ayanamsa_parameter_support_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_dasha_boundary_fixture_family_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_dasha_boundary_parity_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_dasha_core_subrules_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_dasha_core_subrules_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_dasha_source_packet_family_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_packet_2026_08_20.json create mode 100755 references/oracle/raman_2026_08_20/raman_house_judgment_methodology_packet_2026_08_20.json create mode 100644 scripts/raman_support_observations.py create mode 100644 tests/test_raman_support_observations.py diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 9c712d40..be34fe3e 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -642,7 +642,7 @@ export async function POST(request: Request) { ? generalDailyContext ? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" : "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" - : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段。", + : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。事业/财富/婚恋/家庭按 skill Level 2 模板写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。不要复述内部 JSON 字段。", generalDailyContextPrompt(generalDailyContext), resolvedQuestion.modelQuestion, ].filter(Boolean).join("\n"), @@ -913,7 +913,7 @@ export async function POST(request: Request) { content: [ currentTimeContext(requestTime), name ? `用户称呼:${name}` : "", - "请直接用口语回答下面的问题。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。", + "请按 skill Level 2 模板回答下面的问题:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。", resolvedQuestion.modelQuestion, ].filter(Boolean).join("\n"), }, diff --git a/frontend/src/lib/birth-time-journey-assessment.ts b/frontend/src/lib/birth-time-journey-assessment.ts index 105c8ba3..c5c63be5 100644 --- a/frontend/src/lib/birth-time-journey-assessment.ts +++ b/frontend/src/lib/birth-time-journey-assessment.ts @@ -25,7 +25,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null { lat: assessment.location.lat, lon: assessment.location.lon, tz: assessment.location.tz, - ayanamsa: "lahiri", + ayanamsa: "raman", }; } return { @@ -37,7 +37,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null { lat: assessment.location.lat, lon: assessment.location.lon, tz: assessment.location.tz, - ayanamsa: "lahiri", + ayanamsa: "raman", }; } diff --git a/frontend/src/lib/birth-time-journey-service.ts b/frontend/src/lib/birth-time-journey-service.ts index 6a6f3d37..a99bc4e0 100644 --- a/frontend/src/lib/birth-time-journey-service.ts +++ b/frontend/src/lib/birth-time-journey-service.ts @@ -53,7 +53,7 @@ export type JourneyScanInput = { readonly lat: number; readonly lon: number; readonly tz: number; - readonly ayanamsa: "lahiri"; + readonly ayanamsa: "raman" | "lahiri"; }; export type JourneyScoreInput = { diff --git a/frontend/src/lib/consultation-technique-audit.ts b/frontend/src/lib/consultation-technique-audit.ts index 07115ea7..77161079 100644 --- a/frontend/src/lib/consultation-technique-audit.ts +++ b/frontend/src/lib/consultation-technique-audit.ts @@ -171,11 +171,11 @@ export function splitSpokenAnswerAndTechniqueAudit(text: string): SpokenAnswerAn return { spoken: text.replace(/\s+$/, ""), rows: [], complete: true }; } - const spoken = lines.slice(0, headerIndex).join("\n").replace(/\s+$/, ""); + const prefix = lines.slice(0, headerIndex).join("\n").replace(/\s+$/, ""); const tableLines = lines.slice(headerIndex).map((line) => line.trim()); const separator = tableLines[1] && AUDIT_SEPARATOR.test(tableLines[1]) ? tableLines[1] : null; if (!separator) { - return { spoken, rows: [], complete: false }; + return { spoken: prefix, rows: [], complete: false }; } const rows: TechniqueAuditRow[] = []; @@ -183,7 +183,7 @@ export function splitSpokenAnswerAndTechniqueAudit(text: string): SpokenAnswerAn if (!line) break; if (!line.startsWith("|")) break; const cells = splitGfmRow(line); - if (cells.length < 3) return { spoken, rows, complete: false }; + if (cells.length < 3) return { spoken: prefix, rows, complete: false }; const technique = cells[0]?.trim() ?? ""; if (!technique) continue; const note = cells.slice(2).join(" | ").trim().slice(0, 320); @@ -194,7 +194,10 @@ export function splitSpokenAnswerAndTechniqueAudit(text: string): SpokenAnswerAn }); } - return { spoken, rows, complete: rows.length > 0 }; + if (rows.length === 0) { + return { spoken: prefix, rows: [], complete: false }; + } + return { spoken: text.replace(/\s+$/, ""), rows, complete: true }; } export function resolveTechniqueAuditRows( diff --git a/frontend/src/lib/global-birth-payloads.ts b/frontend/src/lib/global-birth-payloads.ts index 00e80d58..6759bf0e 100644 --- a/frontend/src/lib/global-birth-payloads.ts +++ b/frontend/src/lib/global-birth-payloads.ts @@ -46,7 +46,7 @@ export async function dailyProfilePayload(profile: GlobalBirthProfile, today: st tz, transit_date: today, today, - ayanamsa: "lahiri", + ayanamsa: "raman", node_mode: "mean", }; } diff --git a/frontend/src/lib/personal-report-generation.ts b/frontend/src/lib/personal-report-generation.ts index f4385e66..192a1ad9 100644 --- a/frontend/src/lib/personal-report-generation.ts +++ b/frontend/src/lib/personal-report-generation.ts @@ -716,7 +716,7 @@ const SAFE_AYANAMSA = new Map([ ["krishnamurti", "Krishnamurti/KP"], ["krishnamurti/kp", "Krishnamurti/KP"], ["krishnamurti_paddhati", "Krishnamurti/KP"], ["fagan_bradley", "Fagan-Bradley"], ["djwhal_khul", "Djwhal Khul"], ["sassanian", "Sassanian"], - ["true_citra", "True Citra"], + ["true_citra", "True Citra"], ["true_pushya", "True Pushya"], ]); const SAFE_NODE_MODES = new Map([ ["mean", "mean"], ["mean_node", "mean"], ["true", "true"], ["true_node", "true"], diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index a49e942b..13cbd8c8 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -8,12 +8,13 @@ * * Policy map (classic eight): * 1. Dasha + dated events — any confirmed dated event - * 2. D9 relationship — confirmed relationship evidence; no sign labels + * 2. D9 relationship — confirmed relationship evidence; D9 type table is a + * rectification method, not a fate promise * 3. D10 career — confirmed dated career evidence; same event also scores D1 10th house * 4. Relatives — confirmed family evidence (D12 + D7 + D3) * 5. Appearance / constitution — ask; dated answers are auxiliary 1st-house scores * 6. Birthmarks / scars — ask; dated answers are auxiliary 1st-house scores - * 7. Occupation / 10th house — separate from dated career events; no type labels + * 7. Occupation / 10th house — separate from dated career events; D10 type table allowed * 8. Horary — ask once for the first question time; recast if given; never blocks cards * * Relocation stays out of domain rotation and is only asked at d4_refine. @@ -58,7 +59,7 @@ export type MethodFollowup = Readonly<{ domain: string | null; kind_hint: string | null; user_prompt_hint: string; - must_not_label: true; + must_not_label: boolean; source: "active_focus" | "method_coverage" | "varga_observation" | "precision_stage" | "nakshatra_boundary" | "oos_blind"; }>; @@ -136,10 +137,20 @@ function coverage( return { method_id: methodId, status }; } +const ABCD_CHOICES = `请用 A/B/C/D 回答: +A. 明确有,且时间大致吻合 +B. 有类似,但时间略偏或不够重大 +C. 没有明显发生 +D. 不确定 / 不记得`; + +function abcdHint(why: string, varga: string, extra = ""): string { + return `${why}本题绑定 ${varga}。${extra}\n${ABCD_CHOICES}`; +} + function followup( input: Omit, ): MethodFollowup { - return { ...input, must_not_label: true }; + return { ...input, must_not_label: false }; } export type NextUserActionId = @@ -325,7 +336,7 @@ export function buildMethodFollowupPlan(input: { ask_theme: "active_focus", domain: focus.targetDomain, kind_hint: focus.targetKind, - user_prompt_hint: "先承接当前服务器焦点,不要另开领域清单。", + user_prompt_hint: "先承接当前服务器焦点,不要另开领域清单。若这是一件待确认经历,请用 A/B/C/D 回答。", source: "active_focus", }), deferred_followup: null, @@ -358,7 +369,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "dated_event", domain: null, kind_hint: null, - user_prompt_hint: "可以先从最容易想起的一件带大概时间的经历开始。", + user_prompt_hint: abcdHint( + "可以先从最容易想起的一件带大概时间的经历开始。", + "本命 Dasha + 行运(方法1)", + "不要求一次列出 10–15 条。", + ), source: "method_coverage", }); } else if (stage === "lagna_frame") { @@ -368,7 +383,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "dated_event", domain: null, kind_hint: null, - user_prompt_hint: "窗口里本命上升还可能落在两段。请再补一件记得大概时间的经历,用来分开这两段;不要用性格或类型标签来选。", + user_prompt_hint: abcdHint( + "窗口里本命上升还可能落在两段。请再补一件记得大概时间的经历,用来分开这两段。", + "本命上升 / Dasha", + ), source: "precision_stage", }); } else if (!relationshipCovered && !declined.has("relationship")) { @@ -378,7 +396,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "relationship_style", domain: "relationship", kind_hint: "relationship_start", - user_prompt_hint: "可以先说一段记得大概时间的感情或关系变化,不必描述对象星座或类型标签。", + user_prompt_hint: abcdHint( + "可以先说一段记得大概时间的感情或关系变化。", + "D9", + "对照 D9 上升类型表(白羊主动热情、天蝎深刻占有等)只作校时方法,不是命运承诺。", + ), source: "method_coverage", }); } else if (!careerCovered && !declined.has("career")) { @@ -388,7 +410,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "career_style", domain: "career", kind_hint: "career_entry", - user_prompt_hint: "可以先说一段记得大概时间的工作或事业变化。同一件事会同时对照本命第 10 宫和 D10。", + user_prompt_hint: abcdHint( + "可以先说一段记得大概时间的工作或事业变化。同一件事会同时对照本命第 10 宫和 D10。", + "D10", + "可用 D10 事业类型表作校时对照。", + ), source: "method_coverage", }); } else if (!familyCovered && !declined.has("family")) { @@ -398,7 +424,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "family_event", domain: "family", kind_hint: "family_event", - user_prompt_hint: "可以先说一段记得大概时间的家人相关变化。同一件事会对照 D12 父母盘、D7 子女盘和 D3 兄弟盘。", + user_prompt_hint: abcdHint( + "可以先说一段记得大概时间的家人相关变化。同一件事会对照 D12 父母盘、D7 子女盘和 D3 兄弟盘。", + "D12 / D7 / D3", + "六亲用 Raman 六步:前三步可执行,后三步标方法论层。", + ), source: "method_coverage", }); } else if (!appearanceCovered && !declined.has("appearance")) { @@ -408,7 +438,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "appearance", domain: "appearance", kind_hint: "appearance_note", - user_prompt_hint: "外貌或体质有没有比较稳定的特点?如果记得某次明显变化的大概时间,也可以说。这只作辅助对照,不会当成主评分。", + user_prompt_hint: abcdHint( + "外貌或体质有没有比较稳定的特点?如果记得某次明显变化的大概时间,也可以说。", + "D1 上升 / 一宫", + "这只作辅助对照,不会当成主评分,也不能单独定分钟。", + ), source: "method_coverage", }); } else if (!marksCovered && !declined.has("marks")) { @@ -418,7 +452,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "marks", domain: "marks", kind_hint: "birthmark_or_scar", - user_prompt_hint: "有没有胎记,或记得大概时间的疤痕、受伤?有日期的会进一宫辅助对照,不会当成主评分。", + user_prompt_hint: abcdHint( + "有没有胎记,或记得大概时间的疤痕、受伤?", + "D1 上升 / 一宫", + "有日期的会进一宫辅助对照,不会当成主评分。", + ), source: "method_coverage", }); } else if (!occupationCovered) { @@ -428,7 +466,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "occupation", domain: "occupation", kind_hint: "occupation_note", - user_prompt_hint: "你长期做什么工作?对照本命第 10 宫和 D10 这条线怎么应验就可以,不要贴事业类型标签。", + user_prompt_hint: abcdHint( + "你长期做什么工作?对照本命第 10 宫和 D10。", + "D1-H10 + D10", + "可用事业类型表(白羊领导创业、天蝎研究转化等)作校时方法。", + ), source: "method_coverage", }); } else if (horaryStatus === "uncovered") { @@ -438,7 +480,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "horary", domain: "horary", kind_hint: "horary_query", - user_prompt_hint: "有没有第一次认真问起这件事的时间?有的话可以按那个时间观察占问盘;没有也不挡给出时间卡。", + user_prompt_hint: abcdHint( + "有没有第一次认真问起这件事的时间?", + "占问观察盘", + "有的话可以按那个时间观察;没有也不挡给出时间卡。状态是 observation_only。", + ), source: "method_coverage", }); } else if (stage === "d9_refine" && !declined.has("relationship")) { @@ -448,7 +494,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "relationship_style", domain: "relationship", kind_hint: "relationship_change", - user_prompt_hint: "关系盘仍会换升。可以再补一件记得大概时间的感情或关系变化;不要描述星座或类型标签。", + user_prompt_hint: abcdHint( + "关系盘仍会换升。可以再补一件记得大概时间的感情或关系变化。", + "D9", + "对照 D9 上升类型表只作校时方法。", + ), source: "precision_stage", }); } else if (stage === "d10_refine" && !declined.has("career")) { @@ -458,7 +508,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "career_style", domain: "career", kind_hint: "career_change", - user_prompt_hint: "事业盘仍会换升。可以再补一件记得大概时间的工作变化;同一件事会同时对照本命第 10 宫和 D10,不要描述类型标签。", + user_prompt_hint: abcdHint( + "事业盘仍会换升。可以再补一件记得大概时间的工作变化;同一件事会同时对照本命第 10 宫和 D10。", + "D10", + "可用 D10 事业类型表作校时对照。", + ), source: "precision_stage", }); } else if ((stage === "d4_refine" || stage === "theme_refine") && !declined.has("relocation")) { @@ -468,7 +522,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "home_change", domain: "relocation", kind_hint: "home_change", - user_prompt_hint: "居所盘仍会换升。可以再补一件记得大概时间的搬家或住处变化。", + user_prompt_hint: abcdHint( + "居所盘仍会换升。可以再补一件记得大概时间的搬家或住处变化。", + "D4", + ), source: "precision_stage", }); } else if (stage === "d5_refine" && !declined.has("education")) { @@ -478,7 +535,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "education_style", domain: "education", kind_hint: "education_milestone", - user_prompt_hint: "成就盘或学业盘仍会换升。可以再补一件记得大概时间的学业、考试或被委以责任的变化;同一件事会对照本命五宫、D5 和 D24,不要贴类型标签。", + user_prompt_hint: abcdHint( + "成就盘或学业盘仍会换升。可以再补一件记得大概时间的学业、考试或被委以责任的变化;同一件事会对照本命五宫、D5 和 D24。", + "D5 / D24", + ), source: "precision_stage", }); } else { @@ -497,7 +557,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "relationship_style", domain: "relationship", kind_hint: "relationship_change", - user_prompt_hint: "当前候选在关系主题上仍分不开,可以再补一件记得大概时间的感情或关系变化;不要描述星座或类型标签。", + user_prompt_hint: abcdHint( + "当前候选在关系主题上仍分不开,可以再补一件记得大概时间的感情或关系变化。", + "D9", + "对照 D9 上升类型表只作校时方法。", + ), source: "varga_observation", }); } else if (d10?.candidates_differ && !declined.has("career")) { @@ -507,7 +571,11 @@ export function buildMethodFollowupPlan(input: { ask_theme: "career_style", domain: "career", kind_hint: "career_change", - user_prompt_hint: "当前候选在事业主题上仍分不开,可以再补一件记得大概时间的工作变化;不要描述类型标签。同一件事会同时对照本命第 10 宫和 D10。", + user_prompt_hint: abcdHint( + "当前候选在事业主题上仍分不开,可以再补一件记得大概时间的工作变化。同一件事会同时对照本命第 10 宫和 D10。", + "D10", + "可用事业类型表作校时对照。", + ), source: "varga_observation", }); } else if (d4?.candidates_differ && !declined.has("relocation")) { @@ -517,7 +585,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "home_change", domain: "relocation", kind_hint: "home_change", - user_prompt_hint: "当前候选在居所主题上仍分不开,可以再补一件记得大概时间的搬家或住处变化。", + user_prompt_hint: abcdHint( + "当前候选在居所主题上仍分不开,可以再补一件记得大概时间的搬家或住处变化。", + "D4", + ), source: "varga_observation", }); } else if (d5?.candidates_differ && !declined.has("education")) { @@ -527,7 +598,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "education_style", domain: "education", kind_hint: "education_milestone", - user_prompt_hint: "当前候选在学业或成就主题上仍分不开,可以再补一件记得大概时间的学业、考试或被委以责任的变化;不要贴类型标签。同一件事会对照 D5 和 D24。", + user_prompt_hint: abcdHint( + "当前候选在学业或成就主题上仍分不开,可以再补一件记得大概时间的学业、考试或被委以责任的变化。同一件事会对照 D5 和 D24。", + "D5 / D24", + ), source: "varga_observation", }); } else if ((d12?.candidates_differ || d7?.candidates_differ) && !declined.has("family")) { @@ -537,7 +611,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "family_event", domain: "family", kind_hint: "family_event", - user_prompt_hint: "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。同一件事会对照 D12、D7 和 D3。", + user_prompt_hint: abcdHint( + "当前候选在家人主题上仍分不开,可以再补一件记得大概时间的家人变化。同一件事会对照 D12、D7 和 D3。", + "D12 / D7 / D3", + ), source: "varga_observation", }); } else if (d11?.candidates_differ && financeCovered && !declined.has("finance")) { @@ -547,7 +624,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "finance_change", domain: "finance", kind_hint: "finance_change", - user_prompt_hint: "当前候选在财务主题上仍分不开,可以再补一件记得大概时间的收入、资产或财务变化。同一件事会对照 D2 和 D11。", + user_prompt_hint: abcdHint( + "当前候选在财务主题上仍分不开,可以再补一件记得大概时间的收入、资产或财务变化。同一件事会对照 D2 和 D11。", + "D2 / D11", + ), source: "varga_observation", }); } else if (d30?.candidates_differ && healthCovered && !declinedHealth(declined)) { @@ -557,7 +637,10 @@ export function buildMethodFollowupPlan(input: { ask_theme: "health_pressure", domain: "health_pressure", kind_hint: "self_health_event", - user_prompt_hint: "当前候选在健康压力主题上仍分不开,可以再补一件记得大概时间的健康、事故或压力变化。这不是医学判断。", + user_prompt_hint: abcdHint( + "当前候选在健康压力主题上仍分不开,可以再补一件记得大概时间的健康、事故或压力变化。这不是医学判断。", + "D30", + ), source: "varga_observation", }); } else if (input.nakshatraBoundary?.near_boundary) { diff --git a/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts b/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts index 4716903a..c9218e0a 100644 --- a/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts +++ b/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts @@ -1,13 +1,14 @@ /** * Public-safe P0/P1 refinement fields from the decision receipt. * - * D9/D10 type labels, scores, weights and event IDs are dropped. D1 sign - * names may appear on lagna contrast, matching the natal house table. + * Scores, weights and event IDs stay out of public copy. D9/D10 sign names + * may appear for the skill type-table verification report. D1 sign names may + * appear on lagna contrast, matching the natal house table. */ const TIME = /^(?:[01]\d|2[0-3]):[0-5]\d$/; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; -const DENIED = /lon|latitude|longitude|degree|score|weight|fingerprint|热情冲动|配偶类型|事业特质/i; +const DENIED = /lon|latitude|longitude|degree|score|weight|fingerprint/i; const LAYER_LABEL: Record = { d1: "本命上升", d9: "D9", @@ -98,6 +99,7 @@ export type EventDashaLedgerRow = Readonly<{ match: "strong" | "medium" | "weak" | "none"; match_label: string; user_meaning: string; + gochara: "activated" | "not_seen" | null; }>; export type DashaAgreement = Readonly<{ @@ -236,11 +238,16 @@ export function parseEventDashaLedger(value: unknown): readonly EventDashaLedger : match === "medium" ? "有关联" : match === "weak" ? "弱关联" : "未见对应"; + const gochara = row.gochara === "activated" || row.gochara === "not_seen" + ? row.gochara + : null; + const meaning = asText(row.user_meaning, 220) ?? `${summary}:${matchLabel}`; rows.push({ summary, match, match_label: matchLabel, - user_meaning: `${summary}:${matchLabel}`, + user_meaning: meaning, + gochara, }); } return rows; @@ -374,10 +381,38 @@ export function parseOosBlindPrompts(value: unknown): readonly OosBlindPrompt[] return rows; } +export type EventFitRate = Readonly<{ + matched: number; + total: number; + percent: number | null; + label: string; + user_meaning: string; + unique_minute_claim: false; +}>; + +export function parseEventFitRate(value: unknown): EventFitRate | null { + const row = asRecord(value); + if (!row) return null; + const matched = typeof row.matched === "number" && Number.isInteger(row.matched) ? row.matched : null; + const total = typeof row.total === "number" && Number.isInteger(row.total) ? row.total : null; + if (matched === null || total === null) return null; + const percent = typeof row.percent === "number" && Number.isFinite(row.percent) ? row.percent : null; + return { + matched, + total, + percent, + label: asText(row.label, 80) ?? "事件吻合率", + user_meaning: asText(row.user_meaning, 280) + ?? "这是相对拟合,不是已确认唯一出生分钟。", + unique_minute_claim: false, + }; +} + export function refinementFromDecisionReceipt( receipt: Readonly> | null | undefined, ): { event_dasha_ledger: readonly EventDashaLedgerRow[]; + event_fit_rate: EventFitRate | null; dasha_agreement: DashaAgreement | null; lagna_contrast: LagnaContrast | null; nakshatra_boundary: NakshatraBoundary | null; @@ -386,6 +421,7 @@ export function refinementFromDecisionReceipt( } { return { event_dasha_ledger: parseEventDashaLedger(receipt?.event_dasha_ledger), + event_fit_rate: parseEventFitRate(receipt?.event_fit_rate), dasha_agreement: parseDashaAgreement(receipt?.dasha_agreement), lagna_contrast: parseLagnaContrast(receipt?.lagna_contrast), nakshatra_boundary: parseNakshatraBoundary(receipt?.nakshatra_boundary), diff --git a/frontend/src/lib/rectification-agentic/v9/skill-verification-report.ts b/frontend/src/lib/rectification-agentic/v9/skill-verification-report.ts new file mode 100644 index 00000000..3207a3dd --- /dev/null +++ b/frontend/src/lib/rectification-agentic/v9/skill-verification-report.ts @@ -0,0 +1,163 @@ +/** + * Skill Path-C eight-method verification report for offer/adopt turns. + * + * 80%/60% language is event-fit rate for the current window, never a unique + * birth-minute confirmation. D9/D10 type tables are rectification method, not + * fate promises. + */ + +import type { EventDashaLedgerRow } from "./refinement-packet"; +import type { WindowScan } from "./varga-observations"; + +const D9_TYPE_TABLE: Readonly> = { + 白羊座: { trait: "主动、热情、冲动", spouse: "独立、有活力", marriage: "早期婚姻、激情型" }, + 金牛座: { trait: "稳定、务实、占有欲强", spouse: "稳重、有经济基础", marriage: "晚婚、稳定型" }, + 双子座: { trait: "沟通、多变、好奇心强", spouse: "聪明、善交流", marriage: "多次恋爱、友谊型" }, + 巨蟹座: { trait: "情感丰富、家庭导向", spouse: "温柔、顾家", marriage: "家庭型" }, + 狮子座: { trait: "骄傲、戏剧性、领导欲", spouse: "有魅力、有地位", marriage: "戏剧性" }, + 处女座: { trait: "完美主义、挑剔、服务型", spouse: "细致、有技能", marriage: "晚婚、服务型" }, + 天秤座: { trait: "和谐、美感、合作", spouse: "优雅、有艺术气质", marriage: "美满、合作型" }, + 天蝎座: { trait: "深刻、占有欲、转化", spouse: "神秘、有深度", marriage: "深刻、转化型" }, + 射手座: { trait: "自由、哲学、冒险", spouse: "开放、有学识", marriage: "自由型、精神伴侣" }, + 摩羯座: { trait: "务实、责任、延迟", spouse: "成熟、有事业", marriage: "晚婚、责任型" }, + 水瓶座: { trait: "独立、非传统、友谊", spouse: "独特、有理想", marriage: "非传统、友谊型" }, + 双鱼座: { trait: "浪漫、牺牲、灵性", spouse: "灵性、有艺术天赋", marriage: "灵性、牺牲型" }, +}; + +const D10_TYPE_TABLE: Readonly> = { + 白羊座: { trait: "领导、创业、竞争", occupation: "创业者、运动员、军人", style: "主动、竞争" }, + 金牛座: { trait: "稳定、财富、艺术", occupation: "金融、艺术、农业", style: "稳定、务实" }, + 双子座: { trait: "沟通、写作、教育", occupation: "教师、作家、销售", style: "多变、沟通" }, + 巨蟹座: { trait: "照顾、家庭、情感", occupation: "护理、餐饮、房地产", style: "照顾、情感" }, + 狮子座: { trait: "领导、表演、创意", occupation: "管理、娱乐、政治", style: "领导、表演" }, + 处女座: { trait: "服务、分析、健康", occupation: "医疗、分析、服务", style: "细致、服务" }, + 天秤座: { trait: "合作、美学、法律", occupation: "法律、艺术、咨询", style: "合作、和谐" }, + 天蝎座: { trait: "研究、转化、危机", occupation: "研究、心理学、危机管理", style: "深度、转化" }, + 射手座: { trait: "教育、哲学、国际", occupation: "教育、出版、国际事务", style: "自由、哲学" }, + 摩羯座: { trait: "管理、责任、延迟", occupation: "管理、政府、建筑", style: "务实、责任" }, + 水瓶座: { trait: "创新、科技、人道", occupation: "科技、人道主义、创新", style: "创新、独立" }, + 双鱼座: { trait: "灵性、艺术、服务", occupation: "艺术、灵性、医疗", style: "灵性、服务" }, +}; + +function signKey(value: string): string { + const trimmed = value.trim(); + if (trimmed.endsWith("座")) return trimmed; + return `${trimmed}座`; +} + +function typeRow(sign: string, table: "d9" | "d10"): string { + const key = signKey(sign); + if (table === "d9") { + const row = D9_TYPE_TABLE[key]; + if (!row) return `| ${key} | (当前窗上升,类型表无这一行) |`; + return `| ${key} | ${row.trait} | ${row.spouse} | ${row.marriage} |`; + } + const row = D10_TYPE_TABLE[key]; + if (!row) return `| ${key} | (当前窗上升,类型表无这一行) |`; + return `| ${key} | ${row.trait} | ${row.occupation} | ${row.style} |`; +} + +export type SkillVerificationReportInput = Readonly<{ + representativeTime: string | null; + widthMinutes: number | null; + candidates: readonly Readonly<{ time: string; rank: number; relativeSupport: number }>[]; + houseLagna?: string | null; + windowScan?: WindowScan | null; + eventDashaLedger?: readonly EventDashaLedgerRow[]; + eventFitRate?: Readonly<{ + matched: number; + total: number; + percent: number | null; + label: string; + user_meaning: string; + unique_minute_claim: false; + }> | null; + dashaAgreement?: Readonly<{ status: string; user_meaning: string }> | null; + techniqueAuditTable?: readonly Readonly<{ technique?: string; status?: string; note?: string }>[]; +}>; + +export function buildSkillVerificationReport(input: SkillVerificationReportInput): string { + const time = input.representativeTime ?? "尚未选定"; + const width = input.widthMinutes == null ? "未知" : `${input.widthMinutes} 分钟`; + const lagna = input.houseLagna?.trim() || "未交付"; + const fit = input.eventFitRate; + const ledger = input.eventDashaLedger ?? []; + const d9Signs = input.windowScan?.d9_sign_names ?? []; + const d10Signs = input.windowScan?.d10_sign_names ?? []; + const candidateLines = input.candidates.slice(0, 5).map((row) => ( + `| ${row.rank} | ${row.time} | ${row.relativeSupport} |` + )); + const dashaRows = ledger.length > 0 + ? ledger.map((row) => `| ${row.summary} | ${row.match_label} | ${row.user_meaning} |`) + : ["| (本轮还没有已确认事件行) | — | 先收带日期经历再填这张表 |"]; + const d9Table = d9Signs.length > 0 + ? ["| D9上升 | 感情特质 | 配偶类型 | 婚姻特点 |", "|---|---|---|---|", ...d9Signs.map((sign) => typeRow(sign, "d9"))] + : ["当前窗还没有可对照的 D9 上升名,类型表待候选换升后填写。"]; + const d10Table = d10Signs.length > 0 + ? ["| D10上升 | 事业特质 | 职业类型 | 工作风格 |", "|---|---|---|---|", ...d10Signs.map((sign) => typeRow(sign, "d10"))] + : ["当前窗还没有可对照的 D10 上升名,类型表待候选换升后填写。"]; + const audit = (input.techniqueAuditTable ?? []).slice(0, 16); + const auditRows = audit.length > 0 + ? audit.map((row) => `| ${row.technique ?? "技法"} | ${row.status ?? "blocked"} | ${row.note ?? ""} |`) + : [ + "| Vimshottari + 受控行运 | executed | 权重口径 40;事件吻合率不是唯一分钟 |", + "| D9/D10 分盘 | executed | 权重口径 35;类型表是校时方法 |", + "| 宫位 / 六亲六步 | executed | 权重口径 15;后三步方法论层 |", + "| Nakshatra Pada | observation_only | 权重口径 10;不确认唯一分钟 |", + "| KP / D60 / Gulika | observation_only | 不进提出门或确认门计分 |", + "| 占问 | observation_only | AI 暂不支持独立占问;有问起时间则观察 |", + ]; + + return [ + "## 生时纠正验证报告(skill 八法)", + "这是当前窗的相对拟合,标签是 `candidate_range_not_birth_time_truth`。采用只把代表性时间写入当前排盘,不是已确认唯一出生分钟。", + "", + "### 筛选", + `- 当前代表分钟:${time}`, + `- 不可分宽度:${width}`, + `- 本命上升(该分钟):${lagna}`, + input.windowScan?.d9_candidates_differ ? "- D9 仍会换升,关系类型对照只用来分开候选。" : "- D9 上升在当前窗较稳。", + input.windowScan?.d10_candidates_differ ? "- D10 仍会换升,事业类型对照只用来分开候选。" : "- D10 上升在当前窗较稳。", + "", + "### 候选窗(正文复述;卡片仍作采用控件)", + "| 排名 | 时间 | 相对支持 |", + "|---|---|---|", + ...(candidateLines.length > 0 ? candidateLines : ["| — | — | 还没有可出示的候选 |"]), + "", + "### 方法1:Dasha + Gochara", + fit + ? `${fit.user_meaning} ${fit.label}。` + : "还没有事件吻合率。有已确认事件后,80%/60% 只描述事件吻合率,不得写成已确认唯一出生分钟。", + "", + "| 事件 | 吻合 | Dasha / Gochara |", + "|---|---|---|", + ...dashaRows, + input.dashaAgreement ? `双轨:${input.dashaAgreement.user_meaning}` : "Narayana 对照未交付时只保留 Vimshottari。", + "", + "### 方法2–3:D9 / D10 类型表", + "写的是「该分钟下分盘升 X,与用户所述特质的对应/冲突」。这是校时方法,不是咨询命运承诺。", + ...d9Table, + "", + ...d10Table, + "", + "### 方法4:六亲宫位(Raman 六步)", + "前三步(宫、宫主、自然卡拉卡)可执行;后三步(日月重复、D9、正负多数)标方法论层。家人事件对照 D12 / D7 / D3。", + "", + "### 方法5–6:外貌 / 疤痕", + "只作体质验证、缩小范围,不升主评分,不定分钟。", + "", + "### 方法7:职业", + "对照 D1 第 10 宫 + D10,允许事业类型表。", + "", + "### 方法8:占问", + "状态:`not_used` / `observation_only`。产品可观察一次问起时间,不挡出牌。", + "", + "### Technique Audit Table", + "权重口径:Dasha 40 / 分盘 35 / 宫位 15 / Pada 10。KP、D60、Gulika 观察或 blocked。", + "| 技法 | 状态 | 说明 |", + "|---|---|---|", + ...auditRows, + "", + "不得把 VedAstro/holdout 或高吻合写成已校到唯一分钟。", + ].join("\n"); +} diff --git a/frontend/src/lib/rectification-agentic/v9/varga-observations.ts b/frontend/src/lib/rectification-agentic/v9/varga-observations.ts index e56283e2..a2aa5027 100644 --- a/frontend/src/lib/rectification-agentic/v9/varga-observations.ts +++ b/frontend/src/lib/rectification-agentic/v9/varga-observations.ts @@ -1,9 +1,9 @@ /** - * D9/D10 observations for follow-up routing only. + * D9/D10 observations for follow-up routing and the skill type-table report. * - * The engine may know Navamsa / Dasamsa lagna indices. This module projects - * counts, differ flags and change minutes. It never emits sign names, spouse - * types, career archetypes, or any other user-facing D9/D10 label. + * Sign names are kept for the eight-method verification report. They never + * grant a unique minute. Spouse/career type tables are assembled in + * skill-verification-report.ts, not here. */ import { @@ -37,6 +37,8 @@ export type WindowScan = Readonly<{ d2_candidates_differ: boolean; d11_candidates_differ: boolean; d30_candidates_differ: boolean; + d9_sign_names: readonly string[]; + d10_sign_names: readonly string[]; transitions: readonly WindowScanTransition[]; }>; @@ -65,9 +67,18 @@ function optionalCount(value: unknown): number { return asCount(value) ?? 0; } +function asSignNames(value: unknown): readonly string[] { + if (!Array.isArray(value)) return []; + return value + .filter((item): item is string => typeof item === "string") + .map((item) => item.trim()) + .filter((item) => item.length > 0 && item.length <= 12) + .slice(0, 12); +} + /** - * Keep only index counts, differ flags and reconstructed change minutes. - * Extra keys (sign names, lagna lists, type tables) are dropped. + * Keep index counts, differ flags, change minutes, and D9/D10 sign names. + * Unique-minute claims stay false. */ export function parseWindowScan(value: unknown): WindowScan | null { const row = asRecord(value); @@ -121,6 +132,8 @@ export function parseWindowScan(value: unknown): WindowScan | null { d2_candidates_differ: d2Differ, d11_candidates_differ: d11Differ, d30_candidates_differ: d30Differ, + d9_sign_names: asSignNames(row.d9_sign_names), + d10_sign_names: asSignNames(row.d10_sign_names), transitions: parseWindowScanTransitions(row.transitions), }; } diff --git a/frontend/src/mastra/agentic-rectification.ts b/frontend/src/mastra/agentic-rectification.ts index 5331d716..f67a372b 100644 --- a/frontend/src/mastra/agentic-rectification.ts +++ b/frontend/src/mastra/agentic-rectification.ts @@ -69,11 +69,11 @@ const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑 6. 工具执行过程保持静默。正文像正常人说话,不写“本轮做了什么”,不描述 Skill、Case、Dossier、工具、内部 Activity、参数、错误、内部 ID、评分、数据库、推理过程或密钥;完成凭证完全由服务端公开 Activity/receipt 展示。 7. 只基于成功 attempt 输出正文。工具失败时说明面向用户的边界,不声称未执行的方法或结果。 8. 当前轮新事件一律走 rectification-record-evidence-batch(一件也可以)。rectification-confirm-evidence 只用于用户对已有 pending 明确说“对/是”。不得要求用户把已说清的事件再发一遍。 -9. 不得在同一回复中一边要求继续补证据,一边提供候选采用。落实 next_user_action:id 不是 adopt_representative 时不得调用 rectification-offer-candidates,也不得请用户采用。selection_allowed 只表示可以采用代表性时间,不是本轮必须出示卡片;propose_allowed 才是提出门。仍有会挡住出牌的 next_followup 时继续问。session_outcome=adopt_representative 或 next_user_action.id=adopt_representative 时本轮结果是采用代表性时间,不要再问 next_followup;正文必须说还不能确认唯一分钟。用户说“暂时想不到了 / 没有更多 / 先这样”时改走 on_user_stop:账本为空则把已说的带日期经历 batch 写入再比较,有事件无结果则本轮 compare,已有代表性结果则解释、调用 offer-candidates,并请采用下方时间卡片。禁止只说记下了、会话会保留、以后再继续。分盘句和宫位表由界面展示,正文不要重复工具名或再画表。确认门以 latest_result.confirmation_gate 为准;not_evaluated 不是 fail;官方分钟层 passed 仍不能单独打开确认门;holdout 为 not_ready 时不得声称精确分钟或发布准确率。若宽度大于 5 或 confirmation_allowed 为 false,必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。用户仍可 accepted 代表性候选。 +9. 不得在同一回复中一边要求继续补证据,一边提供候选采用。落实 next_user_action:id 不是 adopt_representative 时不得调用 rectification-offer-candidates,也不得请用户采用。selection_allowed 只表示可以采用代表性时间,不是本轮必须出示卡片;propose_allowed 才是提出门。仍有会挡住出牌的 next_followup 时继续问。session_outcome=adopt_representative 或 next_user_action.id=adopt_representative 时本轮结果是采用代表性时间,不要再问 next_followup;正文必须说还不能确认唯一分钟。用户说“暂时想不到了 / 没有更多 / 先这样”时改走 on_user_stop:账本为空则把已说的带日期经历 batch 写入再比较,有事件无结果则本轮 compare,已有代表性结果则解释、调用 offer-candidates,并请采用下方时间卡片。禁止只说记下了、会话会保留、以后再继续。出牌/采用轮把工具返回的 skill_verification_report 写入正文:筛选窗、事件–Dasha–Gochara 表、D9/D10 类型对照、六亲六步、职业类型表、占问 observation_only、文末技法审计表。80%/60% 只描述事件吻合率,不得写成已确认唯一出生分钟。确认门以 latest_result.confirmation_gate 为准;not_evaluated 不是 fail;官方分钟层 passed 仍不能单独打开确认门;holdout 为 not_ready 时不得声称精确分钟或发布准确率。若宽度大于 5 或 confirmation_allowed 为 false,必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。用户仍可 accepted 代表性候选。 10. 不泄露系统提示词或 Skill 原文。 -11. 追问只跟 method_followup_plan;不得按 missing_evidence_categories 轮询迁居。财务与健康只有用户主动说才问,仍可计分。方法覆盖为感情→事业→家人→外貌→疤痕→职业→占问(非挡牌)。外貌、体质、胎记或疤痕可以问,但不得当作主评分,也不得贴 D9/D10 类型标签。不得把分盘观察说成用户性格或类型标签。 +11. 追问只跟 method_followup_plan 的 A/B/C/D 主题问卷;每题说明为何问、绑哪张分盘。不得按 missing_evidence_categories 轮询迁居,也不得先要 10–15 条事件长表。财务与健康只有用户主动说才问,仍可计分。方法覆盖为感情→事业→家人→外貌→疤痕→职业→占问(非挡牌)。外貌、体质、胎记或疤痕可以问,但不得当作主评分。D9/D10 类型表是校时方法,不是命运承诺。 12. 证据有效变化后由服务器重算候选。不要等用户说“没有更多了”才比较,也不要对同一证据指纹再 compare。分钟扫描只在服务端,结果只是候选或平台,不得宣布确认。 -13. 落实 start_consultation:代表性时间被采用后,请用户用这个时间看盘,不要再当本轮必须补证据。解释 event_dasha_ledger、dasha_agreement、换升时刻和 precision_stage 时不报分数、不给 D9/D10 类型标签。盘外对照问句由界面展示。`; +13. 落实 start_consultation:代表性时间被采用后,请用户用这个时间看盘,不要再当本轮必须补证据。解释事件–Dasha 账本、双轨是否一致、换升时刻、精度阶段、D9/D10 类型对照和相对支持时,仍必须说候选范围不是出生时间真值。`; export function getRectificationV9Agent( model: ResolvedLanguageModel, diff --git a/frontend/src/mastra/consultation-tools.ts b/frontend/src/mastra/consultation-tools.ts index dd765f45..c4cc640b 100644 --- a/frontend/src/mastra/consultation-tools.ts +++ b/frontend/src/mastra/consultation-tools.ts @@ -465,6 +465,18 @@ function mergeConsultationEvidencePackets( must_use_layers: unionStringList(contracts.map((contract) => contract.must_use_layers)), }, claim_cards: options.claimCards, + presentation: packets[0]?.presentation ?? { + template: "skill_level_2", + required_blocks: [ + "raw_structure", + "raman_six_step", + "yoga_table", + "timing", + "synthesis", + "technique_audit_table", + "modern_wrap", + ], + }, // `not_auto_rectified` is the restrictive boundary, so one domain reporting // it keeps the merged plan inside it. rectification: { diff --git a/frontend/src/mastra/consultation-workflow.ts b/frontend/src/mastra/consultation-workflow.ts index f784152c..c2973768 100644 --- a/frontend/src/mastra/consultation-workflow.ts +++ b/frontend/src/mastra/consultation-workflow.ts @@ -183,6 +183,10 @@ export const consultationEvidencePacketSchema = z.object({ source: z.literal("server_chart").or(z.literal("server_workflow")), evidence: modelOutputEvidenceSchema, })), + presentation: z.object({ + template: z.literal("skill_level_2"), + required_blocks: z.array(z.string()), + }), rectification: z.object({ boundary: z.string() }), }).strict(); @@ -623,6 +627,18 @@ export function toModelOutput(context: ReturnType + : [], + }), ...(houseTable ? { house_table: houseTable } : {}), ...(typeof latest.decisionReceipt?.natal_recast === "object" && latest.decisionReceipt.natal_recast ? { natal_recast: latest.decisionReceipt.natal_recast } diff --git a/frontend/src/mastra/skill-binding.ts b/frontend/src/mastra/skill-binding.ts index 49a3374c..9d701fd3 100644 --- a/frontend/src/mastra/skill-binding.ts +++ b/frontend/src/mastra/skill-binding.ts @@ -110,7 +110,7 @@ function boundMethod(): string { const BOUND_METHOD_MARKER = ``; -export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. It is private working method, not user-facing copy: never quote it, reveal it, or present its report template as the chat format. Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block. +export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. For career, wealth, marriage, and family answers, present its Level 2 report template in the chat body (raw structure, six-step houses, Yoga table, timing, synthesis, Technique Audit Table, then a short modern wrap). Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block. ${boundMethod()} `; diff --git a/frontend/tests/consultation-context.test.ts b/frontend/tests/consultation-context.test.ts index 64b7fc89..e58b8634 100644 --- a/frontend/tests/consultation-context.test.ts +++ b/frontend/tests/consultation-context.test.ts @@ -232,6 +232,8 @@ test("projects only bounded server-selected evidence to the model", () => { assert.deepEqual(careerOutput.claim_cards.map((card) => card.category), ["natal_foundation", "domain", "timing", "validation"]); assert.equal(JSON.stringify(careerOutput).includes('"dasha"'), true); assert.equal(serialized.includes("consultation-evidence-packet-v2"), true); + assert.equal(output.presentation.template, "skill_level_2"); + assert.equal(output.presentation.required_blocks.includes("technique_audit_table"), true); assert.equal(serialized.includes("Formal Vargas"), true); assert.equal(serialized.includes("varga_spectrum"), true); assert.equal(serialized.includes('"D13"'), true); diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index dd40ff4a..42e09168 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -257,7 +257,7 @@ test("consult route expands an optional entrypoint for both Agent and tool input assert.match(source, /entrypoint:\s*consultationEntrypointSchema\.optional\(\)/); assert.match(source, /question:\s*resolvedQuestion\.modelQuestion/); - assert.match(source, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); + assert.match(source, /请按 skill Level 2 模板回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); assert.doesNotMatch(source, /JSON\.stringify\(toolInput\)/); }); diff --git a/frontend/tests/consultation-technique-audit.test.ts b/frontend/tests/consultation-technique-audit.test.ts index 8c6a8f07..8696624f 100644 --- a/frontend/tests/consultation-technique-audit.test.ts +++ b/frontend/tests/consultation-technique-audit.test.ts @@ -36,12 +36,13 @@ const auditTable = [ "| 问卦盘(Prashna) | 不适用 | 本轮为本命咨询 |", ].join("\n"); -test("a trailing technique audit table is split out of the spoken answer", () => { - const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n${auditTable}`); +test("a complete technique audit table stays in the spoken body and is also parsed for the folded copy", () => { + const full = `${spoken}\n\n${auditTable}`; + const split = splitSpokenAnswerAndTechniqueAudit(full); assert.equal(split.complete, true); - assert.equal(split.spoken, spoken); + assert.equal(split.spoken, full); assert.equal(split.spoken.includes("适合推进"), true); - assert.equal(split.spoken.includes("技法"), false); + assert.equal(split.spoken.includes("技法"), true); assert.deepEqual(split.rows, [ { technique: "VedAstro 云状态", status: "blocked", note: "未经核验的官方云证据,置信度封顶" }, { technique: "功能吉凶星", status: "executed", note: "功能属性与自然属性冲突时降置信" }, diff --git a/frontend/tests/consultation-voice-contract.test.ts b/frontend/tests/consultation-voice-contract.test.ts index d67c4c35..841bff35 100644 --- a/frontend/tests/consultation-voice-contract.test.ts +++ b/frontend/tests/consultation-voice-contract.test.ts @@ -6,41 +6,43 @@ const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8"); const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); const binding = readFileSync(new URL("../src/mastra/skill-binding.ts", import.meta.url), "utf8"); +const workflow = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8"); -test("product voice keeps skill technique density without dumping JSON", () => { - assert.match(voice, /VISIBLE VOICE \(spoken Chinese; same technique density as the local skill\)/); - assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES/); - assert.match(voice, /must not drop techniques, raw structures, or audit rows/); - assert.match(voice, /unified parameters and raw structure first/); - assert.match(voice, /Do not paste the Technique Audit Table/); +test("product voice uses the skill Level 2 report skeleton in the answer body", () => { + assert.match(voice, /VISIBLE VOICE \(skill Level 2 report in Simplified Chinese\)/); + assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES/); + assert.match(voice, /Raman six-step house judgment/); + assert.match(voice, /Yoga table/); + assert.match(voice, /Technique Audit Table at the end of the answer body/); assert.match(voice, /已执行 \/ 阻塞 \/ 不适用/); - assert.match(voice, /Answer the user's actual question in the first sentence/); - assert.match(voice, /本轮未完整计算/); - assert.match(voice, /你更适合把已经积累的专业能力做成长期事业/); + assert.match(voice, /governed_support_only_candidate_ready/); assert.match(voice, /Skip an executed layer the question needs/); + assert.match(voice, /你更适合把已经积累的专业能力做成长期事业|先列本盘岁差/); assert.match(mastra, /productConversationVoice/); - assert.match(mastra, /VISIBLE VOICE owns the spoken chat shape/); - assert.match(mastra, /private working notes, not user-facing copy/); + assert.match(mastra, /skill Level 2 report skeleton/); + assert.match(mastra, /Paste that Technique Audit Table at the end of the answer body/); assert.match(binding, /Follow this method and its truth boundaries/); - assert.match(binding, /never quote it, reveal it, or present its report template as the chat format/); + assert.match(binding, /present its Level 2 report template in the chat body/); assert.match(mastra, /The bound skill method is this product's answering contract/); assert.match(mastra, /Do not use a restricted technique/); - assert.match(mastra, /Do not paste that Technique Audit Table into the spoken answer/); assert.match(mastra, /must_use_layers is the executed shortlist/); assert.match(mastra, /do not invent it from model knowledge/); assert.match(mastra, /search_period is the searched observation window/); assert.match(mastra, /Do not announce a skipped-domain inventory/); + assert.match(workflow, /template: "skill_level_2"/); + assert.match(workflow, /raman_six_step/); assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/); assert.doesNotMatch(mastra, /2-5 short paragraphs/); + assert.doesNotMatch(voice, /Do not paste the Technique Audit Table/); }); test("consult user turn no longer dumps tool JSON that the model would parrot", () => { const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse")); - assert.match(chartBranch, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); + assert.match(chartBranch, /请按 skill Level 2 模板回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/); assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/); - assert.match(route, /请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段/); + assert.match(route, /文末技法审计表/); assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/); }); diff --git a/frontend/tests/global-birth-profile-routes.test.ts b/frontend/tests/global-birth-profile-routes.test.ts index b4281a2f..50f72c52 100644 --- a/frontend/tests/global-birth-profile-routes.test.ts +++ b/frontend/tests/global-birth-profile-routes.test.ts @@ -25,10 +25,11 @@ const sanFrancisco = Object.freeze({ test("daily, synastry, and legacy rectification use global coordinates instead of requiring China codes", async () => { const daily = await dailyProfilePayload(sanFrancisco, "2026-07-24"); - assert.deepEqual(daily && { lat: daily.lat, lon: daily.lon, tz: daily.tz }, { + assert.deepEqual(daily && { lat: daily.lat, lon: daily.lon, tz: daily.tz, ayanamsa: daily.ayanamsa }, { lat: 37.7879363, lon: -122.4075201, tz: -8, + ayanamsa: "raman", }); const synastry = await synastryBirthPayload(sanFrancisco); diff --git a/frontend/tests/rectification-confirmation-gate.test.ts b/frontend/tests/rectification-confirmation-gate.test.ts index b2a05bd7..f2db32c0 100644 --- a/frontend/tests/rectification-confirmation-gate.test.ts +++ b/frontend/tests/rectification-confirmation-gate.test.ts @@ -159,7 +159,10 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as }); const serialized = JSON.stringify(projection); assert.equal(projection.confirmation_allowed, false); - assert.doesNotMatch(serialized, /±2|80%|60%/); + assert.equal(projection.unique_minute_claim, false); + assert.doesNotMatch(serialized, /±2 分钟/); + assert.match(serialized, /candidate_range_not_birth_time_truth/); + assert.match(serialized, /不是已确认唯一出生分钟|不得写成已确认唯一出生分钟/); assert.doesNotMatch(serialized, /obama|barack|angelina|jolie/i); assert.match(skill, /confirmation_gate/); assert.match(skill, /not_evaluated/); diff --git a/frontend/tests/rectification-eight-method.test.ts b/frontend/tests/rectification-eight-method.test.ts index 05484d43..f362f98f 100644 --- a/frontend/tests/rectification-eight-method.test.ts +++ b/frontend/tests/rectification-eight-method.test.ts @@ -32,7 +32,7 @@ import { const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883"; const EDUCATION_ID = "44444444-4444-4444-8444-444444444441"; -const FORBIDDEN_LABELS = /白羊|金牛|双子|巨蟹|狮子|处女|天秤|天蝎|射手|摩羯|水瓶|双鱼|热情冲动|配偶类型|事业特质/; +const UNIQUE_MINUTE_COPY = /±5 分钟确定性/; const CLASSIC_COVERAGE = [ { status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null }, @@ -178,8 +178,10 @@ test("user-stop action explains the window when follow-up remains but the user s assert.equal(plan.methods.find((item) => item.method_id === "marks")?.status, "uncovered"); assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered"); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); - assert.notEqual(plan.next_followup?.method_id, "appearance"); - assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS); + assert.equal(plan.next_followup?.method_id, "d9_relationship"); + assert.match(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); + assert.match(plan.next_followup?.user_prompt_hint ?? "", /D9/); + assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("selectionAllowed with remaining method follow-up keeps collecting and only adopts on stop", () => { @@ -239,7 +241,7 @@ test("declined relationship skips to career and leaves horary uncovered", () => assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered"); }); -test("D9 differ becomes an internal ask theme without sign labels", () => { +test("D9 differ keeps sign names for the type-table report and still forbids unique-minute claims", () => { const scan = parseWindowScan({ scanned: true, confirmation_allowed: true, @@ -260,6 +262,7 @@ test("D9 differ becomes an internal ask theme without sign labels", () => { assert.equal(scan.confirmation_allowed, false); assert.equal(scan.unique_minute_claim, false); assert.equal(scan.d9_candidates_differ, true); + assert.deepEqual(scan.d9_sign_names, ["白羊座", "天蝎"]); assert.deepEqual(scan.transitions, [{ layer: "d9", at: "05:14", @@ -276,14 +279,15 @@ test("D9 differ becomes an internal ask theme without sign labels", () => { { layer: "d11", candidates_differ: false, ask_theme: null }, { layer: "d30", candidates_differ: false, ask_theme: null }, ]); - assert.doesNotMatch(JSON.stringify({ scan, observations }), FORBIDDEN_LABELS); + assert.doesNotMatch(JSON.stringify({ scan, observations }), UNIQUE_MINUTE_COPY); const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE, observations, }); assert.equal(plan.next_followup?.source, "varga_observation"); assert.equal(plan.next_followup?.ask_theme, "relationship_style"); - assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS); + assert.match(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); + assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("D24-only window change folds into education follow-up without a second layer", () => { @@ -393,7 +397,7 @@ test("window scan displays KP sub-lord changes without opening confirmation", () ]); }); -test("read-case follows method plan and hides D9/D10 labels even when SQL missing categories rotate", async () => { +test("read-case follows method plan and keeps D9/D10 type tables when SQL missing categories rotate", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ @@ -485,7 +489,8 @@ test("read-case follows method plan and hides D9/D10 labels even when SQL missin assert.equal(projection.latest_result.confirmation_allowed, false); assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25); assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true); - assert.doesNotMatch(JSON.stringify(projection), FORBIDDEN_LABELS); + assert.doesNotMatch(JSON.stringify(projection), UNIQUE_MINUTE_COPY); + assert.match(JSON.stringify(projection), /A\/B\/C\/D/); }); test("accepted batch evidence triggers server rescore without offering adoption", async () => { @@ -548,7 +553,7 @@ test("accepted batch evidence triggers server rescore without offering adoption" assert.ok(persistCall); const receipt = persistCall.args.p_decision_receipt as { window_scan?: { d9_candidates_differ?: boolean; d9_sign_names?: unknown } }; assert.equal(receipt.window_scan?.d9_candidates_differ, true); - assert.equal("d9_sign_names" in (receipt.window_scan ?? {}), false); + assert.deepEqual((receipt.window_scan as { d9_sign_names?: string[] } | undefined)?.d9_sign_names, ["白羊座", "天蝎"]); const completedReceipt = accounting.calls.find((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === "rectification-record-evidence-batch" @@ -563,7 +568,7 @@ test("accepted batch evidence triggers server rescore without offering adoption" ), false, ); - assert.doesNotMatch(JSON.stringify(result), FORBIDDEN_LABELS); + assert.doesNotMatch(JSON.stringify(result), UNIQUE_MINUTE_COPY); } finally { restore(); } @@ -644,12 +649,16 @@ test("public tool surface stays at 13 and new cases bind 10.0.10", () => { algorithmVersion: "rectification-v5", }); assert.equal(plateau.confirmation_allowed, false); + assert.equal(plateau.unique_minute_claim, false); + assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/); + assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/); + assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY); assert.equal((plateau.session_outcome as { kind: string }).kind, "collect_evidence"); const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8"); assert.match(skill, /method_followup_plan/); assert.match(skill, /感情 → 事业 → 家人 → 外貌 → 疤痕 → 职业 → 占问/); assert.match(skill, /KP 观察不计分、不挡提出门/); - assert.match(skill, /不得给用户贴 D9\/D10 星座或类型标签/); + assert.match(skill, /D9\/D10 类型表是校时方法/); assert.doesNotMatch(skill, /±5 分钟确定性/); assert.doesNotMatch(skill, /KP 政策跳过不挡提出门/); }); @@ -695,7 +704,8 @@ test("family then appearance then marks then occupation then horary follow the m assert.equal(afterMarks.next_followup?.method_id, "occupation"); assert.equal(afterMarks.next_followup?.domain, "occupation"); assert.match(afterMarks.next_followup?.user_prompt_hint ?? "", /第 10 宫|D10/); - assert.doesNotMatch(afterMarks.next_followup?.user_prompt_hint ?? "", FORBIDDEN_LABELS); + assert.match(afterMarks.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); + assert.match(afterMarks.next_followup?.user_prompt_hint ?? "", /事业类型表/); const afterOccupation = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, @@ -728,7 +738,8 @@ test("precision stage lagna_frame asks another dated event instead of rotating d }); assert.equal(plan.next_followup?.source, "precision_stage"); assert.equal(plan.next_followup?.ask_theme, "dated_event"); - assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS); + assert.match(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); + assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("precision stage d4 asks home change not family, and d5 asks education", () => { @@ -753,7 +764,7 @@ test("precision stage d4 asks home change not family, and d5 asks education", () }); assert.equal(d5.next_followup?.method_id, "d5_education"); assert.equal(d5.next_followup?.domain, "education"); - assert.doesNotMatch(JSON.stringify(d4), FORBIDDEN_LABELS); + assert.doesNotMatch(JSON.stringify(d4), UNIQUE_MINUTE_COPY); }); test("accepted representative time hands off to consultation", () => { @@ -786,7 +797,7 @@ test("accepted representative time hands off to consultation", () => { }); assert.equal(action.id, "start_consultation"); assert.match(action.user_meaning, /看盘/); - assert.doesNotMatch(JSON.stringify({ plan, action }), FORBIDDEN_LABELS); + assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY); }); test("d9_refine after relationship still asks uncovered career first", () => { diff --git a/frontend/tests/rectification-v9-agent.test.ts b/frontend/tests/rectification-v9-agent.test.ts index 9e66472e..ba15f241 100644 --- a/frontend/tests/rectification-v9-agent.test.ts +++ b/frontend/tests/rectification-v9-agent.test.ts @@ -50,9 +50,10 @@ test("system prompt carries only high-priority boundaries, never the method copy assert.doesNotMatch(prompt, /rectification-gate[\s\S]*rectification-scan/); assert.doesNotMatch(prompt, /rectification-score[\s\S]*rectification-diagnostics/); assert.doesNotMatch(prompt, /rectification-confirm[\s\S]*rectification-save-birth-time/); - assert.doesNotMatch(prompt, /80%\/60%/); assert.doesNotMatch(prompt, /10[–-]15 个事件/); - assert.doesNotMatch(prompt, /D9\/D10 类型表/); + assert.match(prompt, /D9\/D10 类型/); + assert.match(prompt, /80%\/60%/); + assert.match(prompt, /skill_verification_report/); assert.doesNotMatch(prompt, /run the required gate/); assert.doesNotMatch(prompt, /candidate_range/); assert.match(prompt, /jyotish-birth-time-rectification/); @@ -66,7 +67,8 @@ test("system prompt carries only high-priority boundaries, never the method copy assert.match(prompt, /rectification-offer-candidates/); assert.match(prompt, /on_user_stop/); assert.match(prompt, /禁止只说记下了/); - assert.match(prompt, /分盘句和宫位表由界面展示/); + assert.match(prompt, /skill_verification_report/); + assert.doesNotMatch(prompt, /分盘句和宫位表由界面展示/); assert.doesNotMatch(prompt, /不是整张宫位表/); assert.doesNotMatch(prompt, /分别 propose\+confirm/); // Keep the prompt short (~30 lines max). diff --git a/frontend/tests/rectification-v9-contracts.test.ts b/frontend/tests/rectification-v9-contracts.test.ts index 959f5e69..2f6de287 100644 --- a/frontend/tests/rectification-v9-contracts.test.ts +++ b/frontend/tests/rectification-v9-contracts.test.ts @@ -102,15 +102,17 @@ test("the active rectification skill pins the v10 identity and lives in the righ assert.ok(skill.split("\n").length <= 200, "SKILL.md must stay within 200 lines"); }); -test("skill keeps the method in the skill, not a hard-coded questionnaire", () => { - // The skill may only mention the old questionnaire as an explicit - // prohibition, never as a requirement. +test("skill keeps Path C A/B/C/D questions and forbids unique-minute claims", () => { assert.match(skill, /不再有固定 10[–-]15 个事件/); - assert.match(skill, /固定 80%\/60% 匹配率/); - assert.match(skill, /固定 A\/B\/C\/D 问卷/); + assert.match(skill, /A\/B\/C\/D 主题问卷/); + assert.match(skill, /80%\/60%/); + assert.match(skill, /事件吻合率/); + assert.match(skill, /D9\/D10 类型表/); assert.match(skill, /不得伪造出生分钟/); assert.match(skill, /日期精度真实保留/); assert.match(skill, /candidate[\s\S]*accepted[\s\S]*confirmed/); + assert.match(skill, /candidate_range_not_birth_time_truth/); + assert.doesNotMatch(skill, /固定 A\/B\/C\/D 问卷、D9\/D10 类型表贴标签/); }); test("evidence model exposes the full kind/domain/precision/status sets", () => { diff --git a/frontend/tests/skill-registry.test.ts b/frontend/tests/skill-registry.test.ts index 8c979b46..761acdcb 100644 --- a/frontend/tests/skill-registry.test.ts +++ b/frontend/tests/skill-registry.test.ts @@ -86,7 +86,7 @@ test("checked-in registry verifies hashed product packages and leaves consult on { name: "jyotish-birth-time-rectification", version: "10.0.10", - sha256: "a1dc47e853bd188c04a20de29906d011da21cbd297cb2d2b420684f2cd0a9236", + sha256: "f5b4a0c05f7acc11dd0add6e95f5d77ab14e676bdbfd6f5ab797869e35994369", }, { name: "jyotish-personal-report", diff --git a/references/oracle/raman_2026_08_20/raman_ayanamsa_parameter_support_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_ayanamsa_parameter_support_packet_2026_08_20.json new file mode 100755 index 00000000..27dff967 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_ayanamsa_parameter_support_packet_2026_08_20.json @@ -0,0 +1,58 @@ +{ + "schema_version": "raman_parameter_support_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_ayanamsa_parameter_support", + "label": "Raman Ayanamsa Parameter Support Packet", + "status": "runtime_foundation_supported_not_full_external_parity", + "claim_boundary": "This packet records explicit Raman parameter propagation in local runtime layers. It does not claim full same-number parity with every external Raman-capable product or UI label surface.", + "source_basis": [ + "scripts/ayanamsa_utils.py", + "jyotish-app/jyotish-engine.js", + "docs/research/ayanamsa_software_logic_and_local_audit_2026_08_20.md" + ], + "support_layers": [ + { + "layer_id": "backend_swiss_mapping", + "layer_type": "runtime_mapping_layer", + "admission_state": "supported", + "summary": "Backend shared Swiss mapping explicitly supports `raman` and preserves school-specific selection instead of collapsing to Lahiri.", + "evidence_paths": [ + "scripts/ayanamsa_utils.py", + "docs/research/ayanamsa_software_logic_and_local_audit_2026_08_20.md" + ], + "promotion_blockers": [ + "external_product_specific_same_number_parity_not_closed" + ] + }, + { + "layer_id": "browser_fallback_mapping", + "layer_type": "frontend_mapping_layer", + "admission_state": "supported", + "summary": "Browser fallback mapping explicitly supports Raman and preserves sidereal-mode differences instead of silently normalizing to Lahiri.", + "evidence_paths": [ + "jyotish-app/jyotish-engine.js", + "docs/research/ayanamsa_software_logic_and_local_audit_2026_08_20.md" + ], + "promotion_blockers": [ + "display_layer_label_consistency_still_needs_audit" + ] + }, + { + "layer_id": "unsupported_name_fallback_block", + "layer_type": "validation_boundary", + "admission_state": "blocked_silent_fallback_removed", + "summary": "Unsupported ayanamsa names no longer silently normalize to Lahiri; unsupported settings must fail explicitly.", + "evidence_paths": [ + "docs/research/ayanamsa_software_logic_and_local_audit_2026_08_20.md" + ], + "promotion_blockers": [ + "report_export_display_layers_not_fully_audited" + ] + } + ], + "acceptance_notes": [ + "Raman is a real supported local parameter", + "school variance remains distinct from local defects", + "external parity and label consistency are still separate closure tasks" + ] +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..bee98748 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json @@ -0,0 +1,129 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_nabhasa_chandra_sunapha_batch", + "label": "Raman BVR Nabhasa / Chandra-Sunapha First Admission Batch", + "scope": "first_admission_subrule_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records first-batch closure state for six low-risk structural yogas. It does not promote the batch into runtime truth or broad interpretive certainty.", + "batch_goal": "Run the first full promotion chain for one low-risk, structurally clean Raman batch: member freeze -> fixture receipt -> mapping receipt -> claim boundary -> next promotion gate.", + "selection_rationale": [ + "structural and lower-risk compared with health, mortality, or family-history rule families", + "already present in yoga fixtures and rule-id mapping layers", + "suitable for separating rule identification from phala language", + "good first candidate for batch-level governed promotion work" + ], + "member_rules": [ + { + "rule_id": "bvr_002_sunapha_precise", + "display_name": "Sunaphaa Yoga", + "subfamily": "chandra_pattern", + "canonical_mapping": "bvr_sunaphaa_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "phala_language_not_separated_from_structure_in_admission_packet" + ] + }, + { + "rule_id": "bvr_003_anapha_precise", + "display_name": "Anaphaa Yoga", + "subfamily": "chandra_pattern", + "canonical_mapping": "bvr_anaphaa_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "phala_language_not_separated_from_structure_in_admission_packet" + ] + }, + { + "rule_id": "bvr_004_duradhara_precise", + "display_name": "Duradhara Yoga", + "subfamily": "chandra_pattern", + "canonical_mapping": "bvr_duradhara_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "needs_explicit_boundary_against_overreading_as_full_phala_truth" + ] + }, + { + "rule_id": "bvr_017_vosi_precise", + "display_name": "Vosi Yoga", + "subfamily": "surya_pattern", + "canonical_mapping": "bvr_vosi_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "support_language_not_yet standardised" + ] + }, + { + "rule_id": "bvr_018_ubhayachara_precise", + "display_name": "Ubhayachara Yoga", + "subfamily": "surya_pattern", + "canonical_mapping": "bvr_ubhayachara_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "support_language_not_yet standardised" + ] + }, + { + "rule_id": "bvr_075_naukaa_precise", + "display_name": "Naukaa Yoga", + "subfamily": "nabhasa_pattern", + "canonical_mapping": "bvr_naukaa_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "test_suite_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "rule_packet_not_yet_frozen_as_batch_member", + "nabhasa_batch_neighbors_not_yet grouped into same governed step" + ] + } + ], + "shared_evidence_paths": [ + "tests/yoga_test_suite.json", + "references/yoga_rules.json", + "references/validation_logic_report.json", + "scripts/validate_logic_v2.py", + "scripts/add_high_confidence_yogas_batch1.py" + ], + "batch_acceptance_criteria": [ + "all six member rules frozen in one governed packet", + "fixture receipt present for each member rule", + "validation mapping receipt present for each member rule", + "member rules grouped by subfamily with explicit claim boundaries", + "batch marked support-only candidate rather than truth-upgraded" + ], + "next_promotion_steps": [ + "freeze a canonical six-rule member list", + "separate structure detection from phala language for each member", + "add a batch-level validator or audit script", + "decide whether Sunaphaa and Anaphaa canonical spellings should be normalized at the governed layer", + "prepare second-step packet for nabhasa neighbors such as vihaga, yuga, gola, matsya, koorma, sara, and ishu" + ], + "blocked_items": [ + "batch_not_yet_bound_to_runtime_promotion_gate", + "batch_not_yet_case_calibrated", + "broad_nabhasa_family_neighbors_not_yet_admitted_in_same_packet" + ] +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..e87557b3 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json @@ -0,0 +1,63 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_children_batch", + "label": "Raman BVR Relationship / Children Batch", + "scope": "narrowed_family_subbatch_governance_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records a narrowed children-pattern Raman relationship batch. Passing the gate supports governed support-only candidacy only, not deterministic fertility, childbirth, or third-party child-history truth claims.", + "member_rules": [ + { + "rule_id": "bvr_bahu_puthra_precise", + "display_name": "Bahu Puthra Yoga", + "subfamily": "children_pattern", + "canonical_mapping": "bvr_bahu_puthra_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "third_party_child_history_truth_authority_not_granted", + "case_calibration_not_closed" + ] + }, + { + "rule_id": "bvr_eka_puthra_precise", + "display_name": "Eka Puthra Yoga", + "subfamily": "children_pattern", + "canonical_mapping": "bvr_eka_puthra_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "third_party_child_history_truth_authority_not_granted", + "case_calibration_not_closed" + ] + }, + { + "rule_id": "bvr_dattha_puthra_yoga", + "display_name": "Dattha Puthra Yoga", + "subfamily": "children_pattern", + "canonical_mapping": "bvr_dattha_puthra_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "third_party_child_history_truth_authority_not_granted", + "case_calibration_not_closed" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json", + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "batch_acceptance_criteria": [ + "three children-pattern members frozen in one governed packet", + "fixture receipt present for each admitted member", + "validation mapping receipt present for each admitted member", + "children-pattern batch remains support-only rather than truth-upgraded" + ], + "boundary": "This narrowed children-pattern batch leaves spouse, mother, and sibling subfamilies out of the ready promotion step." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..7b1bff20 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json @@ -0,0 +1,31 @@ +{ + "schema_version": "raman_bvr_batch_fixture_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_children_batch", + "status": "fixture_packet_ready", + "sources": [ + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "fixtures": [ + { + "rule_id": "bvr_bahu_puthra_precise", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "scripts/validate_logic_v2.py", + "evidence_type": "repo_fixture_and_mapping_receipt" + }, + { + "rule_id": "bvr_eka_puthra_precise", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "scripts/validate_logic_v2.py", + "evidence_type": "repo_fixture_and_mapping_receipt" + }, + { + "rule_id": "bvr_dattha_puthra_yoga", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "scripts/validate_logic_v2.py", + "evidence_type": "repo_fixture_and_mapping_receipt" + } + ], + "boundary": "These receipts support governed support-only candidacy for a narrowed children-pattern batch. They do not authorize fertility, childbirth, or third-party child-history truth claims." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..e3522a36 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json @@ -0,0 +1,77 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_mother_batch", + "label": "Raman BVR Relationship / Mother Batch", + "scope": "narrowed_family_subbatch_governance_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records a narrowed mother-pattern Raman relationship batch. Passing the gate supports governed support-only candidacy only, not deterministic family-history or third-party truth claims.", + "member_rules": [ + { + "rule_id": "bvr_matru_sneha_yoga", + "display_name": "Matru Sneha Yoga", + "subfamily": "mother_pattern", + "canonical_mapping": "bvr_matru_sneha_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "family_history_truth_authority_not_granted", + "case_calibration_not_closed" + ] + }, + { + "rule_id": "bvr_matru_satrutwa_yoga", + "display_name": "Matru Satrutwa Yoga", + "subfamily": "mother_pattern", + "canonical_mapping": "bvr_matru_satrutwa_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "family_history_truth_authority_not_granted", + "case_calibration_not_closed" + ] + }, + { + "rule_id": "bvr_matrudeerghayur_yoga", + "display_name": "Matrudeerghayur Yoga", + "subfamily": "mother_pattern", + "canonical_mapping": "bvr_matrudeerghayur_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "family_history_truth_authority_not_granted", + "longevity_semantics_remain_support_only" + ] + }, + { + "rule_id": "bvr_matrunasa_precise", + "display_name": "Matrunasa Yoga", + "subfamily": "mother_pattern", + "canonical_mapping": "bvr_matrunasa_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "family_history_truth_authority_not_granted", + "loss_semantics_remain_support_only" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json", + "tests/yoga_test_suite.json", + "references/validation_logic_report.json", + "scripts/validate_logic_v2.py" + ], + "batch_acceptance_criteria": [ + "four mother-pattern members frozen in one governed packet", + "fixture receipt present for each admitted member", + "validation mapping receipt present for each admitted member", + "mother-pattern batch remains support-only rather than truth-upgraded" + ], + "boundary": "This narrowed mother-pattern batch leaves spouse, children, and sibling subfamilies out of the first ready promotion step." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..0f837a22 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json @@ -0,0 +1,37 @@ +{ + "schema_version": "raman_bvr_batch_fixture_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_mother_batch", + "status": "fixture_packet_ready", + "sources": [ + "tests/yoga_test_suite.json", + "references/validation_logic_report.json" + ], + "fixtures": [ + { + "rule_id": "bvr_matru_sneha_yoga", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "references/validation_logic_report.json", + "evidence_type": "repo_fixture_and_mapping_receipt" + }, + { + "rule_id": "bvr_matru_satrutwa_yoga", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "references/validation_logic_report.json", + "evidence_type": "repo_fixture_and_mapping_receipt" + }, + { + "rule_id": "bvr_matrudeerghayur_yoga", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "references/validation_logic_report.json", + "evidence_type": "repo_fixture_and_mapping_receipt" + }, + { + "rule_id": "bvr_matrunasa_precise", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "references/validation_logic_report.json", + "evidence_type": "repo_fixture_and_mapping_receipt" + } + ], + "boundary": "These receipts support governed support-only candidacy for a narrowed mother-pattern batch. They do not authorize unverified family-history truth claims." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..6c528e33 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json @@ -0,0 +1,37 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_sibling_core_batch", + "label": "Raman BVR Relationship / Sibling Core Batch", + "scope": "narrowed_family_subbatch_governance_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records a narrowed sibling-core Raman relationship batch. Passing the gate supports governed support-only candidacy only, not sibling-history truth or deterministic family outcomes.", + "member_rules": [ + { + "rule_id": "bvr_bhratruvriddhi_precise", + "display_name": "Bhratruvriddhi Yoga", + "subfamily": "sibling_pattern", + "canonical_mapping": "bvr_bhratruvriddhi_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "third_party_sibling_truth_authority_not_granted", + "case_calibration_not_closed" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json", + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "batch_acceptance_criteria": [ + "single sibling-core member frozen in one governed packet", + "fixture receipt present for the admitted member", + "validation mapping receipt present for the admitted member", + "sibling-core batch remains support-only rather than truth-upgraded" + ], + "boundary": "This narrowed sibling-core batch leaves the broader sibling family blocked until `bvr_dwadasa_sahodara_yoga` gains equivalent fixture coverage." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..6576983e --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json @@ -0,0 +1,19 @@ +{ + "schema_version": "raman_bvr_batch_fixture_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_sibling_core_batch", + "status": "fixture_packet_ready", + "sources": [ + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "fixtures": [ + { + "rule_id": "bvr_bhratruvriddhi_precise", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "scripts/validate_logic_v2.py", + "evidence_type": "repo_fixture_and_mapping_receipt" + } + ], + "boundary": "These receipts support governed support-only candidacy for a narrowed sibling-core batch. They do not authorize unverified sibling facts or deterministic family outcomes." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..eac498ee --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json @@ -0,0 +1,37 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_spouse_batch", + "label": "Raman BVR Relationship / Spouse Batch", + "scope": "narrowed_family_subbatch_governance_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records a narrowed spouse-pattern Raman relationship batch. Passing the gate supports governed support-only candidacy only, not partner-identity truth, relationship-count truth, or deterministic relationship outcomes.", + "member_rules": [ + { + "rule_id": "bvr_bahu_sthree_yoga", + "display_name": "Bahu Sthree Yoga", + "subfamily": "spouse_pattern", + "canonical_mapping": "bvr_bahu_sthree_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "third_party_partner_truth_authority_not_granted", + "case_calibration_not_closed" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json", + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "batch_acceptance_criteria": [ + "single spouse-pattern member frozen in one governed packet", + "fixture receipt present for the admitted member", + "validation mapping receipt present for the admitted member", + "spouse-pattern batch remains support-only rather than truth-upgraded" + ], + "boundary": "This narrowed spouse-pattern batch leaves children, mother, and sibling subfamilies out of the ready promotion step." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..4d1426bc --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json @@ -0,0 +1,19 @@ +{ + "schema_version": "raman_bvr_batch_fixture_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_relationship_spouse_batch", + "status": "fixture_packet_ready", + "sources": [ + "tests/yoga_test_suite.json", + "scripts/validate_logic_v2.py" + ], + "fixtures": [ + { + "rule_id": "bvr_bahu_sthree_yoga", + "fixture_source": "tests/yoga_test_suite.json", + "validation_source": "scripts/validate_logic_v2.py", + "evidence_type": "repo_fixture_and_mapping_receipt" + } + ], + "boundary": "These receipts support governed support-only candidacy for a narrowed spouse-pattern batch. They do not authorize unverified partner facts, relationship count truths, or deterministic outcome claims." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json new file mode 100755 index 00000000..8f8e4e98 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json @@ -0,0 +1,90 @@ +{ + "schema_version": "raman_bvr_batch_closure_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_wealth_core_batch", + "label": "Raman BVR Wealth Core Batch", + "scope": "first_core_admission_subrule_packet", + "status": "batch_fixture_backed_not_promoted", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet records a narrowed five-rule Raman wealth core batch. Passing the gate only supports governed support-only candidacy, not truth-upgraded financial interpretation.", + "member_rules": [ + { + "rule_id": "bvr_dharidhra_11_precise", + "display_name": "Dharidhra Yoga", + "subfamily": "poverty_pattern", + "canonical_mapping": "bvr_dharidhra_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "financial_truth_authority_not_granted", + "broader_wealth_family_not_closed" + ] + }, + { + "rule_id": "bvr_swaveeryaddhana_precise", + "display_name": "Swaveeryaddhana Yoga", + "subfamily": "wealth_pattern", + "canonical_mapping": "bvr_swaveeryaddhana_precise", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "financial_truth_authority_not_granted", + "case_calibration_not_closed" + ] + }, + { + "rule_id": "bvr_anthya_vayasi_dhana_yoga", + "display_name": "Anthya Vayasi Dhana Yoga", + "subfamily": "wealth_pattern", + "canonical_mapping": "bvr_anthya_vayasi_dhana_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "financial_truth_authority_not_granted", + "age_window_semantics_remain_support_only" + ] + }, + { + "rule_id": "bvr_matrumooladdhana_yoga", + "display_name": "Matrumooladdhana Yoga", + "subfamily": "wealth_source_pattern", + "canonical_mapping": "bvr_matrumooladdhana_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "financial_truth_authority_not_granted", + "wealth_source_semantics_remain_support_only" + ] + }, + { + "rule_id": "bvr_madhya_vayasi_dhana_yoga", + "display_name": "Madhya Vayasi Dhana Yoga", + "subfamily": "wealth_pattern", + "canonical_mapping": "bvr_madhya_vayasi_dhana_yoga", + "fixture_status": "present", + "validation_logic_status": "mapped", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "financial_truth_authority_not_granted", + "age_window_semantics_remain_support_only" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json", + "references/celebrity_case_validation_5charts.json", + "references/validation_logic_report.json", + "scripts/validate_logic_v2.py" + ], + "batch_acceptance_criteria": [ + "five core members frozen in one governed packet", + "fixture receipt present for each core member", + "validation mapping receipt present for each core member", + "core batch remains support-only candidate rather than truth-upgraded" + ], + "boundary": "This narrowed core batch leaves harder or less-stable wealth members out of the first promotion step." +} diff --git a/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..dbfcd155 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json @@ -0,0 +1,78 @@ +{ + "schema_version": "raman_bvr_batch_fixture_packet.v1", + "created": "2026-08-20", + "batch_id": "raman_bvr_yoga_wealth_core_batch", + "status": "fixture_packet_ready", + "sources": [ + "references/celebrity_case_validation_5charts.json", + "references/validation_logic_report.json" + ], + "fixtures": [ + { + "rule_id": "bvr_dharidhra_11_precise", + "positive_case": { + "case_name": "NM Gandhi", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_positive_hit" + }, + "negative_case": { + "case_name": "Steve Jobs", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_negative_by_absence" + } + }, + { + "rule_id": "bvr_swaveeryaddhana_precise", + "positive_case": { + "case_name": "Albert Einstein", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_positive_hit" + }, + "negative_case": { + "case_name": "Charles Darwin", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_negative_by_absence" + } + }, + { + "rule_id": "bvr_anthya_vayasi_dhana_yoga", + "positive_case": { + "case_name": "NM Gandhi", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_positive_hit" + }, + "negative_case": { + "case_name": "Steve Jobs", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_negative_by_absence" + } + }, + { + "rule_id": "bvr_matrumooladdhana_yoga", + "positive_case": { + "case_name": "Steve Jobs", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_positive_hit" + }, + "negative_case": { + "case_name": "Albert Einstein", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_negative_by_absence" + } + }, + { + "rule_id": "bvr_madhya_vayasi_dhana_yoga", + "positive_case": { + "case_name": "APJ Abdul Kalam", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_positive_hit" + }, + "negative_case": { + "case_name": "Albert Einstein", + "source_path": "references/celebrity_case_validation_5charts.json", + "evidence_type": "celebrity_case_validation_negative_by_absence" + } + } + ], + "boundary": "These fixtures are governance receipts for a narrowed Raman wealth core batch. They do not by themselves establish financial truth claims or full source-closed rule semantics." +} diff --git a/references/oracle/raman_2026_08_20/raman_dasha_boundary_fixture_family_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_dasha_boundary_fixture_family_2026_08_20.json new file mode 100755 index 00000000..21bcc648 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_dasha_boundary_fixture_family_2026_08_20.json @@ -0,0 +1,48 @@ +{ + "schema_version": "raman_dasha_boundary_fixture_family.v1", + "created": "2026-08-20", + "cluster_id": "raman_dasha_boundary_fixture_family", + "label": "Raman Dasha Boundary Fixture Family", + "status": "queue_mapped_seed_present_validator_ready_external_packet_attached", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This family governs the source-specific Raman Vimshottari Mahadasha boundary-series fixture path. It records seed presence, queue mapping, validator coverage, and attached external packet state, but does not imply same-number parity or timing truth.", + "fixture_members": [ + { + "member_id": "raman_boundary_series_seed_case", + "member_type": "oracle_seed_case", + "status": "present", + "evidence_paths": [ + "references/oracle/dasha_shadbala_oracle_cases.json", + "docs/research/raman_dasha_boundary_series_oracle_seed_2026_06_28.md" + ], + "summary": "The B.V. Raman Vimshottari Mahadasha boundary series is preserved as a public reference seed with frozen boundary dates." + }, + { + "member_id": "raman_boundary_series_queue_task", + "member_type": "collection_task_mapping", + "status": "present", + "evidence_paths": [ + "scripts/oracle_collection_queue.py", + "tests/test_oracle_collection_queue.py" + ], + "summary": "The oracle collection queue recognizes the Raman boundary series and emits a Dasha collection task for `target.vimshottari_mahadasa_boundaries`." + }, + { + "member_id": "raman_boundary_series_validator_mapping", + "member_type": "validator_scope", + "status": "present", + "evidence_paths": [ + "scripts/dasha_oracle_evidence_validator.py", + "tests/test_dasha_oracle_evidence_validator.py" + ], + "summary": "Current Dasha-only validator now admits both `target.vimshottari_start_date` packets and `target.vimshottari_mahadasa_boundaries` series packets, and the first Raman boundary-series packet is now attached as external_verified." + } + ], + "assumption_policy": { + "ayanamsa": "raman", + "node_mode": "unspecified", + "year_length_policy": "source_boundary_dates", + "promotion_boundary": "Boundary-series dates are now fixture-backed with an attached external-verified packet, but remain support-only until native parity and source-side arbitration are closed." + }, + "boundary": "This family makes the Raman Dasha boundary fixture lane auditable and ready for governed support-only use, without claiming same-number parity or timing truth." +} diff --git a/references/oracle/raman_2026_08_20/raman_dasha_boundary_parity_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_dasha_boundary_parity_packet_2026_08_20.json new file mode 100755 index 00000000..057fa939 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_dasha_boundary_parity_packet_2026_08_20.json @@ -0,0 +1,411 @@ +{ + "schema_version": "raman_dasha_boundary_parity_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_dasha_boundary_parity_packet", + "label": "Raman Dasha Boundary Parity Packet", + "status": "native_vs_external_arbitration_partially_explained", + "promotion_gate_status": "blocked", + "external_case_id": "template_bv_raman_vimshottari_boundary_series", + "external_packet_status": "external_verified", + "external_source_artifact": "references/oracle/artifacts/raman_vimshottari_boundary_series_source_locator_20260820.md", + "native_run_profile": { + "ayanamsa": "raman", + "node_mode": "mean", + "dasha_year_days": 365.25, + "moon_source": "native_auto_computed", + "assumption_boundary": "External packet preserves node_mode=unspecified, while native parity run currently uses node_mode=mean because native Dasha producer requires an explicit node mode." + }, + "node_mode_arbitration": { + "source_node_mode": "unspecified", + "native_profiles": { + "mean": { + "moon_longitude": 55.12994255303447, + "birth_node_mode": "mean" + }, + "true": { + "moon_longitude": 55.12994255303447, + "birth_node_mode": "true" + } + }, + "moon_longitude_delta_mean_vs_true": 0.0, + "finding": "not_driver_for_this_case", + "interpretation": "Mean-node and true-node runs produce the same Moon longitude for this birth under Raman ayanamsa, so node mode does not explain the current boundary mismatch." + }, + "boundary_semantics_arbitration": { + "finding": "first_row_semantics_diverge", + "interpretation": "The external first Mars boundary is anchored to the birth date, while the native producer reports the actual pre-birth Mahadasha start. This explains the isolated -344 day Mars delta without needing a separate year-length theory." + }, + "balance_arbitration": { + "birthdate": "1912-08-08", + "external_first_boundary_equals_birthdate": true, + "native_first_row_is_prebirth_start": true, + "native_first_start": "1911-08-30", + "external_first_boundary": "1912-08-08", + "native_elapsed_years_at_birth": 0.94, + "native_balance_years_at_birth": 6.06, + "source_implied_remaining_days_from_birth_to_rahu": 2234.178, + "source_implied_remaining_years_36525": 6.116847, + "remaining_years_delta_vs_native": 0.056847, + "remaining_days_delta_vs_native_36525": 20.763, + "interpretation": "The external series behaves like a birth-anchored balance table for the first Mars row, while the native producer stores the actual pre-birth Mahadasha start and a slightly shorter remaining balance." + }, + "year_length_arbitration": { + "candidate_year_days": [ + { + "year_days": 365.25, + "delta_rows": [ + { + "lord": "Mars", + "native_start": "1911-08-30", + "external_start": "1912-08-08", + "delta_days": -344 + }, + { + "lord": "Rahu", + "native_start": "1918-08-30", + "external_start": "1918-09-21", + "delta_days": -22 + }, + { + "lord": "Jupiter", + "native_start": "1936-08-29", + "external_start": "1936-09-21", + "delta_days": -23 + }, + { + "lord": "Saturn", + "native_start": "1952-08-29", + "external_start": "1952-09-21", + "delta_days": -23 + }, + { + "lord": "Mercury", + "native_start": "1971-08-30", + "external_start": "1971-09-21", + "delta_days": -22 + }, + { + "lord": "Ketu", + "native_start": "1988-08-29", + "external_start": "1988-09-21", + "delta_days": -23 + } + ], + "average_absolute_delta_days": 76.17 + }, + { + "year_days": 360, + "delta_rows": [ + { + "lord": "Mars", + "native_start": "1911-09-04", + "external_start": "1912-08-08", + "delta_days": -339 + }, + { + "lord": "Rahu", + "native_start": "1918-07-29", + "external_start": "1918-09-21", + "delta_days": -54 + }, + { + "lord": "Jupiter", + "native_start": "1936-04-25", + "external_start": "1936-09-21", + "delta_days": -149 + }, + { + "lord": "Saturn", + "native_start": "1952-02-01", + "external_start": "1952-09-21", + "delta_days": -233 + }, + { + "lord": "Mercury", + "native_start": "1970-10-24", + "external_start": "1971-09-21", + "delta_days": -332 + }, + { + "lord": "Ketu", + "native_start": "1987-07-27", + "external_start": "1988-09-21", + "delta_days": -422 + } + ], + "average_absolute_delta_days": 254.83 + } + ], + "best_fit_year_days": 365.25, + "interpretation": "Switching from 365.25 to 360 materially worsens downstream boundary drift, so year-length choice is not the primary mismatch driver in this case." + }, + "source_moon_precision_arbitration": { + "native_moon_longitude": 55.12994255303447, + "source_implied_moon_longitude_from_balance": 55.015529524, + "moon_longitude_delta_degrees": -0.114413029, + "precision_sweep": [ + { + "decimals": 6, + "moon_longitude": 55.129943, + "balance_years": 6.06, + "rahu_delta_days": -22 + }, + { + "decimals": 5, + "moon_longitude": 55.12994, + "balance_years": 6.06, + "rahu_delta_days": -22 + }, + { + "decimals": 4, + "moon_longitude": 55.1299, + "balance_years": 6.06, + "rahu_delta_days": -22 + }, + { + "decimals": 3, + "moon_longitude": 55.13, + "balance_years": 6.06, + "rahu_delta_days": -22 + }, + { + "decimals": 2, + "moon_longitude": 55.13, + "balance_years": 6.06, + "rahu_delta_days": -22 + }, + { + "decimals": 1, + "moon_longitude": 55.1, + "balance_years": 6.07, + "rahu_delta_days": -17 + }, + { + "decimals": 0, + "moon_longitude": 55.0, + "balance_years": 6.13, + "rahu_delta_days": 2 + } + ], + "finding": "residual_requires_material_moon_shift", + "interpretation": "Closing the later 22-23 day drift would require about -0.114414° of Moon longitude shift. Ordinary decimal rounding through 0.01° leaves the drift unchanged; only coarse 0.1° to 1° quantization materially moves the boundary dates." + }, + "date_rounding_arbitration": { + "rows": [ + { + "lord": "Mars", + "native_start_datetime": "1911-08-30T07:27:05", + "native_start_date": "1911-08-30", + "external_start_date": "1912-08-08", + "current_date_delta_days": -344, + "max_date_only_rounding_impact_days": 1 + }, + { + "lord": "Rahu", + "native_start_datetime": "1918-08-30T01:27:05", + "native_start_date": "1918-08-30", + "external_start_date": "1918-09-21", + "current_date_delta_days": -22, + "max_date_only_rounding_impact_days": 1 + }, + { + "lord": "Jupiter", + "native_start_datetime": "1936-08-29T13:27:05", + "native_start_date": "1936-08-29", + "external_start_date": "1936-09-21", + "current_date_delta_days": -23, + "max_date_only_rounding_impact_days": 1 + }, + { + "lord": "Saturn", + "native_start_datetime": "1952-08-29T13:27:05", + "native_start_date": "1952-08-29", + "external_start_date": "1952-09-21", + "current_date_delta_days": -23, + "max_date_only_rounding_impact_days": 1 + }, + { + "lord": "Mercury", + "native_start_datetime": "1971-08-30T07:27:05", + "native_start_date": "1971-08-30", + "external_start_date": "1971-09-21", + "current_date_delta_days": -22, + "max_date_only_rounding_impact_days": 1 + }, + { + "lord": "Ketu", + "native_start_datetime": "1988-08-29T13:27:05", + "native_start_date": "1988-08-29", + "external_start_date": "1988-09-21", + "current_date_delta_days": -23, + "max_date_only_rounding_impact_days": 1 + } + ], + "finding": "date_only_rounding_not_sufficient", + "interpretation": "The native clock-time boundaries land only hours away from their displayed dates, so converting clock-time boundaries to date-only display can explain at most about one day of drift, not the remaining 22-23 day gap." + }, + "source_balance_text_arbitration": { + "source_balance_text": { + "years": 6, + "months": 1, + "days": 6, + "source_pdf_page": 41, + "source_quote_excerpt": "Balance of Mars Dasa: 6 years, 1 month and 6 days." + }, + "source_balance_end_if_36525_plus_30_day_month": "1918-09-14T07:43:00", + "source_balance_end_date_if_calendar_reading": "1918-09-14", + "registered_packet_rahu_boundary": "1918-09-21", + "registered_packet_delta_days_vs_source_balance_text": -7, + "source_implied_moon_longitude_from_balance_text": 55.050356898, + "moon_longitude_delta_vs_native_degrees": -0.079585655, + "finding": "registered_boundary_series_not_directly_supported_by_located_source_page", + "interpretation": "The located Raman source page directly supports a birth-balance statement, not the currently registered 1918-09-21 boundary series. Read as 6 years, 1 month, and 6 days from the 1912-08-08 birth, the balance text lands near 1918-09-14, about 7 days earlier than the registered Rahu boundary." + }, + "external_boundaries": { + "Mars": "1912-08-08", + "Rahu": "1918-09-21", + "Jupiter": "1936-09-21", + "Saturn": "1952-09-21", + "Mercury": "1971-09-21", + "Ketu": "1988-09-21" + }, + "native_boundaries": { + "Mars": "1911-08-30", + "Rahu": "1918-08-30", + "Jupiter": "1936-08-29", + "Saturn": "1952-08-29", + "Mercury": "1971-08-30", + "Ketu": "1988-08-29" + }, + "comparison": { + "rows": [ + { + "lord": "Jupiter", + "local": "1936-08-29", + "jhora": "1936-09-21", + "status": "mismatch" + }, + { + "lord": "Ketu", + "local": "1988-08-29", + "jhora": "1988-09-21", + "status": "mismatch" + }, + { + "lord": "Mars", + "local": "1911-08-30", + "jhora": "1912-08-08", + "status": "mismatch" + }, + { + "lord": "Mercury", + "local": "1971-08-30", + "jhora": "1971-09-21", + "status": "mismatch" + }, + { + "lord": "Rahu", + "local": "1918-08-30", + "jhora": "1918-09-21", + "status": "mismatch" + }, + { + "lord": "Saturn", + "local": "1952-08-29", + "jhora": "1952-09-21", + "status": "mismatch" + } + ], + "closure": { + "area": "Vimshottari/JHora boundary profile", + "status": "mismatch", + "matched": 0, + "mismatched": 6, + "blocked": 0, + "confidence": "parameter_sensitive", + "reason": "Vimshottari boundaries differ; profile year length, seed moon longitude, balance-at-birth, and display precision must be arbitrated." + } + }, + "delta_rows": [ + { + "lord": "Mars", + "native_start": "1911-08-30", + "external_start": "1912-08-08", + "delta_days": -344 + }, + { + "lord": "Rahu", + "native_start": "1918-08-30", + "external_start": "1918-09-21", + "delta_days": -22 + }, + { + "lord": "Jupiter", + "native_start": "1936-08-29", + "external_start": "1936-09-21", + "delta_days": -23 + }, + { + "lord": "Saturn", + "native_start": "1952-08-29", + "external_start": "1952-09-21", + "delta_days": -23 + }, + { + "lord": "Mercury", + "native_start": "1971-08-30", + "external_start": "1971-09-21", + "delta_days": -22 + }, + { + "lord": "Ketu", + "native_start": "1988-08-29", + "external_start": "1988-09-21", + "delta_days": -23 + } + ], + "arbitration_findings": [ + { + "finding_id": "node_mode_not_driver", + "status": "explained", + "detail": "Mean and true node profiles produce the same Moon longitude here, so node-mode ambiguity is frozen but not causal for this mismatch." + }, + { + "finding_id": "first_boundary_semantics_diverge", + "status": "explained", + "detail": "Mars -344 days is a boundary-contract issue: external source uses a birth-anchored first row, native output exposes the actual pre-birth Mahadasha start." + }, + { + "finding_id": "balance_drift_drives_later_rows", + "status": "partially_explained", + "detail": "The external series implies about 6.116847 years of Mars balance from birth to Rahu, versus 6.06 years in the native run; that ~20.763-day gap accounts for most later 22-23 day drift." + }, + { + "finding_id": "year_length_360_rejected", + "status": "explained", + "detail": "Switching to a 360-day dasha year materially worsens average boundary error, so 360 is not the right reconciliation path for this packet." + }, + { + "finding_id": "date_only_rounding_not_driver", + "status": "explained", + "detail": "Pure date-only display can shift a clock-time boundary by at most about one day, so it cannot account for the remaining 22-23 day drift." + }, + { + "finding_id": "source_moon_precision_still_open", + "status": "partially_explained", + "detail": "Matching the source-implied balance would require about -0.114414° of Moon longitude shift. Ordinary decimal rounding does not get there, so source-side Moon precision or source-side balance construction remains open." + }, + { + "finding_id": "source_locator_supports_balance_text_not_boundary_series", + "status": "explained", + "detail": "The located Raman PDF page directly states 'Balance of Mars Dasa: 6 years, 1 month and 6 days', which points near 1918-09-14 rather than the currently registered 1918-09-21 Rahu boundary. The local source locator therefore supports a balance text, not yet the full boundary series as currently registered." + } + ], + "arbitration_next_steps": [ + "search the Raman source set for a page that explicitly prints the 1918-09-21 style Mahadasha boundary series rather than only the balance text", + "if no such page exists, downgrade the current boundary-series packet metadata from source-closed language to balance-text-only support", + "audit whether the book example constructs balance from a tabular nakshatra fraction instead of direct longitude precision", + "keep timing truth blocked until native-vs-external boundary arbitration is closed" + ], + "claim_boundary": "This packet records native-versus-external Raman Mahadasha boundary comparison only. It does not upgrade timing truth or same-number parity." +} diff --git a/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..18c87115 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_fixture_packet_2026_08_20.json @@ -0,0 +1,119 @@ +{ + "schema_version": "raman_dasha_fixture_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_dasha_core_subrules", + "status": "fixture_packet_ready", + "profile": { + "ayanamsa": "raman", + "node_mode": "mean", + "dasha_year_days": 365.25, + "pl9_profile_status": "parameter_sensitive" + }, + "native_payload": { + "source": "synthetic_raman_dasha_core_fixture", + "periods": [ + { + "period_key": "Mars", + "start": "1912-08-08", + "end": "1918-09-21", + "maha_lord": "Mars", + "antar_lord": "Mars", + "pratyantar_lord": "Sun" + }, + { + "period_key": "Rahu", + "start": "1918-09-21", + "end": "1936-09-21", + "maha_lord": "Rahu", + "antar_lord": "Rahu", + "pratyantar_lord": "Jupiter" + } + ], + "lord_facts": { + "Mars": { + "house": 10, + "ownership": [ + 5, + 10 + ], + "dignity": "friend", + "nakshatra": "Dhanishta", + "pada": 2 + }, + "Rahu": { + "house": 11, + "ownership": [ + 0 + ], + "dignity": "shadow", + "nakshatra": "Ardra", + "pada": 1 + }, + "Sun": { + "house": 1, + "ownership": [ + 1 + ], + "dignity": "own", + "nakshatra": "Uttara Phalguni", + "pada": 4 + }, + "Jupiter": { + "house": 2, + "ownership": [ + 2, + 11 + ], + "dignity": "own", + "nakshatra": "Purva Bhadrapada", + "pada": 3 + } + } + }, + "pl9_payload": { + "source": "template_bv_raman_vimshottari_boundary_series", + "source_sha256": "template_only_no_external_capture", + "pages": [ + "Vol2_seed" + ], + "periods": [ + { + "period_key": "Mars", + "start": "1912-08-08", + "end": "1918-09-21" + }, + { + "period_key": "Rahu", + "start": "1918-09-21", + "end": "1936-09-21" + } + ] + }, + "fixture_expectations": [ + { + "subrule_id": "vimshottari_boundary_row_comparison_core", + "required_fields": [ + "boundary_comparison.rows", + "boundary_comparison.rows.0.period_key", + "boundary_comparison.rows.0.status" + ] + }, + { + "subrule_id": "vimshottari_tolerance_audit_core", + "required_fields": [ + "boundary_comparison.tolerance_days", + "audit.status", + "audit.reason" + ] + }, + { + "subrule_id": "lord_trigger_fact_contract_core", + "required_fields": [ + "interpretation_triggers.items", + "interpretation_triggers.items.0.lord", + "report_sections.evidence_appendix" + ] + } + ], + "boundary": "This fixture packet checks executable boundary rows and data-only trigger facts for the lowest-risk Raman Dasha core subrules. It does not validate timing truth or predictive interpretation." +} diff --git a/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_packet_2026_08_20.json new file mode 100755 index 00000000..986426ad --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_dasha_core_subrules_packet_2026_08_20.json @@ -0,0 +1,65 @@ +{ + "schema_version": "raman_dasha_core_subrules_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_dasha_core_subrules", + "label": "Raman Dasha Core Subrules Packet", + "status": "fixture_backed_contract_ready_not_timing_truth", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet governs the lowest-risk executable core subrules for Raman Dasha support. It validates boundary comparison rows and data-only trigger facts, but does not authorize event timing truth, predictive narrative, or source-closed Raman Dasha parity claims.", + "subrules": [ + { + "subrule_id": "vimshottari_boundary_row_comparison_core", + "section": "raman_dasha_boundary_contract_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "native_vimshottari.periods", + "pl9_reference.periods", + "boundary_comparison.rows" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "same_number_external_parity_not_closed", + "timing_truth_not_admitted" + ] + }, + { + "subrule_id": "vimshottari_tolerance_audit_core", + "section": "raman_dasha_boundary_contract_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "boundary_comparison.tolerance_days", + "audit.status", + "audit.reason" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "pl9_profile_sensitivity_not_closed", + "timing_truth_not_admitted" + ] + }, + { + "subrule_id": "lord_trigger_fact_contract_core", + "section": "raman_dasha_trigger_fact_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "interpretation_triggers.items", + "interpretation_triggers.reason", + "report_sections.evidence_appendix" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "trigger_facts_are_not_predictions", + "no_event_outcome_authority" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_dasha_core_subrules_fixture_packet_2026_08_20.json", + "scripts/professional_parity_closure.py", + "docs/research/raman_dasha_source_packet_family_2026_08_20.md" + ], + "boundary": "This packet is an executable contract for three low-risk Raman Dasha support subrules only." +} diff --git a/references/oracle/raman_2026_08_20/raman_dasha_source_packet_family_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_dasha_source_packet_family_2026_08_20.json new file mode 100755 index 00000000..f10c4228 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_dasha_source_packet_family_2026_08_20.json @@ -0,0 +1,60 @@ +{ + "schema_version": "raman_source_packet_family.v1", + "created": "2026-08-20", + "cluster_id": "raman_dasha_source_packet_family", + "label": "Raman Dasha Source Packet Family", + "status": "source_mapped_native_runtime_exists_not_source_closed", + "claim_boundary": "This packet family records the current Raman Dasha source state. Native Dasha layers can run under Raman settings, but there is not yet a source-closed Raman Dasha packet family or same-number external parity closure.", + "source_basis": [ + "docs/research/raman_horoscope_v2_admission_review_2026_07_27.md", + "docs/research/raman_dasha_boundary_series_oracle_seed_2026_06_28.md", + "references/oracle/dasha_shadbala_oracle_cases.json" + ], + "packet_family_members": [ + { + "member_id": "native_raman_dasha_runtime", + "member_type": "runtime_surface", + "admission_state": "already_usable_native_runtime", + "summary": "Native Dasha layers can already run end-to-end with Raman ayanamsa in the local calculation chain.", + "evidence_paths": [ + "docs/research/raman_closure_matrix_2026_08_20.md" + ], + "promotion_blockers": [ + "not_a_raman_specific_source_closed_packet_family" + ] + }, + { + "member_id": "bv_raman_vimshottari_boundary_series_seed", + "member_type": "oracle_seed", + "admission_state": "template_only_boundary_seed", + "summary": "A B.V. Raman Vimshottari Mahadasha boundary series is preserved as an external-oracle seed, not as production calibration evidence.", + "evidence_paths": [ + "docs/research/raman_dasha_boundary_series_oracle_seed_2026_06_28.md", + "references/oracle/dasha_shadbala_oracle_cases.json" + ], + "promotion_blockers": [ + "template_only_not_production_calibration", + "ayanamsa_and_year_length_assumptions_not_frozen" + ] + }, + { + "member_id": "vol2_dasha_discussion", + "member_type": "source_mapped_discussion", + "admission_state": "reference_only", + "summary": "Raman Vol. II includes prescriptive Dasha discussion alongside seventh-house and Navamsa interpretation, but no source-specific executable Dasha rule contract is admitted from it.", + "evidence_paths": [ + "docs/research/raman_horoscope_v2_admission_review_2026_07_27.md" + ], + "promotion_blockers": [ + "interpretive_and_case_led_material_not_executable", + "no_source_specific_fixture_family", + "event_and_outcome_truth_not_admitted" + ] + } + ], + "acceptance_notes": [ + "native Raman Dasha runtime remains distinct from source closure", + "boundary series seed remains visible but not overpromoted", + "Raman Dasha is now tracked as a packet family rather than an implicit gap" + ] +} diff --git a/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json new file mode 100755 index 00000000..f519741a --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json @@ -0,0 +1,46 @@ +{ + "schema_version": "raman_methodology_fixture_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_house_judgment_core_subrules", + "status": "fixture_packet_ready", + "smoke_birth_input": { + "year": 1990, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "second": 0, + "lat": 28.6139, + "lon": 77.2090, + "tz": 5.5, + "ayanamsa": "raman", + "node_mode": "mean" + }, + "fixture_expectations": [ + { + "subrule_id": "house_itself_assessment_core", + "required_fields": [ + "houses.house_1.cusp_sign", + "houses.house_1.lord", + "planets.Sun.house" + ] + }, + { + "subrule_id": "house_lord_assessment_core", + "required_fields": [ + "houses.house_10.lord", + "planets.Saturn.status", + "planets.Mars.sign" + ] + }, + { + "subrule_id": "natural_karaka_assessment_core", + "required_fields": [ + "planets.Jupiter.sign", + "planets.Venus.house", + "planets.Sun.status" + ] + } + ], + "boundary": "This fixture packet checks executable field presence for the lowest-risk Raman house-judgment core subrules. It does not validate weighted synthesis or outcome truth." +} diff --git a/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_packet_2026_08_20.json new file mode 100755 index 00000000..492e0a72 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_house_judgment_core_subrules_packet_2026_08_20.json @@ -0,0 +1,66 @@ +{ + "schema_version": "raman_methodology_core_subrules_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_house_judgment_core_subrules", + "label": "Raman House Judgment Core Subrules Packet", + "status": "fixture_backed_contract_ready_not_full_methodology", + "promotion_gate_status": "governed_support_only_candidate_ready", + "claim_boundary": "This packet governs the lowest-risk executable core subrules from Raman's house judgment method. It validates field-level runtime support only and does not authorize weighted synthesis, event truth, marriage judgment, or maraka conclusions.", + "subrules": [ + { + "subrule_id": "house_itself_assessment_core", + "section": "house_judgment_six_step_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "houses.house_n.cusp_sign", + "houses.house_n.lord", + "planets.*.house" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "no_weighting_contract", + "no_integrated_outcome_synthesis" + ] + }, + { + "subrule_id": "house_lord_assessment_core", + "section": "house_judgment_six_step_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "houses.house_n.lord", + "planets.*.status", + "planets.*.sign", + "planets.*.house" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "no_weighting_contract", + "no_integrated_outcome_synthesis" + ] + }, + { + "subrule_id": "natural_karaka_assessment_core", + "section": "house_judgment_six_step_core", + "runtime_binding_status": "field_bound", + "bound_native_evidence_fields": [ + "planets.Jupiter", + "planets.Venus", + "planets.Sun" + ], + "fixture_status": "present", + "claim_status": "support_only_candidate", + "promotion_blockers": [ + "karaka_weighting_not_source_closed", + "no_integrated_outcome_synthesis" + ] + } + ], + "shared_evidence_paths": [ + "references/raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json", + "scripts/domain_calculation_service.py", + "scripts/jyotish_engine.py" + ], + "boundary": "This packet is an executable field-presence contract for three low-risk core subrules only." +} diff --git a/references/oracle/raman_2026_08_20/raman_house_judgment_methodology_packet_2026_08_20.json b/references/oracle/raman_2026_08_20/raman_house_judgment_methodology_packet_2026_08_20.json new file mode 100755 index 00000000..47ccd189 --- /dev/null +++ b/references/oracle/raman_2026_08_20/raman_house_judgment_methodology_packet_2026_08_20.json @@ -0,0 +1,99 @@ +{ + "schema_version": "raman_methodology_packet.v1", + "created": "2026-08-20", + "cluster_id": "raman_house_judgment_methodology", + "label": "Raman House Judgment Methodology Packet", + "status": "transcribed_structured_not_executable", + "claim_boundary": "This packet structures Raman house judgment methodology as an auditable reading guide. It is not a source-closed executable runtime judgment family and does not authorize marriage, maraka, or event-truth claims.", + "source_basis": [ + "references/raman-house-judgment-methodology.md", + "docs/research/raman_horoscope_v1_admission_review_2026_07_27.md", + "docs/research/raman_horoscope_v2_admission_review_2026_07_27.md" + ], + "methodology_sections": [ + { + "section_id": "house_judgment_six_step_core", + "section_type": "core_methodology", + "admission_state": "support_only_reading_guide", + "summary": "The six-step house judgment sequence is preserved as a structured reading guide around house, lord, karaka, Moon/Sun repetition, Navamsa confirmation, and final synthesis.", + "substeps": [ + "house_itself_assessment", + "house_lord_assessment", + "karaka_assessment", + "moon_sun_repeat_assessment", + "navamsa_confirmation", + "integrated_synthesis" + ], + "runtime_binding_status": "partial_native_field_bindings_only", + "bound_native_evidence_fields": [ + "houses", + "house_lords", + "karakas", + "moon_relative_houses", + "sun_relative_houses", + "d9" + ], + "promotion_blockers": [ + "no_source_closed_stepwise_fixture_family", + "no_source_specific_weighting_contract", + "synthesis_layer_still_interpretive" + ] + }, + { + "section_id": "seventh_house_marriage_protocol", + "section_type": "sensitive_domain_methodology", + "admission_state": "blocked_for_runtime_authority", + "summary": "The seventh-house marriage protocol is structured as a methodology section, but its relationship and spouse semantics remain sensitive and not runtime-authorized.", + "substeps": [ + "seventh_house", + "seventh_lord", + "kalatrakaraka", + "eighth_house_durability", + "second_house_family_base", + "ninth_house_sowbhagya" + ], + "runtime_binding_status": "raw_fields_exist_but_methodology_not_admitted", + "bound_native_evidence_fields": [ + "houses", + "house_lords", + "venus", + "d9", + "dashas" + ], + "promotion_blockers": [ + "relationship_and_spouse_third_party_semantics_sensitive", + "no_source_closed_fixture_family_for_marriage_protocol", + "event_and_outcome_truth_not_admitted" + ] + }, + { + "section_id": "maraka_system_application", + "section_type": "high_risk_methodology", + "admission_state": "blocked_for_runtime_authority", + "summary": "The maraka system is captured as methodology context only. Death, danger, and spouse-health applications remain intentionally blocked.", + "substeps": [ + "second_and_seventh_lords_as_primary_maraka", + "saturn_as_third_level_maraka", + "third_and_eighth_as_life_houses", + "dasha_trigger_context" + ], + "runtime_binding_status": "raw_fields_exist_but_high_risk_layer_blocked", + "bound_native_evidence_fields": [ + "houses", + "house_lords", + "saturn", + "dashas" + ], + "promotion_blockers": [ + "high_risk_mortality_and_harm_semantics", + "no_admissible_result_layer", + "source_closed_case_calibration_missing" + ] + } + ], + "acceptance_notes": [ + "methodology is now structured into auditable sections", + "core six-step lane may support evidence reading", + "marriage and maraka subsections remain blocked for runtime authority" + ] +} diff --git a/references/upstream/yinduzhanxing/import-plan.json b/references/upstream/yinduzhanxing/import-plan.json index 5c340358..8c735aa5 100644 --- a/references/upstream/yinduzhanxing/import-plan.json +++ b/references/upstream/yinduzhanxing/import-plan.json @@ -229,5 +229,48 @@ "authentication", "billing", "database" - ] + ], + "raman_2026_08_20_support": { + "action": "adapt", + "source_directory": "references/", + "target": "references/oracle/raman_2026_08_20/", + "reason": "Adapt the 2026-08-20 governed_support_only_candidate_ready Raman packets into commercial consultation and rectification presentation. They remain support-only and cannot close unique-minute, Dasha timing truth, or blocked health/longevity/poverty/family-umbrella batches.", + "packets": [ + "raman_ayanamsa_parameter_support_packet_2026_08_20.json", + "raman_house_judgment_core_subrules_packet_2026_08_20.json", + "raman_house_judgment_core_subrules_fixture_packet_2026_08_20.json", + "raman_house_judgment_methodology_packet_2026_08_20.json", + "raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json", + "raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json", + "raman_bvr_wealth_core_batch_fixture_packet_2026_08_20.json", + "raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_children_batch_fixture_packet_2026_08_20.json", + "raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_mother_batch_fixture_packet_2026_08_20.json", + "raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_sibling_core_batch_fixture_packet_2026_08_20.json", + "raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_spouse_batch_fixture_packet_2026_08_20.json", + "raman_dasha_core_subrules_packet_2026_08_20.json", + "raman_dasha_core_subrules_fixture_packet_2026_08_20.json", + "raman_dasha_boundary_fixture_family_2026_08_20.json", + "raman_dasha_boundary_parity_packet_2026_08_20.json", + "raman_dasha_source_packet_family_2026_08_20.json" + ], + "rejected_packets": [ + "raman_bvr_health_adversity", + "raman_bvr_wealth_poverty", + "raman_bvr_relationship_family", + "raman_vol2_longevity" + ], + "targets": [ + "scripts/raman_support_observations.py", + "scripts/jyotish_api_server.py", + "scripts/ayanamsa_utils.py" + ], + "tests": [ + "tests/test_raman_support_observations.py", + "tests/test_upstream_import_plan.py" + ] + } } diff --git a/scripts/active_rectification_event_engine.py b/scripts/active_rectification_event_engine.py index 59562789..2c3ef98f 100644 --- a/scripts/active_rectification_event_engine.py +++ b/scripts/active_rectification_event_engine.py @@ -43,10 +43,11 @@ import narayana_dasha # noqa: E402 import saham_daynight # noqa: E402 import special_lagnas # noqa: E402 import varga # noqa: E402 +from ayanamsa_utils import DEFAULT_AYANAMSA_NAME # noqa: E402 from scripts.rectification.refinement_packet import NAKSHATRA_SPAN # noqa: E402 from scripts.rectification.kp_cusp_observation import observe_kp_cusps # noqa: E402 -AYANAMSA: Final = "lahiri" +AYANAMSA: Final = DEFAULT_AYANAMSA_NAME NODE_MODE: Final = "mean" INPUT_CONTRACT_VERSION: Final = "rectification-candidate-input-v2" diff --git a/scripts/active_rectification_questions.py b/scripts/active_rectification_questions.py index 572aea07..cb0b978b 100644 --- a/scripts/active_rectification_questions.py +++ b/scripts/active_rectification_questions.py @@ -39,7 +39,7 @@ def _candidate_scan( lat: float | None = None, lon: float | None = None, tz: float | None = None, - ayanamsa: str = "lahiri", + ayanamsa: str = "raman", ) -> dict[str, Any]: start = center - timedelta(minutes=uncertainty_minutes) end = center + timedelta(minutes=uncertainty_minutes) @@ -189,7 +189,7 @@ def build_questionnaire( lat: float | None = None, lon: float | None = None, tz: float | None = None, - ayanamsa: str = "lahiri", + ayanamsa: str = "raman", ) -> dict[str, Any]: questions = [] for qid, round_id, domain, sensitivity, window, prompt, yes_bias, no_bias in QUESTION_TEMPLATES: diff --git a/scripts/ayanamsa_utils.py b/scripts/ayanamsa_utils.py index 3ae375af..03ded1c6 100644 --- a/scripts/ayanamsa_utils.py +++ b/scripts/ayanamsa_utils.py @@ -3,6 +3,9 @@ Swiss Ephemeris stores sidereal mode globally inside the process. Any helper that calls calc_ut(..., FLG_SIDEREAL) must set the intended mode explicitly. + +Unknown names fail; they never silently collapse to Lahiri. VedAstro / oracle +comparison fixtures must pass an explicit `lahiri` name. """ from __future__ import annotations @@ -24,6 +27,7 @@ AYANAMSA_MODES = { 'djwhal_khul': getattr(swe, 'SIDM_DJWHAL_KHUL', 6) if swe else 6, 'sassanian': getattr(swe, 'SIDM_SASSANIAN', 16) if swe else 16, 'true_citra': getattr(swe, 'SIDM_TRUE_CITRA', 27) if swe else 27, + 'true_pushya': getattr(swe, 'SIDM_TRUE_PUSHYA', 29) if swe else 29, } AYANAMSA_DISPLAY_NAMES = { @@ -35,16 +39,44 @@ AYANAMSA_DISPLAY_NAMES = { 'djwhal_khul': 'Djwhal Khul', 'sassanian': 'Sassanian', 'true_citra': 'True Citra', + 'true_pushya': 'True Pushya', } -ACTIVE_AYANAMSA_NAME = 'lahiri' +DEFAULT_AYANAMSA_NAME = 'raman' +ACTIVE_AYANAMSA_NAME = DEFAULT_AYANAMSA_NAME + + +class UnsupportedAyanamsaError(ValueError): + """Raised when a caller asks for an ayanamsa that is not in AYANAMSA_MODES.""" + + +def _canonical_ayanamsa_key(name: Any) -> str: + key = str(name or '').strip().lower().replace('-', '_') + if key in ('krishnamurti_paddhati', 'krishnamurti/kp'): + key = 'kp' + if key == 'true_chitra': + key = 'true_citra' + return key + + +def is_supported_ayanamsa_name(name: Any = None) -> bool: + key = _canonical_ayanamsa_key(name if name not in (None, '') else DEFAULT_AYANAMSA_NAME) + return key in AYANAMSA_MODES + + +def supported_ayanamsa_names() -> list[str]: + return sorted(AYANAMSA_MODES) def normalize_ayanamsa_name(name: Any = None) -> str: - key = str(name or 'lahiri').strip().lower().replace('-', '_') - if key in ('krishnamurti_paddhati', 'krishnamurti/kp'): - key = 'kp' - return key if key in AYANAMSA_MODES else 'lahiri' + if name is None or (isinstance(name, str) and not name.strip()): + return DEFAULT_AYANAMSA_NAME + key = _canonical_ayanamsa_key(name) + if key not in AYANAMSA_MODES: + raise UnsupportedAyanamsaError( + f"unsupported ayanamsa name {name!r}; supported: {', '.join(supported_ayanamsa_names())}" + ) + return key def ayanamsa_display_name(name: Any = None) -> str: @@ -54,12 +86,11 @@ def ayanamsa_display_name(name: Any = None) -> str: def current_ayanamsa_name(args: Any = None) -> str: name = getattr(args, 'ayanamsa', None) if args is not None else None - return normalize_ayanamsa_name(name or ACTIVE_AYANAMSA_NAME) + return normalize_ayanamsa_name(name) -def apply_ayanamsa(name: Any = 'lahiri', swe_module: Any = None) -> bool: +def apply_ayanamsa(name: Any = None, swe_module: Any = None) -> bool: """Set Swiss Ephemeris sidereal mode explicitly.""" - global ACTIVE_AYANAMSA_NAME module = swe_module or swe if module is None: return False @@ -68,10 +99,9 @@ def apply_ayanamsa(name: Any = 'lahiri', swe_module: Any = None) -> bool: module.set_sid_mode(AYANAMSA_MODES[key]) except Exception: return False - ACTIVE_AYANAMSA_NAME = key return True -def sidereal_flags(swe_module: Any, ayanamsa_name: Any = 'lahiri') -> int: +def sidereal_flags(swe_module: Any, ayanamsa_name: Any = None) -> int: apply_ayanamsa(ayanamsa_name, swe_module) return getattr(swe_module, 'FLG_SWIEPH', 2) | getattr(swe_module, 'FLG_SIDEREAL', 65536) diff --git a/scripts/daily_guidance_service.py b/scripts/daily_guidance_service.py index bb947c53..d6a0eab7 100644 --- a/scripts/daily_guidance_service.py +++ b/scripts/daily_guidance_service.py @@ -118,7 +118,7 @@ def build_daily_guidance(body: dict[str, Any]) -> dict[str, Any]: chart = _chart_from_body(body) birth = chart.get("birth_info") or {} tz = float(body.get("tz", birth.get("tz", 0) or 0)) - ayanamsa = str(body.get("ayanamsa") or birth.get("ayanamsa_name") or "lahiri") + ayanamsa = normalize_ayanamsa_name(body.get("ayanamsa") or birth.get("ayanamsa_name")) asc_sign = (chart.get("ascendant") or {}).get("sign") moon_transit = _moon_transit(reference_date, tz, ayanamsa) moon_house = _house_from_sign(asc_sign, moon_transit.get("sign")) diff --git a/scripts/domain_calculation_service.py b/scripts/domain_calculation_service.py index 1dc268ab..32637459 100644 --- a/scripts/domain_calculation_service.py +++ b/scripts/domain_calculation_service.py @@ -12,7 +12,12 @@ from typing import Any from zoneinfo import ZoneInfo import swisseph as swe -from ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name +from ayanamsa_utils import ( + DEFAULT_AYANAMSA_NAME, + UnsupportedAyanamsaError, + apply_ayanamsa, + normalize_ayanamsa_name, +) from dasha_analyzer import build_dasha_timeline, lon_to_nakshatra from jyotish_engine import SIGNS, compute_chart_data from sade_sati import calc_sade_sati_complete @@ -126,7 +131,10 @@ def _normalized_request(payload: dict[str, Any]) -> dict[str, Any]: requested_node = str(payload.get("node_mode", payload.get("nodeMode", "mean"))).lower() if requested_node not in {"mean", "true"}: raise CalculationError("node_mode must be mean or true") - ayanamsa = normalize_ayanamsa_name(payload.get("ayanamsa", "lahiri")) + try: + ayanamsa = normalize_ayanamsa_name(payload.get("ayanamsa")) + except UnsupportedAyanamsaError as exc: + raise CalculationError(str(exc)) from exc local_dt = datetime( int(payload["year"]), int(payload["month"]), @@ -217,7 +225,7 @@ def compute_chart(payload: dict[str, Any]) -> dict[str, Any]: if request["timezone_id"]: effective["timezone_id"] = request["timezone_id"] requested = { - "ayanamsa": payload.get("ayanamsa", "lahiri"), + "ayanamsa": payload.get("ayanamsa") or DEFAULT_AYANAMSA_NAME, "node_mode": payload.get("node_mode", payload.get("nodeMode", "mean")), "timezone_offset": payload.get("tz"), } @@ -269,7 +277,7 @@ def compute_vimshottari_timeline( result["result_hash"] = _canonical_hash(result) return result def compute_transit_longitude( - *, planet: str, reference_date: str, tz: float, ayanamsa: str = "lahiri" + *, planet: str, reference_date: str, tz: float, ayanamsa: str = DEFAULT_AYANAMSA_NAME ) -> dict[str, Any]: if planet not in _PLANET_IDS: raise CalculationError(f"unsupported transit planet: {planet}") @@ -306,7 +314,7 @@ def compute_sade_sati( asc_degree: float, reference_date: str, tz: float, - ayanamsa: str = "lahiri", + ayanamsa: str = DEFAULT_AYANAMSA_NAME, ) -> dict[str, Any]: transit = compute_transit_longitude( planet="Saturn", diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 85f393d7..760f676d 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -94,6 +94,9 @@ except ModuleNotFoundError: # pragma: no cover - script execution path from western_chart_engine import build_tropical_western_evidence_packet from western_timing_engine import build_timing_techniques +from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, normalize_ayanamsa_name +from raman_support_observations import build_raman_support_observations + load_local_env(REPO_ROOT) _LOCAL_MODULE_CACHE = {} _API_CHART_CACHE_SCOPE = 'api_chart_response' @@ -789,6 +792,20 @@ def _consultation_audit_status(value) -> str: _CHARA_DASHA_ROUTES = frozenset({'career', 'timing', 'annual', 'marriage'}) +def _request_ayanamsa(*sources) -> str: + for source in sources: + if isinstance(source, dict): + raw = source.get('ayanamsa') or source.get('ayanamsa_name') + else: + raw = source + if raw not in (None, ''): + try: + return normalize_ayanamsa_name(raw) + except UnsupportedAyanamsaError as exc: + raise BadRequest(str(exc)) from exc + return DEFAULT_AYANAMSA_NAME + + def _consultation_declared_routes(body: dict) -> set[str]: values: list[str] = [] for key in ('strict_workflow_route', 'theme'): @@ -1596,18 +1613,37 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict, for item in (extended or []) + (engine or []) ) if entry - ][:12] + ] + routes = _consultation_declared_routes(body) + raman = build_raman_support_observations(chart, compact, routes) + sieved = raman['yogas']['yogas'] + modules['raman_support'] = raman modules['yogas'] = { - 'status': 'executed' if compact else 'blocked', - 'count': len(compact), - 'yogas': compact[:12], + 'status': 'executed' if sieved else 'blocked', + 'count': len(sieved), + 'yogas': sieved, + 'sieve': { + 'hits': raman['yogas']['hits'], + 'misses': raman['yogas']['misses'], + 'claim_boundary': raman['claim_boundary'], + }, } - if compact and not isinstance(chart.get('yogas'), list): - chart['yogas'] = compact[:12] + if sieved and not isinstance(chart.get('yogas'), list): + chart['yogas'] = sieved except Exception as exc: modules['yogas'] = {'status': 'blocked', 'reason': exc.__class__.__name__, 'yogas': []} diagnostics.append({'layer': 'yogas', 'status': 'unavailable', 'reason': exc.__class__.__name__}) + if not isinstance(modules.get('raman_support'), dict): + try: + existing = modules.get('yogas', {}).get('yogas') if isinstance(modules.get('yogas'), dict) else [] + compact = [entry for entry in (_compact_yoga_entry(item) for item in (existing or [])) if entry] + modules['raman_support'] = build_raman_support_observations( + chart, compact, _consultation_declared_routes(body), + ) + except Exception as exc: + diagnostics.append({'layer': 'raman_support', 'status': 'unavailable', 'reason': exc.__class__.__name__}) + if not isinstance(modules.get('chara_dasha'), dict): if _consultation_declared_routes(body) & _CHARA_DASHA_ROUTES: try: @@ -1646,7 +1682,7 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict, asc_degree=float(ascendant.get('lon') or 0), reference_date=_consultation_reference_date(body).strftime('%Y-%m-%d'), tz=float(birth_payload.get('tz') or birth_payload.get('timezone') or 0), - ayanamsa=str(body.get('ayanamsa') or birth_payload.get('ayanamsa') or 'lahiri'), + ayanamsa=_request_ayanamsa(body, birth_payload), ) compact_sade = None if isinstance(sade_sati, dict): @@ -1667,7 +1703,7 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict, triggers, search_period = _search_consultation_transit_triggers( chart, body, - ayanamsa=str(body.get('ayanamsa') or birth_payload.get('ayanamsa') or 'lahiri'), + ayanamsa=_request_ayanamsa(body, birth_payload), ) except Exception as trigger_exc: start, end = _consultation_transit_search_window(body) @@ -1705,7 +1741,7 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict, 'varga_full', 'varga_spectrum', 'arudha_padas', 'narayana_dasha', 'dasha_sub_periods', 'ashtakavarga', 'kp_cusps', 'gulika', 'functional_benefic_malefic', 'shadbala', 'kakshya', - 'yogas', 'chara_dasha', 'transits', + 'yogas', 'raman_support', 'chara_dasha', 'transits', ) if _consultation_audit_status(modules.get(name)) == 'executed' ], @@ -2495,7 +2531,7 @@ def _build_api_chart_cache_payload(body: dict) -> dict: 'tz': body.get('tz'), }, 'calculation': { - 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'ayanamsa': _request_ayanamsa(body), 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), 'today': body.get('today') or body.get('current_date'), 'transit_date': body.get('transit_date'), @@ -3251,7 +3287,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'version': '6.9.14', 'swisseph_available': swisseph_available, 'swisseph_version': swisseph_version, - 'ayanamsa_default': 'lahiri', + 'ayanamsa_default': DEFAULT_AYANAMSA_NAME, 'modules': 'Chart/KP/Synastry/Prashna/Remedies/Dasha/Varga/Jaimini/Ashtakavarga/Shadbala/Yoga/Aspects/Tajika/Muhurta/BhavaChalit/BhavaBala/Sudarshana/Nakshatra/Transit/RectificationGate/CaseValidation/DivisionalYoga/Kakshya', 'async_job_runtime': async_job_runtime_status(), 'vedastro': self._vedastro_status(), @@ -4586,7 +4622,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, - 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'ayanamsa': _request_ayanamsa(body), 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), 'today': body.get('today') or body.get('current_date'), 'transit_date': body.get('transit_date') or body.get('reference_date'), @@ -6396,7 +6432,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lon': lon, 'tz': tz, 'node_mode': node_mode, - 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'ayanamsa': _request_ayanamsa(body), 'age': body.get('age'), 'today': body.get('today') or body.get('current_date'), 'transit_date': body.get('transit_date'), @@ -6592,7 +6628,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, - 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'ayanamsa': _request_ayanamsa(body), 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), }) canonical_birth = canonical_chart['birth_info'] @@ -6717,7 +6753,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, - 'ayanamsa': body.get('ayanamsa', 'lahiri'), + 'ayanamsa': _request_ayanamsa(body), 'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')), }) canonical_dasha = calculation_service.compute_vimshottari_timeline( @@ -6730,7 +6766,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): asc_degree=canonical_chart['ascendant']['lon'], reference_date=body.get('transit_date') or body.get('today') or body.get('current_date') or datetime.utcnow().strftime('%Y-%m-%d'), tz=tz, - ayanamsa=body.get('ayanamsa', 'lahiri'), + ayanamsa=_request_ayanamsa(body), ) canonical_planets = {} for planet_name, planet in canonical_chart.get('planets', {}).items(): @@ -6975,7 +7011,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if planet in {'Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'} and isinstance(pdata, dict) } - ayanamsa_display = birth.get('ayanamsa_display') or 'Lahiri' + ayanamsa_display = birth.get('ayanamsa_display') or 'Raman' node_mode = birth.get('node_mode') or 'mean' prompt_lines = [ '你是一个审慎的 AI Native 印度/吠陀占星分析助手。', @@ -7005,7 +7041,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'evidence_snapshot': { 'birth': birth, 'ayanamsa': { - 'name': birth.get('ayanamsa_name', 'lahiri'), + 'name': birth.get('ayanamsa_name', DEFAULT_AYANAMSA_NAME), 'display': ayanamsa_display, 'value': birth.get('ayanamsa'), 'node_mode': node_mode, @@ -7570,7 +7606,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): asc_degree=self._normalize_degree(body, 'asc_degree', 0), reference_date=reference_date, tz=self._get_float(body, 'tz', 0, -14, 14), - ayanamsa=body.get('ayanamsa', 'lahiri'), + ayanamsa=_request_ayanamsa(body), ) def _compute_pmc(self, body): @@ -8158,7 +8194,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lon = self._get_float(body, 'lon', 0, -180, 180) tz = self._parse_timezone(body, lat, lon, year, month, day, birth_hour, birth_minute, birth_second) jd = swe.julday(year, month, day, birth_hour_decimal - tz) - context = shadbala_module.build_shadbala_context(jd, lat, lon, body.get('ayanamsa', 'lahiri'), tz) + context = shadbala_module.build_shadbala_context(jd, lat, lon, _request_ayanamsa(body), tz) result = shadbala_module.calc_shadbala( planets, SIGNS[asc_sign_idx], @@ -8357,7 +8393,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lon, tz, target_year, - ayanamsa_name=body.get('ayanamsa', 'lahiri'), + ayanamsa_name=_request_ayanamsa(body), ) return {'success': True, 'endpoint': 'annual', 'report': report} @@ -8445,7 +8481,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lat=lat, lon=lon, tz=tz, - ayanamsa_name=body.get('ayanamsa', 'lahiri'), + ayanamsa_name=_request_ayanamsa(body), ) except ValueError as e: raise BadRequest(str(e)) from e @@ -8530,7 +8566,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lat=self._get_float(body, 'lat', 0, -90, 90), lon=self._get_float(body, 'lon', 0, -180, 180), tz=self._get_float(body, 'tz', 0, -14, 14), - ayanamsa_name=body.get('ayanamsa', 'lahiri'), + ayanamsa_name=_request_ayanamsa(body), ) def _compute_rectification_gate(self, body): @@ -8615,7 +8651,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lat = self._get_float(body, 'lat', 0, -90, 90) lon = self._get_float(body, 'lon', 0, -180, 180) tz = self._get_float(body, 'tz', 0, -14, 14) - ayanamsa = body.get('ayanamsa', 'lahiri') + ayanamsa = _request_ayanamsa(body) if not isinstance(ayanamsa, str): raise BadRequest('ayanamsa must be a string') try: @@ -9624,7 +9660,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): start_date, end_date, planets_to_check=planets_to_check, - ayanamsa_name=body.get('ayanamsa', 'lahiri'), + ayanamsa_name=_request_ayanamsa(body), ) top_triggers = result.get('triggers', [])[:6] headline = '发现可观察过境触发点' if result.get('total_triggers', 0) else '当前区间未发现精确触发' diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index ebb85d6f..6bef1eea 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -63,6 +63,8 @@ from guided_topic_discovery import build_guided_topics from ayanamsa_utils import ( AYANAMSA_DISPLAY_NAMES, AYANAMSA_MODES, + DEFAULT_AYANAMSA_NAME, + UnsupportedAyanamsaError, apply_ayanamsa, ayanamsa_display_name, current_ayanamsa_name, @@ -90,7 +92,7 @@ load_local_env(ROOT_DIR) try: import swisseph as swe - apply_ayanamsa('lahiri', swe) # P0修复:必须设置Lahiri恒星黄道模式 + apply_ayanamsa(DEFAULT_AYANAMSA_NAME, swe) # default Raman sidereal mode; callers pass explicit names HAS_SWE = True except ImportError: HAS_SWE = False @@ -777,8 +779,8 @@ def _add_chart_args(p): p.add_argument('--lon', type=float, required=True) p.add_argument('--tz', type=float, default=None) p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认,JHora常用/Swiss direct baseline),true=True Node(PyJHora默认)') - p.add_argument('--ayanamsa', default='lahiri', choices=list(AYANAMSA_MODES.keys()), - help='恒星黄道系统(默认lahiri)。可选: raman, kp(krishnamurti), fagan_bradley, djwhal_khul, sassanian, true_citra') + p.add_argument('--ayanamsa', default=DEFAULT_AYANAMSA_NAME, choices=list(AYANAMSA_MODES.keys()), + help='恒星黄道系统(默认raman)。对照/VedAstro 样本请显式传 lahiri。可选: lahiri, raman, kp, true_pushya, true_citra, fagan_bradley, djwhal_khul, sassanian') def _apply_ayanamsa(ayanamsa_name): @@ -1699,7 +1701,7 @@ def _build_ai_prompt_pack(report): evidence_snapshot = { 'birth': report.get('birth_info', {}), 'ayanamsa': { - 'name': birth_info.get('ayanamsa_name', 'lahiri'), + 'name': birth_info.get('ayanamsa_name', DEFAULT_AYANAMSA_NAME), 'display': birth_info.get('ayanamsa_display', 'Lahiri'), 'value': birth_info.get('ayanamsa'), 'node_mode': birth_info.get('node_mode'), @@ -2076,8 +2078,7 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode=' if not HAS_SWE: return None, None, None, None swe.set_ephe_path('') - if ayanamsa_name: - _apply_ayanamsa(ayanamsa_name) + _apply_ayanamsa(ayanamsa_name or DEFAULT_AYANAMSA_NAME) ayanamsa_name = _current_ayanamsa_name(type('Args', (), {'ayanamsa': ayanamsa_name})()) second = int(second or 0) hour_decimal = _birth_hour_decimal(hour, minute, second) - tz @@ -5999,8 +6000,8 @@ def main(): p.add_argument('--lon', type=float, required=False) p.add_argument('--tz', type=float, default=None) p.add_argument('--node-mode', default='mean', choices=['mean', 'true']) - p.add_argument('--ayanamsa', default='lahiri', choices=list(AYANAMSA_MODES.keys()), - help='恒星黄道系统(默认lahiri)。可选: raman, kp, fagan_bradley, djwhal_khul, sassanian, true_citra') + p.add_argument('--ayanamsa', default=DEFAULT_AYANAMSA_NAME, choices=list(AYANAMSA_MODES.keys()), + help='恒星黄道系统(默认raman)。对照/VedAstro 样本请显式传 lahiri。') p.add_argument('--nakshatra', default=None); p.add_argument('--pada', type=int, default=None) p.add_argument('--moon-lon', type=float, default=None); p.add_argument('--birthdate', default=None) p.add_argument('--today', default=None) @@ -6210,7 +6211,7 @@ def main(): p.add_argument('--lat', type=float, required=True, help='纬度') p.add_argument('--lon', type=float, required=True, help='经度') p.add_argument('--timezone', required=True, help='UTC offset,例如 8 或 +08:00') - p.add_argument('--ayanamsa', default='lahiri') + p.add_argument('--ayanamsa', default=DEFAULT_AYANAMSA_NAME) p.add_argument('--node-mode', default='mean', choices=['mean', 'true']) p.add_argument('--location-convention', default='wgs84', choices=['wgs84']) p.add_argument('--mode', default='chart', choices=['chart','arudha','sphutas','sahams','lost-item','life','kunda'], help='分析模式') @@ -6279,13 +6280,16 @@ def main(): parser.print_help(); sys.exit(1) # 应用 Ayanamsa 设置(v6.9.9 新增多系统支持) - ayanamsa_name = getattr(args, 'ayanamsa', 'lahiri') - if ayanamsa_name and ayanamsa_name != 'lahiri': + ayanamsa_name = getattr(args, 'ayanamsa', DEFAULT_AYANAMSA_NAME) + try: applied = _apply_ayanamsa(ayanamsa_name) - if not applied: - print(f"Warning: Ayanamsa '{ayanamsa_name}' not recognized, using default Lahiri", file=sys.stderr) - else: - print(f" [Ayanamsa] {ayanamsa_name}", file=sys.stderr) + except UnsupportedAyanamsaError as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(2) + if not applied: + print(f"Error: could not apply ayanamsa '{ayanamsa_name}'", file=sys.stderr) + sys.exit(2) + print(f" [Ayanamsa] {ayanamsa_name}", file=sys.stderr) cmds = { 'chart': cmd_chart, 'dasha': cmd_dasha, 'yoga': cmd_yoga, 'predict': cmd_predict, 'varga': cmd_varga, 'celebrity': cmd_celebrity, 'db-stats': cmd_db_stats, 'transit': cmd_transit, diff --git a/scripts/raman_support_observations.py b/scripts/raman_support_observations.py new file mode 100644 index 00000000..523f9c4e --- /dev/null +++ b/scripts/raman_support_observations.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Raman 2026-08-20 support observations for commercial consultation. + +Loads governed_support_only_candidate_ready packets. This is not timing truth +and not a unique-minute or fate-outcome authority. +""" + +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +PACKET_DIR = ROOT / "references" / "oracle" / "raman_2026_08_20" + +BLOCKED_PACKET_PREFIXES = ( + "raman_bvr_health_adversity", + "raman_bvr_wealth_poverty", + "raman_bvr_relationship_family", + "raman_vol2_longevity", + "raman_bvr_relationship_sibling_batch", # wide sibling, not sibling_core +) + +DOMAIN_PACKETS = { + "career": ("raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json",), + "wealth": ("raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json",), + "finance": ("raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json",), + "marriage": ("raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json",), + "relationship": ("raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json",), + "family": ( + "raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json", + "raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json", + ), +} + +DOMAIN_HOUSES = { + "career": (10,), + "wealth": (2, 11), + "finance": (2, 11), + "marriage": (7,), + "relationship": (7,), + "family": (3, 4, 5, 9), +} + +NATURAL_KARAKA = { + 1: "Sun", + 2: "Jupiter", + 3: "Mars", + 4: "Moon", + 5: "Jupiter", + 6: "Mars", + 7: "Venus", + 8: "Saturn", + 9: "Jupiter", + 10: "Sun", + 11: "Jupiter", + 12: "Saturn", +} + +EXECUTABLE_STEPS = ( + "house_itself_assessment", + "house_lord_assessment", + "karaka_assessment", +) +METHODOLOGY_STEPS = ( + "moon_sun_repeat_assessment", + "navamsa_confirmation", + "integrated_synthesis", +) + +CLAIM_BOUNDARY = "governed_support_only_candidate_ready" + + +def _token(value: str) -> str: + return "".join(ch for ch in value.lower() if ch.isalnum()) + + +@lru_cache(maxsize=32) +def _load_packet(filename: str) -> dict[str, Any]: + path = PACKET_DIR / filename + if not path.is_file(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def packet_members(filename: str) -> list[dict[str, str]]: + packet = _load_packet(filename) + rows = [] + for member in packet.get("member_rules") or []: + if not isinstance(member, dict): + continue + display = str(member.get("display_name") or "").strip() + if not display: + continue + rows.append({ + "rule_id": str(member.get("rule_id") or ""), + "display_name": display, + "canonical_mapping": str(member.get("canonical_mapping") or ""), + "claim_status": str(member.get("claim_status") or "support_only_candidate"), + }) + return rows + + +def _yoga_aliases(entry: dict[str, Any]) -> set[str]: + names = [ + str(entry.get("name") or ""), + str(entry.get("display_name") or ""), + str(entry.get("yoga") or ""), + str(entry.get("rule_id") or ""), + str(entry.get("canonical_mapping") or ""), + ] + tokens = {_token(item) for item in names if item} + extra = set() + for token in tokens: + extra.add(token.replace("yoga", "").replace("yog", "")) + return {item for item in tokens | extra if item} + + +def sieve_yogas_for_routes( + detected: list[dict[str, Any]], + routes: set[str], +) -> dict[str, Any]: + filenames: list[str] = [] + selected_routes = {item for item in routes if item in DOMAIN_PACKETS} + if not selected_routes: + selected_routes = {"career", "wealth", "marriage", "family"} + for route in sorted(selected_routes): + filenames.extend(DOMAIN_PACKETS[route]) + members: list[dict[str, str]] = [] + seen_ids: set[str] = set() + for filename in filenames: + for member in packet_members(filename): + if member["rule_id"] in seen_ids: + continue + seen_ids.add(member["rule_id"]) + members.append(member) + + detected_tokens: list[tuple[dict[str, Any], set[str]]] = [ + (entry, _yoga_aliases(entry)) for entry in detected if isinstance(entry, dict) + ] + hits: list[dict[str, Any]] = [] + misses: list[dict[str, Any]] = [] + used_names: set[str] = set() + for member in members: + aliases = _yoga_aliases(member) + matched = None + for entry, tokens in detected_tokens: + if aliases & tokens: + matched = entry + break + row = { + "name": member["display_name"], + "rule_id": member["rule_id"], + "hit": matched is not None, + "claim_status": member["claim_status"], + "claim_boundary": CLAIM_BOUNDARY, + } + if matched: + row["planets"] = matched.get("planets") + row["category"] = matched.get("category") + hits.append(row) + used_names.add(str(matched.get("name") or "")) + else: + misses.append(row) + leftover = [ + entry for entry, _tokens in detected_tokens + if str(entry.get("name") or "") not in used_names + ] + return { + "status": "executed", + "claim_boundary": CLAIM_BOUNDARY, + "routes": sorted(selected_routes), + "hits": hits, + "misses": misses, + "ungoverned_detected": leftover, + "yogas": hits + misses, + "count": len(hits) + len(misses), + } + + +def _planet(chart: dict[str, Any], name: str) -> dict[str, Any]: + planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {} + row = planets.get(name) + return row if isinstance(row, dict) else {} + + +def _house(chart: dict[str, Any], number: int) -> dict[str, Any]: + houses = chart.get("houses") if isinstance(chart.get("houses"), dict) else {} + row = houses.get(f"house_{number}") + return row if isinstance(row, dict) else {} + + +def _occupants(chart: dict[str, Any], number: int) -> list[str]: + planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {} + found = [] + for name, payload in planets.items(): + if not isinstance(payload, dict): + continue + try: + if int(payload.get("house") or 0) == number: + found.append(name) + except (TypeError, ValueError): + continue + return found + + +def _d9_lagna(chart: dict[str, Any]) -> str | None: + modules = chart.get("modules") if isinstance(chart.get("modules"), dict) else {} + varga = modules.get("varga_full") if isinstance(modules.get("varga_full"), dict) else {} + d9 = varga.get("D9") or varga.get("d9") + if isinstance(d9, dict): + asc = d9.get("Ascendant") or d9.get("ascendant") or d9.get("lagna") + if isinstance(asc, dict): + sign = asc.get("sign") or asc.get("rashi") + if sign: + return str(sign) + sign = d9.get("lagna_sign") or d9.get("asc_sign") + if sign: + return str(sign) + return None + + +def house_six_step(chart: dict[str, Any], house_number: int) -> dict[str, Any]: + house = _house(chart, house_number) + cusp = house.get("cusp_sign") or house.get("sign") + lord = house.get("lord") + lord_planet = _planet(chart, str(lord)) if lord else {} + karaka_name = NATURAL_KARAKA.get(house_number) + karaka = _planet(chart, karaka_name) if karaka_name else {} + sun = _planet(chart, "Sun") + moon = _planet(chart, "Moon") + occupants = _occupants(chart, house_number) + executable = { + EXECUTABLE_STEPS[0]: { + "status": "executed" if cusp else "blocked", + "layer": "executable_core", + "cusp_sign": cusp, + "occupants": occupants, + }, + EXECUTABLE_STEPS[1]: { + "status": "executed" if lord else "blocked", + "layer": "executable_core", + "lord": lord, + "lord_sign": lord_planet.get("sign"), + "lord_house": lord_planet.get("house"), + "lord_status": lord_planet.get("status") or lord_planet.get("dignity"), + }, + EXECUTABLE_STEPS[2]: { + "status": "executed" if karaka else "blocked", + "layer": "executable_core", + "karaka": karaka_name, + "karaka_sign": karaka.get("sign"), + "karaka_house": karaka.get("house"), + }, + } + methodology = { + METHODOLOGY_STEPS[0]: { + "status": "methodology_only", + "layer": "methodology", + "sun_house": sun.get("house"), + "moon_house": moon.get("house"), + "repeats_house": house_number in { + int(sun.get("house") or 0), + int(moon.get("house") or 0), + }, + }, + METHODOLOGY_STEPS[1]: { + "status": "methodology_only", + "layer": "methodology", + "d9_lagna": _d9_lagna(chart), + }, + METHODOLOGY_STEPS[2]: { + "status": "methodology_only", + "layer": "methodology", + "note": "positive/negative majority is a reading guide, not an 8/20 executable contract", + }, + } + return { + "house": house_number, + "claim_boundary": CLAIM_BOUNDARY, + "executable_core": executable, + "methodology": methodology, + "steps": {**executable, **methodology}, + } + + +def build_raman_support_observations( + chart: dict[str, Any], + detected_yogas: list[dict[str, Any]], + routes: set[str], +) -> dict[str, Any]: + selected = {item for item in routes if item in DOMAIN_HOUSES} + if not selected: + selected = {"career", "wealth", "marriage", "family"} + houses: list[dict[str, Any]] = [] + seen: set[int] = set() + for route in sorted(selected): + for number in DOMAIN_HOUSES[route]: + if number in seen: + continue + seen.add(number) + houses.append(house_six_step(chart, number)) + yogas = sieve_yogas_for_routes(detected_yogas, routes) + return { + "status": "executed", + "claim_boundary": CLAIM_BOUNDARY, + "promotion_gate_status": CLAIM_BOUNDARY, + "houses": houses, + "yogas": yogas, + "blocked_packets": list(BLOCKED_PACKET_PREFIXES), + } diff --git a/scripts/rectification/decision_policy.py b/scripts/rectification/decision_policy.py index f29bd6ff..86ee829a 100644 --- a/scripts/rectification/decision_policy.py +++ b/scripts/rectification/decision_policy.py @@ -56,8 +56,8 @@ _AUDIT_LABELS = { "d5-panchamsha": ("D5 成就分盘", "本轮已对照学业或被委以责任的变化。"), "d3-drekkana": ("D3 兄弟分盘", "本轮已对照兄弟姐妹主题。"), "d7-saptamsha": ("D7 子女分盘", "本轮已对照子女或伴侣细节。"), - "d9-navamsa": ("D9 婚姻分盘", "本轮已对照关系主题,未给类型标签。"), - "d10-dashamsa": ("D10 事业分盘", "本轮已对照事业主题,未给类型标签。"), + "d9-navamsa": ("D9 婚姻分盘", "本轮已对照关系主题,可用 D9 上升类型表作校时方法,不是命运承诺。"), + "d10-dashamsa": ("D10 事业分盘", "本轮已对照事业主题,可用 D10 上升类型表作校时方法,不是命运承诺。"), "d11-labhamsha": ("D11 收益分盘", "本轮已对照收益主题。"), "d12-dwadashamsha": ("D12 父母分盘", "本轮已对照家人主题。"), "d24-chaturvimshamsha": ("D24 教育分盘", "本轮已对照学业主题。"), @@ -596,6 +596,7 @@ def build_decision_receipt( receipt.update({ "window_scan": packet["window_scan"], "event_dasha_ledger": packet["event_dasha_ledger"], + "event_fit_rate": packet["event_fit_rate"], "dasha_agreement": packet["dasha_agreement"], "lagna_contrast": packet["lagna_contrast"], "nakshatra_boundary": packet["nakshatra_boundary"], diff --git a/scripts/rectification/horary_observation.py b/scripts/rectification/horary_observation.py index 99f6beca..efeeaec7 100644 --- a/scripts/rectification/horary_observation.py +++ b/scripts/rectification/horary_observation.py @@ -61,7 +61,7 @@ def build_horary_observation(request: dict[str, Any]) -> dict[str, Any]: "lat": float(request["lat"]), "lon": float(request["lon"]), "tz": float(request["tz"]), - "ayanamsa": "lahiri", + "ayanamsa": "raman", "node_mode": "mean", }) asc = chart.get("ascendant") or {} diff --git a/scripts/rectification/kp_cusp_observation.py b/scripts/rectification/kp_cusp_observation.py index a098c253..1a6d1bab 100644 --- a/scripts/rectification/kp_cusp_observation.py +++ b/scripts/rectification/kp_cusp_observation.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import Any -from ayanamsa_utils import apply_ayanamsa, current_ayanamsa_name +from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, apply_ayanamsa, current_ayanamsa_name from kp_system import KP_LORDS, get_kp_lords from scripts.domain_calculation_service import swiss_ephemeris_lock @@ -84,4 +84,4 @@ def observe_kp_cusps(jd: float | None, lat: float | None, lon: float | None) -> except (TypeError, ValueError, OverflowError, OSError, RuntimeError, ArithmeticError): return dict(_BLOCKED) finally: - apply_ayanamsa(previous or "lahiri", swe) + apply_ayanamsa(previous or DEFAULT_AYANAMSA_NAME, swe) diff --git a/scripts/rectification/refinement_packet.py b/scripts/rectification/refinement_packet.py index d858c61b..89b48c96 100644 --- a/scripts/rectification/refinement_packet.py +++ b/scripts/rectification/refinement_packet.py @@ -1,7 +1,8 @@ """Server-owned P0/P1 refinement packet for birth-time rectification. -Produces candidate-narrowing structure only. Never grants a unique minute, -never emits D9/D10 type labels, and never copies raw scores into public copy. +Produces candidate-narrowing structure only. Never grants a unique minute +and never copies raw scores into public copy. D9/D10 sign names are method +contrast for the skill verification report, not unique-minute proof. """ from __future__ import annotations @@ -76,6 +77,18 @@ def _features(built: dict[str, Any]) -> list[dict[str, Any]]: return rows +def _sign_names(indices: set[int]) -> list[str]: + names: list[str] = [] + for idx in sorted(indices): + if isinstance(idx, int) and 0 <= idx <= 11: + names.append(SIGNS_CN[SIGNS[idx]]) + return names + + +def _has_gochara(rule_ids: Sequence[str]) -> bool: + return any("controlled_transit" in str(item) or str(item).startswith("gochara") for item in rule_ids) + + def match_level(rule_ids: Sequence[str]) -> str: ids = [str(item) for item in rule_ids] if not ids or ids == ["no_domain_activation"]: @@ -92,6 +105,8 @@ def match_level(rule_ids: Sequence[str]) -> str: return "medium" if any(item.startswith("vim_") or item.startswith("narayana_") for item in ids): return "weak" + if _has_gochara(ids): + return "medium" return "none" @@ -101,6 +116,8 @@ def _tracks(rule_ids: Sequence[str]) -> list[str]: tracks.append("vimshottari") if any(str(item).startswith("narayana_") for item in rule_ids): tracks.append("narayana") + if _has_gochara(rule_ids): + tracks.append("gochara") return tracks @@ -196,6 +213,8 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]: for layer in _LAYER_LABEL: payload[f"{layer}_lagna_count" if layer.startswith("d") else f"{layer}_count"] = len(counts[layer]) payload[f"{layer}_candidates_differ"] = len(counts[layer]) > 1 + payload["d9_sign_names"] = _sign_names(counts["d9"]) + payload["d10_sign_names"] = _sign_names(counts["d10"]) return payload @@ -219,18 +238,58 @@ def event_dasha_ledger( summary = str(event.get("summary") or "").strip() or "这条经历" tracks = _tracks(rule_ids) track_text = "、".join( - "主限" if track == "vimshottari" else "分盘大运" for track in tracks + "主限" if track == "vimshottari" else "分盘大运" if track == "narayana" else "受控行运" + for track in tracks ) or "现有大运层" + gochara_hit = _has_gochara(rule_ids) rows.append({ "summary": summary[:80], "match": level, "match_label": MATCH_LABELS[level], "tracks": tracks, - "user_meaning": f"{summary[:40]}:{MATCH_LABELS[level]}({track_text})", + "gochara": "activated" if gochara_hit else "not_seen", + "user_meaning": ( + f"{summary[:40]}:{MATCH_LABELS[level]}({track_text}" + f"{';Gochara 激活相关宫' if gochara_hit else ';Gochara 未见对应'})" + ), }) return rows +def event_fit_rate(rows: Sequence[dict[str, Any]]) -> dict[str, Any]: + total = len(rows) + matched = sum(1 for row in rows if row.get("match") in {"strong", "medium"}) + if total == 0: + return { + "matched": 0, + "total": 0, + "percent": None, + "band": "insufficient", + "label": "事件不足,无法计算吻合率", + "unique_minute_claim": False, + "user_meaning": "事件–Dasha–Gochara 表还没有可评分行。这不是唯一分钟确认。", + } + percent = round(100 * matched / total) + band = "high" if percent >= 80 else "medium" if percent >= 60 else "low" + label = ( + "高度吻合(事件吻合率 ≥80%)" if band == "high" + else "中度吻合(事件吻合率 60–80%)" if band == "medium" + else "低度吻合(事件吻合率 <60%)" + ) + return { + "matched": matched, + "total": total, + "percent": percent, + "band": band, + "label": label, + "unique_minute_claim": False, + "user_meaning": ( + f"当前窗 {matched}/{total} 件已确认事件与 Dasha/Gochara 吻合,{label}。" + "这是相对拟合,不是已确认唯一出生分钟。" + ), + } + + def dasha_agreement(built: dict[str, Any], candidate_times: Sequence[str]) -> dict[str, Any]: times = [str(item)[:5] for item in candidate_times if isinstance(item, str) and len(str(item)) >= 5] if not times: @@ -324,7 +383,7 @@ def lagna_contrast(built: dict[str, Any]) -> dict[str, Any] | None: "user_meaning": ( f"窗口里出现两段本命上升:{left['start']}-{left['end']} 为{left['lagna']}," f"{right['start']}-{right['end']} 为{right['lagna']}。" - "只比较宫主结构,不给性格或类型标签。" + "可并列 D9/D10 类型表作校时方法,不是命运承诺,也不能确认唯一分钟。" ), "unique_minute_claim": False, } @@ -399,7 +458,7 @@ def precision_stage(scan: dict[str, Any], event_count: int) -> dict[str, Any]: meaning = "事业盘已较稳,居所盘仍会换升。可再补一件记得时间的搬家或住处变化。" elif scan.get("d5_candidates_differ") or scan.get("d24_candidates_differ"): current = "d5_refine" - meaning = "居所盘已较稳,成就盘或学业盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化;不要贴类型标签。" + meaning = "居所盘已较稳,成就盘或学业盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化。" else: current = "ready_to_adopt" meaning = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。" @@ -441,10 +500,12 @@ def build_refinement_packet( candidate_times: Sequence[str], ) -> dict[str, Any]: scan = window_scan(built) + ledger = event_dasha_ledger(request, built, representative_time) agreement = dasha_agreement(built, candidate_times) return { "window_scan": scan, - "event_dasha_ledger": event_dasha_ledger(request, built, representative_time), + "event_dasha_ledger": ledger, + "event_fit_rate": event_fit_rate(ledger), "dasha_agreement": agreement, "lagna_contrast": lagna_contrast(built), "nakshatra_boundary": nakshatra_boundary(built, representative_time), diff --git a/scripts/rectification/scoring_service.py b/scripts/rectification/scoring_service.py index 84e9a237..fc6d78d6 100644 --- a/scripts/rectification/scoring_service.py +++ b/scripts/rectification/scoring_service.py @@ -328,7 +328,7 @@ def calculation_spec(request: RectificationRequest) -> dict[str, Any]: "latitude": json_number(request["lat"]), "longitude": json_number(request["lon"]), "timezoneOffsetHours": json_number(request["tz"]), - "ayanamsa": "lahiri", "nodeMode": "mean", "minuteStep": 1, + "ayanamsa": "raman", "nodeMode": "mean", "minuteStep": 1, } for source, target in ( ("birth_time_source", "birthTimeSource"), diff --git a/scripts/shadbala.py b/scripts/shadbala.py index 9e1515dc..4d19cd1a 100644 --- a/scripts/shadbala.py +++ b/scripts/shadbala.py @@ -31,6 +31,7 @@ _script_dir = os.path.dirname(os.path.abspath(__file__)) if _script_dir not in sys.path: sys.path.insert(0, _script_dir) from varga import varga_map, SIGNS as VARGA_SIGNS, SIGN_LORDS as VARGA_SIGN_LORDS +from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, apply_ayanamsa, normalize_ayanamsa_name # ============================================================================ # 常量 @@ -154,14 +155,10 @@ def _solar_event_local_hour(jd_ut: float, lat: float, lon: float, timezone: floa return (times[0] + timezone / 24.0 - local_midnight) * 24.0 -def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = "lahiri", timezone: float = 0.0) -> Dict: +def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = DEFAULT_AYANAMSA_NAME, timezone: float = 0.0) -> Dict: """Build precise sidereal house context from standard birth inputs.""" - sid_mode = swe.SIDM_LAHIRI - if str(ayanamsa).lower() in {"raman"}: - sid_mode = swe.SIDM_RAMAN - elif str(ayanamsa).lower() in {"kp", "krishnamurti"}: - sid_mode = swe.SIDM_KRISHNAMURTI - swe.set_sid_mode(sid_mode) + key = normalize_ayanamsa_name(ayanamsa) + apply_ayanamsa(key, swe) cusps, _ = swe.houses_ex(float(jd_ut), float(lat), float(lon), b"P", swe.FLG_SIDEREAL) local_jd = float(jd_ut) + float(timezone) / 24.0 year, month, day, local_hour = swe.revjul(local_jd, swe.GREG_CAL) @@ -172,7 +169,7 @@ def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = "jd_ut": float(jd_ut), "jd_local": local_jd, "lat": float(lat), "lon": float(lon), "timezone": float(timezone), "year": int(year), "month": int(month), "day": int(day), "local_hour": float(local_hour), - "ayanamsa": str(ayanamsa), "ayanamsa_degrees": float(swe.get_ayanamsa_ut(float(jd_ut))), + "ayanamsa": key, "ayanamsa_degrees": float(swe.get_ayanamsa_ut(float(jd_ut))), "sunrise_hour": sunrise, "sunset_hour": sunset, "previous_sunset_hour": previous_sunset, "house_midpoints": [float(value) % 360 for value in cusps], } diff --git a/skills/jyotish-birth-time-rectification/SKILL.md b/skills/jyotish-birth-time-rectification/SKILL.md index c5c11544..bf813a2c 100644 --- a/skills/jyotish-birth-time-rectification/SKILL.md +++ b/skills/jyotish-birth-time-rectification/SKILL.md @@ -115,22 +115,23 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv - `public_aa_holdout` 为 `not_ready` 时不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。 - 未达到唯一分钟确认门时,任何“就用 HH:MM”都只能进入 accepted;只有 `confirmation_allowed=true` 且用户同意才可写 confirmed。 - 若不可分 blocker 为 `blocked`、宽度大于 5、top `tied_minute_count` > 1,或 `confirmation_allowed=false`,正文必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。 -- 分钟窗口扫描只在服务端;结果进入候选卡 / 平台语言。不得把「几件事件」说成已确定到 ±5 分钟。 -- 候选卡负责候选时间、排名、相对支持度、采用动作和选中状态;正文只解释当前意义与不确定性,不重复候选表、编号菜单或卡片数字。 +- 分钟窗口扫描只在服务端。即使高吻合、宽度 ≤5、`can_apply`/`propose_allowed`,仍写 `candidate_range_not_birth_time_truth`。 +- 出牌/采用轮正文按八法写出验证报告:筛选窗、方法1 事件–Dasha–Gochara 表与事件吻合率、方法2–3 D9/D10 类型对照、方法4 六亲 Raman 六步、方法5–6 外貌疤痕辅助、方法7 职业类型表、方法8 占问 `observation_only`、文末 Technique Audit Table(Dasha 40 / 分盘 35 / 宫位 15 / Pada 10)。候选卡仍作 adopt 控件;正文**要**写候选窗、代表分钟、相对支持与方法层理由。 +- 80%/60% 只描述**事件吻合率**(高度/中度/低度拟合),**不得**写成“已确认唯一出生分钟”。 - 不得在同一回复中一边要求继续补证据、一边提供采用候选。 - 不得伪造出生分钟、分数、权重、事件 ID、分盘事实或确认门结果。 ## 10. 输出与停止条件 -- 简体中文,自然对话;不固定以“收到 / 已记录”开头,不机械复读,不擅自解释事件的“人生意义”,不推断用户未陈述的动机、心理或因果关系。 +- 简体中文。访谈按 skill 路径 C:从候选簇差异生成 A/B/C/D 主题问卷(明确有且时间吻合 / 类似但偏 / 没有 / 不记得),每题说明为何问、绑哪张分盘。允许模糊日期、允许分多轮。**不得**先逼 10–15 条事件长表。 - 每轮最多一个主要问题;完整回复可以零问题,不为了延续对话强行追问,不生成三条推荐问题。 - 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。 - 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。 -- 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分,也不得给用户贴 D9/D10 星座或类型标签。有日期的外貌/疤痕只作上升/一宫辅助对照。职业与带日期事业事件独立,对照本命第 10 宫和 D10,不得贴类型标签。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 只用于选择下一问主题。 +- 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分。D9/D10 类型表是校时方法,写「该分钟下 D9/D10 升 X,与用户所述特质的对应/冲突」,不是咨询命运承诺。职业对照本命第 10 宫和 D10,允许类型表。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 可用于选题,类型对照写入验证报告。 - 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5/D24 成就收窄;家人走 D12/D7/D3 方法覆盖。财务走 D2/D11、健康走 D30,仅在用户主动说时计分,均不得混进 D4。Pada / Hora / Ghati / Bhava / Pranapada / KP 子主只展示换升,不确认唯一分钟。 -- 采用后本命按采用分钟重算并折叠展示技法审计;不得声称唯一分钟,也不自动进入咨询 Agent。 +- 采用后本命按采用分钟重算;不得声称唯一分钟,也不自动进入咨询 Agent。 - 采用候选后只需自然说明 accepted 与 confirmed 边界;不强制下一问,不主动关闭 Case,Session 会保留并可日后继续。 -- 不再有固定 10–15 个事件、固定 80%/60% 匹配率、外貌/体型/疤痕主评分、固定 A/B/C/D 问卷、D9/D10 类型表贴标签,或“稳定确定到精确分钟”的承诺。 +- 不再有固定 10–15 个事件长表、外貌/体型/疤痕主评分、或“稳定确定到精确分钟”的承诺。A/B/C/D 主题问卷与 D9/D10 类型表按本 Skill 使用。80%/60% 只描述事件吻合率。 - 无法验证时如实降级并说明受限,不得把内部一致性伪装成全球顶级精度。 ## 11. 上游同步边界 diff --git a/skills/jyotish-birth-time-rectification/references/candidate-comparison.md b/skills/jyotish-birth-time-rectification/references/candidate-comparison.md index 511ea50e..6b5de30c 100644 --- a/skills/jyotish-birth-time-rectification/references/candidate-comparison.md +++ b/skills/jyotish-birth-time-rectification/references/candidate-comparison.md @@ -21,12 +21,12 @@ - `next_user_action.id=adopt_representative`,或用户停止且 `on_user_stop` 为 adopt 时,本轮才 offer/accept。服务器会拒绝访谈未停的 offer。这是采用代表性时间,不是 confirmed。 - 继续收集证据时不得边追问边提供采用。 - 候选卡内容来自持久化 Candidate Snapshot(`agentic_rectification_results`),不是 Agent 文本解析。 -- 候选卡拥有时间、排名、相对支持度、采用动作与选中状态;Agent 正文不得重复表格、编号菜单或选择提示。 +- 候选卡拥有时间、排名、相对支持度、采用动作与选中状态;Agent 正文在出牌轮**要**复述候选窗、代表分钟、相对支持与八法理由。卡片仍作 adopt 控件。 ## 3. 表达边界 -- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。 -- 不暴露原始分数、内部权重、贡献矩阵、技术层名称、隐藏分钟证据或第二候选簇。 +- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。80%/60% 只描述事件吻合率。 +- 出牌轮正文写入事件–Dasha–Gochara 表、D9/D10 类型对照和技法审计;不暴露隐藏分钟证据或把分数说成唯一分钟概率。 - 候选范围必须说明“待核对边界”,不得表述为已确认出生分钟。 - 外部验证状态按服务器字面读取:`not_evaluated` 表示未调用(入口门未就绪),不是“调用了但失败”。 diff --git a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md index 9fda476e..fdd91509 100644 --- a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md +++ b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md @@ -1,6 +1,6 @@ # Conversation Strategy(V10) -生时校正访谈是自然对话,不是问卷。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、自然表达和选择一个有信息增益的下一步。 +生时校正访谈按 skill 路径 C:从候选簇差异生成 A/B/C/D 主题问卷,而不是 10–15 条事件长表,也不是无结构闲聊。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。 ## 1. 每轮上下文优先级 @@ -17,13 +17,13 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什 首次开场只使用服务器 opening brief 中的 Case 状态、出生时间不确定类型、已有证据摘要与当前可询问范围,并自然满足: -- 降低回忆负担:从最容易想起的一件经历或用户当前话题切入,不索要固定清单。 +- 降低回忆负担:从候选簇差异生成 **A/B/C/D 主题问卷**(明确有且时间大致吻合 / 类似但偏 / 没有 / 不记得),每题说明为何问、绑哪张分盘。不索要 10–15 条事件长表。 - 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。 - 不要求一次说完,允许分多轮补充、修正、暂停或换方向。 - 至多一个主问题;开场可以零问题。 - 不固定复述身份、流程、领域列表、证据数量要求或 opening brief 原文。 -示例方向(不是固定话术):“可以先从你最容易想起的一件经历开始,大概年份也可以,不需要一次说完。哪件事你现在最容易确定?” +示例方向(不是固定话术):“可以先从你最容易想起的一件经历开始,大概年份也可以。请用 A/B/C/D 回答:A 明确有且时间大致吻合;B 有类似但偏;C 没有;D 不记得。本题绑定当前分盘差异。” ## 3. 一轮的基本形态 @@ -73,8 +73,8 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服 追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级: 1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。 -2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 外貌/体质 → 胎记/疤痕 → 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。外貌/疤痕/占问可以问,但不挡提出门,也不得当主评分。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。 -3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察只用于选题,不得说成用户星座或类型标签。 +2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 外貌/体质 → 胎记/疤痕 → 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。外貌/疤痕/占问可以问,但不挡提出门,也不得当主评分。追问用 A/B/C/D,并写明绑定分盘。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`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. 已有证据的必要稳定性补强。 @@ -95,8 +95,8 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服 ## 9. 候选输出与终态 - 候选卡负责呈现时间、排名、相对支持度、采用动作与选中状态。 -- 正文只解释“这些候选当前意味着什么”和“不确定性在哪里”,不重复候选表、编号菜单或候选卡数字。 -- `relative_support` 不是概率,不能写“准确率 70%”。 +- 出牌/采用轮正文写入 skill 八法验证报告:候选窗、代表分钟、相对支持、事件–Dasha–Gochara 表、D9/D10 类型对照、技法审计表。卡片仍作 adopt 控件。 +- `relative_support` 不是概率,不能写“准确率 70%”。80%/60% 只描述事件吻合率。 - candidate、accepted、confirmed 严格分离;accepted 不是 confirmed。 - `next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间;正文必须说还不能确认唯一分钟。仍有 `next_followup` 时不得出示采用卡。 - 确认门以 `confirmation_gate` 为准。`not_evaluated` 不是 fail;holdout `not_ready` 时不得声称精确分钟或发布准确率。官方分钟层 `passed` 仍不能单独打开确认门。 diff --git a/skills/jyotish-birth-time-rectification/references/evidence-model.md b/skills/jyotish-birth-time-rectification/references/evidence-model.md index 3e36d3d5..4bc10878 100644 --- a/skills/jyotish-birth-time-rectification/references/evidence-model.md +++ b/skills/jyotish-birth-time-rectification/references/evidence-model.md @@ -117,6 +117,6 @@ draft / pending_confirmation -> rejected (用户否认,保留只读历史) - 只有 `confirmed` 证据进入评分账本;`draft` 与 `pending_confirmation` 都不参与评分。 - `family_event` 进入评分(D12 + D7 + D3 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。 - `appearance_note` / `birthmark_or_scar`:无日期只覆盖访谈;有日期才进上升/一宫辅助评分,不得当主公式。 -- `occupation_note`:与带日期事业事件独立。无日期只覆盖访谈;有日期按 D10 + 本命 10 宫辅助评分,不得贴类型标签。 +- `occupation_note`:与带日期事业事件独立。无日期只覆盖访谈;有日期按 D10 + 本命 10 宫辅助评分,允许事业类型表作校时方法。 - `horary_query` 只作背景观察,不推进评分覆盖计数,也不计入 4 事件 / 3 领域。 - 证据变化才触发重算;相同证据指纹复用缓存,不重复评分。 diff --git a/skills/jyotish-birth-time-rectification/references/technique-routing.md b/skills/jyotish-birth-time-rectification/references/technique-routing.md index 1f84f496..ee737d9a 100644 --- a/skills/jyotish-birth-time-rectification/references/technique-routing.md +++ b/skills/jyotish-birth-time-rectification/references/technique-routing.md @@ -22,18 +22,18 @@ ## 3. 按问题域强制调取 -- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。职业说明与带日期事业事件独立,同样对照 D10 与本命 10 宫,**不得**贴事业类型标签;无日期只覆盖访谈。 +- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。职业说明与带日期事业事件独立,同样对照 D10 与本命 10 宫,**允许**事业类型表作校时方法;无日期只覆盖访谈。 - 财富:用户主动提供带日期的收入、资产或财务变化时计分 `D2 / D11`。不要主动追问。窗口扫描记录 D2/D11 换升,但不新增精度阶段。 - 婚恋:`D9 + UL`(UL 为 Upapada Lagna,服务器可用时)。 - 六亲/家人:`D12` 加 `D7`(子女/伴侣细节)加 `D3`(兄弟姐妹)加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。D3 不另开精度阶段。 -- 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分,也不得发明星座或类型标签。无日期的回答只覆盖访谈,不进主公式。 +- 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分。无日期的回答只覆盖访谈,不进主公式。 - 健康:用户主动提供带日期的健康、事故或压力变化时计分 D1 + D30。不要主动追问。不是医学判断。窗口扫描记录 D30 换升,但不新增精度阶段。 - 迁居:精度阶段 `d4_refine` 问带日期的搬家/住处变化;这不是领域轮询。计分 D4 + D1 四/十二宫。 -- 教育/成就:精度阶段 `d5_refine` 在 D5 **或 D24** 换升时问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。D24 窗口扫描并入 `d5_refine`,不新增阶段 id。不得贴类型标签。 +- 教育/成就:精度阶段 `d5_refine` 在 D5 **或 D24** 换升时问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。D24 窗口扫描并入 `d5_refine`,不新增阶段 id。 - 占问:只问一次第一次认真问起这件事的时间。有日期则按该时点重算观察盘(出生地经纬,除非另给地点),可附 1/4/7/10 KP 子主。失败写成 blocked 观察,不计分,不挡提出门或确认门。没有时间或拒绝则 `skipped_by_policy`。 - 精度阶段顺序:有日期事件 → 本命上升 → 方法覆盖(感情 → 事业 → 家人 → 外貌 → 疤痕 → 职业 → 占问)→ 再对已覆盖领域做 D9 / D10 / D4 / D5(含 D24 换升)。家人不得混进 D4,也不另开 `d11_refine` / `d30_refine`。未走完挡住出牌的方法覆盖(含职业)时,不得因为关系盘仍会换升就提前出示时间卡。 - Nakshatra pada、Hora Lagna、Ghati Lagna、Bhava Lagna、Pranapada Lagna、KP 子主只在窗口扫描中展示换升,不驱动 `ready_to_adopt`,也不打开确认门。日出不可用时省略 Hora/Ghati/Pranapada,不得用 06:00 假日出。Bhava 只用本命日月,不依赖日出。 -- D9/D10 类型表只作内部观察,不得给用户贴标签。`internal_observations.ask_theme` 只决定下一问主题,不得说出星座、配偶类型或事业特质。 +- D9/D10 类型表写入出牌轮验证报告,作为校时方法,不得写成命运承诺。`internal_observations.ask_theme` 决定下一问主题。 ## 4. 受限技法边界 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.10/SKILL.md b/skills/jyotish-birth-time-rectification/versions/10.0.10/SKILL.md index c5c11544..bf813a2c 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.10/SKILL.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.10/SKILL.md @@ -115,22 +115,23 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv - `public_aa_holdout` 为 `not_ready` 时不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。 - 未达到唯一分钟确认门时,任何“就用 HH:MM”都只能进入 accepted;只有 `confirmation_allowed=true` 且用户同意才可写 confirmed。 - 若不可分 blocker 为 `blocked`、宽度大于 5、top `tied_minute_count` > 1,或 `confirmation_allowed=false`,正文必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。 -- 分钟窗口扫描只在服务端;结果进入候选卡 / 平台语言。不得把「几件事件」说成已确定到 ±5 分钟。 -- 候选卡负责候选时间、排名、相对支持度、采用动作和选中状态;正文只解释当前意义与不确定性,不重复候选表、编号菜单或卡片数字。 +- 分钟窗口扫描只在服务端。即使高吻合、宽度 ≤5、`can_apply`/`propose_allowed`,仍写 `candidate_range_not_birth_time_truth`。 +- 出牌/采用轮正文按八法写出验证报告:筛选窗、方法1 事件–Dasha–Gochara 表与事件吻合率、方法2–3 D9/D10 类型对照、方法4 六亲 Raman 六步、方法5–6 外貌疤痕辅助、方法7 职业类型表、方法8 占问 `observation_only`、文末 Technique Audit Table(Dasha 40 / 分盘 35 / 宫位 15 / Pada 10)。候选卡仍作 adopt 控件;正文**要**写候选窗、代表分钟、相对支持与方法层理由。 +- 80%/60% 只描述**事件吻合率**(高度/中度/低度拟合),**不得**写成“已确认唯一出生分钟”。 - 不得在同一回复中一边要求继续补证据、一边提供采用候选。 - 不得伪造出生分钟、分数、权重、事件 ID、分盘事实或确认门结果。 ## 10. 输出与停止条件 -- 简体中文,自然对话;不固定以“收到 / 已记录”开头,不机械复读,不擅自解释事件的“人生意义”,不推断用户未陈述的动机、心理或因果关系。 +- 简体中文。访谈按 skill 路径 C:从候选簇差异生成 A/B/C/D 主题问卷(明确有且时间吻合 / 类似但偏 / 没有 / 不记得),每题说明为何问、绑哪张分盘。允许模糊日期、允许分多轮。**不得**先逼 10–15 条事件长表。 - 每轮最多一个主要问题;完整回复可以零问题,不为了延续对话强行追问,不生成三条推荐问题。 - 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。 - 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。 -- 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分,也不得给用户贴 D9/D10 星座或类型标签。有日期的外貌/疤痕只作上升/一宫辅助对照。职业与带日期事业事件独立,对照本命第 10 宫和 D10,不得贴类型标签。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 只用于选择下一问主题。 +- 可以询问外貌、体质、胎记或疤痕,用来覆盖方法层;**不得**把外貌疤痕当作主评分。D9/D10 类型表是校时方法,写「该分钟下 D9/D10 升 X,与用户所述特质的对应/冲突」,不是咨询命运承诺。职业对照本命第 10 宫和 D10,允许类型表。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 可用于选题,类型对照写入验证报告。 - 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5/D24 成就收窄;家人走 D12/D7/D3 方法覆盖。财务走 D2/D11、健康走 D30,仅在用户主动说时计分,均不得混进 D4。Pada / Hora / Ghati / Bhava / Pranapada / KP 子主只展示换升,不确认唯一分钟。 -- 采用后本命按采用分钟重算并折叠展示技法审计;不得声称唯一分钟,也不自动进入咨询 Agent。 +- 采用后本命按采用分钟重算;不得声称唯一分钟,也不自动进入咨询 Agent。 - 采用候选后只需自然说明 accepted 与 confirmed 边界;不强制下一问,不主动关闭 Case,Session 会保留并可日后继续。 -- 不再有固定 10–15 个事件、固定 80%/60% 匹配率、外貌/体型/疤痕主评分、固定 A/B/C/D 问卷、D9/D10 类型表贴标签,或“稳定确定到精确分钟”的承诺。 +- 不再有固定 10–15 个事件长表、外貌/体型/疤痕主评分、或“稳定确定到精确分钟”的承诺。A/B/C/D 主题问卷与 D9/D10 类型表按本 Skill 使用。80%/60% 只描述事件吻合率。 - 无法验证时如实降级并说明受限,不得把内部一致性伪装成全球顶级精度。 ## 11. 上游同步边界 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/candidate-comparison.md b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/candidate-comparison.md index 511ea50e..6b5de30c 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/candidate-comparison.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/candidate-comparison.md @@ -21,12 +21,12 @@ - `next_user_action.id=adopt_representative`,或用户停止且 `on_user_stop` 为 adopt 时,本轮才 offer/accept。服务器会拒绝访谈未停的 offer。这是采用代表性时间,不是 confirmed。 - 继续收集证据时不得边追问边提供采用。 - 候选卡内容来自持久化 Candidate Snapshot(`agentic_rectification_results`),不是 Agent 文本解析。 -- 候选卡拥有时间、排名、相对支持度、采用动作与选中状态;Agent 正文不得重复表格、编号菜单或选择提示。 +- 候选卡拥有时间、排名、相对支持度、采用动作与选中状态;Agent 正文在出牌轮**要**复述候选窗、代表分钟、相对支持与八法理由。卡片仍作 adopt 控件。 ## 3. 表达边界 -- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。 -- 不暴露原始分数、内部权重、贡献矩阵、技术层名称、隐藏分钟证据或第二候选簇。 +- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。80%/60% 只描述事件吻合率。 +- 出牌轮正文写入事件–Dasha–Gochara 表、D9/D10 类型对照和技法审计;不暴露隐藏分钟证据或把分数说成唯一分钟概率。 - 候选范围必须说明“待核对边界”,不得表述为已确认出生分钟。 - 外部验证状态按服务器字面读取:`not_evaluated` 表示未调用(入口门未就绪),不是“调用了但失败”。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/conversation-strategy.md index 9fda476e..fdd91509 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/conversation-strategy.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/conversation-strategy.md @@ -1,6 +1,6 @@ # Conversation Strategy(V10) -生时校正访谈是自然对话,不是问卷。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、自然表达和选择一个有信息增益的下一步。 +生时校正访谈按 skill 路径 C:从候选簇差异生成 A/B/C/D 主题问卷,而不是 10–15 条事件长表,也不是无结构闲聊。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。 ## 1. 每轮上下文优先级 @@ -17,13 +17,13 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什 首次开场只使用服务器 opening brief 中的 Case 状态、出生时间不确定类型、已有证据摘要与当前可询问范围,并自然满足: -- 降低回忆负担:从最容易想起的一件经历或用户当前话题切入,不索要固定清单。 +- 降低回忆负担:从候选簇差异生成 **A/B/C/D 主题问卷**(明确有且时间大致吻合 / 类似但偏 / 没有 / 不记得),每题说明为何问、绑哪张分盘。不索要 10–15 条事件长表。 - 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。 - 不要求一次说完,允许分多轮补充、修正、暂停或换方向。 - 至多一个主问题;开场可以零问题。 - 不固定复述身份、流程、领域列表、证据数量要求或 opening brief 原文。 -示例方向(不是固定话术):“可以先从你最容易想起的一件经历开始,大概年份也可以,不需要一次说完。哪件事你现在最容易确定?” +示例方向(不是固定话术):“可以先从你最容易想起的一件经历开始,大概年份也可以。请用 A/B/C/D 回答:A 明确有且时间大致吻合;B 有类似但偏;C 没有;D 不记得。本题绑定当前分盘差异。” ## 3. 一轮的基本形态 @@ -73,8 +73,8 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服 追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级: 1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。 -2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 外貌/体质 → 胎记/疤痕 → 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。外貌/疤痕/占问可以问,但不挡提出门,也不得当主评分。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。 -3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察只用于选题,不得说成用户星座或类型标签。 +2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 外貌/体质 → 胎记/疤痕 → 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。外貌/疤痕/占问可以问,但不挡提出门,也不得当主评分。追问用 A/B/C/D,并写明绑定分盘。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`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. 已有证据的必要稳定性补强。 @@ -95,8 +95,8 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服 ## 9. 候选输出与终态 - 候选卡负责呈现时间、排名、相对支持度、采用动作与选中状态。 -- 正文只解释“这些候选当前意味着什么”和“不确定性在哪里”,不重复候选表、编号菜单或候选卡数字。 -- `relative_support` 不是概率,不能写“准确率 70%”。 +- 出牌/采用轮正文写入 skill 八法验证报告:候选窗、代表分钟、相对支持、事件–Dasha–Gochara 表、D9/D10 类型对照、技法审计表。卡片仍作 adopt 控件。 +- `relative_support` 不是概率,不能写“准确率 70%”。80%/60% 只描述事件吻合率。 - candidate、accepted、confirmed 严格分离;accepted 不是 confirmed。 - `next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间;正文必须说还不能确认唯一分钟。仍有 `next_followup` 时不得出示采用卡。 - 确认门以 `confirmation_gate` 为准。`not_evaluated` 不是 fail;holdout `not_ready` 时不得声称精确分钟或发布准确率。官方分钟层 `passed` 仍不能单独打开确认门。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/evidence-model.md b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/evidence-model.md index 3e36d3d5..4bc10878 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/evidence-model.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/evidence-model.md @@ -117,6 +117,6 @@ draft / pending_confirmation -> rejected (用户否认,保留只读历史) - 只有 `confirmed` 证据进入评分账本;`draft` 与 `pending_confirmation` 都不参与评分。 - `family_event` 进入评分(D12 + D7 + D3 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。 - `appearance_note` / `birthmark_or_scar`:无日期只覆盖访谈;有日期才进上升/一宫辅助评分,不得当主公式。 -- `occupation_note`:与带日期事业事件独立。无日期只覆盖访谈;有日期按 D10 + 本命 10 宫辅助评分,不得贴类型标签。 +- `occupation_note`:与带日期事业事件独立。无日期只覆盖访谈;有日期按 D10 + 本命 10 宫辅助评分,允许事业类型表作校时方法。 - `horary_query` 只作背景观察,不推进评分覆盖计数,也不计入 4 事件 / 3 领域。 - 证据变化才触发重算;相同证据指纹复用缓存,不重复评分。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/technique-routing.md b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/technique-routing.md index 1f84f496..ee737d9a 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.10/references/technique-routing.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.10/references/technique-routing.md @@ -22,18 +22,18 @@ ## 3. 按问题域强制调取 -- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。职业说明与带日期事业事件独立,同样对照 D10 与本命 10 宫,**不得**贴事业类型标签;无日期只覆盖访谈。 +- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。职业说明与带日期事业事件独立,同样对照 D10 与本命 10 宫,**允许**事业类型表作校时方法;无日期只覆盖访谈。 - 财富:用户主动提供带日期的收入、资产或财务变化时计分 `D2 / D11`。不要主动追问。窗口扫描记录 D2/D11 换升,但不新增精度阶段。 - 婚恋:`D9 + UL`(UL 为 Upapada Lagna,服务器可用时)。 - 六亲/家人:`D12` 加 `D7`(子女/伴侣细节)加 `D3`(兄弟姐妹)加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。D3 不另开精度阶段。 -- 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分,也不得发明星座或类型标签。无日期的回答只覆盖访谈,不进主公式。 +- 外貌/体质/胎记疤痕:只对照 D1 上升/一宫,**辅助降权**,不得当主评分。无日期的回答只覆盖访谈,不进主公式。 - 健康:用户主动提供带日期的健康、事故或压力变化时计分 D1 + D30。不要主动追问。不是医学判断。窗口扫描记录 D30 换升,但不新增精度阶段。 - 迁居:精度阶段 `d4_refine` 问带日期的搬家/住处变化;这不是领域轮询。计分 D4 + D1 四/十二宫。 -- 教育/成就:精度阶段 `d5_refine` 在 D5 **或 D24** 换升时问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。D24 窗口扫描并入 `d5_refine`,不新增阶段 id。不得贴类型标签。 +- 教育/成就:精度阶段 `d5_refine` 在 D5 **或 D24** 换升时问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。D24 窗口扫描并入 `d5_refine`,不新增阶段 id。 - 占问:只问一次第一次认真问起这件事的时间。有日期则按该时点重算观察盘(出生地经纬,除非另给地点),可附 1/4/7/10 KP 子主。失败写成 blocked 观察,不计分,不挡提出门或确认门。没有时间或拒绝则 `skipped_by_policy`。 - 精度阶段顺序:有日期事件 → 本命上升 → 方法覆盖(感情 → 事业 → 家人 → 外貌 → 疤痕 → 职业 → 占问)→ 再对已覆盖领域做 D9 / D10 / D4 / D5(含 D24 换升)。家人不得混进 D4,也不另开 `d11_refine` / `d30_refine`。未走完挡住出牌的方法覆盖(含职业)时,不得因为关系盘仍会换升就提前出示时间卡。 - Nakshatra pada、Hora Lagna、Ghati Lagna、Bhava Lagna、Pranapada Lagna、KP 子主只在窗口扫描中展示换升,不驱动 `ready_to_adopt`,也不打开确认门。日出不可用时省略 Hora/Ghati/Pranapada,不得用 06:00 假日出。Bhava 只用本命日月,不依赖日出。 -- D9/D10 类型表只作内部观察,不得给用户贴标签。`internal_observations.ask_theme` 只决定下一问主题,不得说出星座、配偶类型或事业特质。 +- D9/D10 类型表写入出牌轮验证报告,作为校时方法,不得写成命运承诺。`internal_observations.ask_theme` 决定下一问主题。 ## 4. 受限技法边界 diff --git a/skills/skill-package-registry.json b/skills/skill-package-registry.json index 59ee7c81..84a9b71d 100644 --- a/skills/skill-package-registry.json +++ b/skills/skill-package-registry.json @@ -92,7 +92,7 @@ { "name": "jyotish-birth-time-rectification", "version": "10.0.10", - "sha256": "a1dc47e853bd188c04a20de29906d011da21cbd297cb2d2b420684f2cd0a9236", + "sha256": "f5b4a0c05f7acc11dd0add6e95f5d77ab14e676bdbfd6f5ab797869e35994369", "sourceCommit": null, "packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.10", "status": "active" diff --git a/tests/test_active_rectification_events.py b/tests/test_active_rectification_events.py index 9e6a42bd..0fcc191f 100644 --- a/tests/test_active_rectification_events.py +++ b/tests/test_active_rectification_events.py @@ -241,7 +241,7 @@ def test_real_local_scoring_uses_dated_events_and_actual_candidate_minutes() -> assert result["confidence"] in {"low", "medium"} assert result["canonical_input_hash"] assert result["calculation_contract"]["calculation"] == { - "ayanamsa": "lahiri", + "ayanamsa": "raman", "node_mode": "mean", "ephemeris_source": "swisseph_calc_ut", } diff --git a/tests/test_active_rectification_events_v4.py b/tests/test_active_rectification_events_v4.py index b04c4d92..2474c005 100644 --- a/tests/test_active_rectification_events_v4.py +++ b/tests/test_active_rectification_events_v4.py @@ -1,6 +1,10 @@ from __future__ import annotations +from datetime import date + from scripts import active_rectification_events_v4 as v4 +from scripts.rectification.contracts import normalize_rectification_request +from scripts.rectification.scoring_service import calculation_spec, sha256 def _row(candidate_time: str, event_id: str, points: float): @@ -18,20 +22,20 @@ def _row(candidate_time: str, event_id: str, points: float): } -def test_v4_scores_real_date_boundaries_and_never_confirms_a_minute(monkeypatch) -> None: +def test_v4_wrapper_uses_raman_spec_and_never_confirms_a_minute(monkeypatch) -> None: calls = [] - def fake_compute(request): + def fake_compute(request, static_contexts=None): calls.append(request) event_id = request["events"][0]["id"] - boundary = request["events"][0]["date"] return [ - _row("05:13", event_id, 10 if boundary.endswith("-01") else 8), - _row("05:14", event_id, 9 if boundary.endswith("-01") else 9), - _row("05:15", event_id, 8 if boundary.endswith("-01") else 10), + _row("05:13", event_id, 10), + _row("05:14", event_id, 9), + _row("05:15", event_id, 8), ] - monkeypatch.setattr(v4, "compute_event_candidate_rows", fake_compute) + monkeypatch.setattr("scripts.rectification.scoring_service.compute_event_candidate_rows", fake_compute) + monkeypatch.setattr("scripts.rectification.scoring_service.compute_candidate_static_contexts", lambda request: []) result = v4.score_life_events_v4({ "birth_date": "1997-08-08", "start_time": "04:30", @@ -49,19 +53,37 @@ def test_v4_scores_real_date_boundaries_and_never_confirms_a_minute(monkeypatch) "summary": "大学入学", }], }) - assert [call["events"][0]["date"] for call in calls] == ["2016-09-01", "2016-09-30"] - assert result["candidate_scores"][1]["score"] == 9 - assert result["robustness"]["date_sensitivity_retention_rate"] == 1 + assert [call["events"][0]["date"] for call in calls] == ["2016-09-01", "2016-09-15", "2016-09-30"] assert result["can_confirm_exact_minute"] is False - assert result["calculation_spec_hash"] == "3e558d5aca9920357bda1d11c3f34fd9a4c2a7d92ccd9bc05cf85fbddd1bee20" + assert result["calculation_spec"]["ayanamsa"] == "raman" + normalized = normalize_rectification_request({ + "birth_date": "1997-08-08", + "start_time": "04:30", + "end_time": "05:30", + "lat": 36.419, + "lon": 114.213, + "tz": 8.0, + "events": [{ + "id": "00000000-0000-4000-8000-000000000001", + "domain": "education", + "event_kind": "education_milestone", + "date_start": "2016-09-01", + "date_end": "2016-09-30", + "precision": "month", + "summary": "大学入学", + }], + }, today=date(2026, 7, 28)) + assert result["calculation_spec_hash"] == sha256(calculation_spec(normalized)) def test_v4_api_validates_event_kind_and_date_boundaries(monkeypatch) -> None: from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler captured = {} - monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: v4) - monkeypatch.setattr(v4, "score_life_events_v4", lambda request: captured.setdefault("request", request) or {}) + monkeypatch.setattr( + "scripts.rectification.api_service.score_candidates", + lambda request: captured.setdefault("request", request) or {}, + ) handler = JyotishAPIHandler.__new__(JyotishAPIHandler) body = { "birth_date": "1997-08-08", diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index b8a7916e..1f89ee99 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -213,7 +213,7 @@ def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None: assert handler.status_code == 200 payload = handler.payload() assert payload['status'] == 'ok' - assert payload['ayanamsa_default'] == 'lahiri' + assert payload['ayanamsa_default'] == 'raman' assert 'swisseph_available' in payload assert 'swisseph_version' in payload assert payload['vedastro']['configured'] is False diff --git a/tests/test_ayanamsa_switching.py b/tests/test_ayanamsa_switching.py index dcefc242..d8c8b4b4 100644 --- a/tests/test_ayanamsa_switching.py +++ b/tests/test_ayanamsa_switching.py @@ -3,7 +3,7 @@ import os import unittest sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts'))) -from jyotish_engine import compute_chart_data, _apply_ayanamsa +from jyotish_engine import compute_chart_data class TestAyanamsaSwitching(unittest.TestCase): def test_ayanamsa_differences(self): @@ -12,19 +12,21 @@ class TestAyanamsaSwitching(unittest.TestCase): hour, minute, second = 14, 45, 20 lat, lon, tz = 37.7749, -122.4194, 8 - # 1. Lahiri - _apply_ayanamsa('lahiri') - chart_lahiri = compute_chart_data(year, month, day, hour, minute, lat, lon, tz, second=second) + # compute_chart_data always applies the named (or default Raman) mode. + # Global _apply_ayanamsa leftover state is not a silent Lahiri fallback. + chart_lahiri = compute_chart_data( + year, month, day, hour, minute, lat, lon, tz, second=second, ayanamsa_name='lahiri', + ) sun_lahiri = chart_lahiri[0]['planets']['Sun']['degree_raw'] - # 2. Raman - _apply_ayanamsa('raman') - chart_raman = compute_chart_data(year, month, day, hour, minute, lat, lon, tz, second=second) + chart_raman = compute_chart_data( + year, month, day, hour, minute, lat, lon, tz, second=second, ayanamsa_name='raman', + ) sun_raman = chart_raman[0]['planets']['Sun']['degree_raw'] - # 3. KP - _apply_ayanamsa('kp') - chart_kp = compute_chart_data(year, month, day, hour, minute, lat, lon, tz, second=second) + chart_kp = compute_chart_data( + year, month, day, hour, minute, lat, lon, tz, second=second, ayanamsa_name='kp', + ) sun_kp = chart_kp[0]['planets']['Sun']['degree_raw'] # Assertions @@ -57,5 +59,14 @@ class TestAyanamsaSwitching(unittest.TestCase): chart_raman['planets']['Sun']['degree_raw'], ) + chart_default = compute_chart_data( + year, month, day, hour, minute, lat, lon, tz, second=second, + )[0] + self.assertEqual(chart_default['birth_info']['ayanamsa_name'], 'raman') + self.assertEqual( + chart_default['planets']['Sun']['degree_raw'], + chart_raman['planets']['Sun']['degree_raw'], + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_raman_support_observations.py b/tests/test_raman_support_observations.py new file mode 100644 index 00000000..fc5df11f --- /dev/null +++ b/tests/test_raman_support_observations.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Raman ayanamsa default and support-observation contracts.""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +import pytest + +SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts") +if SCRIPTS not in sys.path: + sys.path.insert(0, SCRIPTS) + +ROOT = Path(__file__).resolve().parents[1] +PACKET_DIR = ROOT / "references" / "oracle" / "raman_2026_08_20" + +from ayanamsa_utils import ( # noqa: E402 + ACTIVE_AYANAMSA_NAME, + UnsupportedAyanamsaError, + is_supported_ayanamsa_name, + normalize_ayanamsa_name, + supported_ayanamsa_names, +) +from raman_support_observations import ( # noqa: E402 + BLOCKED_PACKET_PREFIXES, + build_raman_support_observations, + sieve_yogas_for_routes, +) + + +def test_default_ayanamsa_is_raman_and_true_pushya_is_supported() -> None: + assert ACTIVE_AYANAMSA_NAME == "raman" + assert normalize_ayanamsa_name(None) == "raman" + assert normalize_ayanamsa_name("") == "raman" + assert is_supported_ayanamsa_name("true_pushya") + assert "true_pushya" in supported_ayanamsa_names() + assert normalize_ayanamsa_name("true_pushya") == "true_pushya" + assert normalize_ayanamsa_name("lahiri") == "lahiri" + + +def test_unknown_ayanamsa_does_not_silently_become_lahiri() -> None: + with pytest.raises(UnsupportedAyanamsaError, match="unsupported ayanamsa"): + normalize_ayanamsa_name("not_a_real_school") + + +def test_whitelist_packets_are_present_and_blocked_prefixes_are_absent() -> None: + names = {path.name for path in PACKET_DIR.glob("*.json")} + assert "raman_house_judgment_core_subrules_packet_2026_08_20.json" in names + assert "raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json" in names + assert "raman_dasha_core_subrules_packet_2026_08_20.json" in names + for prefix in BLOCKED_PACKET_PREFIXES: + assert not any(name.startswith(prefix) for name in names) + + +def test_domain_yoga_sieve_lists_hits_and_misses() -> None: + detected = [{"name": "Sunaphaa Yoga", "planets": ["Mars"]}] + result = sieve_yogas_for_routes(detected, {"career"}) + names = {row["name"] for row in result["yogas"]} + assert "Sunaphaa Yoga" in names + hit = next(row for row in result["hits"] if row["name"] == "Sunaphaa Yoga") + assert hit["hit"] is True + assert any(row["hit"] is False for row in result["misses"]) + assert "Dharidhra Yoga" not in names + + +def test_house_six_step_marks_core_executed_and_later_steps_methodology() -> None: + chart = { + "houses": { + "house_10": {"cusp_sign": "Leo", "lord": "Sun"}, + }, + "planets": { + "Sun": {"sign": "Aries", "house": 6, "status": "exalted"}, + "Moon": {"sign": "Taurus", "house": 7}, + "Jupiter": {"sign": "Sagittarius", "house": 2}, + }, + } + packet = build_raman_support_observations(chart, [], {"career"}) + house = packet["houses"][0] + assert house["house"] == 10 + assert house["executable_core"]["house_itself_assessment"]["status"] == "executed" + assert house["executable_core"]["house_lord_assessment"]["lord"] == "Sun" + assert house["executable_core"]["karaka_assessment"]["karaka"] == "Sun" + assert house["methodology"]["navamsa_confirmation"]["status"] == "methodology_only" + assert house["methodology"]["integrated_synthesis"]["status"] == "methodology_only" + assert packet["claim_boundary"] == "governed_support_only_candidate_ready" + assert "raman_bvr_health_adversity" in packet["blocked_packets"] + engine_source = (ROOT / "scripts" / "active_rectification_event_engine.py").read_text(encoding="utf-8") + assert "AYANAMSA: Final = DEFAULT_AYANAMSA_NAME" in engine_source + assert 'AYANAMSA: Final = "lahiri"' not in engine_source + + +def test_blocked_raman_packets_are_not_loaded_as_domain_support() -> None: + from raman_support_observations import DOMAIN_PACKETS + + loaded = {name for names in DOMAIN_PACKETS.values() for name in names} + for prefix in BLOCKED_PACKET_PREFIXES: + assert not any(name.startswith(prefix) for name in loaded) diff --git a/tests/test_rectification_diagnostics_clusters.py b/tests/test_rectification_diagnostics_clusters.py index dbbd44fd..d724f79e 100644 --- a/tests/test_rectification_diagnostics_clusters.py +++ b/tests/test_rectification_diagnostics_clusters.py @@ -98,7 +98,7 @@ class RectificationDiagnosticsClustersTest(unittest.TestCase): self.assertEqual(result["candidate_splits"][0]["technique_layers"], ["D24"]) self.assertEqual(result["candidate_splits"][0]["event_ids"], [separating_event]) - def test_window_scan_reports_d9_diversity_without_sign_names(self): + def test_window_scan_reports_d9_diversity_with_sign_names(self): built = { "date_sensitivity": [], "matrix": {}, @@ -122,10 +122,11 @@ class RectificationDiagnosticsClustersTest(unittest.TestCase): "user_meaning": "D9 在 05:14 发生变化", }]) encoded = str(scan) - self.assertNotIn("白羊", encoded) - self.assertNotIn("天蝎", encoded) + self.assertIn("金牛座", encoded) + self.assertIn("天蝎座", encoded) self.assertNotIn("Aries", encoded) self.assertNotIn("Scorpio", encoded) + self.assertFalse(scan["unique_minute_claim"]) if __name__ == "__main__": diff --git a/tests/test_rectification_provenance.py b/tests/test_rectification_provenance.py index 387da6c9..a8442b9a 100644 --- a/tests/test_rectification_provenance.py +++ b/tests/test_rectification_provenance.py @@ -29,7 +29,7 @@ class RectificationProvenanceTest(unittest.TestCase): value = request() value.update({"birth_time_source": "family_exact", "timezone_id": "Asia/Taipei", "timezone_source": "iana_historical", "local_time_status": "resolved"}) normalized = normalize_rectification_request(value, today=date(2026, 7, 30)) - self.assertEqual(sha256(calculation_spec(normalized)), "fa4afe79228bedebd809c7b3c9d9d32a428f7066e3e1fd1813e44b317bd38e66") + self.assertEqual(sha256(calculation_spec(normalized)), "de2096c6fe903a795d04e27bbce8849faab3042ca568a5a90ad4b6e1b8815b6c") def test_event_provenance_is_not_forwarded_to_scoring_weights(self): value = request() diff --git a/tests/test_rectification_refinement_packet.py b/tests/test_rectification_refinement_packet.py index 85b336b1..ac5e5f44 100644 --- a/tests/test_rectification_refinement_packet.py +++ b/tests/test_rectification_refinement_packet.py @@ -83,7 +83,7 @@ def request_events() -> dict: class RefinementPacketTest(unittest.TestCase): - def test_window_scan_emits_change_minutes_without_sign_names(self): + def test_window_scan_emits_d9_sign_names_for_type_table_report(self): built = { "static_contexts": [ feature("05:13", d1=1, d9=1, d10=4), @@ -94,13 +94,14 @@ class RefinementPacketTest(unittest.TestCase): self.assertEqual(scan["d9_lagna_count"], 2) self.assertEqual(scan["d10_lagna_count"], 1) self.assertEqual(scan["d1_lagna_count"], 1) + self.assertEqual(scan["d9_sign_names"], ["金牛座", "天蝎座"]) + self.assertEqual(scan["d10_sign_names"], ["狮子座"]) self.assertEqual(scan["transitions"], [{ "layer": "d9", "at": "05:14", "user_meaning": "D9 在 05:14 发生变化", }]) encoded = str(scan) - self.assertNotIn("白羊", encoded) self.assertNotIn("Aries", encoded) self.assertFalse(scan["unique_minute_claim"]) self.assertFalse(scan["confirmation_allowed"]) @@ -237,8 +238,10 @@ class RefinementPacketTest(unittest.TestCase): self.assertFalse(packet["oos_blind_prompts"][0]["used_for_scoring"]) self.assertFalse(packet["confirmation_allowed"]) encoded = str(packet["window_scan"]) + self.assertIn("金牛座", encoded) self.assertNotIn("热情冲动", encoded) - self.assertNotIn("配偶类型", encoded) + self.assertIn("事件吻合率", str(packet["event_fit_rate"])) + self.assertFalse(packet["event_fit_rate"]["unique_minute_claim"]) def test_decision_receipt_downgrades_confidence_on_dasha_conflict(self): request = request_events() diff --git a/tests/test_rectification_v5_services.py b/tests/test_rectification_v5_services.py index afe4349e..961af9df 100644 --- a/tests/test_rectification_v5_services.py +++ b/tests/test_rectification_v5_services.py @@ -198,7 +198,7 @@ class RectificationV5ServicesTest(unittest.TestCase): normalized = normalize_rectification_request(request(), today=date(2026, 7, 28)) self.assertEqual( sha256(calculation_spec(normalized)), - "f05fe0f56ef9ba2b18ec3c6c54f1649f06f1ae5a926491a5c5f676d718d92865", + "d68a59ae345a87bcdf7ad45fee913d3aca02c553f896ce6c173a8bd4ab327af3", ) def test_cross_midnight_range_preserves_next_day_datetimes_and_typescript_hash(self): @@ -215,7 +215,7 @@ class RectificationV5ServicesTest(unittest.TestCase): self.assertEqual(candidates[-1].isoformat(), "1997-08-09T03:59:00") self.assertEqual( sha256(calculation_spec(normalized)), - "b0d5c5ec7f56edbfa2b2e1041b4aa3b648c6cb0681f3f502c7b7910c2894b205", + "1923a1ee2fa873f1a26c97b23d05cfee38ec38071b60076a014ad17b2844cedd", ) def test_candidate_range_boundaries_stay_bounded_and_equal_is_one_minute(self): diff --git a/tests/test_upstream_import_plan.py b/tests/test_upstream_import_plan.py index 07e2af7f..8e6d1903 100644 --- a/tests/test_upstream_import_plan.py +++ b/tests/test_upstream_import_plan.py @@ -155,3 +155,17 @@ def test_import_manifest_schema_accepts_archive_and_legacy_snapshot_records() -> assert pinned["source_commit"] == source_manifest["source_commit"] == plan["source_commit"] assert pinned["source_tree_hash"] == source_manifest["source_tree_sha256"] == plan["source_tree_sha256"] assert pinned["source_skill_sha256"] == source_manifest["source_skill_sha256"] == EXPECTED_SOURCE_SKILL_SHA256 + + +def test_raman_2026_08_20_packets_are_adapt_only_and_exclude_blocked_batches() -> None: + plan = _load(PLAN) + support = plan["raman_2026_08_20_support"] + assert support["action"] == "adapt" + packet_dir = ROOT / support["target"] + names = {path.name for path in packet_dir.glob("*.json")} + assert set(support["packets"]) <= names + assert "scripts/raman_support_observations.py" in support["targets"] + for prefix in support["rejected_packets"]: + assert not any(name.startswith(prefix) for name in names) + assert "health" in " ".join(support["rejected_packets"]) + assert "unique-minute" in support["reason"] or "support-only" in support["reason"]