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:
Jesse_Chen
2026-09-27 16:44:19 +08:00
co-authored by Claude Opus 5.5
parent 33739e3789
commit db17074430
11 changed files with 696 additions and 41 deletions
+1 -1
View File
@@ -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). */
+294 -28
View File
@@ -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;
}