feat(narayana): show the Sanjay Rath version beside the legacy one (T4, BUG-1218)

Display only; scoring and timing conclusions keep the legacy algorithm.
narayana_legacy_label attaches a Rath view (seed, deciding level, direction,
24 MDs, running MD/AD) under modules.narayana_dasha.rath for the consultation
modules, full reading Step 4.13 and the CLI. Reference report: one summary
line under the legacy line plus Rath MD / running-MD AD tables (English terms
added). Evidence card: narayana_rath (label, seed sign, level, running MD) on
timing / annual cards only — other cards sit at 11,979 of 12,000 characters.
Goldens re-captured from the real engine.

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:
Jesse_Chen
2026-10-03 21:09:40 +08:00
co-authored by Claude Opus 5.5
parent 3264831a23
commit bf8d704708
13 changed files with 387 additions and 16 deletions
+28 -3
View File
@@ -331,6 +331,8 @@ export type EvidenceCard = {
timing: {
vimshottari: JsonRecord;
narayana: JsonRecord;
/** BUG-1218: Sanjay Rath Narayana beside the legacy one; display only (scoring and timing stay legacy). */
narayana_rath?: JsonRecord;
};
};
domain_sections: Partial<Record<ConsultationDomain, EvidenceCardDomainSection>>;
@@ -582,7 +584,16 @@ function baseNatal(natal: Record<string, unknown>, gaps: string[]) {
return { ascendant: ascendant ?? null, houses, planets, functional_benefic_malefic: functional, d9 };
}
function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
/**
* BUG-1218 (T4): domains whose card carries the Sanjay Rath Narayana beside
* the legacy one. Other single-domain cards sit at up to 11,979 of the 12,000
* characters (Jobs / marriage golden) and the Rath MD adds about 174, so it
* rides only on the timing-led cards (own 12,000 / 18,000 budget); the report
* shows the full Rath table for every chart.
*/
const RATH_NARAYANA_CARD_DOMAINS: ReadonlySet<ConsultationDomain> = new Set(["timing", "annual"]);
function baseTiming(timing: Record<string, unknown>, gaps: string[], withRath = false) {
const sub = record(timing.dasha_sub_periods);
const current = record(sub.current);
const pratyantar = record(sub.pratyantar_dasha_timeline);
@@ -623,6 +634,19 @@ function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
const algorithmLabel = copy(record(timing.narayana_dasha).algorithm_label);
if (narayana.md && typeof algorithmLabel === "string" && algorithmLabel) narayana.algorithm = algorithmLabel;
if (!narayana.md) gaps.push("base.narayana.md");
// BUG-1218 (T4): the Sanjay Rath version rides beside the legacy one — its
// label, start sign, the level that decided it and the running MD only
// (sub-periods stay in the report; 12,000-character card budget).
const rath = record(record(timing.narayana_dasha).rath);
const rathMd = pickKeys(record(rath.current_dasha).md, NARAYANA_PERIOD_KEYS);
if (withRath && nonEmpty(rathMd) && typeof rath.algorithm_label === "string") {
const narayanaRath: JsonRecord = { algorithm: rath.algorithm_label, md: rathMd };
for (const [from, to] of [["seed_sign", "seed_sign"], ["strength_level_decided", "seed_level"]] as const) {
const value = copy(rath[from]);
if (value !== undefined && value !== null) narayanaRath[to] = value;
}
return { vimshottari, narayana, narayana_rath: narayanaRath };
}
return { vimshottari, narayana };
}
@@ -997,9 +1021,10 @@ export function buildEvidenceCard(
if (sources.length === 0) throw new Error("evidence_card_needs_a_domain");
const gaps: string[] = [];
const first = sources[0]!;
const withRath = sources.some((item) => RATH_NARAYANA_CARD_DOMAINS.has(item.domain));
const base = {
natal: baseNatal(claimEvidence(first.packet, "natal_foundation"), gaps),
timing: baseTiming(claimEvidence(first.packet, "timing"), gaps),
timing: baseTiming(claimEvidence(first.packet, "timing"), gaps, withRath),
};
const domainSections: EvidenceCard["domain_sections"] = {};
const domains: ConsultationDomain[] = [];
@@ -1024,7 +1049,7 @@ export function buildEvidenceCard(
const ownGaps: string[] = [];
const candidate = {
natal: baseNatal(claimEvidence(source.packet, "natal_foundation"), ownGaps),
timing: baseTiming(claimEvidence(source.packet, "timing"), ownGaps),
timing: baseTiming(claimEvidence(source.packet, "timing"), ownGaps, withRath),
};
if (JSON.stringify(candidate) !== JSON.stringify(base)) {
own = candidate;
@@ -348,6 +348,9 @@ const timingKeys = new Set([
"md", "ad", "pd", "years", "startage", "endage", "remainingyears", "pratyantardashatimeline",
// BUG-1215: the running Narayana is the legacy algorithm; its label travels with it.
"algorithmlabel",
// BUG-1218: the Sanjay Rath Narayana beside it (display only): label, start
// sign, deciding level and the running MD.
"rath", "seedsign", "strengthleveldecided",
]);
const validationKeys = new Set([