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
@@ -0,0 +1,66 @@
// TASK-astrologer-rulings-batch3-20261003 T4 (BUG-1218): the Sanjay Rath
// Narayana rides beside the legacy one on the timing-led cards; the legacy
// Narayana keeps its 「旧算法」 label and stays what scoring / timing use.
//
// Fixture: real engine consultation responses for three public AA charts
// (scripts/research/capture_consult_evidence_card_golden.py, AGENTS §7.4);
// expected values are read straight from the raw engine payload.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildEvidenceCard } from "../src/lib/consultation-evidence-card.ts";
import type { ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
import {
consultationWorkflowResponseSchema,
toAgentConsultationContext,
toModelOutput,
} from "../src/mastra/consultation-workflow.ts";
type Json = Record<string, unknown>;
const golden = JSON.parse(readFileSync(
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
"utf8",
)) as { charts: Array<{ id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json }> };
function card(workflow: Json, domain: ConsultationDomain) {
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: domain }), domain);
return buildEvidenceCard([{ domain, packet, context }]);
}
function engineRath(workflow: Json): Json {
return ((((workflow.chart as Json).modules as Json).narayana_dasha as Json).rath ?? {}) as Json;
}
test("timing and annual cards carry the Rath running MD beside the legacy Narayana", () => {
for (const chart of golden.charts) {
for (const [workflow, domain] of [[chart.timing_workflow, "timing"], [chart.annual_workflow, "annual"]] as const) {
const rath = engineRath(workflow);
const md = (rath.current_dasha as Json).md as Json;
const timing = card(workflow, domain).base.timing as Json;
const shown = timing.narayana_rath as Json;
assert.ok(shown, `${chart.id}/${domain} narayana_rath`);
assert.equal(shown.algorithm, rath.algorithm_label);
assert.equal(shown.algorithm, "Sanjay Rath 版(并列参考,未进打分与时间轴结论)");
assert.equal(shown.seed_sign, rath.seed_sign);
assert.equal(shown.seed_level, rath.strength_level_decided);
assert.deepEqual(shown.md, { sign: md.sign, lord: md.lord, years: md.years, start_age: md.start_age, end_age: md.end_age });
// The legacy Narayana is unchanged and still labelled.
assert.equal((timing.narayana as Json).algorithm, "旧算法(从上升起、一律顺行)");
assert.equal(rath.used_for_scoring, false);
}
}
});
test("other domains leave the Rath MD to the report (12,000-character card budget)", () => {
for (const chart of golden.charts) {
for (const domain of ["marriage", "career", "wealth", "health"] as const) {
const timing = card(chart.workflow, domain).base.timing as Json;
assert.equal(timing.narayana_rath, undefined, `${chart.id}/${domain}`);
assert.equal(typeof ((timing.narayana as Json).md as Json).sign, "string");
}
}
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long