fix(report): unblock personal-full themes with real varga, transit, and karaka evidence
The extractor only read an obsolete varga object shape, never issued Transit receipts, and the consultation response omitted Chara Karakas, so career/marriage/timing/wealth all blocked. Supply the live engine shape, fail-closed receipts, and jaimini.chara_karakas without relaxing the evidence plan. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# PROGRESS · 个人报告全主题 blocked 修复(2026-09-02)
|
||||
|
||||
任务书:`TASK-report-blocked-repairs-20260902.md`
|
||||
分支:`codex/report-blocked-repairs-20260902`
|
||||
基线:`origin/staging` @ `e37f8272`(任务书提交;内容基线 `24a1cf37`)
|
||||
|
||||
开工前置:已读 `docs/research/pre_work_error_ledger.md`、`frontend/AGENTS.md`;`docs/BUG_HISTORY.md` 检索 BUG-486 及相邻条目;`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 为 `status=pass`(`remote_visibility_status=verified`)。按任务书从 `origin/staging` 建 worktree。
|
||||
|
||||
---
|
||||
|
||||
## 任务 0 · Golden fixture 与形状契约
|
||||
|
||||
捕获命令(虚构 smoke 出生,非真人):
|
||||
|
||||
```bash
|
||||
.venv/bin/python scripts/capture_report_blocked_repairs_golden.py
|
||||
```
|
||||
|
||||
脚本调用 `JyotishAPIHandler._compute_consultation_workflow`,body 对齐 worker:`entry_mode=direct_chart`,question `请为个人报告计算 <theme> 主题证据`,`plan_version=consultation-plan-v2`,并设 `defer_optional_external_evidence=true` 以跳过 VedAstro(本轮修的是本地 varga / transits / jaimini 层)。主题:career / marriage / timing / wealth。出生:1993-06-15 10:30,lat 36.42 / lon 114.21 / tz 8,city Handan。
|
||||
|
||||
裁剪规则:
|
||||
|
||||
- 保留 `chart.ascendant/planets/houses/dasha`
|
||||
- `modules.varga_full` 只留 `D2_Hora` / `D9_Navamsa` / `D10_Dasamsa` / `D11_Rudramsa` / `D24_Chaturvimsamsa` 的完整值(引擎 D24 名是 Chaturvimsamsa,不是任务书举例的 Siddhamsa)
|
||||
- 保留 `modules.transits`、`modules.jaimini`(含新增 `chara_karakas`)、`narayana_dasha`、`arudha_padas`、`yogas`、`ashtakavarga`、`dasha_sub_periods`
|
||||
- 保留 `consumer_context` 的 route/core_status/available_layers/missing_route_layers/hard_blockers/answer_policy
|
||||
- 保留 machine packet 中与主题闭合相关的 sections
|
||||
- 删除 `ai_prompt_pack`、VedAstro、varga_spectrum、guided_topics、western、technique_audit_table,并压缩行星/宫位/dasha 字段
|
||||
|
||||
入库:`frontend/tests/fixtures/consultation-workflow-report-blocked-repairs-golden.json`(约 154KB)。入库前检索无邮箱、用户 id、真实姓名。
|
||||
|
||||
形状契约:`frontend/tests/report-blocked-repairs.test.ts` 直接读 golden,断言小写 `ascendant`、`sign_index`、`planets` 字典、`house_chart`,以及 `modules.transits.status`、`chara_karakas.AmK/DK`。该测试不依赖前端提取行为。
|
||||
|
||||
红线 9:`report-interpretive-facts.test.ts` 的 `engineVarga()` 原值是 `{ _meta, Ascendant: {sign, sign_idx, degree_in_sign}, Sun/Moon/Mars: {sign, sign_idx} }`(643f6f7a 手造旧形状,锁住了本轮要修的缺陷)。已换成 golden 派生的 D2/D11/D24 真值;断言语义未放宽。
|
||||
|
||||
---
|
||||
|
||||
## 任务 1 · 分盘值形状
|
||||
|
||||
`deriveVargaHousesFromEngine` 现读 `ascendant ?? Ascendant`,索引 `sign_index ?? sign_idx ?? sign`,度数 `degree ?? degree_in_sign`。优先 `varga.planets`(有 `house` 用 house,否则整星座宫推导);无 `planets` 时退回顶层行星键。不复制 `house_chart` 与未知键。`VARGA_KEY_ALIASES.D24` 补上真实键 `D24_Chaturvimsamsa`。
|
||||
|
||||
复现管线(golden 四主题):`charts` 含 D1/D2/D9/D10/D11/D24;wealth 出 claim card;`demoteThemesMissingRequiredCharts` 不触发;D2/D11 回执 executed。
|
||||
|
||||
---
|
||||
|
||||
## 任务 2 · Transit 回执
|
||||
|
||||
提取层读 `chart.modules.transits`:仅 `status === "executed"` 时 upsert `Transit`(`partial` + `executed: true`)。缺失或其他 status 不产回执。
|
||||
|
||||
单元覆盖:executed / 缺失 / `blocked` 三态。timing 在 executed 时出 claim card。
|
||||
|
||||
---
|
||||
|
||||
## 任务 3 · Chara Karaka
|
||||
|
||||
引擎 `_attach_local_consultation_layers` 调用既有 `jaimini.calc_chara_karaka_7`,把结果挂到 `chart.modules.jaimini.chara_karakas`。键同时保留全名(`Amatyakaraka`/`Darakaraka`)与任务书建议的短名(`AmK`/`DK`)。不改 `arudha_padas`。
|
||||
|
||||
前端按 AmK/DK 行星存在性 upsert 回执。Python:`test_local_consultation_layers_attach_chara_karakas_from_jaimini`。
|
||||
|
||||
四主题复现:**claimCards 4,blockedSections 0**;ledger 中 AmK/DK/Transit/D2/D11 均 executed。
|
||||
|
||||
---
|
||||
|
||||
## 九主题逐项(golden career 图层复用到未单独捕获的主题)
|
||||
|
||||
| 主题 | 结果 |
|
||||
|---|---|
|
||||
| career | claim |
|
||||
| marriage | claim |
|
||||
| wealth | claim |
|
||||
| education | claim |
|
||||
| migration_home | claim |
|
||||
| family | claim |
|
||||
| health_pressure | claim |
|
||||
| timing | claim |
|
||||
| general | claim |
|
||||
|
||||
---
|
||||
|
||||
## 任务 4 · 质量门(本地)
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| `frontend/node_modules/.bin/tsc --noEmit` | 通过(exit 0) |
|
||||
| eslint(改动的 generation / 两个测试文件) | 0 error |
|
||||
| `tsx --test` personal-report-generation + generation-v2 + contract + interpretive-facts + report-blocked-repairs | 64 passed / 0 failed(另 generation 单文件 55/55) |
|
||||
| `pytest tests/test_consultation_consumer_context.py` | 28 passed |
|
||||
| `next build` | 未跑(本机质量门已覆盖类型与合同;完整构建留给 staging 推送) |
|
||||
| staging 真实报告重生 + 上一轮 writer/token 欠账 | 未做,需部署后由委托方核对 `/api/health` 的 `.deployment.gitCommit` |
|
||||
|
||||
未改 `.gitea/workflows/**`,未提升 main。
|
||||
|
||||
---
|
||||
|
||||
## 开工回执后的实现顺序
|
||||
|
||||
先落引擎 `chara_karakas` 再捕获 golden(使 fixture 含新层),再修提取层与测试。形状契约只读 JSON,不依赖提取实现。
|
||||
+17
-1
@@ -7485,7 +7485,7 @@
|
||||
- 修复:把文档允许的分盘键归一成 D2/D9/D10/D11/D24(含 `D2_Hora` 等别名),占据星体走 celestial allowlist。write 主题若缺少对应结构化分盘,降成 blocked disclosure,不再组装成会解析失败的文档。终态 parse 失败时把缺分盘与 hash 失配从笼统的 `final_parse_rejected` 分出来;日志仍只写 allowlist token,不含用户或模型原文。不放宽 `.strict()` 文档 schema,也不改 assertionLevel 规则。
|
||||
- 验证:`frontend/tests/personal-report-generation.test.ts` 用 `D2_Hora` / `D11_Rudramsa` 抽出 D2/D11 且财富章成为 claim;`frontend/tests/personal-report-generation-v2.test.ts` 财富 write 只有 D1 时 ready 且财富进 blocked;`frontend/tests/personal-report-contract.test.ts` 财富主题缺 D2/D11 仍拒绝。Gitea run 2298 在 `report-interpretive-facts.test.ts` 6 fail:夹具只有 D2/D11/D24 回执、没有 `varga_full`,降级后 wealth/education 卡被拿掉。补上结构化 D2/D11/D24 后该文件本地绿,并锁「仅回执则 demote」。未用登录会话在 staging 重生。
|
||||
- 防复发:文档分盘 id 必须从引擎 `varga_full` 别名归一,不得只认 `D2` 这种短键。财富/事业/婚恋/教育 write 主题缺对应结构化分盘时必须降级 blocked,不得把 `final_parse_rejected` 当正常完成。新增分盘别名必须同时覆盖提取与文档 `CHART_IDS`。改 `REQUIRED_THEME_CHARTS` 或 demote 后必须跑 `frontend/tests/report-interpretive-facts.test.ts`;期望 write 主题 claim card 的夹具必须带对应结构化分盘,不能只标 machine section used。
|
||||
- 相关记录:BUG-352、BUG-451、BUG-467
|
||||
- 相关记录:BUG-352、BUG-451、BUG-467、BUG-489
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -7520,3 +7520,19 @@
|
||||
- 相关记录:BUG-471、BUG-485、BUG-487
|
||||
- 复发自:BUG-487(服务端拼接后槽位兜底仍可见)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-489 | 个人报告四主题全部 blocked:分盘值形状、Transit 回执与 Chara Karaka 层缺失
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-02
|
||||
- 最近更新:2026-09-02
|
||||
- 影响面:`deriveVargaHousesFromEngine`、`buildReportEvidenceBundleV2`、`_attach_local_consultation_layers`、personal_full `career/marriage/timing/wealth`
|
||||
- 用户现象:staging 真实 personal_full 报告四个请求主题全部 blocked,摘要退化为「报告主题尚未生成」。blocked 理由分别为缺 AmK+Transit、缺 DK、缺 Transit、缺结构化 D2/D11。
|
||||
- 触发条件:个人报告按主题调用 `/api/consultation_workflow`,提取层组 `ReportEvidenceBundleV2`。与出生数据或线上环境无关,本地 smoke 出生可逐字复现。
|
||||
- 根因:三层叠加。`modules.varga_full` 真值是小写 `ascendant` / `sign_index` / `planets` 字典,提取层只读旧形状 `Ascendant` / `sign_idx` / 顶层行星键,所有分盘图提取失败;`modules.transits.status=executed` 存在但从不 upsert Transit 回执;consultation 响应的 `modules.jaimini` 只有 `arudha_padas`,没有 Chara Karaka,AmK/DK 回执无法闭合。BUG-486 的 `45f190a1` / `643f6f7a` 只修了键名别名、demote 兜底和手造旧形状 fixture,测试绿、线上仍挂。
|
||||
- 修复:提取层双形状兼容真实 varga;`transits.status==="executed"` 时 upsert Transit(`partial`+executed,缺或非 executed 不产回执);引擎把 `jaimini.calc_chara_karaka_7` 挂到 `chart.modules.jaimini.chara_karakas`(不改 `arudha_padas`),前端按 AmK/DK 存在性 upsert 回执。不放宽主题最低证据组,不改既有字段形状。
|
||||
- 验证:golden fixture 形状契约;`frontend/tests/report-blocked-repairs.test.ts` 锁双形状、Transit 三态、AmK/DK、四主题 claimCards 4 / blocked 0、九主题全 claim;`tests/test_consultation_consumer_context.py::test_local_consultation_layers_attach_chara_karakas_from_jaimini`;既有 personal-report generation / interpretive / contract / v2 回归绿。未做登录后的 staging 重生(部署后验收)。
|
||||
- 防复发:分盘 fixture 必须来自真实引擎响应,不得再手造 `Ascendant/sign_idx` 形状当 live 契约。Transit / AmK / DK 回执必须来自模块数据存在性,不得用 `available_layers` 名目冒充已执行。新增 `chara_karakas` 不得改写既有 `arudha_padas`。改提取形状后必须跑 `report-blocked-repairs` 与 `report-interpretive-facts`。
|
||||
- 相关记录:BUG-486、BUG-467、BUG-352
|
||||
- 复发自:BUG-486(键名别名与 demote 治标,值形状与缺失层未修)
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -316,7 +316,7 @@ const VARGA_KEY_ALIASES: Readonly<Record<DocumentVargaChartId, readonly string[]
|
||||
D9: ["D9_Navamsa", "D9"],
|
||||
D10: ["D10_Dasamsa", "D10"],
|
||||
D11: ["D11_Rudramsa", "D11"],
|
||||
D24: ["D24_Siddhamsa", "D24"],
|
||||
D24: ["D24_Chaturvimsamsa", "D24_Siddhamsa", "D24"],
|
||||
};
|
||||
|
||||
function canonicalDocumentVargaChartId(rawId: string): DocumentVargaChartId | null {
|
||||
@@ -324,14 +324,53 @@ function canonicalDocumentVargaChartId(rawId: string): DocumentVargaChartId | nu
|
||||
return match ? match[1] as DocumentVargaChartId : null;
|
||||
}
|
||||
|
||||
const VARGA_ENGINE_META_KEYS = new Set([
|
||||
"Ascendant",
|
||||
"ascendant",
|
||||
"planets",
|
||||
"house_chart",
|
||||
"division",
|
||||
"name",
|
||||
"meaning",
|
||||
]);
|
||||
|
||||
function vargaPlanetHouse(row: JsonRecord, ascIndex: number): number | null {
|
||||
const declared = finiteNumber(row.house);
|
||||
if (declared !== null && Number.isInteger(declared) && declared >= 1 && declared <= 12) {
|
||||
return declared;
|
||||
}
|
||||
const planetIndex = signIndex(row.sign_index ?? row.sign_idx ?? row.sign);
|
||||
if (planetIndex === null) return null;
|
||||
return (((planetIndex - ascIndex) % 12) + 12) % 12 + 1;
|
||||
}
|
||||
|
||||
function collectVargaOccupants(
|
||||
entries: Iterable<readonly [string, unknown]>,
|
||||
ascIndex: number,
|
||||
): string[][] {
|
||||
const occupants: string[][] = Array.from({ length: 12 }, () => []);
|
||||
for (const [name, item] of entries) {
|
||||
const row = record(item);
|
||||
if (!row) continue;
|
||||
const house = vargaPlanetHouse(row, ascIndex);
|
||||
if (house === null) continue;
|
||||
const safeName = safeCelestialName(name);
|
||||
if (safeName) occupants[house - 1].push(safeName);
|
||||
}
|
||||
return occupants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divisional charts in the real engine live under modules.varga_full with keys
|
||||
* such as D2_Hora / D9_Navamsa / D10_Dasamsa / D11_Rudramsa (or D2 / D9 / D10 /
|
||||
* D11). Each varga is {Ascendant: {sign_idx|sign}, <planet>: {sign_idx|sign},
|
||||
* _meta, _dignity, ...} — there are no house arrays. House signs are derived
|
||||
* whole-sign from the divisional ascendant and occupants from each planet's
|
||||
* sign index. Missing or non-document vargas are omitted; occupant names go
|
||||
* through the celestial allowlist.
|
||||
* Divisional charts live under modules.varga_full with keys such as D2_Hora /
|
||||
* D9_Navamsa / D10_Dasamsa / D11_Rudramsa (or D2 / D9 / D10 / D11).
|
||||
*
|
||||
* The live engine shape is {ascendant: {sign, sign_index, degree}, planets:
|
||||
* {Sun: {sign, sign_index, house, degree}, ...}, house_chart, division, name,
|
||||
* meaning}. An older fixture shape used {Ascendant: {sign_idx|sign}, <planet>:
|
||||
* {sign_idx|sign}, _meta, ...}. Both are accepted. House signs are whole-sign
|
||||
* from the divisional ascendant. Occupants come from planets.house when
|
||||
* present, otherwise from sign index. house_chart and unknown keys are not
|
||||
* copied. Occupant names go through the celestial allowlist.
|
||||
*/
|
||||
function deriveVargaHousesFromEngine(
|
||||
varga: JsonRecord,
|
||||
@@ -340,22 +379,23 @@ function deriveVargaHousesFromEngine(
|
||||
degree: number;
|
||||
houses: ReportEvidencePacket["chart"]["houses"];
|
||||
}> | null {
|
||||
const ascendant = record(varga.Ascendant);
|
||||
const ascIndex = ascendant ? signIndex(ascendant.sign_idx ?? ascendant.sign) : null;
|
||||
const ascendant = record(varga.ascendant) ?? record(varga.Ascendant);
|
||||
const ascIndex = ascendant
|
||||
? signIndex(ascendant.sign_index ?? ascendant.sign_idx ?? ascendant.sign)
|
||||
: null;
|
||||
if (ascIndex === null) return null;
|
||||
const occupants: string[][] = Array.from({ length: 12 }, () => []);
|
||||
for (const [name, item] of Object.entries(varga)) {
|
||||
if (name.startsWith("_") || name === "Ascendant" || name === "planets") continue;
|
||||
const row = record(item);
|
||||
const planetIndex = row ? signIndex(row.sign_idx ?? row.sign) : null;
|
||||
if (planetIndex === null) continue;
|
||||
const house = (((planetIndex - ascIndex) % 12) + 12) % 12 + 1;
|
||||
const safeName = safeCelestialName(name);
|
||||
if (safeName) occupants[house - 1].push(safeName);
|
||||
}
|
||||
const planets = record(varga.planets);
|
||||
const occupants = planets
|
||||
? collectVargaOccupants(Object.entries(planets), ascIndex)
|
||||
: collectVargaOccupants(
|
||||
Object.entries(varga).filter(([name]) => (
|
||||
!name.startsWith("_") && !VARGA_ENGINE_META_KEYS.has(name)
|
||||
)),
|
||||
ascIndex,
|
||||
);
|
||||
return {
|
||||
ascIndex,
|
||||
degree: finiteNumber(ascendant?.degree_in_sign ?? ascendant?.degree) ?? 0,
|
||||
degree: finiteNumber(ascendant?.degree ?? ascendant?.degree_in_sign) ?? 0,
|
||||
houses: Array.from({ length: 12 }, (_, index) => ({
|
||||
number: index + 1,
|
||||
sign: SIGNS[((ascIndex + index) % 12 + 12) % 12],
|
||||
@@ -1130,6 +1170,17 @@ function readChartModules(workflow: JsonRecord): JsonRecord {
|
||||
return record(record(workflow.chart)?.modules) ?? {};
|
||||
}
|
||||
|
||||
function charaKarakaPlanetPresent(
|
||||
karakas: JsonRecord | null,
|
||||
keys: readonly string[],
|
||||
): boolean {
|
||||
if (!karakas) return false;
|
||||
return keys.some((key) => {
|
||||
const planet = text(record(karakas[key])?.planet);
|
||||
return Boolean(planet && safeCelestialName(planet));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional benefic/malefic table. `yogakarakas` wins over the benefic and
|
||||
* malefic lists so a planet is never reported twice.
|
||||
@@ -1621,6 +1672,20 @@ export function buildReportEvidenceBundleV2(
|
||||
}
|
||||
}
|
||||
|
||||
for (const { workflow } of packets) {
|
||||
const modules = readChartModules(workflow);
|
||||
if (text(record(modules.transits)?.status)?.toLowerCase() === "executed") {
|
||||
upsertReceipt("Transit", "partial", true);
|
||||
}
|
||||
const karakas = record(record(modules.jaimini)?.chara_karakas);
|
||||
if (charaKarakaPlanetPresent(karakas, ["AmK", "Amatyakaraka"])) {
|
||||
upsertReceipt("AmK", "partial", true);
|
||||
}
|
||||
if (charaKarakaPlanetPresent(karakas, ["DK", "Darakaraka"])) {
|
||||
upsertReceipt("DK", "partial", true);
|
||||
}
|
||||
}
|
||||
|
||||
// --- interpretive facts -------------------------------------------------
|
||||
// Bound to receipts that already exist in the ledger; a layer without a
|
||||
// receipt is skipped rather than given a synthetic ref.
|
||||
|
||||
+5330
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { buildReportEvidenceBundleV2 } from "../src/lib/personal-report-generation.ts";
|
||||
import { REPORT_THEME_IDS } from "../src/lib/report-theme-evidence-plan.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const GOLDEN = JSON.parse(readFileSync(
|
||||
new URL("./fixtures/consultation-workflow-report-blocked-repairs-golden.json", import.meta.url),
|
||||
"utf8",
|
||||
)) as {
|
||||
themes: Record<string, JsonRecord>;
|
||||
};
|
||||
|
||||
const SKILL = {
|
||||
name: "jyotish-vedic-astrology",
|
||||
version: "6.9.14",
|
||||
sha256: "a".repeat(64),
|
||||
sourceCommit: "b".repeat(40),
|
||||
} as const;
|
||||
|
||||
const SUBJECT = {
|
||||
displayName: "冒烟用户",
|
||||
birthTimeStatus: "accepted" as const,
|
||||
birthPlaceLabel: "冒烟市",
|
||||
};
|
||||
|
||||
const FOUR_THEMES = ["career", "marriage", "timing", "wealth"] as const;
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function goldenWorkflow(theme: string): JsonRecord {
|
||||
return clone(GOLDEN.themes[theme] ?? GOLDEN.themes.career);
|
||||
}
|
||||
|
||||
function buildBundle(themes: readonly string[], workflows?: readonly JsonRecord[]) {
|
||||
return buildReportEvidenceBundleV2({
|
||||
workflows: themes.map((theme, index) => ({
|
||||
theme,
|
||||
workflow: workflows?.[index] ?? goldenWorkflow(theme),
|
||||
})),
|
||||
subject: SUBJECT,
|
||||
requestedThemes: [...themes],
|
||||
reportType: "personal_full",
|
||||
presentationMode: "default",
|
||||
skillSnapshot: SKILL,
|
||||
});
|
||||
}
|
||||
|
||||
function chartModules(workflow: JsonRecord): JsonRecord {
|
||||
const chart = workflow.chart as JsonRecord;
|
||||
return chart.modules as JsonRecord;
|
||||
}
|
||||
|
||||
function executedTechniques(bundle: ReturnType<typeof buildBundle>): string[] {
|
||||
return bundle.executionLedger
|
||||
.filter((receipt) => receipt.executed && receipt.status !== "blocked")
|
||||
.map((receipt) => receipt.technique)
|
||||
.sort();
|
||||
}
|
||||
|
||||
test("golden fixture locks the live engine varga and transits shape", () => {
|
||||
const vargaFull = chartModules(GOLDEN.themes.career).varga_full as JsonRecord;
|
||||
assert.ok(vargaFull.D2_Hora);
|
||||
assert.ok(vargaFull.D9_Navamsa);
|
||||
assert.ok(vargaFull.D10_Dasamsa);
|
||||
assert.ok(vargaFull.D11_Rudramsa);
|
||||
assert.ok(vargaFull.D24_Chaturvimsamsa);
|
||||
for (const key of ["D2_Hora", "D9_Navamsa", "D10_Dasamsa", "D11_Rudramsa", "D24_Chaturvimsamsa"] as const) {
|
||||
const varga = vargaFull[key] as JsonRecord;
|
||||
const ascendant = varga.ascendant as JsonRecord;
|
||||
const planets = varga.planets as JsonRecord;
|
||||
assert.equal(typeof ascendant.sign, "string", `${key} ascendant.sign`);
|
||||
assert.equal(typeof ascendant.sign_index, "number", `${key} ascendant.sign_index`);
|
||||
assert.equal(typeof ascendant.degree, "number", `${key} ascendant.degree`);
|
||||
assert.equal(varga.Ascendant, undefined, `${key} must not use the old Ascendant key as the live shape`);
|
||||
assert.ok(planets.Sun && typeof (planets.Sun as JsonRecord).sign_index === "number", `${key} planets.Sun`);
|
||||
assert.ok(Array.isArray(varga.house_chart));
|
||||
assert.equal((varga.house_chart as unknown[]).length, 12);
|
||||
}
|
||||
const transits = chartModules(GOLDEN.themes.career).transits as JsonRecord;
|
||||
assert.equal(typeof transits.status, "string");
|
||||
const karakas = (chartModules(GOLDEN.themes.career).jaimini as JsonRecord).chara_karakas as JsonRecord;
|
||||
assert.equal(typeof (karakas.AmK as JsonRecord).planet, "string");
|
||||
assert.equal(typeof (karakas.DK as JsonRecord).planet, "string");
|
||||
});
|
||||
|
||||
test("deriveVargaHousesFromEngine reads the live planets-dict shape and the old top-level shape", () => {
|
||||
const goldenBundle = buildBundle(["wealth"], [goldenWorkflow("wealth")]);
|
||||
const required = ["D1", "D2", "D9", "D10", "D11", "D24"];
|
||||
for (const id of required) {
|
||||
assert.ok(goldenBundle.charts.some((chart) => chart.id === id), `golden missing ${id}`);
|
||||
}
|
||||
const d2 = goldenBundle.charts.find((chart) => chart.id === "D2")!;
|
||||
assert.equal(d2.houses.length, 12);
|
||||
assert.equal(d2.houses[0].sign, "Leo");
|
||||
assert.ok(d2.houses[0].occupants.includes("Sun"));
|
||||
|
||||
const oldShape = goldenWorkflow("wealth");
|
||||
const modules = chartModules(oldShape);
|
||||
// Keep the old 643f6f7a-era shape as a compatibility path: Ascendant/sign_idx
|
||||
// plus top-level planet keys. Live engine responses no longer look like this.
|
||||
modules.varga_full = {
|
||||
D2_Hora: {
|
||||
_meta: { fixture: "legacy-shape" },
|
||||
Ascendant: { sign: "Cancer", sign_idx: 3, degree_in_sign: 8.25 },
|
||||
Sun: { sign: "Leo", sign_idx: 4 },
|
||||
Moon: { sign: "Virgo", sign_idx: 5 },
|
||||
},
|
||||
D11_Rudramsa: {
|
||||
Ascendant: { sign: "Scorpio", sign_idx: 7 },
|
||||
Sun: { sign: "Sagittarius", sign_idx: 8 },
|
||||
},
|
||||
};
|
||||
const oldBundle = buildBundle(["wealth"], [oldShape]);
|
||||
const oldD2 = oldBundle.charts.find((chart) => chart.id === "D2");
|
||||
const oldD11 = oldBundle.charts.find((chart) => chart.id === "D11");
|
||||
assert.ok(oldD2);
|
||||
assert.ok(oldD11);
|
||||
assert.equal(oldD2.houses[0].sign, "Cancer");
|
||||
assert.ok(oldD2.houses[1].occupants.includes("Sun"));
|
||||
assert.equal(oldD11.houses[0].sign, "Scorpio");
|
||||
});
|
||||
|
||||
test("Transit receipt upserts only when modules.transits.status is executed", () => {
|
||||
const executed = buildBundle(["timing"], [goldenWorkflow("timing")]);
|
||||
assert.ok(executed.claimCards.some((card) => card.theme === "timing"));
|
||||
assert.equal(executed.blockedSections.some((section) => section.theme === "timing"), false);
|
||||
const transit = executed.executionLedger.find((receipt) => receipt.technique === "Transit");
|
||||
assert.ok(transit);
|
||||
assert.equal(transit.executed, true);
|
||||
assert.equal(transit.status, "partial");
|
||||
|
||||
const missing = goldenWorkflow("timing");
|
||||
delete chartModules(missing).transits;
|
||||
const missingBundle = buildBundle(["timing"], [missing]);
|
||||
assert.equal(missingBundle.claimCards.some((card) => card.theme === "timing"), false);
|
||||
assert.ok(missingBundle.blockedSections.some((section) => (
|
||||
section.theme === "timing" && section.reason.includes("Transit")
|
||||
)));
|
||||
assert.equal(
|
||||
missingBundle.executionLedger.some((receipt) => receipt.technique === "Transit" && receipt.executed),
|
||||
false,
|
||||
);
|
||||
|
||||
const blocked = goldenWorkflow("timing");
|
||||
(chartModules(blocked).transits as JsonRecord).status = "blocked";
|
||||
const blockedBundle = buildBundle(["timing"], [blocked]);
|
||||
assert.equal(blockedBundle.claimCards.some((card) => card.theme === "timing"), false);
|
||||
assert.equal(
|
||||
blockedBundle.executionLedger.some((receipt) => receipt.technique === "Transit" && receipt.executed),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("AmK and DK receipts require chara_karakas planet rows", () => {
|
||||
const full = buildBundle(["career", "marriage"]);
|
||||
assert.ok(full.claimCards.some((card) => card.theme === "career"));
|
||||
assert.ok(full.claimCards.some((card) => card.theme === "marriage"));
|
||||
assert.ok(executedTechniques(full).includes("AmK"));
|
||||
assert.ok(executedTechniques(full).includes("DK"));
|
||||
|
||||
const noKarakas = goldenWorkflow("career");
|
||||
delete (chartModules(noKarakas).jaimini as JsonRecord).chara_karakas;
|
||||
const careerOnly = buildBundle(["career"], [noKarakas]);
|
||||
const blocked = careerOnly.blockedSections.find((section) => section.theme === "career");
|
||||
assert.ok(blocked);
|
||||
assert.ok(blocked.reason.includes("AmK"));
|
||||
assert.equal(blocked.reason.includes("Transit"), false);
|
||||
});
|
||||
|
||||
test("golden four-theme pipeline emits claim cards and no blocked sections", () => {
|
||||
const bundle = buildBundle(FOUR_THEMES);
|
||||
assert.deepEqual(bundle.claimCards.map((card) => card.theme).sort(), [...FOUR_THEMES].sort());
|
||||
assert.deepEqual(bundle.blockedSections.map((section) => section.theme), []);
|
||||
const chartIds = bundle.charts.map((chart) => chart.id).sort();
|
||||
for (const id of ["D1", "D2", "D9", "D10", "D11", "D24"]) {
|
||||
assert.ok(chartIds.includes(id), `charts missing ${id}: ${chartIds.join(",")}`);
|
||||
}
|
||||
const executed = executedTechniques(bundle);
|
||||
for (const technique of ["AmK", "DK", "Transit", "D2", "D11"]) {
|
||||
assert.ok(executed.includes(technique), `ledger missing executed ${technique}`);
|
||||
}
|
||||
assert.equal(
|
||||
bundle.claimCards.some((card) => card.theme === "wealth")
|
||||
&& !bundle.blockedSections.some((section) => section.reason.includes("结构化分盘")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("nine requested themes close from the same golden chart layers", () => {
|
||||
const bundle = buildBundle(REPORT_THEME_IDS);
|
||||
const claimed = new Set(bundle.claimCards.map((card) => card.theme));
|
||||
const blocked = Object.fromEntries(
|
||||
bundle.blockedSections.map((section) => [section.theme, section.reason]),
|
||||
);
|
||||
const rows = REPORT_THEME_IDS.map((theme) => ({
|
||||
theme,
|
||||
status: claimed.has(theme) ? "claim" : "blocked",
|
||||
reason: blocked[theme] ?? null,
|
||||
}));
|
||||
for (const row of rows) {
|
||||
assert.equal(row.status, "claim", `${row.theme} still blocked: ${row.reason}`);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
buildReportEvidenceBundleV2,
|
||||
@@ -91,14 +92,28 @@ function machineSections(): JsonRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function engineVarga(ascIndex: number): JsonRecord {
|
||||
return {
|
||||
_meta: { fixture: true },
|
||||
Ascendant: { sign: SIGNS[ascIndex], sign_idx: ascIndex, degree_in_sign: 10.5 },
|
||||
Sun: { sign: SIGNS[ascIndex], sign_idx: ascIndex },
|
||||
Moon: { sign: SIGNS[(ascIndex + 1) % 12], sign_idx: (ascIndex + 1) % 12 },
|
||||
Mars: { sign: SIGNS[(ascIndex + 2) % 12], sign_idx: (ascIndex + 2) % 12 },
|
||||
// Previous 643f6f7a fixture used the old engine-incompatible shape:
|
||||
// `{ _meta, Ascendant: { sign, sign_idx, degree_in_sign }, Sun/Moon/Mars: { sign, sign_idx } }`.
|
||||
// That locked the defect this round repairs (frontend only read Ascendant/sign_idx).
|
||||
// Replaced with golden-derived live `modules.varga_full` values (lowercase
|
||||
// ascendant, sign_index, planets dict, house_chart).
|
||||
const GOLDEN_VARGA = (
|
||||
JSON.parse(readFileSync(new URL("./fixtures/consultation-workflow-report-blocked-repairs-golden.json", import.meta.url), "utf8")) as {
|
||||
themes: { career: { chart: { modules: { varga_full: Record<string, JsonRecord> } } } };
|
||||
}
|
||||
).themes.career.chart.modules.varga_full;
|
||||
|
||||
function engineVargaFromGolden(id: "D2" | "D11" | "D24"): JsonRecord {
|
||||
const aliases: Record<typeof id, readonly string[]> = {
|
||||
D2: ["D2_Hora", "D2"],
|
||||
D11: ["D11_Rudramsa", "D11"],
|
||||
D24: ["D24_Chaturvimsamsa", "D24_Siddhamsa", "D24"],
|
||||
};
|
||||
for (const key of aliases[id]) {
|
||||
const value = GOLDEN_VARGA[key];
|
||||
if (value) return value;
|
||||
}
|
||||
throw new Error(`golden varga ${id} missing`);
|
||||
}
|
||||
|
||||
type FixtureOptions = Readonly<{
|
||||
@@ -128,9 +143,9 @@ function workflowFixture(options: FixtureOptions = {}): JsonRecord {
|
||||
// education claim cards (Gitea run 2298).
|
||||
if (options.structuredVargas !== false) {
|
||||
modules.varga_full = {
|
||||
D2: engineVarga(ASC_INDEX),
|
||||
D11: engineVarga((ASC_INDEX + 2) % 12),
|
||||
D24: engineVarga((ASC_INDEX + 4) % 12),
|
||||
D2: engineVargaFromGolden("D2"),
|
||||
D11: engineVargaFromGolden("D11"),
|
||||
D24: engineVargaFromGolden("D24"),
|
||||
};
|
||||
}
|
||||
if (interpretive) {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture trimmed consultation_workflow responses for report blocked-repairs fixtures.
|
||||
|
||||
Smoke birth only: 1993-06-15 10:30, lat 36.42 / lon 114.21 / tz 8.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from consultation_plan_contract import PLAN_VERSION, _ROUTE_CONTRACTS # noqa: E402
|
||||
from jyotish_api_server import JyotishAPIHandler # noqa: E402
|
||||
|
||||
BIRTH = {
|
||||
"year": 1993,
|
||||
"month": 6,
|
||||
"day": 15,
|
||||
"hour": 10,
|
||||
"minute": 30,
|
||||
"lat": 36.42,
|
||||
"lon": 114.21,
|
||||
"tz": 8,
|
||||
"city": "Handan",
|
||||
}
|
||||
THEMES = ("career", "marriage", "timing", "wealth")
|
||||
VARGA_KEEP = (
|
||||
"D2_Hora", "D2",
|
||||
"D9_Navamsa", "D9",
|
||||
"D10_Dasamsa", "D10",
|
||||
"D11_Rudramsa", "D11",
|
||||
"D24_Chaturvimsamsa", "D24_Siddhamsa", "D24",
|
||||
)
|
||||
MODULE_KEEP = (
|
||||
"varga_full",
|
||||
"transits",
|
||||
"jaimini",
|
||||
"narayana_dasha",
|
||||
"arudha_padas",
|
||||
"yogas",
|
||||
"ashtakavarga",
|
||||
"dasha_sub_periods",
|
||||
)
|
||||
CONSUMER_KEEP = (
|
||||
"route",
|
||||
"core_status",
|
||||
"available_layers",
|
||||
"missing_route_layers",
|
||||
"hard_blockers",
|
||||
"answer_policy",
|
||||
)
|
||||
CHART_KEEP = ("success", "ascendant", "planets", "houses", "dasha")
|
||||
|
||||
|
||||
def _handler() -> JyotishAPIHandler:
|
||||
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
|
||||
|
||||
def _body(theme: str) -> dict:
|
||||
contract = _ROUTE_CONTRACTS[theme]
|
||||
return {
|
||||
**BIRTH,
|
||||
"question": f"请为个人报告计算 {theme} 主题证据",
|
||||
"question_text": f"请为个人报告计算 {theme} 主题证据",
|
||||
"theme": list(contract.themes),
|
||||
"entry_mode": "direct_chart",
|
||||
"plan_version": PLAN_VERSION,
|
||||
"strict_workflow_route": contract.resolved_routes[0],
|
||||
"required_layers": list(contract.required_layers),
|
||||
"claim_boundary": contract.claim_boundary,
|
||||
"plan_depth": "standard",
|
||||
"requested_domains": list(contract.requested_domains),
|
||||
"timing_horizon": "next_12_months" if theme == "timing" else None,
|
||||
"precision_boundary": "server_evidence_required",
|
||||
"required_evidence_categories": list(contract.required_evidence_categories),
|
||||
"defer_optional_external_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
def _trim_varga(varga_full: dict) -> dict:
|
||||
trimmed = {}
|
||||
for key in VARGA_KEEP:
|
||||
value = varga_full.get(key)
|
||||
if isinstance(value, dict):
|
||||
trimmed[key] = value
|
||||
if trimmed:
|
||||
return trimmed
|
||||
# Fall back to prefix match if the live keys drifted.
|
||||
for key, value in varga_full.items():
|
||||
if not isinstance(key, str) or not isinstance(value, dict):
|
||||
continue
|
||||
if any(key == keep or key.startswith(f"{keep.split('_')[0]}_") for keep in ("D2", "D9", "D10", "D11", "D24")):
|
||||
if key.startswith(("D2", "D9", "D10", "D11", "D24")):
|
||||
trimmed[key] = value
|
||||
return trimmed
|
||||
|
||||
|
||||
def _trim_workflow(raw: dict) -> dict:
|
||||
chart = raw.get("chart") if isinstance(raw.get("chart"), dict) else {}
|
||||
modules = chart.get("modules") if isinstance(chart.get("modules"), dict) else {}
|
||||
kept_modules = {}
|
||||
for name in MODULE_KEEP:
|
||||
value = modules.get(name)
|
||||
if name == "varga_full" and isinstance(value, dict):
|
||||
kept_modules[name] = _trim_varga(value)
|
||||
elif value is not None:
|
||||
kept_modules[name] = value
|
||||
kept_chart = {key: chart[key] for key in CHART_KEEP if key in chart}
|
||||
if chart.get("success") is not None:
|
||||
kept_chart["success"] = chart.get("success")
|
||||
kept_chart["modules"] = kept_modules
|
||||
consumer = raw.get("consumer_context") if isinstance(raw.get("consumer_context"), dict) else {}
|
||||
machine = raw.get("machine_evidence_packet") if isinstance(raw.get("machine_evidence_packet"), dict) else {}
|
||||
workflow = {
|
||||
"success": bool(raw.get("success")),
|
||||
"chart": kept_chart,
|
||||
"consumer_context": {key: consumer[key] for key in CONSUMER_KEEP if key in consumer},
|
||||
"machine_evidence_packet": {
|
||||
"sections": machine.get("sections") if isinstance(machine.get("sections"), dict) else {},
|
||||
},
|
||||
}
|
||||
return _compact_workflow(workflow)
|
||||
|
||||
|
||||
def _pick(row: dict, keys: tuple[str, ...]) -> dict:
|
||||
return {key: row[key] for key in keys if key in row}
|
||||
|
||||
|
||||
def _compact_workflow(workflow: dict) -> dict:
|
||||
"""Drop bulky unused fields after the extraction-layer keep list."""
|
||||
chart = workflow["chart"]
|
||||
planets = chart.get("planets")
|
||||
if isinstance(planets, dict):
|
||||
chart["planets"] = {
|
||||
name: _pick(row, ("sign", "sign_idx", "sign_index", "lon", "degree", "degree_in_sign", "house", "retrograde"))
|
||||
if isinstance(row, dict) else row
|
||||
for name, row in planets.items()
|
||||
}
|
||||
houses = chart.get("houses")
|
||||
if isinstance(houses, dict):
|
||||
chart["houses"] = {
|
||||
key: _pick(row, ("cusp_sign", "sign", "sign_idx", "lord")) if isinstance(row, dict) else row
|
||||
for key, row in houses.items()
|
||||
}
|
||||
dasha = chart.get("dasha")
|
||||
if isinstance(dasha, dict):
|
||||
periods = [
|
||||
_pick(item, ("lord", "start", "end"))
|
||||
for item in (dasha.get("periods") or [])
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
compact_dasha = {}
|
||||
if periods:
|
||||
compact_dasha["periods"] = periods
|
||||
compact_dasha["mahadashas"] = periods
|
||||
for key in ("current_md", "start_date"):
|
||||
if key in dasha:
|
||||
compact_dasha[key] = dasha[key]
|
||||
chart["dasha"] = compact_dasha
|
||||
modules = chart["modules"]
|
||||
transits = modules.get("transits")
|
||||
if isinstance(transits, dict):
|
||||
modules["transits"] = _pick(transits, ("status", "sade_sati", "search_period", "trigger_count", "boundary"))
|
||||
sav = (modules.get("ashtakavarga") or {}).get("sav") if isinstance(modules.get("ashtakavarga"), dict) else None
|
||||
if isinstance(sav, dict):
|
||||
modules["ashtakavarga"] = {"sav": _pick(sav, ("scores", "total"))}
|
||||
yogas = modules.get("yogas")
|
||||
if isinstance(yogas, dict):
|
||||
rows = [
|
||||
_pick(item, ("name", "category", "hit", "planets"))
|
||||
for item in (yogas.get("yogas") or [])
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
modules["yogas"] = {"status": yogas.get("status"), "count": yogas.get("count"), "yogas": rows}
|
||||
nara = modules.get("narayana_dasha")
|
||||
if isinstance(nara, dict):
|
||||
modules["narayana_dasha"] = _pick(nara, ("current_dasha", "lagna_sign"))
|
||||
arudha = modules.get("arudha_padas")
|
||||
if isinstance(arudha, dict):
|
||||
padas = arudha.get("padas") if isinstance(arudha.get("padas"), dict) else {}
|
||||
modules["arudha_padas"] = {"padas": {key: padas[key] for key in ("A7", "A10", "UL") if key in padas}}
|
||||
jaimini = modules.get("jaimini")
|
||||
if isinstance(jaimini, dict):
|
||||
compact_jaimini = {}
|
||||
if isinstance(jaimini.get("arudha_padas"), dict):
|
||||
padas = jaimini["arudha_padas"].get("padas") if isinstance(jaimini["arudha_padas"].get("padas"), dict) else {}
|
||||
compact_jaimini["arudha_padas"] = {"padas": {key: padas[key] for key in ("A7", "A10", "UL") if key in padas}}
|
||||
karakas = jaimini.get("chara_karakas")
|
||||
if isinstance(karakas, dict):
|
||||
compact_jaimini["chara_karakas"] = {
|
||||
key: _pick(row, ("planet", "degree_in_sign", "rank", "domain", "cn_name")) if isinstance(row, dict) else row
|
||||
for key, row in karakas.items()
|
||||
}
|
||||
modules["jaimini"] = compact_jaimini
|
||||
sub = modules.get("dasha_sub_periods")
|
||||
if isinstance(sub, dict) and isinstance(sub.get("current"), dict):
|
||||
modules["dasha_sub_periods"] = {"current": sub["current"]}
|
||||
sections = workflow["machine_evidence_packet"].get("sections")
|
||||
if isinstance(sections, dict):
|
||||
keep = {
|
||||
"D1", "D2", "D4", "D6", "D7", "D9", "D10", "D11", "D12", "D24", "D30",
|
||||
"A7", "A10", "UL", "dasha_boundaries", "narayana_dasha", "yogas", "ashtakavarga",
|
||||
"planet_degrees", "house_degrees", "dasha_sub_periods",
|
||||
}
|
||||
workflow["machine_evidence_packet"]["sections"] = {key: value for key, value in sections.items() if key in keep}
|
||||
return workflow
|
||||
|
||||
|
||||
def main() -> int:
|
||||
handler = _handler()
|
||||
out_dir = ROOT / "frontend/tests/fixtures"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"source": "scripts/jyotish_api_server.py _compute_consultation_workflow",
|
||||
"smoke_birth": {**BIRTH, "note": "fictional capture identity; not a real person"},
|
||||
"trim": {
|
||||
"keep_varga_keys": list(VARGA_KEEP),
|
||||
"keep_modules": list(MODULE_KEEP),
|
||||
"drop": [
|
||||
"ai_prompt_pack",
|
||||
"vedastro",
|
||||
"varga_spectrum",
|
||||
"guided_topics",
|
||||
"western_spectrum",
|
||||
"technique_audit_table",
|
||||
],
|
||||
},
|
||||
"themes": {},
|
||||
}
|
||||
for theme in THEMES:
|
||||
raw = handler._compute_consultation_workflow(_body(theme))
|
||||
payload["themes"][theme] = _trim_workflow(raw)
|
||||
varga = payload["themes"][theme]["chart"]["modules"].get("varga_full") or {}
|
||||
transits = payload["themes"][theme]["chart"]["modules"].get("transits") or {}
|
||||
jaimini = payload["themes"][theme]["chart"]["modules"].get("jaimini") or {}
|
||||
print(
|
||||
theme,
|
||||
"success=",
|
||||
payload["themes"][theme]["success"],
|
||||
"varga_keys=",
|
||||
sorted(varga),
|
||||
"transits=",
|
||||
transits.get("status"),
|
||||
"karakas=",
|
||||
sorted((jaimini.get("chara_karakas") or {})),
|
||||
)
|
||||
dest = out_dir / "consultation-workflow-report-blocked-repairs-golden.json"
|
||||
dest.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print("wrote", dest, "bytes", dest.stat().st_size)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1502,6 +1502,38 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'arudha_padas', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
existing_jaimini = modules.get('jaimini') if isinstance(modules.get('jaimini'), dict) else {}
|
||||
if planets and not isinstance(existing_jaimini.get('chara_karakas'), dict):
|
||||
try:
|
||||
jaimini = _load_local_module('jaimini')
|
||||
planet_degs = {
|
||||
name: lon % 30
|
||||
for name, lon in _planet_longitudes(planets).items()
|
||||
}
|
||||
ck7 = jaimini.calc_chara_karaka_7(planet_degs) if planet_degs else {}
|
||||
table = ck7.get('karaka_table') if isinstance(ck7, dict) else None
|
||||
if isinstance(table, dict) and table:
|
||||
short_names = {
|
||||
'Atmakaraka': 'AK',
|
||||
'Amatyakaraka': 'AmK',
|
||||
'Bhratrikaraka': 'BK',
|
||||
'Matrikaraka': 'MK',
|
||||
'Putrakaraka': 'PK',
|
||||
'Gnatikaraka': 'GK',
|
||||
'Darakaraka': 'DK',
|
||||
}
|
||||
chara_karakas = {}
|
||||
for full_name, row in table.items():
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
chara_karakas[full_name] = row
|
||||
alias = short_names.get(full_name)
|
||||
if alias:
|
||||
chara_karakas[alias] = row
|
||||
modules.setdefault('jaimini', {})['chara_karakas'] = chara_karakas
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'chara_karakas', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if planets and ascendant and not isinstance(modules.get('narayana_dasha'), dict):
|
||||
try:
|
||||
narayana = _load_local_module('narayana_dasha')
|
||||
|
||||
@@ -160,6 +160,31 @@ def test_local_consultation_layers_include_yogas_transits_and_route_chara() -> N
|
||||
assert 'chara_dasha' not in wealth['local_consultation_layers']['available']
|
||||
|
||||
|
||||
def test_local_consultation_layers_attach_chara_karakas_from_jaimini() -> None:
|
||||
from jaimini import calc_chara_karaka_7
|
||||
|
||||
chart = _attach_local_consultation_layers(
|
||||
_handler(),
|
||||
_base_chart(),
|
||||
dict(_BIRTH),
|
||||
{'current_date': _REFERENCE_DATE, 'strict_workflow_route': 'career'},
|
||||
)
|
||||
jaimini_mod = chart['modules']['jaimini']
|
||||
assert jaimini_mod['arudha_padas']['padas']['A10']['name'] == 'Karma Pada (A10)'
|
||||
karakas = jaimini_mod['chara_karakas']
|
||||
planet_degs = {
|
||||
name: float(data['lon']) % 30
|
||||
for name, data in chart['planets'].items()
|
||||
}
|
||||
expected = calc_chara_karaka_7(planet_degs)['karaka_table']
|
||||
assert karakas['Amatyakaraka']['planet'] == expected['Amatyakaraka']['planet']
|
||||
assert karakas['AmK']['planet'] == expected['Amatyakaraka']['planet']
|
||||
assert karakas['Darakaraka']['planet'] == expected['Darakaraka']['planet']
|
||||
assert karakas['DK']['planet'] == expected['Darakaraka']['planet']
|
||||
assert karakas['AmK']['degree_in_sign'] == expected['Amatyakaraka']['degree_in_sign']
|
||||
assert karakas['DK']['rank'] == expected['Darakaraka']['rank']
|
||||
|
||||
|
||||
def test_consultation_computes_transit_trigger_windows(monkeypatch) -> None:
|
||||
class FakeTransit:
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user