feat(consult): card carries aspects, co-tenants, combustion; yogas by domain
TASK-consult-card-affliction-data-20261001 T1-T4 (BUG-1154~1157, BUG-1159 mitigation). - Engine layer graha_drishti (aspects.calc_house_aspects; combustion via the full reading's own rule, extracted as jyotish_engine.is_combust, orbs unchanged); yoga_domains (fixed Raman packet table by rule_id, with the sieve's hit); slow_transits.saturn_from_moon_house. - Card: planets get aspected_by / conjunct_with / combust (true only); domain house rows get aspected_by; parents roles get lord_rows and significator_rows with functional class, D12 house lord and its sign; health houses [1,6,8,12], education [4,5,9]; annual copies saturn_from_moon_house; yogas filtered by domain tag with hits. - Budget (D6): antardashas as [lord, start, end], D9 vargottama only when true; all 12,000-char assertions unchanged. - Golden regenerated by the real engine; jyotish_api_server.py untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9b937c4adf
commit
19802a4874
@@ -46,6 +46,13 @@ export const EVIDENCE_CARD_LABELS = {
|
||||
/** Who a parents-section entry is about (TASK-consult-plain-answer-20261001 D6). */
|
||||
roleMother: "母亲",
|
||||
roleFather: "父亲",
|
||||
/**
|
||||
* A role planet's functional class (TASK-consult-card-affliction-data-20261001
|
||||
* D4, BUG-1156): which of the engine's functional lists names the planet.
|
||||
*/
|
||||
functionalBenefic: "benefic",
|
||||
functionalMalefic: "malefic",
|
||||
functionalNeutral: "neutral",
|
||||
} as const;
|
||||
|
||||
/** KP stays off every card: exact cusps are still blocked (decision table). */
|
||||
@@ -64,6 +71,12 @@ export type EvidenceCardDomainSpec = Readonly<{
|
||||
karakas: readonly string[];
|
||||
planets: readonly string[];
|
||||
yogas: boolean;
|
||||
/**
|
||||
* Domain tags of the yogas this section takes (BUG-1157): the tag is the
|
||||
* Raman packet that governs the yoga (engine layer `yoga_domains`,
|
||||
* scripts/consultation_native_layers.py YOGA_PACKET_DOMAINS).
|
||||
*/
|
||||
yogaTags: readonly string[];
|
||||
transits: boolean;
|
||||
ashtakavarga: boolean;
|
||||
shadbala: boolean;
|
||||
@@ -115,6 +128,7 @@ const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSp
|
||||
karakas: [],
|
||||
planets: [],
|
||||
yogas: false,
|
||||
yogaTags: [],
|
||||
transits: false,
|
||||
ashtakavarga: false,
|
||||
shadbala: false,
|
||||
@@ -148,35 +162,44 @@ const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSp
|
||||
* BAV; vargas follow the turn's other domain (D9 when alone);
|
||||
* - Chara Dasha leaves every card (lookup); KP is never on a card.
|
||||
* Health carries D30 and education D5 as in the Python focus techniques.
|
||||
* TASK-consult-card-affliction-data-20261001: health adds the 1st and 12th
|
||||
* houses, education the 4th (BUG-1156); yogas are taken by domain tag
|
||||
* (BUG-1157), and parents / children now carry their own.
|
||||
*/
|
||||
export const EVIDENCE_CARD_SPECS: Readonly<Record<ConsultationDomain, EvidenceCardDomainSpec>> = {
|
||||
career: spec({
|
||||
vargas: ["D10"], houses: [10], arudha: ["A10", "AL"], karakas: ["AmK"], yogas: true, transits: true, shadbala: true,
|
||||
vargas: ["D10"], houses: [10], arudha: ["A10", "AL"], karakas: ["AmK"], yogas: true, yogaTags: ["career", "general"],
|
||||
transits: true, shadbala: true,
|
||||
savHouses: [10], slowTransits: "sav",
|
||||
}),
|
||||
marriage: spec({
|
||||
vargas: ["D9"], houses: [7, 5], arudha: ["UL", "A7"], karakas: ["DK"], planets: ["Venus", "Jupiter"], transits: true,
|
||||
punarphoo: true, dayNight: true, vivahSaham: true, doubleTransit: "seventh_and_karakas", gender: true,
|
||||
}),
|
||||
wealth: spec({ vargas: ["D2", "D11"], houses: [2, 11, 9, 5, 8, 12], yogas: true, ashtakavarga: true, shadbala: true }),
|
||||
health: spec({ vargas: ["D6", "D8", "D30"], houses: [6, 8], transits: true, shadbala: true }),
|
||||
education: spec({ vargas: ["D5", "D24"], houses: [5, 9], planets: ["Mercury", "Jupiter"] }),
|
||||
wealth: spec({
|
||||
vargas: ["D2", "D11"], houses: [2, 11, 9, 5, 8, 12], yogas: true, yogaTags: ["wealth", "general"], ashtakavarga: true, shadbala: true,
|
||||
}),
|
||||
health: spec({ vargas: ["D6", "D8", "D30"], houses: [1, 6, 8, 12], transits: true, shadbala: true }),
|
||||
education: spec({ vargas: ["D5", "D24"], houses: [4, 5, 9], planets: ["Mercury", "Jupiter"] }),
|
||||
migration: spec({ vargas: ["D4", "D12"], houses: [4, 12] }),
|
||||
family: spec({ vargas: ["D7", "D12"], houses: [4, 5, 9] }),
|
||||
parents: spec({
|
||||
vargas: ["D12"], houses: [4, 9], planets: ["Sun", "Moon"], karakas: ["MK"],
|
||||
vargas: ["D12"], houses: [4, 9], planets: ["Sun", "Moon"], karakas: ["MK"], yogas: true, yogaTags: ["parents"],
|
||||
roles: [
|
||||
{ key: "mother", label: EVIDENCE_CARD_LABELS.roleMother, house: 4, planets: ["Moon"], karakas: ["MK"], optionalKarakas: [], varga: "D12" },
|
||||
{ key: "father", label: EVIDENCE_CARD_LABELS.roleFather, house: 9, planets: ["Sun"], karakas: [], optionalKarakas: ["PiK"], varga: "D12" },
|
||||
],
|
||||
}),
|
||||
children: spec({ vargas: ["D7"], houses: [5], karakas: ["PK"], planets: ["Jupiter"] }),
|
||||
children: spec({ vargas: ["D7"], houses: [5], karakas: ["PK"], planets: ["Jupiter"], yogas: true, yogaTags: ["children"] }),
|
||||
annual: spec({ vargas: ["D1"], houses: [1], annualChart: true, annualHouses: true, slowTransits: "key_dates" }),
|
||||
timing: spec({
|
||||
vargas: ["D1", "D9"], planets: ["Saturn", "Jupiter"], transits: true, followVargas: true,
|
||||
slowTransits: "nodes_sav_bav", doubleTransit: "all_houses",
|
||||
}),
|
||||
general: spec({ vargas: ["D9", "D10", "D2"], houses: [1, 10, 7, 2], arudha: ["A10"], yogas: true, transits: true, ashtakavarga: true, shadbala: true }),
|
||||
general: spec({
|
||||
vargas: ["D9", "D10", "D2"], houses: [1, 10, 7, 2], arudha: ["A10"], yogas: true, yogaTags: ["general"], transits: true,
|
||||
ashtakavarga: true, shadbala: true,
|
||||
}),
|
||||
};
|
||||
|
||||
/** The formal vargas the engine materialises (varga_spectrum.formal). */
|
||||
@@ -379,16 +402,52 @@ const ZODIAC_SIGNS = [
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
] as const;
|
||||
|
||||
/** The sign and occupants of house `number` counted from a varga's own lagna. */
|
||||
/**
|
||||
* Sign rulership, the table the engine's functional layer uses
|
||||
* (scripts/functional_benefics.py SIGN_LORDS). Used only to name a varga
|
||||
* house's lord; the lord's varga sign is then read from the varga row.
|
||||
*/
|
||||
const SIGN_LORDS: Readonly<Record<(typeof ZODIAC_SIGNS)[number], string>> = {
|
||||
Aries: "Mars", Taurus: "Venus", Gemini: "Mercury", Cancer: "Moon", Leo: "Sun", Virgo: "Mercury",
|
||||
Libra: "Venus", Scorpio: "Mars", Sagittarius: "Jupiter", Capricorn: "Saturn", Aquarius: "Saturn", Pisces: "Jupiter",
|
||||
};
|
||||
|
||||
/**
|
||||
* The sign and occupants of house `number` counted from a varga's own lagna,
|
||||
* plus that sign's lord and the lord's sign in the same varga (BUG-1156).
|
||||
*/
|
||||
function vargaHouse(row: unknown, number: number): JsonRecord | undefined {
|
||||
const lagna = record(row).lagna;
|
||||
const start = typeof lagna === "string" ? ZODIAC_SIGNS.indexOf(lagna as (typeof ZODIAC_SIGNS)[number]) : -1;
|
||||
if (start < 0) return undefined;
|
||||
const sign = ZODIAC_SIGNS[(start + number - 1) % 12]!;
|
||||
const occupants = Object.entries(record(record(row).planets))
|
||||
const vargaPlanets = record(record(row).planets);
|
||||
const occupants = Object.entries(vargaPlanets)
|
||||
.filter(([, value]) => value === sign)
|
||||
.map(([name]) => name);
|
||||
return { house: number, sign, occupants };
|
||||
const lord = SIGN_LORDS[sign];
|
||||
const lordSign = vargaPlanets[lord];
|
||||
return { house: number, sign, occupants, lord, ...(typeof lordSign === "string" ? { lord_sign: lordSign } : {}) };
|
||||
}
|
||||
|
||||
const ROLE_PLANET_KEYS = ["house", "sign", "status", "conjunct_with", "aspected_by", "combust"] as const;
|
||||
|
||||
/**
|
||||
* A planet's full row for a role entry (BUG-1156): the base row's placement,
|
||||
* co-tenants, aspects and combustion, and its functional class selected from
|
||||
* the functional layer's own lists.
|
||||
*/
|
||||
function rolePlanetRow(base: EvidenceCard["base"], name: string): JsonRecord | undefined {
|
||||
const hit = caseInsensitive(base.natal.planets, name);
|
||||
if (!hit) return undefined;
|
||||
const row = pickKeys(hit[1], ROLE_PLANET_KEYS);
|
||||
const functional = base.natal.functional_benefic_malefic;
|
||||
const listed = (key: string) => Array.isArray(functional[key]) && (functional[key] as Json[]).includes(hit[0]);
|
||||
const klass = listed("functional_benefics") ? EVIDENCE_CARD_LABELS.functionalBenefic
|
||||
: listed("functional_malefics") ? EVIDENCE_CARD_LABELS.functionalMalefic
|
||||
: listed("functional_neutrals") ? EVIDENCE_CARD_LABELS.functionalNeutral
|
||||
: undefined;
|
||||
return klass ? { ...row, functional: klass } : row;
|
||||
}
|
||||
const ARUDHA_KEYS = ["sign", "lord"] as const;
|
||||
/** The engine writes the Arudha Lagna as pada A1 (its own name: "Arudha Lagna (AL)"). */
|
||||
@@ -420,6 +479,29 @@ function baseNatal(natal: Record<string, unknown>, gaps: string[]) {
|
||||
if (nonEmpty(picked)) planets[name] = picked;
|
||||
}
|
||||
if (!nonEmpty(planets)) gaps.push("base.planets");
|
||||
// BUG-1154 / BUG-1155: who aspects each planet, and combustion, copied from
|
||||
// the engine's graha_drishti layer; co-tenants are selected from the
|
||||
// planets' own `house`. Nothing is derived when the layer is missing. The
|
||||
// houses' own aspects sit on the domain house rows (domainSection).
|
||||
// Budget (D6): like `combust` (only when true), both lists appear only when
|
||||
// non-empty.
|
||||
const drishti = record(natal.graha_drishti);
|
||||
const drishtiPlanets = record(drishti.planets);
|
||||
const listed = (value: unknown) => (Array.isArray(value) && value.length ? copy(value) : undefined);
|
||||
if (drishti.status !== "executed") gaps.push("base.aspects");
|
||||
for (const [name, row] of Object.entries(planets)) {
|
||||
const out = row as JsonRecord;
|
||||
const house = out.house;
|
||||
const coTenants = typeof house === "number"
|
||||
? Object.entries(planets).filter(([other, value]) => other !== name && record(value).house === house).map(([other]) => other)
|
||||
: [];
|
||||
if (coTenants.length) out.conjunct_with = coTenants;
|
||||
if (drishti.status !== "executed") continue;
|
||||
const source = record(drishtiPlanets[name]);
|
||||
const by = listed(source.aspected_by);
|
||||
if (by !== undefined) out.aspected_by = by;
|
||||
if (source.combust === true) out.combust = true;
|
||||
}
|
||||
const functional = pickKeys(natal.functional_benefic_malefic, FUNCTIONAL_KEYS);
|
||||
if (!nonEmpty(functional)) gaps.push("base.functional_benefic_malefic");
|
||||
const summary = record(natal.d9_summary);
|
||||
@@ -431,6 +513,9 @@ function baseNatal(natal: Record<string, unknown>, gaps: string[]) {
|
||||
const rows: JsonRecord = {};
|
||||
for (const [name, row] of Object.entries(record(summary.planets))) {
|
||||
const picked = pickKeys(row, D9_ROW_KEYS);
|
||||
// Budget (TASK-consult-card-affliction-data-20261001 D6): like `combust`,
|
||||
// `vargottama` appears only when true.
|
||||
if (picked.vargottama === false) delete picked.vargottama;
|
||||
if (nonEmpty(picked)) rows[name] = picked;
|
||||
}
|
||||
d9.planets = rows;
|
||||
@@ -457,7 +542,14 @@ function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
|
||||
set("next_antardasha", sub.next);
|
||||
set("next_pratyantardasha", pratyantar.next);
|
||||
if (Array.isArray(sub.boundaries)) {
|
||||
const rows = sub.boundaries.map((row) => pickKeys(row, PERIOD_KEYS)).filter(nonEmpty);
|
||||
// Budget (TASK-consult-card-affliction-data-20261001 D6, short arrays):
|
||||
// each antardasha is [lord, start, end] when the engine gave all three,
|
||||
// otherwise the picked object as before. Values are unchanged.
|
||||
const rows = sub.boundaries.map((row): Json => {
|
||||
const picked = pickKeys(row, PERIOD_KEYS);
|
||||
const tuple = PERIOD_KEYS.map((key) => picked[key]);
|
||||
return tuple.every((value) => typeof value === "string") ? tuple as Json[] : picked;
|
||||
}).filter((row) => Array.isArray(row) || nonEmpty(row as JsonRecord));
|
||||
if (rows.length) vimshottari.antardashas_in_mahadasha = rows;
|
||||
}
|
||||
for (const key of ["mahadasha", "antardasha", "pratyantardasha"]) {
|
||||
@@ -540,6 +632,9 @@ function domainSection(
|
||||
houses = uniqueNumbers([...houses, ...ownedBy(base, antardasha), ...ownedBy(base, nextAntardasha), ...jupiter, ...saturn]);
|
||||
}
|
||||
const owned = record(base.natal.functional_benefic_malefic.owned_houses);
|
||||
// BUG-1155: who aspects each selected house (engine graha_drishti layer).
|
||||
const drishti = record(natal.graha_drishti);
|
||||
const houseAspects = drishti.status === "executed" ? record(drishti.houses) : undefined;
|
||||
for (const number of houses) {
|
||||
const sign = base.natal.houses[String(number)];
|
||||
if (typeof sign !== "string") {
|
||||
@@ -554,7 +649,8 @@ function domainSection(
|
||||
const lords = Object.entries(owned)
|
||||
.filter(([, houses]) => Array.isArray(houses) && houses.includes(number))
|
||||
.map(([name]) => name);
|
||||
section.houses[String(number)] = { sign, occupants, lords };
|
||||
const aspectedBy = houseAspects ? copy(record(houseAspects[String(number)]).aspected_by) : undefined;
|
||||
section.houses[String(number)] = { sign, occupants, lords, ...(aspectedBy !== undefined ? { aspected_by: aspectedBy } : {}) };
|
||||
}
|
||||
if (rule.planets.length) {
|
||||
const focus: string[] = [];
|
||||
@@ -600,11 +696,23 @@ function domainSection(
|
||||
section.karakas = { ...(section.karakas ?? {}), [code]: picked };
|
||||
karakas.push(code);
|
||||
}
|
||||
const houseLords = Array.isArray(houseRow.lords) ? houseRow.lords.filter((name): name is string => typeof name === "string") : [];
|
||||
const significators = role.planets.filter((name) => caseInsensitive(base.natal.planets, name));
|
||||
const rowsFor = (names: readonly string[]) => {
|
||||
const rows: JsonRecord = {};
|
||||
for (const name of names) {
|
||||
const row = rolePlanetRow(base, name);
|
||||
if (row) rows[name] = row;
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
const entry: JsonRecord = {
|
||||
label: role.label,
|
||||
house: role.house,
|
||||
house_lords: Array.isArray(houseRow.lords) ? houseRow.lords : [],
|
||||
significators: role.planets.filter((name) => caseInsensitive(base.natal.planets, name)),
|
||||
house_lords: houseLords,
|
||||
significators,
|
||||
lord_rows: rowsFor(houseLords),
|
||||
significator_rows: rowsFor(significators),
|
||||
};
|
||||
if (karakas.length) entry.karakas = karakas;
|
||||
if (role.varga) {
|
||||
@@ -621,7 +729,20 @@ function domainSection(
|
||||
const rows = Array.isArray(yogas.yogas) ? yogas.yogas.map(record) : [];
|
||||
const names = rows.map((row) => row.name).filter((name): name is string => typeof name === "string");
|
||||
const boundaries = [...new Set(rows.map((row) => row.claim_boundary).filter((value): value is string => typeof value === "string"))];
|
||||
if (names.length || typeof yogas.count === "number") {
|
||||
const tagged = record(natal.yoga_domains);
|
||||
if (tagged.status === "executed" && Array.isArray(tagged.rows)) {
|
||||
// BUG-1157: only this domain's tags; `hits` are the names the engine's
|
||||
// sieve matched in the chart (the rest are the packet's other rules).
|
||||
const own = tagged.rows.map(record).filter((row) => (
|
||||
typeof row.name === "string" && typeof row.domain === "string" && rule.yogaTags.includes(row.domain)
|
||||
));
|
||||
section.yogas = {
|
||||
names: own.map((row) => row.name as string),
|
||||
hits: own.filter((row) => row.hit === true).map((row) => row.name as string),
|
||||
...(boundaries.length ? { claim_boundaries: boundaries } : {}),
|
||||
};
|
||||
} else if (names.length || typeof yogas.count === "number") {
|
||||
gaps.push(`${domain}.yoga_domains`);
|
||||
section.yogas = {
|
||||
...(typeof yogas.count === "number" ? { count: yogas.count } : {}),
|
||||
names,
|
||||
@@ -699,9 +820,12 @@ function domainSection(
|
||||
} else {
|
||||
section.slow_transits = {
|
||||
current: rows,
|
||||
// BUG-1156: transit Saturn's house from the natal Moon (engine value).
|
||||
...(typeof slow.saturn_from_moon_house === "number" ? { saturn_from_moon_house: slow.saturn_from_moon_house } : {}),
|
||||
key_dates: dates.map((row) => copy(row)).filter((row): row is Json => row !== undefined),
|
||||
...(copy(slow.window) !== undefined ? { window: copy(slow.window)! } : {}),
|
||||
};
|
||||
if (typeof slow.saturn_from_moon_house !== "number") gaps.push(`${domain}.saturn_from_moon_house`);
|
||||
}
|
||||
}
|
||||
if (rule.doubleTransit !== "none") {
|
||||
@@ -779,6 +903,11 @@ function onCard(card: Pick<EvidenceCard, "domain_sections" | "base">, section: E
|
||||
if (code === "D9" && typeof card.base.natal.d9.lagna === "string") return true;
|
||||
return sections.some((item) => item && code in item.vargas);
|
||||
}
|
||||
// Yogas are on a card only by domain tag (BUG-1157); the full list stays
|
||||
// one lookup away unless a section carries it untagged.
|
||||
if (section === "yogas") {
|
||||
return sections.some((item) => item?.yogas !== undefined && !Array.isArray(item.yogas.hits));
|
||||
}
|
||||
// Chara Dasha, KP and the v2 optional layers are never on a card.
|
||||
const key = ({
|
||||
yogas: "yogas",
|
||||
|
||||
@@ -527,10 +527,18 @@ const nativeVivahSahamKeys = new Set([
|
||||
"status", "source", "longitude", "sign", "degreeinsign", "house", "natalconjuncts", "planet", "diffdeg", "boundary",
|
||||
]);
|
||||
const nativeDayNightKeys = new Set(["status", "source", "isday"]);
|
||||
// BUG-1154: whole-sign graha drishti and combustion (build_graha_drishti).
|
||||
const nativeGrahaDrishtiKeys = new Set([
|
||||
"status", "reason", "source", "rule", "planets", "houses", "aspectshouses", "aspectedby", "combust",
|
||||
]);
|
||||
// BUG-1157: each yoga's domain tag (the Raman packet that governs it) and the sieve's hit.
|
||||
const nativeYogaDomainKeys = new Set(["status", "reason", "source", "rows", "name", "hit", "domain"]);
|
||||
const nativeSlowTransitKeys = new Set([
|
||||
"status", "source", "referencedate", "window", "start", "end", "current", "sign", "degreeinsign",
|
||||
"retrograde", "natalhouse", "sav", "bav", "keydates", "kind", "date", "body", "fromsign", "tosign",
|
||||
"tonatalhouse", "motion", "housesinwindow", "boundary",
|
||||
// BUG-1156: transit Saturn's house counted from the natal Moon.
|
||||
"saturnfrommoonhouse",
|
||||
]);
|
||||
const nativeDoubleTransitKeys = new Set([
|
||||
"status", "source", "date", "activehouses", "conclusions", "house", "layer", "target", "strength",
|
||||
@@ -583,6 +591,8 @@ function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationCo
|
||||
["punarphoo", nativePunarphooKeys],
|
||||
["vivah_saham", nativeVivahSahamKeys],
|
||||
["day_night", nativeDayNightKeys],
|
||||
["graha_drishti", nativeGrahaDrishtiKeys],
|
||||
["yoga_domains", nativeYogaDomainKeys],
|
||||
]));
|
||||
return Object.keys(output).length ? output : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user