fix(ayanamsa): pin Lahiri on reference sets and add a user setting
Product default stays Raman. Public-case revalidation and fixture dashas now pass --ayanamsa lahiri; profiles store the four selectable values. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-03 — 岁差成为用户设置(默认仍是 Raman)
|
||||
|
||||
自 `80102459`(2026-08-20)起,产品计算默认岁差已是 Raman,但参考集对照仍按 Lahiri 口径写死期望值,API 回执里的岁差标签也和实际计算对不上。本轮**不改**默认值(仍是 Raman),把岁差做成账户设置:`raman` / `lahiri` / `kp` / `true_pushya`。公开案例复验与夹具大运显式传 `--ayanamsa lahiri`。升级后老用户可能看到 Moon nakshatra 或大运主星与 8 月 20 日前的 Lahiri 盘不同;可在「星盘资料 → 编辑本人资料」里切回 Lahiri。切换只影响之后的新计算。
|
||||
|
||||
## 2026-08-30 — 非收敛区间提议不再是无出口死胡同(Skill 10.0.13 不变)
|
||||
|
||||
训练门已开、覆盖层仍挡住确认、可渲染区分题又问不出来时,助手会说「可以先按当前区间看盘,也可以再补一件记得时间的经历」,但时间卡、下一问、区间数值都不给。本轮把这句话改成与真实承载一致。
|
||||
|
||||
@@ -7872,3 +7872,19 @@
|
||||
- 相关记录:BUG-416、BUG-431、BUG-498
|
||||
- 复发自:BUG-431
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-511 | 公开案例复验因默认岁差改 Raman 而失败,标签与计算不一致
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-03
|
||||
- 最近更新:2026-09-03
|
||||
- 影响面:`tests/run_real_case_revalidation.py`、`scripts/jyotish_api_server.py` 岁差回执、账户 `profiles.ayanamsa`、咨询/报告/校正/每日星语请求口径
|
||||
- 用户现象:发布门真实案例复验 `valid=false`,gated 通过率掉到门槛以下。同一张虚构盘在旧生产与当前默认下 Moon nakshatra / 首段大运不同。回执上的岁差名有时写成 Lahiri 或 Raman,与该次实际计算不是同一套。
|
||||
- 触发条件:`80102459` 把引擎默认改成 Raman 后,参考集调用没有显式锁定 Lahiri;VedAstro `ayanamsa_policy` 与部分回执字段仍写死字面量。
|
||||
- 根因:参考集对照依赖「默认值恰好是 Lahiri」;产品默认改为 Raman 后口径漂移。三处标签用字面量兜底,不读该次请求实际岁差。账户没有用户可选岁差。
|
||||
- 修复:参考集与夹具大运显式 `--ayanamsa lahiri`。API 回执与 VedAstro 对照改用该次请求实际岁差。`profiles.ayanamsa` 默认 `raman`,设置里可选四个值;咨询、报告、校正、星盘库、每日星语走 `resolveAyanamsa`。`DEFAULT_AYANAMSA_NAME` 本轮不改。
|
||||
- 验证:夹具大运日期不变;前端 `resolveAyanamsa`、账户 PATCH 四值校验、咨询 toolInput 带 profile 岁差、校正快照带岁差但不进 fingerprint;`npm run test:db` 跑迁移。公开案例复验命令见本轮 PROGRESS。
|
||||
- 防复发:参考集与外部对照必须显式传岁差,不得依赖默认值。回执只显示实际名。岁差不进校正 `baselineFingerprint`,不重算历史报告。
|
||||
- 相关记录:无
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# PROGRESS · 仓库整备(2026-09-03)
|
||||
|
||||
工作树:`/Users/jesse/Downloads/Copse/astrology/.worktrees/repo-hygiene-20260903`
|
||||
分支:`codex/repo-hygiene-20260903`
|
||||
基线:开工后快进到 `origin/staging` @ `58cae371`(含 BUG-510 与上游同步任务书)。任务书:`docs/tasks/TASK-repo-hygiene-20260903.md`。
|
||||
未碰 `main`。未改 `DEFAULT_AYANAMSA_NAME`(保持 `raman`)。未改校正打分/阈值/golden。未改 `page.tsx`(仍 2041 行)。`scripts/jyotish_api_server.py` 仍 11120 行(与 rebase 后 HEAD 相同)。
|
||||
|
||||
BUG 编号:开工时 `docs/BUG_HISTORY.md` 最大号是 **BUG-510**。本轮岁差记 **BUG-511**。
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 0 岁差设置 + 参考集锁口径 | 待验收 | 本提交。产品 staging 实测仍欠 |
|
||||
| 1 发布门本地可复现 | 未开始 | 任务 0 先推 |
|
||||
| 2 注册表白名单 | 未开始 | |
|
||||
| 3 仓库地址 | 未开始 | |
|
||||
| 4 早期日志搬家 | 未开始 | |
|
||||
| 5 远端分支清理 | 未开始 | |
|
||||
|
||||
## 环境备忘
|
||||
|
||||
- 本 worktree **没有** `.venv`。预检与 Python 测试用主仓 `/Users/jesse/Downloads/Copse/astrology/yinduzhanxing/.venv`(`mcp` 1.28.1,`hypothesis` 已装)。
|
||||
- 任务书写把迁移放进 `frontend/db/migrations/`。实际 `frontend/scripts/db-migrate.mjs` 会跑 `frontend/db/migrations` **和** `frontend/supabase/migrations`。账户资料列修复历来放 supabase(`account-api` 还断言同类 SQL 不在 `db/migrations`)。本轮唯一迁移是 `frontend/supabase/migrations/20260903010000_profile_ayanamsa.sql`,避免两个目录同名撞车。
|
||||
- 产品文案里的「账户与出生资料」在现界面是 **星盘资料** 里的本人编辑表,不是 `个人资料`(MVP 合同禁止在那里放出生 UI)。岁差单选加在本人星盘表单,未改 `page.tsx`。
|
||||
- `npm run test:db` 与 `npx tsx --test tests/rectification-*.test.ts` 不可并行:默认 `JYOTISHA_POSTGRES_MAX_FIXTURES=2`,抢槽时前几个库测会报泛化 `database migration failed`。隔离重跑后绿。
|
||||
|
||||
## 任务 0
|
||||
|
||||
### 0.a 参考集锁口径
|
||||
|
||||
- `tests/run_real_case_revalidation.py`:`REFERENCE_AYANAMSA = "lahiri"`,引擎 argv `--ayanamsa lahiri`,报告 `reference_ayanamsa`。
|
||||
- `scripts/local_accuracy_report.py` 同口径。
|
||||
- 夹具大运:`base` 加 `--ayanamsa lahiri`。三栏:原值=依赖默认;新值=显式 lahiri;原因=夹具日期是 Lahiri 口径。期望日期未改。
|
||||
|
||||
### 0.b 标签与计算一致
|
||||
|
||||
- `scripts/jyotish_api_server.py` 经 `_request_ayanamsa` / `DEFAULT_AYANAMSA_NAME` / `ayanamsa_display_name`,不再写死 `'lahiri'` / `'Raman'`。
|
||||
- `_request_ayanamsa` 也读 `ayanamsa_policy`,VedAstro 对照与该次请求同口径。
|
||||
- `DEFAULT_AYANAMSA_NAME` 仍是 `'raman'`。
|
||||
- 验收 grep(`scripts/jyotish_api_server.py` + `frontend/src`,排除 test):只剩 `frontend/src/lib/ayanamsa.ts` 选项/默认,以及 `personal-report-generation.ts` 的 `SAFE_AYANAMSA` 显示映射。
|
||||
|
||||
### 0.c 用户设置
|
||||
|
||||
- `frontend/src/lib/ayanamsa.ts`:`resolveAyanamsa`,默认 `raman`。
|
||||
- 迁移:`profiles.ayanamsa text not null default 'raman'` + 四值 check + `grant update (ayanamsa)` 给 `authenticated`。
|
||||
- 咨询、声明窗口、报告 worker、每日星语、合盘、校正 v9 引擎请求、生时评估 scan 均传实际岁差。
|
||||
- 校正 `V9BaselineSnapshot` 新写入带 `ayanamsa`;**不进** `baselineFingerprint`。
|
||||
- 岁差不进 `declarationFields` / `concurrencyFields`,改设置不 409、不清确认。
|
||||
|
||||
### 偏离
|
||||
|
||||
- 迁移目录:supabase,原因见环境备忘。
|
||||
- UI 位置:星盘资料本人编辑,不是不存在的「账户与出生资料」面板。
|
||||
|
||||
### 命令摘要
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `python tests/run_real_case_revalidation.py` | `valid: true`,`reference_ayanamsa: lahiri`,gated **66/66**,`pass_rate: 1.0` |
|
||||
| `pytest …test_fixture_dasha_timeline…` + `test_api_server_growth_contract.py` | 4 passed;夹具日期未改;API 行数 11120 |
|
||||
| `scripts/user_invocation_acceptance_check.py` | `status: pass`,`checks.user_invocation_tests: true`,`external_adapter_status: complete`(本机 PyJHora 可用) |
|
||||
| `npx tsc --noEmit` | 0 错 |
|
||||
| `npx eslint .` | 0 error,73 既有 warning |
|
||||
| 前端聚焦:ayanamsa / account-api / consultation-route-service / server-owned-birth-profile / global-birth-profile-routes / settings-mvp / rectification-v9-case-service / consultation-workflow-request / consultation-birth-accuracy / consultation-agentic-runtime / personal-report-api / rectification-v9-engine-contract | fail=0 |
|
||||
| `npm run test:db`(`--test-concurrency=1`,单独跑) | **34 pass / 0 fail** |
|
||||
| `npx tsx --test tests/rectification-*.test.ts` 隔离重跑库测 | ingest P0 + v9-database **10 pass / 0 fail**;未改 scorer/golden |
|
||||
| `wc -l frontend/src/app/page.tsx` | 2041,与 HEAD 相同 |
|
||||
|
||||
产品负责人 staging 实测(虚构盘 2000-01-01 12:00 UTC 0/0:Lahiri=Swati,Raman=Vishakha)仍欠。部署 SHA 待合入 staging 后补。
|
||||
|
||||
## 任务 1–5
|
||||
|
||||
尚未开始。
|
||||
@@ -91,7 +91,7 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-repo-hygiene-20260903.md` | — | 岁差默认 Raman + 用户设置可选、参考集锁口径(P0,单独先推)、发布门本地可复现、注册表白名单、仓库地址、早期日志搬家、远端分支清理 | 待领取 | 分支 `codex/repo-hygiene-20260903` |
|
||||
| `TASK-repo-hygiene-20260903.md` | `PROGRESS-repo-hygiene-20260903.md` | 岁差默认 Raman + 用户设置可选、参考集锁口径(P0,单独先推)、发布门本地可复现、注册表白名单、仓库地址、早期日志搬家、远端分支清理 | 执行中 | 分支 `codex/repo-hygiene-20260903` |
|
||||
|
||||
### 上游同步与能力接入
|
||||
|
||||
|
||||
@@ -200,6 +200,16 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3:
|
||||
- **Surface:** canvas fill, hairline border, `--shadow-elevated`; hover uses the warm card surface. No utility-class shadows.
|
||||
- **Accessibility:** a real button in document order with a visible label matching its accessible name, 44px target, and the focus ring; the icon is decorative.
|
||||
|
||||
### Ayanamsa preference
|
||||
|
||||
The self-profile editor in 星盘资料 (`ChartLibraryPanel`) owns the ayanamsa radios. Personal 个人资料 stays account basics only.
|
||||
|
||||
- **Structure:** four 44px stacked radios reused from `.theme-preference-option`, plus a caption under the group. Raman is marked 默认. Each option has a one-line description; selected rows show the same `Check` as the theme panel.
|
||||
- **Values:** `raman` (default) / `lahiri` / `kp` / `true_pushya`. Copy lives in `frontend/src/lib/ayanamsa.ts`.
|
||||
- **Hint:** “切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。”
|
||||
- **Placement:** after `ProfileFields` on the self-profile form only. Not on other-chart save, not on `page.tsx`.
|
||||
- **States:** default, hover, selected (`aria-checked` only), keyboard focus. Switching does not clear a confirmed birth minute.
|
||||
|
||||
### Rectification surface
|
||||
|
||||
The birth-time rectification session is the consultation transcript plus a house board; questions live inside the assistant message that asked them. It shares every waiting vocabulary with the consultation surface; nothing here spins or says "loading" after the reveal.
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function GET() {
|
||||
const admin = createAdminSupabaseClient();
|
||||
let { data: profile, error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,name,birth_time,rectification_case_id")
|
||||
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa,name,birth_time,rectification_case_id")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function GET() {
|
||||
...withoutDeclaredWindow.data,
|
||||
declared_window_start: null,
|
||||
declared_window_end: null,
|
||||
ayanamsa: undefined,
|
||||
};
|
||||
profileError = withoutDeclaredWindow.error;
|
||||
} else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) {
|
||||
@@ -82,6 +83,7 @@ export async function GET() {
|
||||
birth_place_provider_id: undefined,
|
||||
timezone_id: undefined,
|
||||
timezone_source: undefined,
|
||||
ayanamsa: undefined,
|
||||
} : null;
|
||||
profileError = fallback.error;
|
||||
} else {
|
||||
@@ -281,6 +283,7 @@ export async function PATCH(request: Request) {
|
||||
...(payload.birth_place_provider_id !== undefined ? { birth_place_provider_id: payload.birth_place_provider_id } : {}),
|
||||
...(payload.timezone_id !== undefined ? { timezone_id: payload.timezone_id } : {}),
|
||||
...(payload.timezone_source !== undefined ? { timezone_source: payload.timezone_source } : {}),
|
||||
...(payload.ayanamsa !== undefined ? { ayanamsa: payload.ayanamsa } : {}),
|
||||
};
|
||||
const withoutCoordinates = baseProfile;
|
||||
const invalidatesUnconfirmedApplication = Object.keys(applicationPatch).length > 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { parseBirthTimeProfile } from "@/lib/birth-time-journey-adapters";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { BirthProfileTimezoneError, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import {
|
||||
createJyotishBirthTimeJourneyEngine,
|
||||
@@ -96,7 +97,7 @@ export async function POST(request: Request) {
|
||||
case "assess": {
|
||||
const { data: profile, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset")
|
||||
.select("birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset,ayanamsa")
|
||||
.eq("id", user.id)
|
||||
.maybeSingle();
|
||||
if (error) throw new BirthTimeJourneyStoreError("load_case");
|
||||
@@ -107,7 +108,10 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
const assessment = parseBirthTimeProfile(await resolveMissingBirthTimezoneOffset(profile));
|
||||
return responseWithJourneyMetric(service.assess(user.id, assessment), "turn_advanced");
|
||||
return responseWithJourneyMetric(
|
||||
service.assess(user.id, { ...assessment, ayanamsa: resolveAyanamsa(profile) }),
|
||||
"turn_advanced",
|
||||
);
|
||||
}
|
||||
case "answer_question":
|
||||
return responseWithJourneyMetric(service.answerQuestion(
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function POST(request: Request) {
|
||||
async loadProfile(profileUserId) {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa")
|
||||
.eq("id", profileUserId)
|
||||
.single();
|
||||
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
|
||||
|
||||
@@ -20,7 +20,7 @@ function createProfileRepository(
|
||||
async loadProfile(userId) {
|
||||
return admin
|
||||
.from("profiles")
|
||||
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,onboarding_payload,onboarding_version,onboarding_generated_at")
|
||||
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa,onboarding_payload,onboarding_version,onboarding_generated_at")
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function POST(request: Request) {
|
||||
if (userId) {
|
||||
const result = await supabase
|
||||
.from("profiles")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_status,latitude,longitude,timezone_offset,birth_place_label")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_status,latitude,longitude,timezone_offset,birth_place_label,ayanamsa")
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
profile = result.data ?? null;
|
||||
|
||||
@@ -1623,6 +1623,10 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.theme-preference-option:hover, .theme-preference-option[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.theme-preference-option > svg { width: 18px; height: 18px; color: var(--color-ink-tertiary); }
|
||||
.theme-preference-option > svg:last-child { color: var(--color-action); }
|
||||
.ayanamsa-preference-option { grid-template-columns: minmax(0, 1fr) 18px; }
|
||||
.ayanamsa-preference-option[aria-checked="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.ayanamsa-preference-option small { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); font-weight: 400; }
|
||||
.ayanamsa-preference-hint { margin: var(--space-3) 0 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.account-menu-danger, .account-menu-danger > svg { color: var(--color-danger); }
|
||||
.account-menu-danger[data-highlighted] { background: var(--color-danger-muted); }
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import {
|
||||
AYANAMSA_OPTIONS,
|
||||
AYANAMSA_SWITCH_HINT,
|
||||
type AyanamsaName,
|
||||
} from "@/lib/ayanamsa";
|
||||
|
||||
export function AyanamsaPreferenceField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
readonly value: AyanamsaName;
|
||||
readonly onChange: (next: AyanamsaName) => void;
|
||||
}) {
|
||||
return (
|
||||
<section className="sheet-section ayanamsa-preference" aria-labelledby="ayanamsa-preference-title">
|
||||
<div className="section-heading">
|
||||
<b id="ayanamsa-preference-title">岁差</b>
|
||||
<small>用来把黄道转成印度占星用的恒星黄道</small>
|
||||
</div>
|
||||
<div className="theme-preference-options" role="radiogroup" aria-labelledby="ayanamsa-preference-title">
|
||||
{AYANAMSA_OPTIONS.map((option) => {
|
||||
const selected = value === option.value;
|
||||
return (
|
||||
<button
|
||||
className="theme-preference-option ayanamsa-preference-option"
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onChange(option.value)}
|
||||
>
|
||||
<span>
|
||||
{option.label}
|
||||
<small>{option.description}</small>
|
||||
</span>
|
||||
{selected ? <Check aria-hidden="true" /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="ayanamsa-preference-hint">{AYANAMSA_SWITCH_HINT}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,18 @@ import {
|
||||
readProfile,
|
||||
upsertSelfChart,
|
||||
} from "@/lib/home-profile";
|
||||
import { emptyProfile, timestamp, type Account, type ChartLibraryRecord, type ChartRelationship, type Profile, type SynastryRelationshipType, type SynastryReportCard } from "@/lib/home-types";
|
||||
import {
|
||||
emptyProfile,
|
||||
timestamp,
|
||||
type Account,
|
||||
type ChartLibraryRecord,
|
||||
type ChartRelationship,
|
||||
type Profile,
|
||||
type SynastryRelationshipType,
|
||||
type SynastryReportCard,
|
||||
} from "@/lib/home-types";
|
||||
import { AyanamsaPreferenceField } from "@/components/ayanamsa-preference-field";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
export type ChartLibraryPanelProps = {
|
||||
readonly account: Account | null;
|
||||
@@ -179,6 +190,10 @@ export function ChartLibraryPanel({
|
||||
<form className="profile-form chart-library-form" onSubmit={saveProfile}>
|
||||
<div className="section-heading"><b>编辑本人星盘</b><small>这份资料会用于默认解盘与新对话。</small></div>
|
||||
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="self-profile-name" />
|
||||
<AyanamsaPreferenceField
|
||||
value={resolveAyanamsa(profileDraft)}
|
||||
onChange={(ayanamsa) => setProfileDraft({ ...profileDraft, ayanamsa })}
|
||||
/>
|
||||
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
|
||||
<div className="dialog-actions">
|
||||
<button className="button-secondary" type="button" onClick={() => { setEditingSelfChart(false); setProfileDraft(profile); }}>取消编辑</button>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
readProfile,
|
||||
selectedBirthPlace,
|
||||
} from "@/lib/home-profile";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { createStartGreeting } from "@/lib/onboarding-client";
|
||||
import {
|
||||
timestamp,
|
||||
@@ -226,6 +227,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
timezone_id: nextProfile.timezoneId || null,
|
||||
timezone_offset: birthPlace?.tz ?? null,
|
||||
timezone_source: nextProfile.timezoneSource || null,
|
||||
ayanamsa: resolveAyanamsa(nextProfile),
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { isBirthClockTime, parseBirthDate } from "./birth-time-intake-model.ts";
|
||||
import { AYANAMSA_VALUES } from "./ayanamsa.ts";
|
||||
|
||||
const nullableTrimmedString = (maximum: number) => z.string().trim().min(1).max(maximum).nullable();
|
||||
const nullableBirthDate = z.string().refine((value) => parseBirthDate(value) !== undefined, {
|
||||
@@ -52,6 +53,7 @@ export const accountProfilePatchSchema = z.object({
|
||||
birth_place_provider_id: nullableTrimmedString(160).optional(),
|
||||
timezone_id: nullableTrimmedString(80).optional(),
|
||||
timezone_source: z.literal("iana_historical").nullable().optional(),
|
||||
ayanamsa: z.enum(AYANAMSA_VALUES).optional(),
|
||||
}).strict().superRefine((value, context) => {
|
||||
const source = value.birth_time_source;
|
||||
const time = value.reported_birth_time;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export const AYANAMSA_VALUES = ["raman", "lahiri", "kp", "true_pushya"] as const;
|
||||
export type AyanamsaName = (typeof AYANAMSA_VALUES)[number];
|
||||
export const DEFAULT_AYANAMSA: AyanamsaName = "raman";
|
||||
|
||||
export const AYANAMSA_OPTIONS = [
|
||||
{
|
||||
value: "raman",
|
||||
label: "Raman(默认)",
|
||||
description: "本站现在默认用的岁差。",
|
||||
},
|
||||
{
|
||||
value: "lahiri",
|
||||
label: "Lahiri",
|
||||
description: "公开星历和教材里最常见的岁差。",
|
||||
},
|
||||
{
|
||||
value: "kp",
|
||||
label: "KP",
|
||||
description: "克里希那穆提体系用的岁差。",
|
||||
},
|
||||
{
|
||||
value: "true_pushya",
|
||||
label: "True Pushya",
|
||||
description: "True Pushya 岁差。",
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
value: AyanamsaName;
|
||||
label: string;
|
||||
description: string;
|
||||
}>;
|
||||
|
||||
export const AYANAMSA_SWITCH_HINT =
|
||||
"切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。";
|
||||
|
||||
export function isAyanamsaName(value: unknown): value is AyanamsaName {
|
||||
return typeof value === "string" && (AYANAMSA_VALUES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function resolveAyanamsa(
|
||||
profile?: { ayanamsa?: unknown } | null,
|
||||
): AyanamsaName {
|
||||
const raw = profile?.ayanamsa;
|
||||
if (typeof raw === "string") {
|
||||
const key = raw.trim().toLowerCase().replace(/-/g, "_");
|
||||
if (key === "krishnamurti" || key === "krishnamurti_paddhati") return "kp";
|
||||
if (isAyanamsaName(key)) return key;
|
||||
}
|
||||
return DEFAULT_AYANAMSA;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { chinaLocations } from "@/data/china-locations";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
import { finiteBirthNumber, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
|
||||
export type BirthRectificationProfile = {
|
||||
@@ -11,6 +12,7 @@ export type BirthRectificationProfile = {
|
||||
longitude?: number | null;
|
||||
timezoneId?: string;
|
||||
timezoneOffset?: number | null;
|
||||
ayanamsa?: AyanamsaName | string | null;
|
||||
};
|
||||
|
||||
export async function payloadFromProfile(profile: BirthRectificationProfile) {
|
||||
@@ -33,5 +35,6 @@ export async function payloadFromProfile(profile: BirthRectificationProfile) {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(resolved),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import type {
|
||||
BirthTimeAssessment,
|
||||
ScanStability,
|
||||
} from "./birth-time-journey.ts";
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
|
||||
function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
function scanInput(assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown }): JourneyScanInput | null {
|
||||
if (assessment.source === "unknown") return null;
|
||||
if (assessment.source === "period_only") {
|
||||
const periodScan = {
|
||||
@@ -25,7 +26,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
lat: assessment.location.lat,
|
||||
lon: assessment.location.lon,
|
||||
tz: assessment.location.tz,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(assessment),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -37,7 +38,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
lat: assessment.location.lat,
|
||||
lon: assessment.location.lon,
|
||||
tz: assessment.location.tz,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(assessment),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ function questionnaireStability(
|
||||
|
||||
export async function scanAssessment(
|
||||
engine: Pick<LegacyBirthTimeJourneyEngine, "scan">,
|
||||
assessment: BirthTimeAssessment,
|
||||
assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown },
|
||||
): Promise<{
|
||||
readonly stability: ScanStability;
|
||||
readonly questionnaire: RectificationQuestionnaire | null;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { DynamicStoredFields, LegacyStoredFields } from "./birth-time-journ
|
||||
import { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError } from "./birth-time-journey-errors.ts";
|
||||
import { createDynamicJourneyMethods } from "./birth-time-dynamic-service-methods.ts";
|
||||
import { createDynamicCandidateConfirmation } from "./birth-time-dynamic-candidate-confirmation.ts";
|
||||
import type { AyanamsaName } from "./ayanamsa.ts";
|
||||
|
||||
export { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError };
|
||||
|
||||
@@ -53,7 +54,7 @@ export type JourneyScanInput = {
|
||||
readonly lat: number;
|
||||
readonly lon: number;
|
||||
readonly tz: number;
|
||||
readonly ayanamsa: "raman" | "lahiri";
|
||||
readonly ayanamsa: AyanamsaName;
|
||||
};
|
||||
|
||||
export type JourneyScoreInput = {
|
||||
@@ -207,7 +208,7 @@ export function createBirthTimeJourneyService(ports: BirthTimeJourneyPorts) {
|
||||
const dynamicMethods = createDynamicJourneyMethods(ports);
|
||||
const dynamicCandidates = createDynamicCandidateConfirmation(ports);
|
||||
return {
|
||||
async assess(userId: string, assessment: BirthTimeAssessment): Promise<VersionedJourneyResponse | DynamicVersionedJourneyResponse> {
|
||||
async assess(userId: string, assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown }): Promise<VersionedJourneyResponse | DynamicVersionedJourneyResponse> {
|
||||
const scan = await scanAssessment(ports.engine, assessment);
|
||||
const snapshot = assessBirthTime(assessment, scan.stability);
|
||||
const persisted = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ConsultationBirthTimeMode,
|
||||
} from "./consultation-birth-time-mode.ts";
|
||||
import type { GeneralDailyReference } from "./general-daily-panchanga.ts";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "./ayanamsa.ts";
|
||||
|
||||
export type ConsultationProfileTruthErrorCode =
|
||||
| "profile_unavailable"
|
||||
@@ -42,6 +43,7 @@ type ServerChartToolInput = Readonly<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
tz: number;
|
||||
ayanamsa: AyanamsaName;
|
||||
declared_accuracy: DeclaredBirthAccuracy["declaredAccuracy"];
|
||||
time_source: string;
|
||||
}>;
|
||||
@@ -84,6 +86,7 @@ export type DeclaredBirthWindowConsultation = Readonly<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
tz: number;
|
||||
ayanamsa: AyanamsaName;
|
||||
rangeStart: string;
|
||||
rangeEnd: string;
|
||||
}>;
|
||||
@@ -352,6 +355,7 @@ function serverChartFromProfile(
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
declared_accuracy: accuracy.declaredAccuracy,
|
||||
time_source: accuracy.timeSource,
|
||||
}),
|
||||
@@ -435,6 +439,7 @@ function declaredWindowFromProfile(value: unknown): DeclaredBirthWindowConsultat
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
rangeStart: range.startTime,
|
||||
rangeEnd: range.endTime,
|
||||
}),
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function fetchDeclaredWindowChart(
|
||||
lat: toolInput.lat,
|
||||
lon: toolInput.lon,
|
||||
tz: toolInput.tz,
|
||||
ayanamsa: toolInput.ayanamsa,
|
||||
range_start: toolInput.rangeStart,
|
||||
range_end: toolInput.rangeEnd,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { chinaLocations } from "@/data/china-locations";
|
||||
import { finiteBirthNumber, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
|
||||
export type GlobalBirthProfile = {
|
||||
name?: string;
|
||||
@@ -13,6 +14,7 @@ export type GlobalBirthProfile = {
|
||||
longitude?: number | null;
|
||||
timezoneId?: string;
|
||||
timezoneOffset?: number | null;
|
||||
ayanamsa?: AyanamsaName | string;
|
||||
};
|
||||
|
||||
function resolvedLocation(profile: GlobalBirthProfile) {
|
||||
@@ -46,7 +48,7 @@ export async function dailyProfilePayload(profile: GlobalBirthProfile, today: st
|
||||
tz,
|
||||
transit_date: today,
|
||||
today,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
node_mode: "mean",
|
||||
};
|
||||
}
|
||||
@@ -76,5 +78,6 @@ export async function synastryBirthPayload(profile: GlobalBirthProfile) {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type Profile,
|
||||
type SynastryRelationshipType,
|
||||
} from "@/lib/home-types";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
export function findProvince(code: string) {
|
||||
return china.provinces.find((province) => province.code === code);
|
||||
@@ -303,6 +304,7 @@ export function readProfile(value: unknown): Profile {
|
||||
latitude,
|
||||
longitude,
|
||||
timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
...(chartRelationship ? { chartRelationship } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BeamAvatar } from "@/lib/beam-avatar";
|
||||
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
|
||||
import type { BirthTimeDraft } from "@/lib/birth-time-intake-model";
|
||||
import { DEFAULT_AYANAMSA, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
import type { AgentActivityView, ChatMessage } from "@/lib/chat-message-view";
|
||||
import type { ConsultationTimelineRow } from "@/lib/consultation-run-timeline";
|
||||
import type { PublicThinkingSection } from "@/lib/consultation-thinking-plan";
|
||||
@@ -28,6 +29,7 @@ export type Profile = BirthTimeDraft & {
|
||||
longitude: number | null;
|
||||
timezoneOffset: number | null;
|
||||
rectificationCaseId: string;
|
||||
ayanamsa: AyanamsaName;
|
||||
chartRelationship?: ChartRelationship;
|
||||
};
|
||||
export type ChartRelationship = "self" | "partner" | "family" | "friend" | "client" | "other";
|
||||
@@ -230,6 +232,7 @@ export const emptyProfile: Profile = {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
timezoneOffset: null,
|
||||
ayanamsa: DEFAULT_AYANAMSA,
|
||||
};
|
||||
export type StoredDailyStarlanguage = {
|
||||
readonly day: string;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import type { ConsultationInput } from "@/mastra";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import type {
|
||||
ReportAgentPort,
|
||||
ReportEvidenceBundleV2,
|
||||
@@ -415,6 +416,7 @@ export async function resolveReportCreate(deps: ReportCreateCoreDeps): Promise<R
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
city: birthPlaceLabel,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
question: `请为个人报告计算 ${theme} 主题证据`,
|
||||
theme,
|
||||
entryMode: "direct_chart",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { resolveReportBirthClock } from "@/lib/personal-report-route-core";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { completeUsage, releaseUsage } from "@/lib/consultation-billing";
|
||||
import type { ConsultationInput } from "@/mastra/consultation-workflow";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
const PROFILE_COLUMNS = [
|
||||
"name",
|
||||
@@ -33,6 +34,7 @@ const PROFILE_COLUMNS = [
|
||||
"longitude",
|
||||
"timezone_offset",
|
||||
"birth_place_label",
|
||||
"ayanamsa",
|
||||
].join(",");
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
@@ -119,6 +121,7 @@ async function generateProductionReport(context: PersonalReportWorkerGenerationC
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
city: birthPlaceLabel,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
question: `请为个人报告计算 ${rawTheme} 主题证据`,
|
||||
theme: rawTheme as ConsultationInput["theme"],
|
||||
entryMode: "direct_chart",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createHash } from "node:crypto";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { resolveMissingBirthTimezoneOffset } from "../../birth-profile-timezone.ts";
|
||||
import { declaredClockRange } from "../../declared-birth-window.ts";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "../../ayanamsa.ts";
|
||||
import { normalizePersistedBirthDate } from "../../birth-time-intake-model.ts";
|
||||
import {
|
||||
resolveActiveSkillPackage,
|
||||
@@ -46,6 +47,7 @@ export type V9BaselineSnapshot = Readonly<{
|
||||
active_birth_time: string | null;
|
||||
uncertainty_before_minutes: number | null;
|
||||
uncertainty_after_minutes: number | null;
|
||||
ayanamsa: AyanamsaName;
|
||||
}>;
|
||||
|
||||
export type V9RectificationProfile = Readonly<{
|
||||
@@ -167,7 +169,7 @@ export async function loadV9RectificationProfile(
|
||||
const { data, error } = await accounting
|
||||
.from("profiles")
|
||||
.select(
|
||||
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset",
|
||||
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset,ayanamsa",
|
||||
)
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
@@ -213,6 +215,7 @@ export async function loadV9RectificationProfile(
|
||||
active_birth_time: null,
|
||||
uncertainty_before_minutes: uncertaintyBefore,
|
||||
uncertainty_after_minutes: uncertaintyAfter,
|
||||
ayanamsa: resolveAyanamsa(row),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type WindowScan,
|
||||
} from "./varga-observations";
|
||||
import { questionContractVersionIsCompatible } from "./probe-question-contract";
|
||||
import { resolveAyanamsa } from "../../ayanamsa.ts";
|
||||
|
||||
export class RectificationEngineError extends Error {
|
||||
readonly code: string;
|
||||
@@ -475,6 +476,7 @@ function engineRequestBody(input: {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(snapshot),
|
||||
events: input.events,
|
||||
birth_time_source: snapshot.birth_time_source,
|
||||
timezone_id: snapshot.timezone_id,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { GlobalBirthProfile } from "./global-birth-payloads.ts";
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
|
||||
const usableActiveStatuses = new Set(["accepted", "confirmed"]);
|
||||
|
||||
export const ACCOUNT_BIRTH_SELECT =
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id" as const;
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa" as const;
|
||||
|
||||
export type AccountBirthRow = Readonly<{
|
||||
name?: unknown;
|
||||
@@ -20,6 +21,7 @@ export type AccountBirthRow = Readonly<{
|
||||
longitude?: unknown;
|
||||
timezone_offset?: unknown;
|
||||
timezone_id?: unknown;
|
||||
ayanamsa?: unknown;
|
||||
}>;
|
||||
|
||||
function asAccountBirthRow(row: unknown): AccountBirthRow {
|
||||
@@ -84,6 +86,7 @@ export function globalBirthProfileFromAccountRow(row: unknown): GlobalBirthProfi
|
||||
longitude: finiteNumber(accountRow.longitude) ?? null,
|
||||
timezoneOffset: finiteNumber(accountRow.timezone_offset) ?? null,
|
||||
timezoneId: text(accountRow.timezone_id),
|
||||
ayanamsa: resolveAyanamsa(accountRow),
|
||||
birthTimeStatus: text(accountRow.birth_time_status),
|
||||
};
|
||||
}
|
||||
@@ -106,6 +109,7 @@ export function globalBirthProfileFromStoredChart(value: unknown): GlobalBirthPr
|
||||
longitude: record.longitude,
|
||||
timezone_offset: record.timezoneOffset ?? record.timezone_offset,
|
||||
timezone_id: record.timezoneId ?? record.timezone_id,
|
||||
ayanamsa: record.ayanamsa,
|
||||
});
|
||||
if (!profile.date || !profile.time) return null;
|
||||
return profile;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { consultationEvidenceCategoryValues, createConsultationPlan, type ConsultationPlan } from "../lib/consultation-plan.ts";
|
||||
import { consultationThemeValues, projectConsultationWorkflowRequest } from "../lib/consultation-workflow-request.ts";
|
||||
import { AYANAMSA_VALUES, DEFAULT_AYANAMSA } from "../lib/ayanamsa.ts";
|
||||
|
||||
export const consultationInputSchema = z.object({
|
||||
year: z.number().int().min(1900).max(2100),
|
||||
@@ -11,6 +12,7 @@ export const consultationInputSchema = z.object({
|
||||
lat: z.number().min(-90).max(90),
|
||||
lon: z.number().min(-180).max(180),
|
||||
tz: z.number().min(-12).max(14),
|
||||
ayanamsa: z.enum(AYANAMSA_VALUES).default(DEFAULT_AYANAMSA),
|
||||
city: z.string().trim().min(1).max(120),
|
||||
question: z.string().trim().min(1).max(500),
|
||||
theme: z.enum(consultationThemeValues),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
begin;
|
||||
|
||||
alter table public.profiles
|
||||
add column if not exists ayanamsa text not null default 'raman';
|
||||
|
||||
alter table public.profiles
|
||||
drop constraint if exists profiles_ayanamsa_check;
|
||||
|
||||
alter table public.profiles
|
||||
add constraint profiles_ayanamsa_check
|
||||
check (ayanamsa in ('raman', 'lahiri', 'kp', 'true_pushya'));
|
||||
|
||||
grant update (ayanamsa) on table public.profiles to authenticated;
|
||||
|
||||
commit;
|
||||
@@ -47,6 +47,7 @@ test("account GET falls back when global birthplace columns are not migrated yet
|
||||
assert.match(getSource, /select\("credits,active_birth_time[^"]*timezone_offset(?:,[^"]*)?"\)/);
|
||||
assert.match(getSource, /birth_place_label: undefined/);
|
||||
assert.match(getSource, /timezone_id: undefined/);
|
||||
assert.match(getSource, /timezone_source,ayanamsa,name,birth_time,rectification_case_id/);
|
||||
});
|
||||
|
||||
test("account API no longer reads or returns retired rectification cases", () => {
|
||||
@@ -92,6 +93,11 @@ test("profile patch schema validates calendar, clock, source requirements, and l
|
||||
timezone_source: "iana_historical",
|
||||
}).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ name: "只改称呼" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "lahiri" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "kp" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "true_pushya" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "raman" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "fagan" }).success, false);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({
|
||||
...valid,
|
||||
reported_birth_time: null,
|
||||
@@ -200,6 +206,10 @@ test("ordinary declaration edits clear stale candidate application but never ove
|
||||
...candidate,
|
||||
birth_time_status: "confirmed",
|
||||
}, edited), {});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
birth_time_status: "confirmed",
|
||||
}, { ayanamsa: "lahiri" }), {});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
active_birth_time: null,
|
||||
@@ -457,6 +467,19 @@ test("reported birth-time status repair is forward-only and applied by the self-
|
||||
assert.match(selfHostedMigrator, /supabaseCompatibilityDirectory,/);
|
||||
});
|
||||
|
||||
test("profile ayanamsa is added by the self-hosted migrator with Raman as the default", () => {
|
||||
const ayanamsaMigration = readFileSync(
|
||||
new URL("../supabase/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(ayanamsaMigration, /add column if not exists ayanamsa text not null default 'raman'/);
|
||||
assert.match(ayanamsaMigration, /'raman', 'lahiri', 'kp', 'true_pushya'/);
|
||||
assert.equal(
|
||||
existsSync(new URL("../db/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url)),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("existing exact family declarations are forward-repaired without claiming confirmation", () => {
|
||||
assert.match(acceptedExactFamilyMigration, /update public\.profiles/);
|
||||
assert.match(acceptedExactFamilyMigration, /active_birth_time = reported_birth_time/);
|
||||
@@ -484,6 +507,13 @@ test("account PATCH uses the shared validator and never writes client birth_time
|
||||
assert.match(patchSource, /"active_birth_time"/);
|
||||
assert.match(source, /latitude,longitude,timezone_offset/);
|
||||
assert.match(source, /birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source/);
|
||||
assert.match(source, /payload\.ayanamsa !== undefined \? \{ ayanamsa: payload\.ayanamsa \}/);
|
||||
assert.match(patchSource, /ayanamsa: z\.enum\(AYANAMSA_VALUES\)\.optional\(\)/);
|
||||
const declarationBlock = patchSource.slice(
|
||||
patchSource.indexOf("const declarationFields"),
|
||||
patchSource.indexOf("const concurrencyFields"),
|
||||
);
|
||||
assert.doesNotMatch(declarationBlock, /ayanamsa/);
|
||||
assert.match(source, /applyAccountProfileConcurrencyGuards/);
|
||||
assert.match(source, /最新确认结果已保留/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
AYANAMSA_VALUES,
|
||||
DEFAULT_AYANAMSA,
|
||||
isAyanamsaName,
|
||||
resolveAyanamsa,
|
||||
} from "../src/lib/ayanamsa.ts";
|
||||
|
||||
test("resolveAyanamsa defaults to Raman and accepts the four product values", () => {
|
||||
assert.equal(DEFAULT_AYANAMSA, "raman");
|
||||
assert.deepEqual([...AYANAMSA_VALUES], ["raman", "lahiri", "kp", "true_pushya"]);
|
||||
assert.equal(resolveAyanamsa(), "raman");
|
||||
assert.equal(resolveAyanamsa(null), "raman");
|
||||
assert.equal(resolveAyanamsa({}), "raman");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "lahiri" }), "lahiri");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "KP" }), "kp");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "krishnamurti" }), "kp");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "true-pushya" }), "true_pushya");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "fagan" }), "raman");
|
||||
assert.equal(isAyanamsaName("raman"), true);
|
||||
assert.equal(isAyanamsaName("sidereal"), false);
|
||||
});
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
|
||||
const serverChart = {
|
||||
name: "测试",
|
||||
toolInput: { year: 1990, month: 1, day: 2, hour: 3, minute: 4, city: "台北", lat: 25.03, lon: 121.56, tz: 8, declared_accuracy: "15min" as const, time_source: "family_vague" },
|
||||
toolInput: { year: 1990, month: 1, day: 2, hour: 3, minute: 4, city: "台北", lat: 25.03, lon: 121.56, tz: 8, ayanamsa: "raman" as const, declared_accuracy: "15min" as const, time_source: "family_vague" },
|
||||
truth: {
|
||||
birthDate: "1990-01-02", reportedBirthTime: "03:04", activeBirthTime: null,
|
||||
selectedTimeKind: "reported" as const, birthTimeSource: "reported", birthTimeStatus: "reported",
|
||||
|
||||
@@ -58,6 +58,7 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
lat: 36.420487,
|
||||
lon: 114.209936,
|
||||
tz: 8,
|
||||
ayanamsa: "raman",
|
||||
declared_accuracy: "15min",
|
||||
time_source: "family_vague",
|
||||
});
|
||||
@@ -72,6 +73,32 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
});
|
||||
});
|
||||
|
||||
test("route service passes the profile ayanamsa into both natal and declared-window tool input", async () => {
|
||||
const natal = await prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
mode: "unverified_birth_time",
|
||||
loadProfile: async () => ({ ...profile, ayanamsa: "lahiri" }),
|
||||
reserve: async () => "reserved",
|
||||
});
|
||||
assert.equal(natal.serverChart?.toolInput.ayanamsa, "lahiri");
|
||||
|
||||
const windowed = await prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
mode: "general_no_birth_time",
|
||||
loadProfile: async () => ({
|
||||
...profile,
|
||||
ayanamsa: "kp",
|
||||
reported_birth_time: null,
|
||||
active_birth_time: null,
|
||||
birth_time_source: "period_only",
|
||||
birth_time_period: "evening",
|
||||
birth_time_status: "reported",
|
||||
}),
|
||||
reserve: async () => "reserved",
|
||||
});
|
||||
assert.equal(windowed.declaredWindow?.toolInput.ayanamsa, "kp");
|
||||
});
|
||||
|
||||
|
||||
test("pre-reserve plan guard sees resolved server mode and blocks billing on failure", async () => {
|
||||
const order: string[] = [];
|
||||
@@ -158,6 +185,7 @@ test("global normalized places use their exact coordinates and historical offset
|
||||
lat: 25.033,
|
||||
lon: 121.5654,
|
||||
tz: 9,
|
||||
ayanamsa: "raman",
|
||||
declared_accuracy: "15min",
|
||||
time_source: "family_vague",
|
||||
});
|
||||
@@ -316,6 +344,7 @@ test("stale general mode upgrades period-only profiles to a declared birth windo
|
||||
assert.equal(prepared.serverChart, null);
|
||||
assert.equal(prepared.declaredWindow?.toolInput.rangeStart, "18:00");
|
||||
assert.equal(prepared.declaredWindow?.toolInput.rangeEnd, "22:59");
|
||||
assert.equal(prepared.declaredWindow?.toolInput.ayanamsa, "raman");
|
||||
assert.equal("hour" in (prepared.declaredWindow?.toolInput ?? {}), false);
|
||||
assert.equal("minute" in (prepared.declaredWindow?.toolInput ?? {}), false);
|
||||
assert.deepEqual(prepared.generalDailyReference, {
|
||||
@@ -355,6 +384,7 @@ test("consult route constructs workflow input from the route service rather than
|
||||
|
||||
assert.match(route, new RegExp(select));
|
||||
assert.match(route, /prepareConsultationRoute/);
|
||||
assert.match(route, /timezone_id,timezone_source,ayanamsa/);
|
||||
assert.match(route, /\.\.\.prepared\.serverChart\.toolInput/);
|
||||
const toolInput = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
|
||||
assert.doesNotMatch(toolInput.slice(0, toolInput.indexOf("const workflowContext")), /\.\.\.parsed\.data/);
|
||||
|
||||
@@ -48,6 +48,7 @@ test("timing input projects only legal private workflow fields", async () => {
|
||||
lat: 25,
|
||||
lon: 121,
|
||||
tz: 8,
|
||||
ayanamsa: "raman",
|
||||
city: "台北",
|
||||
question: "未来哪些阶段值得把握?",
|
||||
theme: "timing",
|
||||
|
||||
@@ -33,18 +33,23 @@ test("daily, synastry, and legacy rectification use global coordinates instead o
|
||||
});
|
||||
|
||||
const synastry = await synastryBirthPayload(sanFrancisco);
|
||||
assert.deepEqual({ lat: synastry.lat, lon: synastry.lon, tz: synastry.tz }, {
|
||||
assert.deepEqual({ lat: synastry.lat, lon: synastry.lon, tz: synastry.tz, ayanamsa: synastry.ayanamsa }, {
|
||||
lat: 37.7879363,
|
||||
lon: -122.4075201,
|
||||
tz: -8,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
|
||||
const legacy = await payloadFromProfile(sanFrancisco);
|
||||
assert.deepEqual(legacy && { lat: legacy.lat, lon: legacy.lon, tz: legacy.tz }, {
|
||||
assert.deepEqual(legacy && { lat: legacy.lat, lon: legacy.lon, tz: legacy.tz, ayanamsa: legacy.ayanamsa }, {
|
||||
lat: 37.7879363,
|
||||
lon: -122.4075201,
|
||||
tz: -8,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
|
||||
const lahiriDaily = await dailyProfilePayload({ ...sanFrancisco, ayanamsa: "lahiri" }, "2026-07-24");
|
||||
assert.equal(lahiriDaily?.ayanamsa, "lahiri");
|
||||
});
|
||||
|
||||
test("shared timezone resolver supports camelCase browser profiles and preserves the historical reference time", async () => {
|
||||
|
||||
@@ -79,6 +79,7 @@ test("loadV9RectificationProfile uses a server-owned radius around reported time
|
||||
assert.equal(profile.baselineFingerprint.length, 64);
|
||||
assert.equal(profile.baseline.uncertainty_before_minutes, 10);
|
||||
assert.equal(profile.baseline.uncertainty_after_minutes, 10);
|
||||
assert.equal(profile.baseline.ayanamsa, "raman");
|
||||
assert.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
|
||||
assert.ok(!("password" in profile.baseline));
|
||||
});
|
||||
@@ -98,6 +99,18 @@ test("profile fingerprint ignores display labels and prior accepted minutes", as
|
||||
assert.equal(first.baselineFingerprint, presentationOnly.baselineFingerprint);
|
||||
});
|
||||
|
||||
test("ayanamsa is stored on new snapshots but does not change the baseline fingerprint", async () => {
|
||||
const raman = await loadV9RectificationProfile(fakeAccounting({
|
||||
profile: completeProfile,
|
||||
}), "user-1");
|
||||
const lahiri = await loadV9RectificationProfile(fakeAccounting({
|
||||
profile: { ...completeProfile, ayanamsa: "lahiri" },
|
||||
}), "user-1");
|
||||
assert.equal(raman.baseline.ayanamsa, "raman");
|
||||
assert.equal(lahiri.baseline.ayanamsa, "lahiri");
|
||||
assert.equal(raman.baselineFingerprint, lahiri.baselineFingerprint);
|
||||
});
|
||||
|
||||
test("loadV9RectificationProfile normalizes PostgreSQL Date birth dates", async () => {
|
||||
const dateProfile = {
|
||||
...completeProfile,
|
||||
@@ -425,6 +438,7 @@ test("homepage and new opens ignore active time and legacy uncertainty for their
|
||||
active_birth_time: null,
|
||||
uncertainty_before_minutes: 10,
|
||||
uncertainty_after_minutes: 10,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ test("stored other-chart JSON keeps the library camelCase shape", () => {
|
||||
longitude: 116.4,
|
||||
timezoneOffset: 8,
|
||||
timezoneId: "Asia/Shanghai",
|
||||
ayanamsa: "raman",
|
||||
birthTimeStatus: undefined,
|
||||
});
|
||||
});
|
||||
@@ -82,7 +83,7 @@ test("daily and synastry routes share a literal account-birth select string", ()
|
||||
const synastry = readFileSync(new URL("../src/app/api/synastry/route.ts", import.meta.url), "utf8");
|
||||
assert.equal(
|
||||
ACCOUNT_BIRTH_SELECT,
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id",
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa",
|
||||
);
|
||||
assert.match(daily, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
assert.match(synastry, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
|
||||
@@ -33,10 +33,13 @@ test("personal profile is limited to account basics and links to chart settings"
|
||||
|
||||
test("the chart library owns self-profile editing and keeps other-chart save separate", () => {
|
||||
const charts = readFileSync(new URL("../src/components/chart-library-panel.tsx", import.meta.url), "utf8");
|
||||
const personalProfile = between(page, " renderProfile() {", " renderChartLibrary() {");
|
||||
assert.match(charts, /编辑本人资料/);
|
||||
assert.match(charts, /onSubmit=\{saveProfile\}/);
|
||||
assert.match(charts, /onSubmit=\{saveOtherChart\}/);
|
||||
assert.match(charts, /setEditingSelfChart\(true\)/);
|
||||
assert.match(charts, /AyanamsaPreferenceField/);
|
||||
assert.doesNotMatch(personalProfile, /AyanamsaPreferenceField/);
|
||||
});
|
||||
|
||||
test("the general dialog renders the shared theme preference panel", () => {
|
||||
|
||||
@@ -106,7 +106,7 @@ except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
release_heavy_compute_slot,
|
||||
)
|
||||
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, normalize_ayanamsa_name
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, ayanamsa_display_name, normalize_ayanamsa_name
|
||||
from raman_support_observations import build_raman_support_observations
|
||||
|
||||
load_local_env(REPO_ROOT)
|
||||
@@ -807,7 +807,7 @@ _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')
|
||||
raw = source.get('ayanamsa') or source.get('ayanamsa_name') or source.get('ayanamsa_policy')
|
||||
else:
|
||||
raw = source
|
||||
if raw not in (None, ''):
|
||||
@@ -2850,7 +2850,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
|
||||
'lat': birth_payload.get('lat'),
|
||||
'lon': birth_payload.get('lon'),
|
||||
'tz': birth_payload.get('tz'),
|
||||
'ayanamsa_policy': birth_payload.get('ayanamsa') or 'lahiri',
|
||||
'ayanamsa_policy': _request_ayanamsa(birth_payload),
|
||||
'node_policy': birth_payload.get('node_mode') or birth_payload.get('nodeMode') or 'mean',
|
||||
}, route='overview', reference_date=reference_date, case_id='api_chart')
|
||||
if isinstance(vedastro_evidence, dict):
|
||||
@@ -4014,7 +4014,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
'ayanamsa_policy': body.get('ayanamsa_policy') or body.get('ayanamsa') or 'lahiri',
|
||||
'ayanamsa_policy': _request_ayanamsa(body),
|
||||
'node_policy': body.get('node_policy') or body.get('node_mode') or 'mean',
|
||||
}
|
||||
result = _load_local_module('vedastro_service_adapter').run_range_scan_for_case(
|
||||
@@ -6944,13 +6944,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
except ImportError:
|
||||
fallback = self._fallback_chart(
|
||||
year, month, day, hour, minute, second, lat, lon, tz,
|
||||
skip_vedastro_main_entry_overview=bool(body.get('skip_vedastro_main_entry_overview')),
|
||||
skip_vedastro_main_entry_overview=bool(body.get('skip_vedastro_main_entry_overview')), ayanamsa_name=_request_ayanamsa(body),
|
||||
)
|
||||
return _store_api_chart_response_cache(cache_payload, fallback)
|
||||
|
||||
def _fallback_chart(
|
||||
self, year, month, day, hour, minute, second, lat, lon, tz,
|
||||
*, skip_vedastro_main_entry_overview=False,
|
||||
*, skip_vedastro_main_entry_overview=False, ayanamsa_name=DEFAULT_AYANAMSA_NAME,
|
||||
):
|
||||
"""无Swiss Ephemeris时的简化计算"""
|
||||
import hashlib
|
||||
@@ -6996,7 +6996,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'second': int(second),
|
||||
'tz': f"UTC{'+' if tz >= 0 else ''}{tz}",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'lon': lon, 'ayanamsa_name': ayanamsa_name, 'ayanamsa_display': ayanamsa_display_name(ayanamsa_name),
|
||||
},
|
||||
'ascendant': {'sign': asc_sign, 'sign_idx': asc_sign_idx},
|
||||
'planets': planets, 'houses': houses,
|
||||
@@ -7027,7 +7027,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
'ayanamsa': 'lahiri',
|
||||
'ayanamsa': ayanamsa_name,
|
||||
'node_mode': 'mean',
|
||||
})
|
||||
_attach_guided_topics(result)
|
||||
@@ -7083,7 +7083,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 'Raman'
|
||||
ayanamsa_display = birth.get('ayanamsa_display') or ayanamsa_display_name(birth.get('ayanamsa_name') or birth.get('ayanamsa'))
|
||||
node_mode = birth.get('node_mode') or 'mean'
|
||||
prompt_lines = [
|
||||
'你是一个审慎的 AI Native 印度/吠陀占星分析助手。',
|
||||
|
||||
@@ -227,6 +227,7 @@ def build_report() -> dict[str, Any]:
|
||||
}
|
||||
return {
|
||||
"scope": "local_jyotish_accuracy_report",
|
||||
"reference_ayanamsa": "lahiri",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"summary": summary,
|
||||
"checks": checks,
|
||||
|
||||
@@ -26,6 +26,7 @@ INDASTRO_CASES = ROOT / INDASTRO_CASES_RELATIVE
|
||||
|
||||
DEFAULT_MIN_PASS_RATE = 0.98
|
||||
DEFAULT_DEGREE_TOLERANCE = 1.0
|
||||
REFERENCE_AYANAMSA = "lahiri"
|
||||
CONTROVERSIAL_HINTS = [
|
||||
"内部矛盾",
|
||||
"需进一步验证",
|
||||
@@ -60,6 +61,8 @@ def run_engine(case: dict[str, Any], python: str) -> dict[str, Any]:
|
||||
str(case["lon"]),
|
||||
"--tz",
|
||||
str(case["tz"]),
|
||||
"--ayanamsa",
|
||||
REFERENCE_AYANAMSA,
|
||||
]
|
||||
completed = subprocess.run(
|
||||
cmd,
|
||||
@@ -178,6 +181,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
|
||||
|
||||
return {
|
||||
"valid": pass_rate >= args.min_pass_rate and not failures,
|
||||
"reference_ayanamsa": REFERENCE_AYANAMSA,
|
||||
"scope": "public real-person chart revalidation; not event_prediction_accuracy",
|
||||
"min_pass_rate": args.min_pass_rate,
|
||||
"pass_rate": round(pass_rate, 4),
|
||||
|
||||
@@ -85,6 +85,7 @@ def test_user_entrypoint_can_start_from_guided_topics_prompt() -> None:
|
||||
|
||||
|
||||
def test_fixture_dasha_timeline_rejects_workbuddy_regression_claims() -> None:
|
||||
# 原值=依赖引擎默认岁差;新值=显式 lahiri;原因=夹具日期是 Lahiri 口径。
|
||||
base = [
|
||||
sys.executable,
|
||||
"scripts/jyotish_engine.py",
|
||||
@@ -107,6 +108,8 @@ def test_fixture_dasha_timeline_rejects_workbuddy_regression_claims() -> None:
|
||||
"0",
|
||||
"--years",
|
||||
"45",
|
||||
"--ayanamsa",
|
||||
"lahiri",
|
||||
]
|
||||
|
||||
observed = {}
|
||||
|
||||
Reference in New Issue
Block a user