();
+ const chartIds = fencedReportChartIds(projected);
for (const match of projected.matchAll(/```jyotish-chart\r?\n([\s\S]*?)```/g)) {
const block = parseReportChartBlock(match[1]);
if (!block) continue;
const occurrence = blocks.filter(item => item.group === "chart" && item.id.startsWith(`chart:${block.id}:`)).length + 1;
- chartIds.add(block.id);
// Only the immediately preceding SVG belongs to this occurrence (D9 may repeat).
// The public projection has already rejected unsafe SVG; never recover raw HTML.
const preceding = projected.slice(0, match.index);
diff --git a/frontend/src/lib/vedic-chart-tables.json b/frontend/src/lib/vedic-chart-tables.json
index 271f5113..ef143c75 100644
--- a/frontend/src/lib/vedic-chart-tables.json
+++ b/frontend/src/lib/vedic-chart-tables.json
@@ -1,5 +1,5 @@
{
- "_comment": "Chart-page reference tables. Mirrors jyotish_engine.SIGN_LORDS / EXALTATION / DEBILITATION, the Rahu-Virgo / Ketu-Pisces own-sign rule in jyotish_engine._get_dignity_level, and divisional_charts_extended.VargaType meanings. tests/test_chart_page_vedic_tables_contract.py fails if either side drifts.",
+ "_comment": "Engine tables the chart page reads. vargaMeaning = divisional_charts_extended.VargaType meaning; dignityLabels = jyotish_engine.DIGNITY_LABELS (the D1 status wording). Pinned by tests/test_chart_page_vedic_tables_contract.py.",
"vargaMeaning": {
"1": "本命盘",
"2": "财富",
@@ -22,40 +22,16 @@
"45": "全面判断",
"60": "前世业力"
},
- "signLords": {
- "Aries": "Mars",
- "Taurus": "Venus",
- "Gemini": "Mercury",
- "Cancer": "Moon",
- "Leo": "Sun",
- "Virgo": "Mercury",
- "Libra": "Venus",
- "Scorpio": "Mars",
- "Sagittarius": "Jupiter",
- "Capricorn": "Saturn",
- "Aquarius": "Saturn",
- "Pisces": "Jupiter"
- },
- "exaltation": {
- "Sun": "Aries",
- "Moon": "Taurus",
- "Mars": "Capricorn",
- "Mercury": "Virgo",
- "Jupiter": "Cancer",
- "Venus": "Pisces",
- "Saturn": "Libra"
- },
- "debilitation": {
- "Sun": "Libra",
- "Moon": "Scorpio",
- "Mars": "Cancer",
- "Mercury": "Pisces",
- "Jupiter": "Capricorn",
- "Venus": "Virgo",
- "Saturn": "Aries"
- },
- "nodeOwnSign": {
- "Rahu": "Virgo",
- "Ketu": "Pisces"
+ "dignityLabels": {
+ "EXALTED": "入旺(Exalted)",
+ "MOOLATRIKONA": "本垣(Moolatrikona)",
+ "OWN_SIGN": "入庙(Own Sign)",
+ "GREAT_FRIEND": "极友(Great Friend)",
+ "FRIEND": "入友(Friendly Sign)",
+ "NEUTRAL": "中性(Neutral)",
+ "ENEMY": "入敌(Enemy Sign)",
+ "GREAT_ENEMY": "极敌(Great Enemy)",
+ "DEBILITATED": "落陷(Debilitated)",
+ "NEECHA_BHANGA": "落陷取消(Neecha Bhanga)"
}
}
\ No newline at end of file
diff --git a/frontend/tests/chart-varga-table.test.tsx b/frontend/tests/chart-varga-table.test.tsx
index 0f8b1294..829ed4e4 100644
--- a/frontend/tests/chart-varga-table.test.tsx
+++ b/frontend/tests/chart-varga-table.test.tsx
@@ -90,13 +90,34 @@ test("a divisional chart's table carries that chart's signs, degrees and houses"
assert.match(ascD9, /射手/, "D9 ascendant is Sagittarius");
});
-test("the D1 table is byte-for-byte the table the page drew before BUG-1076", async () => {
+test("the D1 table is byte-for-byte the table the page drew before BUG-1076, ascendant nakshatra aside", async () => {
const view = await vargaView();
const markup = render(view, "D1");
const table = markup.match(/[\s\S]*?<\/table><\/div>/)?.[0];
- // Rendered from the baseline commit 5dc977ef with the same golden.
+ // Rendered from the baseline commit 5dc977ef with the same golden. The only
+ // change since is BUG-1201: the engine now gives the ascendant its nakshatra
+ // (Leo 29°23′ = Uttara Phalguni pada 1, lord Sun), where the baseline had dashes.
const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8");
- assert.equal(table, baseline);
+ const dashes = '
— | — | — | — | ';
+ const filled = 'Uttara Phalguni | ☉ | '
+ + '1 | — | ';
+ assert.equal(baseline.split(dashes).length, 2, "the baseline's ascendant row is the only all-dash row");
+ assert.equal(table, baseline.replace(dashes, filled));
+});
+
+// BUG-1201: the ascendant row's nakshatra cells were always dashes.
+test("the D1 ascendant row shows the engine's nakshatra, lord and pada", async () => {
+ const view = await vargaView();
+ assert.equal(view.vedic.ascendant?.nakshatra, "Uttara Phalguni");
+ assert.equal(view.vedic.ascendant?.pada, 1);
+ const ascendant = row(render(view, "D1"), "上升");
+ assert.match(ascendant, /Uttara Phalguni<\/td>/);
+ assert.match(ascendant, /is-sun/, "Uttara Phalguni's lord is the Sun");
+ // An older cached chart without the fields still renders, with dashes.
+ const chart = structuredClone(golden.chart);
+ for (const key of ["nakshatra", "nakshatra_pada", "nakshatra_lord"]) delete (chart.ascendant as Record)[key];
+ const old = buildChartView({ chart, varga: null, chara: null, western: null, profile, asOf: "2026-09-15" });
+ assert.equal(old.vedic.ascendant?.nakshatra, null);
});
test("divisional rows carry engine dignity and vargottama, and drop nakshatra columns", async () => {
@@ -106,18 +127,23 @@ test("divisional rows carry engine dignity and vargottama, and drop nakshatra co
const saturn = d9.rows.find((item) => item.name === "Saturn");
// D1 Saturn is in Capricorn and D9 Saturn is in Capricorn: own sign, vargottama.
assert.equal(saturn?.sign, "Capricorn");
- assert.equal(saturn?.dignity, "own");
+ assert.equal(saturn?.dignity, "OWN_SIGN");
assert.equal(saturn?.vargottama, true);
const sun = d9.rows.find((item) => item.name === "Sun");
assert.equal(sun?.sign, "Libra");
- assert.equal(sun?.dignity, "debilitated");
+ assert.equal(sun?.dignity, "DEBILITATED");
assert.equal(sun?.vargottama, false);
const markup = render(view, "D9");
const header = markup.match(/([\s\S]*?)<\/thead>/)?.[1] ?? "";
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
assert.doesNotMatch(header, /星宿|宿主|pada/);
- assert.match(row(markup, "土星"), />自宫<[\s\S]*>同位);
- assert.match(row(markup, "太阳"), />落陷);
+ // Same wording as the D1 status column (BUG-1202), friendship levels included.
+ assert.match(row(markup, "土星"), />入庙\(Own Sign\)<[\s\S]*>同位);
+ assert.match(row(markup, "太阳"), />落陷\(Debilitated\));
+ assert.match(row(markup, "月亮"), />入友\(Friendly Sign\));
+ assert.match(row(markup, "火星"), />中性\(Neutral\));
+ assert.match(row(markup, "水星"), />入敌\(Enemy Sign\));
+ assert.match(row(markup, "木星"), />极敌\(Great Enemy\));
assert.match(row(markup, "上升"), />不适用, "the ascendant has no sign dignity");
// Every divisional chart the engine returned has its own rows, D1 has none.
for (const varga of view.vedic.vargas) {
@@ -181,7 +207,7 @@ test("valid false and null results never turn supported columns off", async () =
copy.ascendant!.vargottama = false;
const markup = tableMarkup(chartViewVargaSchema.parse(copy));
assertColumns(markup, true, true);
- assert.doesNotMatch(markup.match(/([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|>入旺<|>自宫<|>落陷);
+ assert.doesNotMatch(markup.match(/([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|\((?:Exalted|Own Sign|Debilitated|Neutral)\));
assert.match(row(markup, "上升"), />不适用);
});
@@ -200,6 +226,24 @@ test("capability schema rejects wrong types and direct cache rendering requires
}
});
+// BUG-1202: the dignity column is the engine's; it is never recomputed here.
+test("a divisional payload without engine dignity hides the column instead of deriving it", () => {
+ const varga = structuredClone(golden.varga_full);
+ for (const item of Object.values(varga.result)) {
+ for (const planet of Object.values(item.planets)) delete planet.dignity_level;
+ }
+ const view = buildChartView({ chart: structuredClone(golden.chart), varga: varga as unknown as Record, chara: null, western: null, profile, asOf: "2026-09-15" });
+ const d9 = view.vedic.vargas.find((item) => item.id === "D9")!;
+ assert.equal(d9.supportsDignity, false);
+ assert.equal(d9.supportsVargottama, true);
+ assert.ok(d9.rows!.every((item) => item.dignity === null));
+ assertColumns(tableMarkup(d9), false, true);
+ const unknown = structuredClone(golden.varga_full);
+ unknown.result.D9_Navamsa!.planets.Sun!.dignity_level = "SOMETHING_NEW";
+ const mixed = buildChartView({ chart: structuredClone(golden.chart), varga: unknown as unknown as Record, chara: null, western: null, profile, asOf: "2026-09-15" });
+ assert.equal(mixed.vedic.vargas.find((item) => item.id === "D9")!.supportsDignity, false);
+});
+
test("missing or invalid source positions are not manufactured as negative same-sign results", () => {
const chart = structuredClone(golden.chart);
const varga = structuredClone(golden.varga_full);
diff --git a/frontend/tests/chart-view-varga-dignity.test.ts b/frontend/tests/chart-view-varga-dignity.test.ts
index 24c5e217..a6f95bda 100644
--- a/frontend/tests/chart-view-varga-dignity.test.ts
+++ b/frontend/tests/chart-view-varga-dignity.test.ts
@@ -1,19 +1,24 @@
import assert from "node:assert/strict";
import test from "node:test";
-import { vargaDignity, vargaMeaning } from "../src/lib/chart-view-varga.ts";
+import { VARGA_DIGNITY_LABEL, VARGA_DIGNITY_LEVELS, vargaDignityLevel, vargaMeaning } from "../src/lib/chart-view-varga.ts";
-// The tables are pinned to the engine by tests/test_chart_page_vedic_tables_contract.py;
-// this pins the precedence (exalted, own, debilitated) the Python test mirrors.
-test("sign dignity follows the engine's precedence and node own signs", () => {
- assert.equal(vargaDignity("Sun", "Aries"), "exalted");
- assert.equal(vargaDignity("Sun", "Leo"), "own");
- assert.equal(vargaDignity("Sun", "Libra"), "debilitated");
- assert.equal(vargaDignity("Mercury", "Virgo"), "exalted", "exaltation wins over own sign");
- assert.equal(vargaDignity("Rahu", "Virgo"), "own");
- assert.equal(vargaDignity("Ketu", "Pisces"), "own");
- assert.equal(vargaDignity("Rahu", "Taurus"), null, "the engine has no node exaltation");
- assert.equal(vargaDignity("Moon", "Leo"), null);
+// BUG-1202: the level is the engine's (`/api/varga_full` dignity_level); the
+// page only validates it and labels it with the D1 status wording, pinned to
+// jyotish_engine.DIGNITY_LABELS by tests/test_chart_page_vedic_tables_contract.py.
+test("engine dignity levels are accepted as-is and anything else is unknown", () => {
+ for (const level of VARGA_DIGNITY_LEVELS) assert.equal(vargaDignityLevel(level), level);
+ assert.equal(vargaDignityLevel("exalted"), null, "the retired three-level values are not engine levels");
+ assert.equal(vargaDignityLevel(undefined), null);
+ assert.equal(vargaDignityLevel(1), null);
+});
+
+test("every engine level has the D1 status wording", () => {
+ assert.equal(VARGA_DIGNITY_LABEL.EXALTED, "入旺(Exalted)");
+ assert.equal(VARGA_DIGNITY_LABEL.OWN_SIGN, "入庙(Own Sign)");
+ assert.equal(VARGA_DIGNITY_LABEL.ENEMY, "入敌(Enemy Sign)");
+ assert.equal(VARGA_DIGNITY_LABEL.NEUTRAL, "中性(Neutral)");
+ assert.deepEqual(Object.keys(VARGA_DIGNITY_LABEL).sort(), [...VARGA_DIGNITY_LEVELS].sort());
});
test("varga meanings come from the engine table including D40, D45 and D60", () => {
diff --git a/frontend/tests/fixtures/chart-view-golden.json b/frontend/tests/fixtures/chart-view-golden.json
index 3aceaacc..8d47e425 100644
--- a/frontend/tests/fixtures/chart-view-golden.json
+++ b/frontend/tests/fixtures/chart-view-golden.json
@@ -1,5 +1,5 @@
{
- "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones).",
+ "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones). 2026-10-03 (BUG-1201/1202): chart.ascendant nakshatra fields from jyotish_engine._nakshatra_fields(ascendant.lon) and varga_full.result[*].planets[*].dignity_level from scripts/varga_dignity.annotate_varga_dignity on the stored positions (the same calls /api/chart and /api/varga_full now make).",
"chart": {
"success": true,
"birth": {
@@ -26,7 +26,10 @@
"degree_in_sign": 29.3994,
"degree_in_sign_raw": 29.399370365697422,
"lon": 149.3994,
- "lord": "Sun"
+ "lord": "Sun",
+ "nakshatra": "Uttara Phalguni",
+ "nakshatra_pada": 1,
+ "nakshatra_lord": "Sun"
},
"planets": {
"Sun": {
@@ -324,63 +327,72 @@
"sign_index": 2,
"degree": 1.5338,
"house": 11,
- "absolute_degree": 61.5338
+ "absolute_degree": 61.5338,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 18.6462,
"house": 7,
- "absolute_degree": 318.6462
+ "absolute_degree": 318.6462,
+ "dignity_level": "FRIEND"
},
"Mars": {
"sign": "Pisces",
"sign_index": 11,
"degree": 18.5242,
"house": 8,
- "absolute_degree": 348.5242
+ "absolute_degree": 348.5242,
+ "dignity_level": "GREAT_FRIEND"
},
"Mercury": {
"sign": "Taurus",
"sign_index": 1,
"degree": 12.8439,
"house": 10,
- "absolute_degree": 42.8439
+ "absolute_degree": 42.8439,
+ "dignity_level": "GREAT_FRIEND"
},
"Jupiter": {
"sign": "Gemini",
"sign_index": 2,
"degree": 23.5398,
"house": 11,
- "absolute_degree": 83.5398
+ "absolute_degree": 83.5398,
+ "dignity_level": "NEUTRAL"
},
"Venus": {
"sign": "Aries",
"sign_index": 0,
"degree": 26.1082,
"house": 9,
- "absolute_degree": 26.1082
+ "absolute_degree": 26.1082,
+ "dignity_level": "FRIEND"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 1.774,
"house": 6,
- "absolute_degree": 271.774
+ "absolute_degree": 271.774,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 17.4381,
"house": 6,
- "absolute_degree": 287.4381
+ "absolute_degree": 287.4381,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Cancer",
"sign_index": 3,
"degree": 17.4381,
"house": 12,
- "absolute_degree": 107.4381
+ "absolute_degree": 107.4381,
+ "dignity_level": "GREAT_ENEMY"
}
},
"house_chart": [
@@ -430,63 +442,72 @@
"sign_index": 4,
"degree": 3.0675,
"house": 2,
- "absolute_degree": 123.0675
+ "absolute_degree": 123.0675,
+ "dignity_level": "MOOLATRIKONA"
},
"Moon": {
"sign": "Cancer",
"sign_index": 3,
"degree": 7.2923,
"house": 1,
- "absolute_degree": 97.2923
+ "absolute_degree": 97.2923,
+ "dignity_level": "OWN_SIGN"
},
"Mars": {
"sign": "Leo",
"sign_index": 4,
"degree": 7.0484,
"house": 2,
- "absolute_degree": 127.0484
+ "absolute_degree": 127.0484,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Cancer",
"sign_index": 3,
"degree": 25.6877,
"house": 1,
- "absolute_degree": 115.6877
+ "absolute_degree": 115.6877,
+ "dignity_level": "GREAT_ENEMY"
},
"Jupiter": {
"sign": "Cancer",
"sign_index": 3,
"degree": 17.0797,
"house": 1,
- "absolute_degree": 107.0797
+ "absolute_degree": 107.0797,
+ "dignity_level": "EXALTED"
},
"Venus": {
"sign": "Cancer",
"sign_index": 3,
"degree": 22.2165,
"house": 1,
- "absolute_degree": 112.2165
+ "absolute_degree": 112.2165,
+ "dignity_level": "GREAT_ENEMY"
},
"Saturn": {
"sign": "Cancer",
"sign_index": 3,
"degree": 3.5481,
"house": 1,
- "absolute_degree": 93.5481
+ "absolute_degree": 93.5481,
+ "dignity_level": "GREAT_ENEMY"
},
"Rahu": {
"sign": "Leo",
"sign_index": 4,
"degree": 4.8762,
"house": 2,
- "absolute_degree": 124.8762
+ "absolute_degree": 124.8762,
+ "dignity_level": "GREAT_ENEMY"
},
"Ketu": {
"sign": "Leo",
"sign_index": 4,
"degree": 4.8762,
"house": 2,
- "absolute_degree": 124.8762
+ "absolute_degree": 124.8762,
+ "dignity_level": "GREAT_ENEMY"
}
},
"house_chart": [
@@ -531,63 +552,72 @@
"sign_index": 2,
"degree": 4.6013,
"house": 3,
- "absolute_degree": 64.6013
+ "absolute_degree": 64.6013,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Gemini",
"sign_index": 2,
"degree": 25.9385,
"house": 3,
- "absolute_degree": 85.9385
+ "absolute_degree": 85.9385,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Cancer",
"sign_index": 3,
"degree": 25.5725,
"house": 4,
- "absolute_degree": 115.5725
+ "absolute_degree": 115.5725,
+ "dignity_level": "DEBILITATED"
},
"Mercury": {
"sign": "Virgo",
"sign_index": 5,
"degree": 8.5316,
"house": 6,
- "absolute_degree": 158.5316
+ "absolute_degree": 158.5316,
+ "dignity_level": "EXALTED"
},
"Jupiter": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 10.6195,
"house": 11,
- "absolute_degree": 310.6195
+ "absolute_degree": 310.6195,
+ "dignity_level": "FRIEND"
},
"Venus": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 18.3247,
"house": 9,
- "absolute_degree": 258.3247
+ "absolute_degree": 258.3247,
+ "dignity_level": "FRIEND"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 5.3221,
"house": 10,
- "absolute_degree": 275.3221
+ "absolute_degree": 275.3221,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Taurus",
"sign_index": 1,
"degree": 22.3144,
"house": 2,
- "absolute_degree": 52.3144
+ "absolute_degree": 52.3144,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 22.3144,
"house": 8,
- "absolute_degree": 232.3144
+ "absolute_degree": 232.3144,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -638,63 +668,72 @@
"sign_index": 2,
"degree": 6.1351,
"house": 2,
- "absolute_degree": 66.1351
+ "absolute_degree": 66.1351,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Leo",
"sign_index": 4,
"degree": 14.5847,
"house": 4,
- "absolute_degree": 134.5847
+ "absolute_degree": 134.5847,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Virgo",
"sign_index": 5,
"degree": 14.0967,
"house": 5,
- "absolute_degree": 164.0967
+ "absolute_degree": 164.0967,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Leo",
"sign_index": 4,
"degree": 21.3754,
"house": 4,
- "absolute_degree": 141.3754
+ "absolute_degree": 141.3754,
+ "dignity_level": "GREAT_FRIEND"
},
"Jupiter": {
"sign": "Pisces",
"sign_index": 11,
"degree": 4.1593,
"house": 11,
- "absolute_degree": 334.1593
+ "absolute_degree": 334.1593,
+ "dignity_level": "OWN_SIGN"
},
"Venus": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 14.433,
"house": 9,
- "absolute_degree": 284.433
+ "absolute_degree": 284.433,
+ "dignity_level": "NEUTRAL"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 7.0961,
"house": 9,
- "absolute_degree": 277.0961
+ "absolute_degree": 277.0961,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Cancer",
"sign_index": 3,
"degree": 9.7525,
"house": 3,
- "absolute_degree": 99.7525
+ "absolute_degree": 99.7525,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 9.7525,
"house": 9,
- "absolute_degree": 279.7525
+ "absolute_degree": 279.7525,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -743,63 +782,72 @@
"sign_index": 2,
"degree": 7.6689,
"house": 7,
- "absolute_degree": 67.6689
+ "absolute_degree": 67.6689,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Taurus",
"sign_index": 1,
"degree": 3.2309,
"house": 6,
- "absolute_degree": 33.2309
+ "absolute_degree": 33.2309,
+ "dignity_level": "EXALTED"
},
"Mars": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 2.6209,
"house": 3,
- "absolute_degree": 302.6209
+ "absolute_degree": 302.6209,
+ "dignity_level": "ENEMY"
},
"Mercury": {
"sign": "Pisces",
"sign_index": 11,
"degree": 4.2193,
"house": 4,
- "absolute_degree": 334.2193
+ "absolute_degree": 334.2193,
+ "dignity_level": "DEBILITATED"
},
"Jupiter": {
"sign": "Virgo",
"sign_index": 5,
"degree": 27.6992,
"house": 10,
- "absolute_degree": 177.6992
+ "absolute_degree": 177.6992,
+ "dignity_level": "GREAT_ENEMY"
},
"Venus": {
"sign": "Leo",
"sign_index": 4,
"degree": 10.5412,
"house": 9,
- "absolute_degree": 130.5412
+ "absolute_degree": 130.5412,
+ "dignity_level": "NEUTRAL"
},
"Saturn": {
"sign": "Virgo",
"sign_index": 5,
"degree": 8.8701,
"house": 10,
- "absolute_degree": 158.8701
+ "absolute_degree": 158.8701,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 27.1906,
"house": 12,
- "absolute_degree": 237.1906
+ "absolute_degree": 237.1906,
+ "dignity_level": "FRIEND"
},
"Ketu": {
"sign": "Taurus",
"sign_index": 1,
"degree": 27.1906,
"house": 6,
- "absolute_degree": 57.1906
+ "absolute_degree": 57.1906,
+ "dignity_level": "FRIEND"
}
},
"house_chart": [
@@ -849,63 +897,72 @@
"sign_index": 2,
"degree": 9.2026,
"house": 6,
- "absolute_degree": 69.2026
+ "absolute_degree": 69.2026,
+ "dignity_level": "ENEMY"
},
"Moon": {
"sign": "Taurus",
"sign_index": 1,
"degree": 21.877,
"house": 5,
- "absolute_degree": 51.877
+ "absolute_degree": 51.877,
+ "dignity_level": "EXALTED"
},
"Mars": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 21.1451,
"house": 12,
- "absolute_degree": 261.1451
+ "absolute_degree": 261.1451,
+ "dignity_level": "GREAT_FRIEND"
},
"Mercury": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 17.0631,
"house": 1,
- "absolute_degree": 287.0631
+ "absolute_degree": 287.0631,
+ "dignity_level": "ENEMY"
},
"Jupiter": {
"sign": "Libra",
"sign_index": 6,
"degree": 21.239,
"house": 10,
- "absolute_degree": 201.239
+ "absolute_degree": 201.239,
+ "dignity_level": "NEUTRAL"
},
"Venus": {
"sign": "Virgo",
"sign_index": 5,
"degree": 6.6495,
"house": 9,
- "absolute_degree": 156.6495
+ "absolute_degree": 156.6495,
+ "dignity_level": "DEBILITATED"
},
"Saturn": {
"sign": "Cancer",
"sign_index": 3,
"degree": 10.6442,
"house": 7,
- "absolute_degree": 100.6442
+ "absolute_degree": 100.6442,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Libra",
"sign_index": 6,
"degree": 14.6287,
"house": 10,
- "absolute_degree": 194.6287
+ "absolute_degree": 194.6287,
+ "dignity_level": "GREAT_FRIEND"
},
"Ketu": {
"sign": "Aries",
"sign_index": 0,
"degree": 14.6287,
"house": 4,
- "absolute_degree": 14.6287
+ "absolute_degree": 14.6287,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -956,63 +1013,72 @@
"sign_index": 2,
"degree": 10.7364,
"house": 5,
- "absolute_degree": 70.7364
+ "absolute_degree": 70.7364,
+ "dignity_level": "ENEMY"
},
"Moon": {
"sign": "Gemini",
"sign_index": 2,
"degree": 10.5232,
"house": 5,
- "absolute_degree": 70.5232
+ "absolute_degree": 70.5232,
+ "dignity_level": "NEUTRAL"
},
"Mars": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 9.6693,
"house": 12,
- "absolute_degree": 279.6693
+ "absolute_degree": 279.6693,
+ "dignity_level": "EXALTED"
},
"Mercury": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 29.907,
"house": 12,
- "absolute_degree": 299.907
+ "absolute_degree": 299.907,
+ "dignity_level": "ENEMY"
},
"Jupiter": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 14.7788,
"house": 10,
- "absolute_degree": 224.7788
+ "absolute_degree": 224.7788,
+ "dignity_level": "GREAT_FRIEND"
},
"Venus": {
"sign": "Libra",
"sign_index": 6,
"degree": 2.7577,
"house": 9,
- "absolute_degree": 182.7577
+ "absolute_degree": 182.7577,
+ "dignity_level": "MOOLATRIKONA"
},
"Saturn": {
"sign": "Cancer",
"sign_index": 3,
"degree": 12.4182,
"house": 6,
- "absolute_degree": 102.4182
+ "absolute_degree": 102.4182,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 2.0668,
"house": 10,
- "absolute_degree": 212.0668
+ "absolute_degree": 212.0668,
+ "dignity_level": "FRIEND"
},
"Ketu": {
"sign": "Taurus",
"sign_index": 1,
"degree": 2.0668,
"house": 4,
- "absolute_degree": 32.0668
+ "absolute_degree": 32.0668,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -1061,63 +1127,72 @@
"sign_index": 2,
"degree": 12.2702,
"house": 4,
- "absolute_degree": 72.2702
+ "absolute_degree": 72.2702,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Gemini",
"sign_index": 2,
"degree": 29.1694,
"house": 4,
- "absolute_degree": 89.1694
+ "absolute_degree": 89.1694,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Pisces",
"sign_index": 11,
"degree": 28.1934,
"house": 1,
- "absolute_degree": 358.1934
+ "absolute_degree": 358.1934,
+ "dignity_level": "GREAT_FRIEND"
},
"Mercury": {
"sign": "Aries",
"sign_index": 0,
"degree": 12.7508,
"house": 2,
- "absolute_degree": 12.7508
+ "absolute_degree": 12.7508,
+ "dignity_level": "FRIEND"
},
"Jupiter": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 8.3187,
"house": 10,
- "absolute_degree": 248.3187
+ "absolute_degree": 248.3187,
+ "dignity_level": "MOOLATRIKONA"
},
"Venus": {
"sign": "Libra",
"sign_index": 6,
"degree": 28.866,
"house": 8,
- "absolute_degree": 208.866
+ "absolute_degree": 208.866,
+ "dignity_level": "OWN_SIGN"
},
"Saturn": {
"sign": "Virgo",
"sign_index": 5,
"degree": 14.1922,
"house": 7,
- "absolute_degree": 164.1922
+ "absolute_degree": 164.1922,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 19.5049,
"house": 11,
- "absolute_degree": 289.5049
+ "absolute_degree": 289.5049,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Cancer",
"sign_index": 3,
"degree": 19.5049,
"house": 5,
- "absolute_degree": 109.5049
+ "absolute_degree": 109.5049,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -1168,63 +1243,72 @@
"sign_index": 6,
"degree": 13.8039,
"house": 11,
- "absolute_degree": 193.8039
+ "absolute_degree": 193.8039,
+ "dignity_level": "DEBILITATED"
},
"Moon": {
"sign": "Pisces",
"sign_index": 11,
"degree": 17.8156,
"house": 4,
- "absolute_degree": 347.8156
+ "absolute_degree": 347.8156,
+ "dignity_level": "FRIEND"
},
"Mars": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 16.7176,
"house": 1,
- "absolute_degree": 256.7176
+ "absolute_degree": 256.7176,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Aries",
"sign_index": 0,
"degree": 25.5947,
"house": 5,
- "absolute_degree": 25.5947
+ "absolute_degree": 25.5947,
+ "dignity_level": "ENEMY"
},
"Jupiter": {
"sign": "Taurus",
"sign_index": 1,
"degree": 1.8585,
"house": 6,
- "absolute_degree": 31.8585
+ "absolute_degree": 31.8585,
+ "dignity_level": "GREAT_ENEMY"
},
"Venus": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 24.9742,
"house": 12,
- "absolute_degree": 234.9742
+ "absolute_degree": 234.9742,
+ "dignity_level": "FRIEND"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 15.9663,
"house": 2,
- "absolute_degree": 285.9663
+ "absolute_degree": 285.9663,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Gemini",
"sign_index": 2,
"degree": 6.9431,
"house": 7,
- "absolute_degree": 66.9431
+ "absolute_degree": 66.9431,
+ "dignity_level": "FRIEND"
},
"Ketu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 6.9431,
"house": 1,
- "absolute_degree": 246.9431
+ "absolute_degree": 246.9431,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -1275,63 +1359,72 @@
"sign_index": 2,
"degree": 15.3377,
"house": 2,
- "absolute_degree": 75.3377
+ "absolute_degree": 75.3377,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Leo",
"sign_index": 4,
"degree": 6.4617,
"house": 4,
- "absolute_degree": 126.4617
+ "absolute_degree": 126.4617,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Taurus",
"sign_index": 1,
"degree": 5.2418,
"house": 1,
- "absolute_degree": 35.2418
+ "absolute_degree": 35.2418,
+ "dignity_level": "ENEMY"
},
"Mercury": {
"sign": "Taurus",
"sign_index": 1,
"degree": 8.4385,
"house": 1,
- "absolute_degree": 38.4385
+ "absolute_degree": 38.4385,
+ "dignity_level": "NEUTRAL"
},
"Jupiter": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 25.3983,
"house": 9,
- "absolute_degree": 295.3983
+ "absolute_degree": 295.3983,
+ "dignity_level": "DEBILITATED"
},
"Venus": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 21.0825,
"house": 8,
- "absolute_degree": 261.0825
+ "absolute_degree": 261.0825,
+ "dignity_level": "FRIEND"
},
"Saturn": {
"sign": "Virgo",
"sign_index": 5,
"degree": 17.7403,
"house": 5,
- "absolute_degree": 167.7403
+ "absolute_degree": 167.7403,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 24.3812,
"house": 10,
- "absolute_degree": 324.3812
+ "absolute_degree": 324.3812,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Leo",
"sign_index": 4,
"degree": 24.3812,
"house": 4,
- "absolute_degree": 144.3812
+ "absolute_degree": 144.3812,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -1381,63 +1474,72 @@
"sign_index": 2,
"degree": 16.8715,
"house": 1,
- "absolute_degree": 76.8715
+ "absolute_degree": 76.8715,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Leo",
"sign_index": 4,
"degree": 25.1079,
"house": 3,
- "absolute_degree": 145.1079
+ "absolute_degree": 145.1079,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Taurus",
"sign_index": 1,
"degree": 23.766,
"house": 12,
- "absolute_degree": 53.766
+ "absolute_degree": 53.766,
+ "dignity_level": "ENEMY"
},
"Mercury": {
"sign": "Taurus",
"sign_index": 1,
"degree": 21.2824,
"house": 12,
- "absolute_degree": 51.2824
+ "absolute_degree": 51.2824,
+ "dignity_level": "NEUTRAL"
},
"Jupiter": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 18.9382,
"house": 9,
- "absolute_degree": 318.9382
+ "absolute_degree": 318.9382,
+ "dignity_level": "ENEMY"
},
"Venus": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 17.1907,
"house": 8,
- "absolute_degree": 287.1907
+ "absolute_degree": 287.1907,
+ "dignity_level": "NEUTRAL"
},
"Saturn": {
"sign": "Virgo",
"sign_index": 5,
"degree": 19.5143,
"house": 4,
- "absolute_degree": 169.5143
+ "absolute_degree": 169.5143,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Pisces",
"sign_index": 11,
"degree": 11.8193,
"house": 10,
- "absolute_degree": 341.8193
+ "absolute_degree": 341.8193,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Virgo",
"sign_index": 5,
"degree": 11.8193,
"house": 4,
- "absolute_degree": 161.8193
+ "absolute_degree": 161.8193,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -1487,63 +1589,72 @@
"sign_index": 2,
"degree": 18.4053,
"house": 12,
- "absolute_degree": 78.4053
+ "absolute_degree": 78.4053,
+ "dignity_level": "ENEMY"
},
"Moon": {
"sign": "Virgo",
"sign_index": 5,
"degree": 13.7541,
"house": 3,
- "absolute_degree": 163.7541
+ "absolute_degree": 163.7541,
+ "dignity_level": "GREAT_FRIEND"
},
"Mars": {
"sign": "Libra",
"sign_index": 6,
"degree": 12.2902,
"house": 4,
- "absolute_degree": 192.2902
+ "absolute_degree": 192.2902,
+ "dignity_level": "ENEMY"
},
"Mercury": {
"sign": "Libra",
"sign_index": 6,
"degree": 4.1262,
"house": 4,
- "absolute_degree": 184.1262
+ "absolute_degree": 184.1262,
+ "dignity_level": "NEUTRAL"
},
"Jupiter": {
"sign": "Pisces",
"sign_index": 11,
"degree": 12.478,
"house": 9,
- "absolute_degree": 342.478
+ "absolute_degree": 342.478,
+ "dignity_level": "OWN_SIGN"
},
"Venus": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 13.299,
"house": 8,
- "absolute_degree": 313.299
+ "absolute_degree": 313.299,
+ "dignity_level": "GREAT_FRIEND"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 21.2884,
"house": 7,
- "absolute_degree": 291.2884
+ "absolute_degree": 291.2884,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Cancer",
"sign_index": 3,
"degree": 29.2574,
"house": 1,
- "absolute_degree": 119.2574
+ "absolute_degree": 119.2574,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 29.2574,
"house": 7,
- "absolute_degree": 299.2574
+ "absolute_degree": 299.2574,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -1593,63 +1704,72 @@
"sign_index": 10,
"degree": 24.5404,
"house": 12,
- "absolute_degree": 324.5404
+ "absolute_degree": 324.5404,
+ "dignity_level": "NEUTRAL"
},
"Moon": {
"sign": "Pisces",
"sign_index": 11,
"degree": 28.3388,
"house": 1,
- "absolute_degree": 358.3388
+ "absolute_degree": 358.3388,
+ "dignity_level": "FRIEND"
},
"Mars": {
"sign": "Leo",
"sign_index": 4,
"degree": 26.3869,
"house": 6,
- "absolute_degree": 146.3869
+ "absolute_degree": 146.3869,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Pisces",
"sign_index": 11,
"degree": 25.5016,
"house": 1,
- "absolute_degree": 355.5016
+ "absolute_degree": 355.5016,
+ "dignity_level": "DEBILITATED"
},
"Jupiter": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 16.6373,
"house": 12,
- "absolute_degree": 316.6373
+ "absolute_degree": 316.6373,
+ "dignity_level": "FRIEND"
},
"Venus": {
"sign": "Taurus",
"sign_index": 1,
"degree": 27.7319,
"house": 3,
- "absolute_degree": 57.7319
+ "absolute_degree": 57.7319,
+ "dignity_level": "OWN_SIGN"
},
"Saturn": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 28.3845,
"house": 11,
- "absolute_degree": 298.3845
+ "absolute_degree": 298.3845,
+ "dignity_level": "OWN_SIGN"
},
"Rahu": {
"sign": "Libra",
"sign_index": 6,
"degree": 9.0099,
"house": 8,
- "absolute_degree": 189.0099
+ "absolute_degree": 189.0099,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Aries",
"sign_index": 0,
"degree": 9.0099,
"house": 2,
- "absolute_degree": 9.0099
+ "absolute_degree": 9.0099,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -1699,63 +1819,72 @@
"sign_index": 7,
"degree": 0.6754,
"house": 1,
- "absolute_degree": 210.6754
+ "absolute_degree": 210.6754,
+ "dignity_level": "NEUTRAL"
},
"Moon": {
"sign": "Libra",
"sign_index": 6,
"degree": 12.9235,
"house": 12,
- "absolute_degree": 192.9235
+ "absolute_degree": 192.9235,
+ "dignity_level": "FRIEND"
},
"Mars": {
"sign": "Cancer",
"sign_index": 3,
"degree": 10.4836,
"house": 9,
- "absolute_degree": 100.4836
+ "absolute_degree": 100.4836,
+ "dignity_level": "DEBILITATED"
},
"Mercury": {
"sign": "Virgo",
"sign_index": 5,
"degree": 16.8771,
"house": 11,
- "absolute_degree": 166.8771
+ "absolute_degree": 166.8771,
+ "dignity_level": "EXALTED"
},
"Jupiter": {
"sign": "Capricorn",
"sign_index": 9,
"degree": 20.7967,
"house": 3,
- "absolute_degree": 290.7967
+ "absolute_degree": 290.7967,
+ "dignity_level": "DEBILITATED"
},
"Venus": {
"sign": "Virgo",
"sign_index": 5,
"degree": 12.1649,
"house": 11,
- "absolute_degree": 162.1649
+ "absolute_degree": 162.1649,
+ "dignity_level": "NEECHA_BHANGA"
},
"Saturn": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 5.4806,
"house": 4,
- "absolute_degree": 305.4806
+ "absolute_degree": 305.4806,
+ "dignity_level": "MOOLATRIKONA"
},
"Rahu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 18.7624,
"house": 2,
- "absolute_degree": 258.7624
+ "absolute_degree": 258.7624,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Gemini",
"sign_index": 2,
"degree": 18.7624,
"house": 8,
- "absolute_degree": 78.7624
+ "absolute_degree": 78.7624,
+ "dignity_level": "FRIEND"
}
},
"house_chart": [
@@ -1806,63 +1935,72 @@
"sign_index": 5,
"degree": 6.8105,
"house": 3,
- "absolute_degree": 156.8105
+ "absolute_degree": 156.8105,
+ "dignity_level": "ENEMY"
},
"Moon": {
"sign": "Libra",
"sign_index": 6,
"degree": 27.5082,
"house": 4,
- "absolute_degree": 207.5082
+ "absolute_degree": 207.5082,
+ "dignity_level": "ENEMY"
},
"Mars": {
"sign": "Virgo",
"sign_index": 5,
"degree": 24.5803,
"house": 3,
- "absolute_degree": 174.5803
+ "absolute_degree": 174.5803,
+ "dignity_level": "GREAT_ENEMY"
},
"Mercury": {
"sign": "Taurus",
"sign_index": 1,
"degree": 8.2525,
"house": 11,
- "absolute_degree": 38.2525
+ "absolute_degree": 38.2525,
+ "dignity_level": "GREAT_FRIEND"
},
"Jupiter": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 24.956,
"house": 8,
- "absolute_degree": 324.956
+ "absolute_degree": 324.956,
+ "dignity_level": "ENEMY"
},
"Venus": {
"sign": "Aries",
"sign_index": 0,
"degree": 26.5979,
"house": 10,
- "absolute_degree": 26.5979
+ "absolute_degree": 26.5979,
+ "dignity_level": "ENEMY"
},
"Saturn": {
"sign": "Leo",
"sign_index": 4,
"degree": 12.5767,
"house": 2,
- "absolute_degree": 132.5767
+ "absolute_degree": 132.5767,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Leo",
"sign_index": 4,
"degree": 28.5148,
"house": 2,
- "absolute_degree": 148.5148
+ "absolute_degree": 148.5148,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Leo",
"sign_index": 4,
"degree": 28.5148,
"house": 2,
- "absolute_degree": 148.5148
+ "absolute_degree": 148.5148,
+ "dignity_level": "NEUTRAL"
}
},
"house_chart": [
@@ -1911,63 +2049,72 @@
"sign_index": 3,
"degree": 11.4118,
"house": 10,
- "absolute_degree": 101.4118
+ "absolute_degree": 101.4118,
+ "dignity_level": "GREAT_FRIEND"
},
"Moon": {
"sign": "Gemini",
"sign_index": 2,
"degree": 23.4467,
"house": 9,
- "absolute_degree": 83.4467
+ "absolute_degree": 83.4467,
+ "dignity_level": "NEUTRAL"
},
"Mars": {
"sign": "Pisces",
"sign_index": 11,
"degree": 20.1529,
"house": 6,
- "absolute_degree": 350.1529
+ "absolute_degree": 350.1529,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 16.784,
"house": 3,
- "absolute_degree": 256.784
+ "absolute_degree": 256.784,
+ "dignity_level": "FRIEND"
},
"Jupiter": {
"sign": "Pisces",
"sign_index": 11,
"degree": 5.5755,
"house": 6,
- "absolute_degree": 335.5755
+ "absolute_degree": 335.5755,
+ "dignity_level": "OWN_SIGN"
},
"Venus": {
"sign": "Pisces",
"sign_index": 11,
"degree": 14.9226,
"house": 6,
- "absolute_degree": 344.9226
+ "absolute_degree": 344.9226,
+ "dignity_level": "EXALTED"
},
"Saturn": {
"sign": "Libra",
"sign_index": 6,
"degree": 17.8988,
"house": 1,
- "absolute_degree": 197.8988
+ "absolute_degree": 197.8988,
+ "dignity_level": "EXALTED"
},
"Rahu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 20.8292,
"house": 3,
- "absolute_degree": 260.8292
+ "absolute_degree": 260.8292,
+ "dignity_level": "NEUTRAL"
},
"Ketu": {
"sign": "Gemini",
"sign_index": 2,
"degree": 20.8292,
"house": 9,
- "absolute_degree": 80.8292
+ "absolute_degree": 80.8292,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -2015,63 +2162,72 @@
"sign_index": 0,
"degree": 9.2026,
"house": 12,
- "absolute_degree": 9.2026
+ "absolute_degree": 9.2026,
+ "dignity_level": "EXALTED"
},
"Moon": {
"sign": "Gemini",
"sign_index": 2,
"degree": 2.7695,
"house": 2,
- "absolute_degree": 62.7695
+ "absolute_degree": 62.7695,
+ "dignity_level": "NEUTRAL"
},
"Mars": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 24.4657,
"house": 8,
- "absolute_degree": 264.4657
+ "absolute_degree": 264.4657,
+ "dignity_level": "NEUTRAL"
},
"Mercury": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 3.1644,
"house": 8,
- "absolute_degree": 243.1644
+ "absolute_degree": 243.1644,
+ "dignity_level": "ENEMY"
},
"Jupiter": {
"sign": "Gemini",
"sign_index": 2,
"degree": 23.7437,
"house": 2,
- "absolute_degree": 83.7437
+ "absolute_degree": 83.7437,
+ "dignity_level": "GREAT_ENEMY"
},
"Venus": {
"sign": "Taurus",
"sign_index": 1,
"degree": 6.6495,
"house": 1,
- "absolute_degree": 36.6495
+ "absolute_degree": 36.6495,
+ "dignity_level": "OWN_SIGN"
},
"Saturn": {
"sign": "Taurus",
"sign_index": 1,
"degree": 10.6442,
"house": 1,
- "absolute_degree": 40.6442
+ "absolute_degree": 40.6442,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 20.3929,
"house": 8,
- "absolute_degree": 260.3929
+ "absolute_degree": 260.3929,
+ "dignity_level": "GREAT_ENEMY"
},
"Ketu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 20.3929,
"house": 8,
- "absolute_degree": 260.3929
+ "absolute_degree": 260.3929,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -2118,63 +2274,72 @@
"sign_index": 8,
"degree": 1.3509,
"house": 6,
- "absolute_degree": 241.3509
+ "absolute_degree": 241.3509,
+ "dignity_level": "NEUTRAL"
},
"Moon": {
"sign": "Libra",
"sign_index": 6,
"degree": 25.847,
"house": 4,
- "absolute_degree": 205.847
+ "absolute_degree": 205.847,
+ "dignity_level": "ENEMY"
},
"Mars": {
"sign": "Cancer",
"sign_index": 3,
"degree": 20.9672,
"house": 1,
- "absolute_degree": 110.9672
+ "absolute_degree": 110.9672,
+ "dignity_level": "DEBILITATED"
},
"Mercury": {
"sign": "Gemini",
"sign_index": 2,
"degree": 3.7541,
"house": 12,
- "absolute_degree": 63.7541
+ "absolute_degree": 63.7541,
+ "dignity_level": "OWN_SIGN"
},
"Jupiter": {
"sign": "Taurus",
"sign_index": 1,
"degree": 11.5933,
"house": 11,
- "absolute_degree": 41.5933
+ "absolute_degree": 41.5933,
+ "dignity_level": "NEUTRAL"
},
"Venus": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 24.3298,
"house": 8,
- "absolute_degree": 324.3298
+ "absolute_degree": 324.3298,
+ "dignity_level": "GREAT_FRIEND"
},
"Saturn": {
"sign": "Pisces",
"sign_index": 11,
"degree": 10.9612,
"house": 9,
- "absolute_degree": 340.9612
+ "absolute_degree": 340.9612,
+ "dignity_level": "FRIEND"
},
"Rahu": {
"sign": "Sagittarius",
"sign_index": 8,
"degree": 7.5247,
"house": 6,
- "absolute_degree": 247.5247
+ "absolute_degree": 247.5247,
+ "dignity_level": "GREAT_ENEMY"
},
"Ketu": {
"sign": "Gemini",
"sign_index": 2,
"degree": 7.5247,
"house": 12,
- "absolute_degree": 67.5247
+ "absolute_degree": 67.5247,
+ "dignity_level": "ENEMY"
}
},
"house_chart": [
@@ -2224,63 +2389,72 @@
"sign_index": 4,
"degree": 9.0197,
"house": 5,
- "absolute_degree": 129.0197
+ "absolute_degree": 129.0197,
+ "dignity_level": "MOOLATRIKONA"
},
"Moon": {
"sign": "Taurus",
"sign_index": 1,
"degree": 29.0779,
"house": 2,
- "absolute_degree": 59.0779
+ "absolute_degree": 59.0779,
+ "dignity_level": "EXALTED"
},
"Mars": {
"sign": "Aquarius",
"sign_index": 10,
"degree": 23.5881,
"house": 11,
- "absolute_degree": 323.5881
+ "absolute_degree": 323.5881,
+ "dignity_level": "FRIEND"
},
"Mercury": {
"sign": "Leo",
"sign_index": 4,
"degree": 7.9734,
"house": 5,
- "absolute_degree": 127.9734
+ "absolute_degree": 127.9734,
+ "dignity_level": "NEUTRAL"
},
"Jupiter": {
"sign": "Taurus",
"sign_index": 1,
"degree": 9.2925,
"house": 2,
- "absolute_degree": 39.2925
+ "absolute_degree": 39.2925,
+ "dignity_level": "NEUTRAL"
},
"Venus": {
"sign": "Cancer",
"sign_index": 3,
"degree": 4.8711,
"house": 4,
- "absolute_degree": 94.8711
+ "absolute_degree": 94.8711,
+ "dignity_level": "NEUTRAL"
},
"Saturn": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 19.8313,
"house": 8,
- "absolute_degree": 229.8313
+ "absolute_degree": 229.8313,
+ "dignity_level": "NEUTRAL"
},
"Rahu": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 4.7153,
"house": 8,
- "absolute_degree": 214.7153
+ "absolute_degree": 214.7153,
+ "dignity_level": "FRIEND"
},
"Ketu": {
"sign": "Taurus",
"sign_index": 1,
"degree": 4.7153,
"house": 2,
- "absolute_degree": 34.7153
+ "absolute_degree": 34.7153,
+ "dignity_level": "FRIEND"
}
},
"house_chart": [
@@ -2328,63 +2502,72 @@
"sign_index": 5,
"degree": 2.0263,
"house": 4,
- "absolute_degree": 152.0263
+ "absolute_degree": 152.0263,
+ "dignity_level": "FRIEND"
},
"Moon": {
"sign": "Pisces",
"sign_index": 11,
"degree": 8.7705,
"house": 10,
- "absolute_degree": 338.7705
+ "absolute_degree": 338.7705,
+ "dignity_level": "FRIEND"
},
"Mars": {
"sign": "Aries",
"sign_index": 0,
"degree": 1.4508,
"house": 11,
- "absolute_degree": 1.4508
+ "absolute_degree": 1.4508,
+ "dignity_level": "MOOLATRIKONA"
},
"Mercury": {
"sign": "Gemini",
"sign_index": 2,
"degree": 20.6312,
"house": 1,
- "absolute_degree": 80.6312
+ "absolute_degree": 80.6312,
+ "dignity_level": "OWN_SIGN"
},
"Jupiter": {
"sign": "Taurus",
"sign_index": 1,
"degree": 2.39,
"house": 12,
- "absolute_degree": 32.39
+ "absolute_degree": 32.39,
+ "dignity_level": "NEUTRAL"
},
"Venus": {
"sign": "Leo",
"sign_index": 4,
"degree": 6.4948,
"house": 3,
- "absolute_degree": 126.4948
+ "absolute_degree": 126.4948,
+ "dignity_level": "NEUTRAL"
},
"Saturn": {
"sign": "Aries",
"sign_index": 0,
"degree": 16.4418,
"house": 11,
- "absolute_degree": 16.4418
+ "absolute_degree": 16.4418,
+ "dignity_level": "DEBILITATED"
},
"Rahu": {
"sign": "Scorpio",
"sign_index": 7,
"degree": 26.2871,
"house": 6,
- "absolute_degree": 236.2871
+ "absolute_degree": 236.2871,
+ "dignity_level": "ENEMY"
},
"Ketu": {
"sign": "Taurus",
"sign_index": 1,
"degree": 26.2871,
"house": 12,
- "absolute_degree": 56.2871
+ "absolute_degree": 56.2871,
+ "dignity_level": "FRIEND"
}
},
"house_chart": [
diff --git a/frontend/tests/report-calculation-charts.test.tsx b/frontend/tests/report-calculation-charts.test.tsx
new file mode 100644
index 00000000..4afe3fd4
--- /dev/null
+++ b/frontend/tests/report-calculation-charts.test.tsx
@@ -0,0 +1,48 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import React from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import test from "node:test";
+
+import { ReportCalculationCharts } from "../src/components/personal-report/report-calculation-charts.tsx";
+import { buildReportExportBlocks } from "../src/lib/report-export-blocks.ts";
+import { fencedReportChartIds } from "../src/lib/report-chart-block.ts";
+import { assembleLongformCharts } from "../src/lib/personal-report-generation.ts";
+
+Object.assign(globalThis, { React });
+
+// Real engine output and its real reader body (fictional chart).
+const fixture = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8"));
+const engine = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8"));
+const charts = assembleLongformCharts(engine, "ev-cover-longform");
+
+function render(markdown: string, language: "zh" | "en" = "zh"): string {
+ return renderToStaticMarkup(React.createElement(ReportCalculationCharts, { charts, markdown, language }));
+}
+
+// BUG-1203: the body already drew these charts; the tail drew them again.
+test("a report whose body draws every chart has no trailing chart section", () => {
+ const drawn = fencedReportChartIds(fixture.markdown);
+ assert.ok(charts.length > 0);
+ for (const chart of charts) assert.ok(drawn.has(chart.id), chart.id);
+ assert.equal(render(fixture.markdown), "");
+ assert.equal(render(fixture.markdown, "en"), "");
+});
+
+test("an older body without chart fences keeps its charts, without the moved caveat", () => {
+ const markup = render("# 报告\n\n正文");
+ assert.match(markup, /分盘核对<\/h2>/);
+ assert.equal((markup.match(/personal-report-chart-card/g) ?? []).length, charts.filter(chart => chart.houses.some(house => house.occupants.length)).length);
+ assert.doesNotMatch(markup, /出生时间敏感/);
+});
+
+test("the page and the export skip the same charts", () => {
+ const first = /```jyotish-chart\n[\s\S]*?```/.exec(fixture.markdown)![0];
+ const drawn = [...fencedReportChartIds(first)];
+ assert.equal(drawn.length, 1);
+ const markup = render(first);
+ const pageIds = charts.filter(chart => !drawn.includes(chart.id) && chart.houses.some(house => house.occupants.length)).map(chart => chart.id);
+ assert.equal((markup.match(/personal-report-chart-card/g) ?? []).length, pageIds.length);
+ const exported = buildReportExportBlocks({ markdown: first, charts }).filter(block => block.group === "chart" && !/^chart:[^:]+:\d+$/.test(block.id)); // fence charts carry an occurrence suffix
+ assert.deepEqual(exported.map(block => block.id.replace(/^chart:/, "")), pageIds);
+});
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index f16007ac..19114f56 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -2575,7 +2575,7 @@ def _vedastro_runtime_fingerprint() -> dict:
def _build_api_chart_cache_payload(body: dict) -> dict:
return {
- 'cache_schema_version': 4,
+ 'cache_schema_version': 5,
'birth': {
'year': body.get('year'),
'month': body.get('month'),
@@ -7820,7 +7820,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
result = {}
for varga in selected:
key = f'D{varga.division}_{varga.varga_name}'
- result[key] = calc._calculate_single_varga(varga, planet_lons, asc_lon)
+ result[key] = _load_local_module('varga_dignity').annotate_varga_dignity(calc._calculate_single_varga(varga, planet_lons, asc_lon))
except KeyError as e:
raise BadRequest('unsupported standard division; use custom mode for arbitrary D-N') from e
except ValueError as e:
diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py
index 4f74b7f7..5bc4afb8 100644
--- a/scripts/jyotish_engine.py
+++ b/scripts/jyotish_engine.py
@@ -701,6 +701,20 @@ def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
return 'NEUTRAL'
+def varga_dignity_level(planet, varga_planets):
+ """Dignity of a body inside a divisional chart (Vimsopaka and the chart page alike).
+
+ The temporary half of the compound relationship is read from that varga's
+ own positions, as the Vimsopaka path always has. Returns None when the body
+ has no usable sign.
+ """
+ pinfo = varga_planets.get(planet) if isinstance(varga_planets, dict) else None
+ if not isinstance(pinfo, dict) or pinfo.get('sign') not in SIGN_LORDS:
+ return None
+ deg_in_sign = pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30)
+ return _get_dignity_level(planet, pinfo['sign'], deg_in_sign, _build_dignity_context(varga_planets))
+
+
def _get_planet_status_label(planet, sign, deg_in_sign=None, planets_data=None):
"""Return the user-facing D1 dignity label for chart output."""
return DIGNITY_LABELS.get(_get_dignity_level(planet, sign, deg_in_sign, planets_data), '中性')
@@ -12228,6 +12242,15 @@ def _load_strict_evidence_collector():
# ============================================================================
# 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取)
# ============================================================================
+def _nakshatra_fields(sidereal_lon):
+ """Nakshatra, pada and lord of a sidereal longitude — planets and the ascendant alike."""
+ nak_span = 360.0 / 27
+ lon = sidereal_lon % 360
+ pada = int((lon % nak_span) / (nak_span / 4)) + 1
+ name, lord, _ = NAKSHATRA_LIST[int(lon / nak_span) % 27]
+ return {"nakshatra": name, "nakshatra_pada": pada, "nakshatra_lord": lord}
+
+
def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='mean', second=0, ayanamsa_name=None):
"""计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)。node_mode: mean|true。"""
if not HAS_SWE:
@@ -12264,7 +12287,8 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
"degree_in_sign": round(deg_in_sign, 4),
"degree_in_sign_raw": deg_in_sign,
"lon": round(asc_deg, 4),
- "lord": SIGN_LORDS[asc_sign]}
+ "lord": SIGN_LORDS[asc_sign],
+ **_nakshatra_fields(asc_deg)}
for i in range(12):
c = (asc_lon[i] - ayanamsa) % 360
@@ -12273,7 +12297,6 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
"cusp_sign_cn": SIGNS_CN[SIGNS[si]], "cusp_degree": round(c, 4),
"lord": SIGN_LORDS[SIGNS[si]]}
- nak_span = 360.0 / 27
planets_swe = {**BASE_PLANETS_SWE, 'Rahu': node_pid}
calc_flags = getattr(swe, 'FLG_SWIEPH', 2)
for pname, pid in planets_swe.items():
@@ -12283,26 +12306,22 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
si = int(lon_p / 30); d_in_s = lon_p - si * 30; sign = SIGNS[si]
retro = spd < 0
house = ((si - asc_idx) % 12) + 1
- ni = int(lon_p / nak_span); pada = int((lon_p % nak_span) / (nak_span / 4)) + 1
- nak_n, nak_l, _ = NAKSHATRA_LIST[ni % 27]
result["planets"][pname] = {
"sign": sign, "sign_cn": SIGNS_CN[sign], "degree": round(lon_p, 4),
"degree_raw": lon_p,
"degree_in_sign": round(d_in_s, 4), "degree_in_sign_raw": d_in_s,
"house": house, "status": "",
"retrograde": retro, "speed": round(spd, 6),
- "nakshatra": nak_n, "nakshatra_pada": pada, "nakshatra_lord": nak_l}
+ **_nakshatra_fields(lon_p)}
if pname == 'Rahu':
klon = (lon_p + 180) % 360; ksi = int(klon / 30); kd = klon - ksi * 30
- kni = int(klon / nak_span); kp = int((klon % nak_span) / (nak_span / 4)) + 1
- kn, kl, _ = NAKSHATRA_LIST[kni % 27]
result["planets"]["Ketu"] = {
"sign": SIGNS[ksi], "sign_cn": SIGNS_CN[SIGNS[ksi]],
"degree": round(klon, 4), "degree_raw": klon,
"degree_in_sign": round(kd, 4), "degree_in_sign_raw": kd,
"house": ((ksi - asc_idx) % 12) + 1, "status": "",
"retrograde": True, "speed": round(spd, 6),
- "nakshatra": kn, "nakshatra_pada": kp, "nakshatra_lord": kl}
+ **_nakshatra_fields(klon)}
except Exception as e:
result["planets"][pname] = {"error": str(e)}
@@ -15689,10 +15708,12 @@ def cmd_full_reading(args):
# 不使用 _dignity 字典(只有 Exalted/Debilitated/Own Sign)
pinfo = vdata.get(pn, {})
if isinstance(pinfo, dict) and 'sign' in pinfo:
- dignity_context = planets if div == 1 else _build_dignity_context(vdata)
- dl_key = _get_dignity_level(pn, pinfo['sign'],
- pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
- dignity_context)
+ if div == 1:
+ dl_key = _get_dignity_level(pn, pinfo['sign'],
+ pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
+ planets)
+ else:
+ dl_key = varga_dignity_level(pn, vdata)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
elif div == 1:
# D1 直接用本命盘数据
diff --git a/scripts/pl9_reader_english_terms.py b/scripts/pl9_reader_english_terms.py
index 7e4a04c5..c0e35c21 100644
--- a/scripts/pl9_reader_english_terms.py
+++ b/scripts/pl9_reader_english_terms.py
@@ -35,6 +35,8 @@ _PHRASES: dict[str, str] = {
"特殊上升与派生点补充": "Special Lagnas and Points (supplement)",
"敏感点与特殊落点": "Sensitive Points and Special Placements",
"本命与分盘北印度图盘": "Natal and Divisional Charts (North Indian)",
+ "分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。":
+ "Divisional charts are sensitive to birth time. The raw calculations are here for checking and do not by themselves make a conclusion more certain.",
" 数值位置表": " Position Table",
"Shadbala / Bhava Bala 摘要": "Shadbala / Bhava Bala Summary",
"Avastha 行星状态": "Avastha Planetary States",
@@ -84,7 +86,7 @@ _PHRASES: dict[str, str] = {
"Sign lord, star lord, sub lord and sub-sub lord: the rulers of the planet's sign, nakshatra and KP subdivisions. Strength ratio: Shadbala against the required strength; 1 or above meets it.",
"| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |": "| Body | Sign | Degree in sign | Longitude | Whole-sign house |",
"| House | Sign | Lord | Score | 关键因素 |": "| House | Sign | Lord | Score | Key factors |",
- "| House | Sign | SAV | BAV/承接 | Full with Lagna |": "| House | Sign | SAV | BAV / carry | Full with Lagna |",
+ "| 合计 | - |": "| Total | - |",
"| 序号 | 主运 | 起始 | 结束 | 年数 |": "| No. | Mahadasha | Start | End | Years |",
"| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |": "| Vimshottari AD | Mahadasha | Antardasha | Start | End |",
"| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |": "| Vimshottari PD | Mahadasha | Antardasha | Pratyantardasha | Start | End |",
diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py
index c24641a4..26d66d0d 100644
--- a/scripts/pl9_reader_export.py
+++ b/scripts/pl9_reader_export.py
@@ -1008,7 +1008,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
]
ready_natal_specs = [spec for spec in natal_specs if spec[2]]
if ready_natal_specs:
- lines.extend(['### p6-p17 本命与分盘北印度图盘', ''])
+ lines.extend(['### p6-p17 本命与分盘北印度图盘', '', '分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。', ''])
for page, label, _ready in ready_natal_specs:
lines.extend([f'##### p{page} · {label}', ''])
# The original PL9 charts are visual, but their sign placements are
@@ -1140,12 +1140,13 @@ def render_pl9_parity_markdown(packet: dict) -> str:
)
lines.append('')
lines.extend([
- '### p33 Declination / Kranti / Speed',
+ '### p33 Declination / Speed',
'',
- '| Planet | Degree | Declination | Kranti | Speed |',
- '|--------|--------|-------------|--------|-------|',
+ '| Planet | Degree | Declination | Speed |',
+ '|--------|--------|-------------|-------|',
])
- kranti_planets = _dict(_dict(strengths.get('kranti_local')).get('planets'))
+ # Kranti has no local producer (materials.kranti stays blocked), so
+ # the reader shows no Kranti column rather than a column of dashes.
speed_planets = _dict(_dict(strengths.get('speed_display_local')).get('planets'))
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(shadbala_planets.get(planet))
@@ -1153,11 +1154,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
continue
kala = _dict(row.get('kala_bala'))
planet_row = _dict((packet.get('core_chart') or {}).get('planets', {}).get(planet)) if isinstance((packet.get('core_chart') or {}).get('planets'), dict) else {}
- kranti_degrees = _dict(kranti_planets.get(planet)).get('kranti_degrees')
speed_dms = _dict(speed_planets.get(planet)).get('speed_dms') or _num(planet_row.get('speed'), 6)
lines.append(
f"| {_cell(_token(planet))} | {_cell(_degree(planet_row.get('degree_in_sign') or planet_row.get('longitude')))} | "
- f"{_cell(_degree(kala.get('declination')))} | {_cell(_degree(kranti_degrees))} | {_cell(speed_dms)} |"
+ f"{_cell(_degree(kala.get('declination')))} | {_cell(speed_dms)} |"
)
lines.append('')
avasthas = _dict(advanced.get('avasthas'))
@@ -1329,15 +1329,38 @@ def render_pl9_parity_markdown(packet: dict) -> str:
lines.extend([
'### p53 Ashtakavarga 完整宫位分数',
'',
- '| House | Sign | SAV | BAV/承接 | Full with Lagna |',
- '|-------|------|-----|----------|-----------------|',
+ '| House | Sign | SAV | Lagna BAV | SAV + Lagna |',
+ '|-------|------|-----|-----------|-------------|',
])
+ # `house_scores_full[*].sav_score` is the with-Lagna total (386
+ # scale), never plain SAV (BUG-1200). Split it from the engine's
+ # per-sign SAV and the Lagna BAV; ashtakavarga.py stays untouched
+ # because it belongs to the frozen rectification identity.
+ sign_list = list(globals().get('SIGNS', []))
+ lagna_bindus = _dict(_dict(ashtakavarga.get('bav')).get('Lagna')).get('bindus')
+ splits = []
for row in full_rows:
+ sign = row.get('sign')
+ sav_value = sav_scores.get(sign)
+ lagna_value = None
+ if isinstance(lagna_bindus, list) and len(lagna_bindus) == 12 and sign in sign_list:
+ lagna_value = lagna_bindus[sign_list.index(sign)]
+ consistent = (
+ isinstance(sav_value, int) and isinstance(lagna_value, int)
+ and sav_value + lagna_value == row.get('sav_score')
+ )
+ splits.append((sav_value, lagna_value) if consistent else None)
+ split = all(splits)
+ for row, parts in zip(full_rows, splits):
+ sav_value, lagna_value = parts if split else (None, None)
lines.append(
f"| {_cell(row.get('house'))} | {_cell(_token(row.get('sign')))} | "
- f"{_cell(row.get('sav') or row.get('sav_score') or row.get('score'))} | "
- f"{_cell(row.get('bav') or row.get('bav_score') or row.get('bindus'))} | "
- f"{_cell(row.get('full_with_lagna') or row.get('full_with_lagna_house_score'))} |"
+ f"{_cell(sav_value)} | {_cell(lagna_value)} | {_cell(row.get('sav_score'))} |"
+ )
+ if split:
+ lines.append(
+ f"| 合计 | - | {sum(parts[0] for parts in splits)} | "
+ f"{sum(parts[1] for parts in splits)} | {sum(row['sav_score'] for row in full_rows)} |"
)
lines.append('')
bav = _dict(ashtakavarga.get('bav'))
diff --git a/scripts/varga_dignity.py b/scripts/varga_dignity.py
new file mode 100644
index 00000000..428c35cb
--- /dev/null
+++ b/scripts/varga_dignity.py
@@ -0,0 +1,24 @@
+"""Engine dignity on each body of a `/api/varga_full` divisional chart (BUG-1202).
+
+The chart page's divisional tables show the same ten-level dignity as the D1
+"状态" column. The level is `jyotish_engine.varga_dignity_level`, the function
+the Vimsopaka path uses, so the page never carries a second rule set.
+"""
+
+from __future__ import annotations
+
+from jyotish_engine import varga_dignity_level
+
+
+def annotate_varga_dignity(varga: dict) -> dict:
+ """Add `dignity_level` to every body of one `_calculate_single_varga` result."""
+ planets = varga.get("planets") if isinstance(varga, dict) else None
+ if not isinstance(planets, dict):
+ return varga
+ for name, row in planets.items():
+ if not isinstance(row, dict):
+ continue
+ level = varga_dignity_level(name, planets)
+ if level:
+ row["dignity_level"] = level
+ return varga
diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py
index a396f0a8..fbb84082 100644
--- a/tests/test_api_server_security.py
+++ b/tests/test_api_server_security.py
@@ -3413,6 +3413,30 @@ def test_varga_full_endpoint_returns_extended_standard_divisions() -> None:
assert result['result']['D81_Navamsa-Navamsa']['planets']['Moon']['house'] in range(1, 13)
+def test_varga_full_standard_divisions_carry_the_vimsopaka_dignity_level() -> None:
+ """BUG-1202: the chart page shows this level; it is the Vimsopaka function's."""
+ import jyotish_engine
+ from varga import calc_all_vargas
+
+ handler = _handler()
+ planets = _sample_planets()
+ result = handler._compute_varga_full({
+ 'planets': planets,
+ 'ascendant': {'lon': 92.0},
+ 'divisions': ['D9', 'D10'],
+ })
+ d9 = result['result']['D9_Navamsa']['planets']
+ assert {name: row['dignity_level'] for name, row in d9.items()} == {
+ name: jyotish_engine.varga_dignity_level(name, d9) for name in d9
+ }
+ assert all(row['dignity_level'] in jyotish_engine.DIGNITY_LABELS for row in d9.values())
+ # The Vimsopaka path reads the same positions from varga.calc_all_vargas.
+ vimsopaka_d9 = calc_all_vargas({name: row['lon'] for name, row in planets.items()}, 92.0, None)['D9_Navamsa']
+ for name, row in d9.items():
+ assert vimsopaka_d9[name]['sign'] == row['sign'], name
+ assert jyotish_engine.varga_dignity_level(name, vimsopaka_d9) == row['dignity_level'], name
+
+
def test_varga_full_endpoint_supports_custom_and_composite_modes() -> None:
handler = _handler()
custom = handler._compute_varga_full({
diff --git a/tests/test_chart_page_vedic_tables_contract.py b/tests/test_chart_page_vedic_tables_contract.py
index e49633c0..45cef283 100644
--- a/tests/test_chart_page_vedic_tables_contract.py
+++ b/tests/test_chart_page_vedic_tables_contract.py
@@ -1,9 +1,9 @@
"""The chart page's dignity and varga-meaning tables must be the engine's.
-`frontend/src/lib/vedic-chart-tables.json` is read by the chart-page mapper so
-the divisional tables can show 入旺 / 自宫 / 落陷 without a second hand-written
-rule set. This test pins it to the constants `jyotish_engine._get_dignity_level`
-uses and to `divisional_charts_extended.VargaType`.
+`frontend/src/lib/vedic-chart-tables.json` is read by the chart page: varga
+meanings (`divisional_charts_extended.VargaType`) and the dignity wording
+(`jyotish_engine.DIGNITY_LABELS`). The dignity level itself comes from
+`/api/varga_full` (`varga_dignity_level`, BUG-1202); the page never derives it.
"""
from __future__ import annotations
@@ -27,33 +27,11 @@ TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_tex
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
-def _table_dignity(planet: str, sign: str) -> str | None:
- """Same precedence as `vargaDignity` in frontend/src/lib/chart-view-varga.ts."""
- if TABLES["exaltation"].get(planet) == sign:
- return "EXALTED"
- if TABLES["signLords"].get(sign) == planet or TABLES["nodeOwnSign"].get(planet) == sign:
- return "OWN_SIGN"
- if TABLES["debilitation"].get(planet) == sign:
- return "DEBILITATED"
- return None
-
-
-def test_tables_equal_engine_constants() -> None:
- assert TABLES["signLords"] == jyotish_engine.SIGN_LORDS
- assert TABLES["exaltation"] == jyotish_engine.EXALTATION
- assert TABLES["debilitation"] == jyotish_engine.DEBILITATION
-
-
-def test_every_planet_sign_pair_matches_engine_dignity_level() -> None:
- for planet in PLANETS:
- for sign in jyotish_engine.SIGN_LORDS:
- # No degree and no chart context: the varga table shows the sign
- # dignity only (no moolatrikona band, no neecha bhanga, no friendship).
- engine = jyotish_engine._get_dignity_level(planet, sign)
- expected = engine if engine in {"EXALTED", "OWN_SIGN", "DEBILITATED"} else None
- if engine == "NEECHA_BHANGA":
- expected = "DEBILITATED"
- assert _table_dignity(planet, sign) == expected, (planet, sign, engine)
+def test_dignity_labels_equal_engine_status_wording() -> None:
+ # BUG-1202: divisional rows carry the engine's level and the page labels it
+ # with the D1 status wording; the three-level page table is retired.
+ assert TABLES["dignityLabels"] == jyotish_engine.DIGNITY_LABELS
+ assert {"signLords", "exaltation", "debilitation", "nodeOwnSign"}.isdisjoint(TABLES)
def test_varga_meanings_equal_engine_varga_types() -> None:
@@ -131,3 +109,17 @@ def test_d9_projected_ascendant_matches_native_summary(d9_projection: tuple[dict
assert projected["ascendant"]["sign"] == native["lagna"]
assert projected["ascendant"]["natalSign"] == golden["chart"]["ascendant"]["sign"]
assert projected["ascendant"]["vargottama"] is native["lagna_vargottama"]
+
+
+def test_d9_projected_dignity_is_the_engine_level(d9_projection: tuple[dict, dict]) -> None:
+ golden, projected = d9_projection
+ d9_planets = golden["varga_full"]["result"]["D9_Navamsa"]["planets"]
+ levels = set()
+ for row in projected["rows"]:
+ expected = jyotish_engine.varga_dignity_level(row["name"], d9_planets)
+ assert expected is not None
+ assert row["dignity"] == expected == d9_planets[row["name"]]["dignity_level"]
+ levels.add(expected)
+ # The golden must show that friendship levels reach the page, not only
+ # exalted / own / debilitated.
+ assert {"OWN_SIGN", "DEBILITATED", "NEUTRAL", "ENEMY", "FRIEND"} <= levels
diff --git a/tests/test_report_chart_blank_columns.py b/tests/test_report_chart_blank_columns.py
new file mode 100644
index 00000000..527d63b3
--- /dev/null
+++ b/tests/test_report_chart_blank_columns.py
@@ -0,0 +1,130 @@
+"""Blank report and chart columns (TASK-report-chart-blank-columns-20261003).
+
+BUG-1199 Kranti column with no producer; BUG-1200 Ashtakavarga house table
+labelled the with-Lagna total as SAV (split in the reader: ashtakavarga.py is
+in the frozen rectification identity and stays byte-identical); BUG-1201 ascendant without nakshatra;
+BUG-1203 the divisional-chart caveat moved to the chart section. Every chart
+here is fictional.
+"""
+
+from __future__ import annotations
+
+import copy
+import json
+import re
+from pathlib import Path
+
+import pytest
+
+from scripts.ashtakavarga import BAV_TOTALS, EXPECTED_SAV_TOTAL, SIGNS
+from scripts.jyotish_engine import NAKSHATRA_LIST, _nakshatra_fields, compute_chart_data
+from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
+from tests.test_report_english_edition import CASES, _packet
+
+ROOT = Path(__file__).resolve().parents[1]
+CAVEAT_ZH = "分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。"
+
+
+def _independent_nakshatra(lon: float) -> tuple[str, int, str]:
+ minutes = (lon % 360) * 60
+ index = int(minutes // 800) # 13°20′ = 800′
+ pada = int((minutes % 800) // 200) + 1 # 3°20′ = 200′
+ name, lord, _ = NAKSHATRA_LIST[index]
+ return name, pada, lord
+
+
+@pytest.mark.parametrize("lon,expected", [
+ (0.0, ("Ashwini", 1, "Ketu")),
+ (13.33, ("Ashwini", 4, "Ketu")),
+ (13.34, ("Bharani", 1, "Venus")),
+ (134.53, ("Purva Phalguni", 1, "Venus")),
+ (149.3994, ("Uttara Phalguni", 1, "Sun")),
+ (359.99, ("Revati", 4, "Mercury")),
+ (360.0, ("Ashwini", 1, "Ketu")),
+])
+def test_nakshatra_fields_at_boundaries(lon, expected) -> None:
+ fields = _nakshatra_fields(lon)
+ assert (fields["nakshatra"], fields["nakshatra_pada"], fields["nakshatra_lord"]) == expected
+
+
+def test_chart_ascendant_carries_its_nakshatra_and_planets_are_unchanged() -> None:
+ golden = json.loads((ROOT / "frontend/tests/fixtures/chart-view-golden.json").read_text(encoding="utf-8"))
+ chart, *_ = compute_chart_data(1990, 6, 15, 12, 0, 39.9042, 116.4074, 8.0, ayanamsa_name="raman")
+ asc = chart["ascendant"]
+ assert (asc["nakshatra"], asc["nakshatra_pada"], asc["nakshatra_lord"]) == _independent_nakshatra(asc["lon"])
+ assert {k: asc[k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")} == {
+ k: golden["chart"]["ascendant"][k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")
+ }
+ for name, planet in chart["planets"].items():
+ stored = golden["chart"]["planets"][name]
+ assert (planet["nakshatra"], planet["nakshatra_pada"], planet["nakshatra_lord"]) == (
+ stored["nakshatra"], stored["nakshatra_pada"], stored["nakshatra_lord"]), name
+
+
+@pytest.fixture(scope="module")
+def editions() -> tuple[str, str, dict]:
+ packet = _packet(CASES["day"])
+ zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
+ english = copy.deepcopy(packet)
+ english["report_language"] = "en"
+ return zh, _pl9_export_markdown_for_edition(english, "reader_main"), packet
+
+
+def _table_after(markdown: str, heading: str) -> list[list[str]]:
+ lines = markdown.splitlines()
+ start = next(i for i, line in enumerate(lines) if line.startswith("### ") and heading in line)
+ rows = []
+ for line in lines[start + 1:]:
+ if line.startswith("### ") or (rows and not line.startswith("|")):
+ break
+ if line.startswith("|") and not set(line) <= set("|- "):
+ rows.append([cell.strip() for cell in line.strip("|").split("|")])
+ return rows
+
+
+def test_declination_table_has_no_kranti_column(editions) -> None:
+ for markdown, heading in ((editions[0], "Declination / Speed"), (editions[1], "Declination / Speed")):
+ assert "Kranti" not in markdown
+ rows = _table_after(markdown, heading)
+ assert rows[0] == ["Planet", "Degree", "Declination", "Speed"]
+ assert len(rows) == 8 and all(len(row) == 4 and "-" not in row for row in rows)
+
+
+def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
+ zh, en, packet = editions
+ expected_sav = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sav"]["scores"]
+ for markdown, heading, header, total in (
+ (zh, "Ashtakavarga 完整宫位分数", ["House", "Sign", "SAV", "上升 BAV", "SAV + 上升"], "合计"),
+ (en, "Ashtakavarga Full House Scores", ["House", "Sign", "SAV", "Lagna BAV", "SAV + Lagna"], "Total"),
+ ):
+ rows = _table_after(markdown, heading)
+ assert rows[0] == header
+ body, totals = rows[1:13], rows[13]
+ for row in body:
+ sav, lagna, full = (int(cell) for cell in row[2:])
+ assert sav + lagna == full
+ assert totals == [total, "-", str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])]
+ # SAV equals the per-sign SAV table, Lagna BAV the Lagna bindus, and the last
+ # column the engine's `house_scores_full` total that rectification reads.
+ ashtakavarga = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]
+ full = {row["sign"]: row["sav_score"] for row in ashtakavarga["house_scores_full"].values()}
+ for row in _table_after(en, "Ashtakavarga Full House Scores")[1:13]:
+ assert int(row[2]) == expected_sav[row[1]]
+ assert int(row[3]) == ashtakavarga["bav"]["Lagna"]["bindus"][SIGNS.index(row[1])]
+ assert int(row[4]) == full[row[1]]
+
+
+def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> None:
+ packet = copy.deepcopy(editions[2])
+ del packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["bav"]["Lagna"]
+ rows = _table_after(_pl9_export_markdown_for_edition(packet, "reader_main"), "Ashtakavarga 完整宫位分数")
+ assert len(rows) == 13, "no total row without the split"
+ for row in rows[1:]:
+ assert row[2:4] == ["-", "-"]
+ assert row[4].isdigit()
+
+
+def test_chart_section_carries_the_divisional_caveat(editions) -> None:
+ zh, en, _ = editions
+ assert re.search(r"### 本命与分盘北印度图盘\n\n" + re.escape(CAVEAT_ZH) + r"\n", zh)
+ assert re.search(r"### Natal and Divisional Charts \(North Indian\)\n\nDivisional charts are sensitive to birth time\.", en)
|