feat(consult): evidence card v2 per the astrologer's review
TASK-consult-evidence-card-v2-20260927 T3. Card version evidence-card-v2. - Base section (every card): the engine's D9 summary (D9 lagna, each planet's D9 sign and dignity, Vargottama, D1/D9 reversals); D9 houses and aspects stay out. - Career: + AL (engine pada A1), 10H SAV, SAV of the Jupiter / Saturn transit signs. - Marriage: + 5H / 5L, day / night, Punarphoo (observation_only), Double Transit on 7H / 7L (house-7 run) and DK / UL, Vivah Saham, gender unknown. - Wealth: 8H / 12H named. - Annual: the annual Tajika chart verbatim (parameter_sensitive), or 年盘未接入 when the pack is blocked / not attached (never natal data); houses 1 + running / next AD lords' houses + the year's Jupiter / Saturn transit houses, with their basis; ingress / station dates only. - Timing: Rahu / Ketu with ingress dates, Jupiter / Saturn SAV and BAV, Double Transit conclusions (no degrees); vargas follow the turn's other domain, D9 when alone. - Chara Dasha leaves every card; KP is never on a card. The lookup enum adds karakamsha, dispositor_chains, inter_chart_linkage, argala, moon_transit; a KP lookup travels with its blocked note. - System prompt and tool descriptions name the D9 summary, the parameter_sensitive / observation_only tags, 年盘未接入 and the lookups. - Telemetry card version / agentVersion bumped to v2 (three-column notes in the touched tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
33739e3789
commit
db17074430
@@ -129,7 +129,7 @@ export const EVIDENCE_CARD_TELEMETRY_FIELDS = [
|
||||
|
||||
export const evidenceCardTelemetrySchema = z.object({
|
||||
domains: z.array(consultationDomainSchema).min(1).max(2),
|
||||
cardVersion: z.literal("evidence-card-v1"),
|
||||
cardVersion: z.literal("evidence-card-v2"),
|
||||
cardChars: countSchema,
|
||||
cardTokenEstimate: countSchema,
|
||||
citedFieldIds: z.array(z.string().min(1).max(120).regex(/^[A-Za-z][A-Za-z0-9._-]*$/, "invalid card field id")).max(64),
|
||||
|
||||
@@ -44,6 +44,11 @@ const LOOKUP_SECTION_LABELS: Readonly<Record<string, string>> = {
|
||||
kakshya: "Kakshya",
|
||||
vimshottari_mahadashas: "全部大运起止",
|
||||
domain_thematic_evidence: "这个主题的证据明细",
|
||||
karakamsha: "灵魂星在九分盘的落点(Karakamsha)",
|
||||
dispositor_chains: "宫主链",
|
||||
inter_chart_linkage: "本命、九分盘、十分盘的联动",
|
||||
argala: "门闩(Argala)",
|
||||
moon_transit: "月亮行运",
|
||||
};
|
||||
|
||||
/** Plain words for one evidence-lookup section (TASK-consult-evidence-card-20260927 T5). */
|
||||
|
||||
@@ -3,7 +3,8 @@ import { consultationDomainDefinition } from "./consultation-domain-registry.ts"
|
||||
|
||||
/**
|
||||
* The evidence card: what the answer-writing model reads about the chart
|
||||
* (TASK-consult-evidence-card-20260927, D1).
|
||||
* (TASK-consult-evidence-card-20260927, D1; v2 per
|
||||
* TASK-consult-evidence-card-v2-20260927, the astrologer's review).
|
||||
*
|
||||
* The consultation still calculates the full spectrum (D6); the full result
|
||||
* stays on the server for receipts, the 「本轮技法」 panel, reports and the
|
||||
@@ -17,12 +18,29 @@ import { consultationDomainDefinition } from "./consultation-domain-registry.ts"
|
||||
* the three layers the projection does not carry (chart Shadbala summary,
|
||||
* Ashtakavarga house scores, chara karakas), from the engine's own values in
|
||||
* the agent context. Nothing is rewritten, rounded, converted or translated;
|
||||
* what is missing is listed in `gaps`, never filled in.
|
||||
* what is missing is listed in `gaps`, never filled in. v2 adds the native
|
||||
* technique layers the engine now attaches (scripts/consultation_native_layers.py):
|
||||
* a D9 summary in every base section, the annual Tajika chart, Double
|
||||
* Transit, Punarphoo, Vivah Saham and the slow-planet transits. The only
|
||||
* card-authored strings are `EVIDENCE_CARD_LABELS` (and domain ids that point
|
||||
* at another section); selections (which houses, which conclusions) are made
|
||||
* here, values never.
|
||||
*
|
||||
* Pure functions, no React.
|
||||
*/
|
||||
|
||||
export const EVIDENCE_CARD_VERSION = "evidence-card-v1" as const;
|
||||
export const EVIDENCE_CARD_VERSION = "evidence-card-v2" as const;
|
||||
|
||||
/** The only strings the card writes itself (decision table, 2026-09-27). */
|
||||
export const EVIDENCE_CARD_LABELS = {
|
||||
/** The product does not record gender yet (TASK-consult-gender-optional follows). */
|
||||
genderUnknown: "性别未知",
|
||||
/** The annual pack could not be produced reliably; natal data is never put in its place. */
|
||||
annualNotAttached: "年盘未接入",
|
||||
} as const;
|
||||
|
||||
/** KP stays off every card: exact cusps are still blocked (decision table). */
|
||||
export const EVIDENCE_LOOKUP_KP_NOTE = "KP 精确宫头仍 blocked:只作参考,不得作为结论依据";
|
||||
|
||||
/** D4: the one line the card carries instead of the long status layers. */
|
||||
export const EVIDENCE_CARD_BACKSTAGE_LINE = "全网资料核对、真实案例校准:未做,置信度封顶";
|
||||
@@ -40,7 +58,27 @@ export type EvidenceCardDomainSpec = Readonly<{
|
||||
transits: boolean;
|
||||
ashtakavarga: boolean;
|
||||
shadbala: boolean;
|
||||
charaDasha: boolean;
|
||||
/** SAV of these houses alone (career: 10H), without the full Ashtakavarga block. */
|
||||
savHouses: readonly number[];
|
||||
/**
|
||||
* Slow-planet transits: `sav` Jupiter / Saturn sign, natal house and SAV
|
||||
* (career); `nodes_sav_bav` Jupiter / Saturn with SAV and BAV plus Rahu /
|
||||
* Ketu and their ingress dates (timing); `key_dates` the four bodies now and
|
||||
* every ingress / station date of the next twelve months (annual).
|
||||
*/
|
||||
slowTransits: "none" | "sav" | "nodes_sav_bav" | "key_dates";
|
||||
/** `seventh_and_karakas`: 7H / 7L (house 7 run) and DK / UL; `all_houses`: every conclusion. */
|
||||
doubleTransit: "none" | "seventh_and_karakas" | "all_houses";
|
||||
punarphoo: boolean;
|
||||
dayNight: boolean;
|
||||
vivahSaham: boolean;
|
||||
genderUnknown: boolean;
|
||||
/** The annual Tajika chart (annual route only). */
|
||||
annualChart: boolean;
|
||||
/** Houses of the running and next AD lords and of the year's Jupiter / Saturn transits. */
|
||||
annualHouses: boolean;
|
||||
/** Vargas follow the other domain of the turn; `vargas` is the default when alone. */
|
||||
followVargas: boolean;
|
||||
}>;
|
||||
|
||||
const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSpec, "vargas">): EvidenceCardDomainSpec => ({
|
||||
@@ -52,31 +90,56 @@ const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSp
|
||||
transits: false,
|
||||
ashtakavarga: false,
|
||||
shadbala: false,
|
||||
charaDasha: false,
|
||||
savHouses: [],
|
||||
slowTransits: "none",
|
||||
doubleTransit: "none",
|
||||
punarphoo: false,
|
||||
dayNight: false,
|
||||
vivahSaham: false,
|
||||
genderUnknown: false,
|
||||
annualChart: false,
|
||||
annualHouses: false,
|
||||
followVargas: false,
|
||||
...value,
|
||||
});
|
||||
|
||||
/**
|
||||
* Domain sections, ported from the research draft `CARD_SPECS`
|
||||
* (scripts/research/consult_evidence_card_lib.py; docs/research/
|
||||
* consult_evidence_card_draft_2026_09_27.json) as the single source. Health
|
||||
* carries D30 and education D5 as in the Python focus techniques. The draft has
|
||||
* no timing / annual card: those two follow the registry and the must-use
|
||||
* layers (Vimshottari + Narayana are in every base section; D1 / D9 as the
|
||||
* divisional check, transits, Chara Dasha).
|
||||
* consult_evidence_card_draft_2026_09_27.json) as the single source, with the
|
||||
* v2 decision table on top (TASK-consult-evidence-card-v2-20260927):
|
||||
* - career: + AL, 10H SAV, SAV of the Jupiter / Saturn transit signs;
|
||||
* - marriage: + 5H / 5L, day / night, Punarphoo (observation_only), Double
|
||||
* Transit on 7H / 7L / DK / UL, Vivah Saham, gender unknown;
|
||||
* - wealth: 8H / 12H named;
|
||||
* - annual: the annual Tajika chart, houses of the running / next AD lords and
|
||||
* of the year's Jupiter / Saturn transits, ingress and station dates only;
|
||||
* - timing: Rahu / Ketu, Double Transit conclusions, Jupiter / Saturn SAV and
|
||||
* 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.
|
||||
*/
|
||||
export const EVIDENCE_CARD_SPECS: Readonly<Record<ConsultationDomain, EvidenceCardDomainSpec>> = {
|
||||
career: spec({ vargas: ["D10"], houses: [10], arudha: ["A10"], karakas: ["AmK"], yogas: true, transits: true, shadbala: true }),
|
||||
marriage: spec({ vargas: ["D9"], houses: [7], arudha: ["UL", "A7"], karakas: ["DK"], planets: ["Venus", "Jupiter"], transits: true }),
|
||||
wealth: spec({ vargas: ["D2", "D11"], houses: [2, 11, 9, 5], yogas: true, ashtakavarga: true, shadbala: true }),
|
||||
career: spec({
|
||||
vargas: ["D10"], houses: [10], arudha: ["A10", "AL"], karakas: ["AmK"], yogas: true, 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", genderUnknown: 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"] }),
|
||||
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"] }),
|
||||
children: spec({ vargas: ["D7"], houses: [5], karakas: ["PK"], planets: ["Jupiter"] }),
|
||||
annual: spec({ vargas: ["D1"], houses: [1], transits: true, charaDasha: true }),
|
||||
timing: spec({ vargas: ["D1", "D9"], planets: ["Saturn", "Jupiter"], transits: true, charaDasha: true }),
|
||||
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 }),
|
||||
};
|
||||
|
||||
@@ -120,6 +183,12 @@ export const EVIDENCE_LOOKUP_SECTIONS = [
|
||||
"kakshya",
|
||||
"vimshottari_mahadashas",
|
||||
"domain_thematic_evidence",
|
||||
// Evidence card v2: optional per the astrologer's table, never on a card.
|
||||
"karakamsha",
|
||||
"dispositor_chains",
|
||||
"inter_chart_linkage",
|
||||
"argala",
|
||||
"moon_transit",
|
||||
] as const satisfies readonly (
|
||||
| `varga:${typeof FORMAL_VARGA_CODES[number]}`
|
||||
| "varga:research_dn"
|
||||
@@ -129,6 +198,7 @@ export const EVIDENCE_LOOKUP_SECTIONS = [
|
||||
| "yogas" | "ashtakavarga" | "shadbala" | "transits" | "chara_dasha" | "arudha_padas"
|
||||
| "chara_karakas" | "kp_cusps" | "gulika" | "kakshya" | "vimshottari_mahadashas"
|
||||
| "domain_thematic_evidence"
|
||||
| "karakamsha" | "dispositor_chains" | "inter_chart_linkage" | "argala" | "moon_transit"
|
||||
)[];
|
||||
|
||||
export type EvidenceLookupSection = typeof EVIDENCE_LOOKUP_SECTIONS[number];
|
||||
@@ -147,7 +217,18 @@ export type EvidenceCardDomainSection = {
|
||||
transits?: Json;
|
||||
ashtakavarga?: JsonRecord;
|
||||
shadbala?: Json;
|
||||
chara_dasha?: Json;
|
||||
sav_by_house?: JsonRecord;
|
||||
slow_transits?: Json;
|
||||
double_transit?: Json;
|
||||
punarphoo?: Json;
|
||||
day_night?: Json;
|
||||
vivah_saham?: Json;
|
||||
gender?: typeof EVIDENCE_CARD_LABELS.genderUnknown;
|
||||
annual_chart?: Json;
|
||||
/** Why each annual house is on the card (AD lords' houses, the year's Jupiter / Saturn transits). */
|
||||
house_basis?: JsonRecord;
|
||||
/** The turn's other domain whose section already carries the vargas this section follows. */
|
||||
vargas_follow?: ConsultationDomain;
|
||||
/**
|
||||
* Present only when this domain's own base differs from the card's base
|
||||
* (the first domain's). The same chart gives the same base; a difference is
|
||||
@@ -165,6 +246,8 @@ export type EvidenceCard = {
|
||||
houses: JsonRecord;
|
||||
planets: JsonRecord;
|
||||
functional_benefic_malefic: JsonRecord;
|
||||
/** D9 lagna, each planet's D9 sign and dignity, Vargottama, D1↔D9 reversals. */
|
||||
d9: JsonRecord;
|
||||
};
|
||||
timing: {
|
||||
vimshottari: JsonRecord;
|
||||
@@ -235,6 +318,15 @@ const PERIOD_KEYS = ["lord", "start", "end"] as const;
|
||||
const NARAYANA_PERIOD_KEYS = ["sign", "lord", "years", "start_age", "end_age"] as const;
|
||||
const KARAKA_KEYS = ["planet", "degree_in_sign", "rank"] as const;
|
||||
const ARUDHA_KEYS = ["sign", "lord"] as const;
|
||||
/** The engine writes the Arudha Lagna as pada A1 (its own name: "Arudha Lagna (AL)"). */
|
||||
const ARUDHA_ENGINE_KEY: Readonly<Record<string, string>> = { AL: "A1" };
|
||||
const D9_ROW_KEYS = ["d9_sign", "d9_dignity", "vargottama"] as const;
|
||||
const DOUBLE_TRANSIT_ROW_KEYS = ["layer", "target", "strength", "jupiter_pac", "saturn_pac"] as const;
|
||||
const DOUBLE_TRANSIT_ALL_KEYS = ["house", "layer", "target", "strength"] as const;
|
||||
const KARAKA_TARGET_KEYS = ["target", "jupiter_pac", "saturn_pac", "double_transit"] as const;
|
||||
const ANNUAL_KEYS = [
|
||||
"status", "varsha_year", "annual_lagna", "varshesha", "muntha", "mudda_dasha", "tajika_aspects", "boundary",
|
||||
] as const;
|
||||
|
||||
function nonEmpty(value: JsonRecord) {
|
||||
return Object.keys(value).length > 0;
|
||||
@@ -257,7 +349,24 @@ function baseNatal(natal: Record<string, unknown>, gaps: string[]) {
|
||||
if (!nonEmpty(planets)) gaps.push("base.planets");
|
||||
const functional = pickKeys(natal.functional_benefic_malefic, FUNCTIONAL_KEYS);
|
||||
if (!nonEmpty(functional)) gaps.push("base.functional_benefic_malefic");
|
||||
return { ascendant: ascendant ?? null, houses, planets, functional_benefic_malefic: functional };
|
||||
const summary = record(natal.d9_summary);
|
||||
const d9: JsonRecord = {};
|
||||
if (typeof summary.lagna === "string") {
|
||||
d9.lagna = summary.lagna;
|
||||
const lagnaVargottama = copy(summary.lagna_vargottama);
|
||||
if (lagnaVargottama !== undefined) d9.lagna_vargottama = lagnaVargottama;
|
||||
const rows: JsonRecord = {};
|
||||
for (const [name, row] of Object.entries(record(summary.planets))) {
|
||||
const picked = pickKeys(row, D9_ROW_KEYS);
|
||||
if (nonEmpty(picked)) rows[name] = picked;
|
||||
}
|
||||
d9.planets = rows;
|
||||
const reversals = copy(summary.reversals);
|
||||
if (reversals !== undefined) d9.reversals = reversals;
|
||||
} else {
|
||||
gaps.push("base.d9");
|
||||
}
|
||||
return { ascendant: ascendant ?? null, houses, planets, functional_benefic_malefic: functional, d9 };
|
||||
}
|
||||
|
||||
function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
|
||||
@@ -293,11 +402,27 @@ function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
|
||||
return { vimshottari, narayana };
|
||||
}
|
||||
|
||||
/** Vargas the timing section follows: the other domain's (D1 aside), else the spec default. */
|
||||
export type EvidenceCardVargaPlan = Readonly<{ codes: readonly string[]; follow?: ConsultationDomain }>;
|
||||
|
||||
function uniqueNumbers(values: readonly unknown[]): number[] {
|
||||
const out: number[] = [];
|
||||
for (const value of values) if (typeof value === "number" && !out.includes(value)) out.push(value);
|
||||
return out;
|
||||
}
|
||||
|
||||
function ownedBy(base: EvidenceCard["base"], lord: unknown): number[] {
|
||||
if (typeof lord !== "string") return [];
|
||||
const houses = record(base.natal.functional_benefic_malefic.owned_houses)[lord];
|
||||
return Array.isArray(houses) ? uniqueNumbers(houses) : [];
|
||||
}
|
||||
|
||||
function domainSection(
|
||||
domain: ConsultationDomain,
|
||||
source: EvidenceCardSource,
|
||||
base: EvidenceCard["base"],
|
||||
gaps: string[],
|
||||
vargaPlan?: EvidenceCardVargaPlan,
|
||||
): EvidenceCardDomainSection {
|
||||
const rule = EVIDENCE_CARD_SPECS[domain];
|
||||
const natal = claimEvidence(source.packet, "natal_foundation");
|
||||
@@ -311,13 +436,37 @@ function domainSection(
|
||||
vargas: {},
|
||||
houses: {},
|
||||
};
|
||||
for (const code of rule.vargas) {
|
||||
for (const code of vargaPlan?.codes ?? rule.vargas) {
|
||||
const chartRow = pickKeys(formal[code], ["lagna", "planets"]);
|
||||
if (nonEmpty(chartRow)) section.vargas[code] = chartRow;
|
||||
else gaps.push(`${domain}.varga.${code}`);
|
||||
}
|
||||
if (vargaPlan?.follow) section.vargas_follow = vargaPlan.follow;
|
||||
const slow = record(timing.slow_transits);
|
||||
let houses: number[] = [...rule.houses];
|
||||
if (rule.annualHouses) {
|
||||
const antardasha = record(base.timing.vimshottari.antardasha).lord;
|
||||
const nextAntardasha = record(base.timing.vimshottari.next_antardasha).lord;
|
||||
const inWindow = record(slow.houses_in_window);
|
||||
const jupiter = Array.isArray(inWindow.Jupiter) ? uniqueNumbers(inWindow.Jupiter) : [];
|
||||
const saturn = Array.isArray(inWindow.Saturn) ? uniqueNumbers(inWindow.Saturn) : [];
|
||||
if (!Array.isArray(inWindow.Jupiter) || !Array.isArray(inWindow.Saturn)) gaps.push(`${domain}.transit_houses`);
|
||||
const basis: JsonRecord = {};
|
||||
if (typeof antardasha === "string") {
|
||||
basis.antardasha_lord = antardasha;
|
||||
basis.antardasha_houses = ownedBy(base, antardasha);
|
||||
}
|
||||
if (typeof nextAntardasha === "string") {
|
||||
basis.next_antardasha_lord = nextAntardasha;
|
||||
basis.next_antardasha_houses = ownedBy(base, nextAntardasha);
|
||||
}
|
||||
basis.jupiter_transit_houses = jupiter;
|
||||
basis.saturn_transit_houses = saturn;
|
||||
section.house_basis = basis;
|
||||
houses = uniqueNumbers([...houses, ...ownedBy(base, antardasha), ...ownedBy(base, nextAntardasha), ...jupiter, ...saturn]);
|
||||
}
|
||||
const owned = record(base.natal.functional_benefic_malefic.owned_houses);
|
||||
for (const number of rule.houses) {
|
||||
for (const number of houses) {
|
||||
const sign = base.natal.houses[String(number)];
|
||||
if (typeof sign !== "string") {
|
||||
gaps.push(`${domain}.house.${number}`);
|
||||
@@ -346,7 +495,7 @@ function domainSection(
|
||||
const padas = record(record(natal.arudha_padas).padas);
|
||||
const arudha: JsonRecord = {};
|
||||
for (const code of rule.arudha) {
|
||||
const picked = pickKeys(padas[code], ARUDHA_KEYS);
|
||||
const picked = pickKeys(padas[ARUDHA_ENGINE_KEY[code] ?? code], ARUDHA_KEYS);
|
||||
if (nonEmpty(picked)) arudha[code] = picked;
|
||||
else gaps.push(`${domain}.arudha.${code}`);
|
||||
}
|
||||
@@ -382,11 +531,6 @@ function domainSection(
|
||||
if (transits !== undefined) section.transits = transits;
|
||||
else gaps.push(`${domain}.transits`);
|
||||
}
|
||||
if (rule.charaDasha) {
|
||||
const chara = copy(timing.chara_dasha);
|
||||
if (chara !== undefined) section.chara_dasha = chara;
|
||||
else gaps.push(`${domain}.chara_dasha`);
|
||||
}
|
||||
if (rule.ashtakavarga) {
|
||||
const table = record(localLayers.ashtakavarga);
|
||||
const houseScores = record(table.house_scores);
|
||||
@@ -412,21 +556,130 @@ function domainSection(
|
||||
if (shadbala !== undefined && shadbala !== null) section.shadbala = shadbala;
|
||||
else gaps.push(`${domain}.shadbala`);
|
||||
}
|
||||
if (rule.savHouses.length) {
|
||||
const houseScores = record(record(localLayers.ashtakavarga).house_scores);
|
||||
const byHouse: JsonRecord = {};
|
||||
for (const number of rule.savHouses) {
|
||||
const row = pickKeys(houseScores[`house_${number}`], ["sign", "sav_score", "level"]);
|
||||
if (nonEmpty(row)) byHouse[String(number)] = row;
|
||||
else gaps.push(`${domain}.sav.${number}`);
|
||||
}
|
||||
section.sav_by_house = byHouse;
|
||||
}
|
||||
if (rule.slowTransits !== "none") {
|
||||
const current = record(slow.current);
|
||||
const rows: JsonRecord = {};
|
||||
const bodies = rule.slowTransits === "sav" ? ["Jupiter", "Saturn"] : ["Jupiter", "Saturn", "Rahu", "Ketu"];
|
||||
const keys = {
|
||||
sav: ["sign", "natal_house", "sav"],
|
||||
nodes_sav_bav: ["sign", "degree_in_sign", "retrograde", "natal_house", "sav", "bav"],
|
||||
key_dates: ["sign", "natal_house"],
|
||||
}[rule.slowTransits];
|
||||
for (const body of bodies) {
|
||||
const picked = pickKeys(current[body], keys);
|
||||
if (nonEmpty(picked)) rows[body] = picked;
|
||||
else gaps.push(`${domain}.slow_transits.${body}`);
|
||||
}
|
||||
const dates = Array.isArray(slow.key_dates) ? slow.key_dates.map(record) : [];
|
||||
if (rule.slowTransits === "sav") {
|
||||
section.slow_transits = rows;
|
||||
} else if (rule.slowTransits === "nodes_sav_bav") {
|
||||
section.slow_transits = {
|
||||
current: rows,
|
||||
node_ingresses: dates.filter((row) => row.body === "rahu" || row.body === "ketu")
|
||||
.map((row) => copy(row))
|
||||
.filter((row): row is Json => row !== undefined),
|
||||
...(copy(slow.window) !== undefined ? { window: copy(slow.window)! } : {}),
|
||||
};
|
||||
} else {
|
||||
section.slow_transits = {
|
||||
current: rows,
|
||||
key_dates: dates.map((row) => copy(row)).filter((row): row is Json => row !== undefined),
|
||||
...(copy(slow.window) !== undefined ? { window: copy(slow.window)! } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (rule.doubleTransit !== "none") {
|
||||
const double = record(timing.double_transit);
|
||||
const conclusions = Array.isArray(double.conclusions) ? double.conclusions.map(record) : [];
|
||||
if (!Array.isArray(double.conclusions)) {
|
||||
gaps.push(`${domain}.double_transit`);
|
||||
} else if (rule.doubleTransit === "seventh_and_karakas") {
|
||||
const karakas = record(double.karaka_targets);
|
||||
const dk = pickKeys(karakas.DK, KARAKA_TARGET_KEYS);
|
||||
const ul = pickKeys(karakas.UL, KARAKA_TARGET_KEYS);
|
||||
section.double_transit = {
|
||||
...(typeof double.date === "string" ? { date: double.date } : {}),
|
||||
house_7: conclusions.filter((row) => row.house === 7).map((row) => pickKeys(row, DOUBLE_TRANSIT_ROW_KEYS)),
|
||||
...(nonEmpty(dk) ? { DK: dk } : {}),
|
||||
...(nonEmpty(ul) ? { UL: ul } : {}),
|
||||
...(typeof record(karakas.DK).method === "string" ? { karaka_method: record(karakas.DK).method as string } : {}),
|
||||
...(typeof double.boundary === "string" ? { boundary: double.boundary } : {}),
|
||||
};
|
||||
if (!nonEmpty(dk)) gaps.push(`${domain}.double_transit.DK`);
|
||||
if (!nonEmpty(ul)) gaps.push(`${domain}.double_transit.UL`);
|
||||
} else {
|
||||
section.double_transit = {
|
||||
...(typeof double.date === "string" ? { date: double.date } : {}),
|
||||
...(copy(double.active_houses) !== undefined ? { active_houses: copy(double.active_houses)! } : {}),
|
||||
conclusions: conclusions.map((row) => pickKeys(row, DOUBLE_TRANSIT_ALL_KEYS)),
|
||||
...(typeof double.boundary === "string" ? { boundary: double.boundary } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (rule.punarphoo) {
|
||||
const punarphoo = record(natal.punarphoo);
|
||||
if (typeof punarphoo.natal_present === "boolean") {
|
||||
section.punarphoo = {
|
||||
...pickKeys(punarphoo, ["status", "natal_present", "orb_deg"]),
|
||||
saturn_return_to_natal_moon: pickKeys(punarphoo.saturn_return_to_natal_moon, ["active", "orb_deg"]),
|
||||
...pickKeys(punarphoo, ["claim_boundary"]),
|
||||
};
|
||||
} else {
|
||||
gaps.push(`${domain}.punarphoo`);
|
||||
}
|
||||
}
|
||||
if (rule.dayNight) {
|
||||
const dayNight = pickKeys(natal.day_night, ["is_day"]);
|
||||
if (nonEmpty(dayNight)) section.day_night = dayNight;
|
||||
else gaps.push(`${domain}.day_night`);
|
||||
}
|
||||
if (rule.vivahSaham) {
|
||||
const vivah = pickKeys(natal.vivah_saham, ["longitude", "sign", "degree_in_sign", "house", "natal_conjuncts"]);
|
||||
if (typeof vivah.house === "number") section.vivah_saham = vivah;
|
||||
else gaps.push(`${domain}.vivah_saham`);
|
||||
}
|
||||
if (rule.genderUnknown) section.gender = EVIDENCE_CARD_LABELS.genderUnknown;
|
||||
if (rule.annualChart) {
|
||||
const annual = record(timing.annual_tajika);
|
||||
if (annual.status === "parameter_sensitive") {
|
||||
section.annual_chart = pickKeys(annual, ANNUAL_KEYS);
|
||||
} else {
|
||||
// Blocked or not attached: say so; never put natal data in its place.
|
||||
if (!Object.keys(annual).length) gaps.push(`${domain}.annual_chart`);
|
||||
section.annual_chart = {
|
||||
status: EVIDENCE_CARD_LABELS.annualNotAttached,
|
||||
...(typeof annual.reason === "string" ? { reason: annual.reason } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
function onCard(card: Pick<EvidenceCard, "domain_sections">, section: EvidenceLookupSection) {
|
||||
function onCard(card: Pick<EvidenceCard, "domain_sections" | "base">, section: EvidenceLookupSection) {
|
||||
const sections = Object.values(card.domain_sections);
|
||||
if (section.startsWith("varga:")) {
|
||||
const code = section.slice("varga:".length);
|
||||
// The base D9 summary already holds the D9 lagna and every planet's D9 sign.
|
||||
if (code === "D9" && typeof card.base.natal.d9.lagna === "string") return true;
|
||||
return sections.some((item) => item && code in item.vargas);
|
||||
}
|
||||
// Chara Dasha, KP and the v2 optional layers are never on a card.
|
||||
const key = ({
|
||||
yogas: "yogas",
|
||||
ashtakavarga: "ashtakavarga",
|
||||
shadbala: "shadbala",
|
||||
transits: "transits",
|
||||
chara_dasha: "chara_dasha",
|
||||
} as const)[section as "yogas"];
|
||||
return key ? sections.some((item) => item && item[key] !== undefined) : false;
|
||||
}
|
||||
@@ -445,6 +698,19 @@ export function buildEvidenceCard(sources: readonly EvidenceCardSource[]): Evide
|
||||
};
|
||||
const domainSections: EvidenceCard["domain_sections"] = {};
|
||||
const domains: ConsultationDomain[] = [];
|
||||
const planFor = (domain: ConsultationDomain): EvidenceCardVargaPlan | undefined => {
|
||||
if (!EVIDENCE_CARD_SPECS[domain].followVargas) return undefined;
|
||||
const other = sources.map((item) => item.domain).find((item) => (
|
||||
!EVIDENCE_CARD_SPECS[item].followVargas
|
||||
&& EVIDENCE_CARD_SPECS[item].vargas.some((code) => code !== "D1")
|
||||
));
|
||||
if (!other) return undefined;
|
||||
// D1 stays; the other section already carries the followed vargas.
|
||||
return {
|
||||
codes: EVIDENCE_CARD_SPECS[domain].vargas.filter((code) => code === "D1"),
|
||||
follow: other,
|
||||
};
|
||||
};
|
||||
for (const source of sources) {
|
||||
if (domains.includes(source.domain)) continue;
|
||||
domains.push(source.domain);
|
||||
@@ -460,7 +726,7 @@ export function buildEvidenceCard(sources: readonly EvidenceCardSource[]): Evide
|
||||
gaps.push(`${source.domain}.base_differs`, ...ownGaps.map((gap) => `${source.domain}.${gap}`));
|
||||
}
|
||||
}
|
||||
const section = domainSection(source.domain, source, own, gaps);
|
||||
const section = domainSection(source.domain, source, own, gaps, planFor(source.domain));
|
||||
if (own !== base) section.base = own;
|
||||
domainSections[source.domain] = section;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user