feat(consult): Rath running MD on every domain card, budget 12,500 / 18,500 (BUG-1221)

Product decision 2026-10-03 (changes the BUG-1160/1161 budget): every
domain card carries the Sanjay Rath Narayana running MD beside the legacy
one, in its short form (label, start sign, MD sign, start / end age).
Card budget 12,000 -> 12,500, with the method checklist 18,000 -> 18,500;
three budget assertions rewritten with old / new / reason notes.

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 22:49:30 +08:00
co-authored by Claude Opus 5.5
parent 8ca06c4f20
commit 11225171ee
7 changed files with 117 additions and 54 deletions
@@ -177,7 +177,10 @@ test("only the consultation tool builds the methodology: reports and other surfa
// 原值: 标题「every single-domain turn stays within 12,000 characters, contract and checklist included」;全部 12 个领域 total ≤ 12,000
// 新值: 年运、时运按事业 / 婚姻 / 财富同口径:契约 + 卡 ≤ 12,000、含清单 total ≤ 18,000;其余领域 total 仍 ≤ 12,000
// 原因: 产品 2026-10-02 决定(BUG-1160/1161):年运 / 时运单领域轮加通用读法与年运清单后约 12,980,原口径无余量
test("size: single-domain turns stay within 12,000 characters with the checklist; annual and timing within 12,000 without it and 18,000 with it", async () => {
// 原值(BUG-1221 前): 上限 12,000 / 12,000 / 18,000;标题同上一组三栏的新值
// 新值: 其余领域 total ≤ 12,500;年运、时运 契约 + 卡 ≤ 12,500、含清单 ≤ 18,500;标题改为 12,500 / 18,500
// 原因: 产品 2026-10-03 决定全部领域卡都放 Rath 当前大运(乔布斯婚恋 total 11,979 → 12,113),卡片预算 12,000 → 12,500、含清单 18,000 → 18,500(BUG-1221,修改 BUG-1160/1161 的预算决定)
test("size: single-domain turns stay within 12,500 characters with the checklist; annual and timing within 12,500 without it and 18,500 with it", async () => {
const serverChart = {
name: "public",
toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" },
@@ -200,10 +203,10 @@ test("size: single-domain turns stay within 12,000 characters with the checklist
assert.equal(state.modelVisibleChars, total);
if (domain === "annual" || domain === "timing") {
const contractAndCard = JSON.stringify({ ...(view as Json), methodology: undefined }).length;
assert.ok(contractAndCard <= 12_000, `${chart.id}/${domain}: contract + card ${contractAndCard}`);
assert.ok(total <= 18_000, `${chart.id}/${domain}: ${total}`);
assert.ok(contractAndCard <= 12_500, `${chart.id}/${domain}: contract + card ${contractAndCard}`);
assert.ok(total <= 18_500, `${chart.id}/${domain}: ${total}`);
} else {
assert.ok(total <= 12_000, `${chart.id}/${domain}: ${total}`);
assert.ok(total <= 12_500, `${chart.id}/${domain}: ${total}`);
}
}
}
@@ -275,7 +275,10 @@ test("the 「本轮技法」 rows are exactly the rows the full result produced
}
});
test("model-visible size falls to card scale; only the method checklist can push it past 12,000", async () => {
// 原值: 上限 12,000(契约 + 卡;非清单重领域 total)/ 18,000(含清单 total);标题「past 12,000」
// 新值: 上限 12,500 / 18,500;标题「past 12,500」
// 原因: 产品 2026-10-03 决定全部领域卡都放 Rath 当前大运,卡片预算 12,000 → 12,500、含清单 18,000 → 18,500(BUG-1221,修改 BUG-1160/1161 的预算决定)
test("model-visible size falls to card scale; only the method checklist can push it past 12,500", async () => {
// 原值: new Set(["career", "marriage", "wealth"])
// 新值: 加 annual、timing(契约 + 卡 ≤ 12,000,含清单 ≤ 18,000)
// 原因: 产品 2026-10-02 决定年运 / 时运与事业 / 婚姻 / 财富同口径,通用读法与年运清单下发(BUG-1160/1161)
@@ -287,9 +290,9 @@ test("model-visible size falls to card scale; only the method checklist can push
const { methodology, ...rest } = view;
const withoutMethod = JSON.stringify(rest).length;
assert.equal(state.modelVisibleChars, total);
assert.ok(withoutMethod <= 12_000, `${chart.id}/${domain}: contract + card ${withoutMethod}`);
if (!methodHeavy.has(domain)) assert.ok(total <= 12_000, `${chart.id}/${domain}: ${total}`);
assert.ok(total <= 18_000, `${chart.id}/${domain}: ${total} (methodology ${JSON.stringify(methodology ?? null).length})`);
assert.ok(withoutMethod <= 12_500, `${chart.id}/${domain}: contract + card ${withoutMethod}`);
if (!methodHeavy.has(domain)) assert.ok(total <= 12_500, `${chart.id}/${domain}: ${total}`);
assert.ok(total <= 18_500, `${chart.id}/${domain}: ${total} (methodology ${JSON.stringify(methodology ?? null).length})`);
}
}
});
@@ -336,7 +336,10 @@ test("the lookup enum carries the optional layers and returns the engine's own v
assert.equal(dig(result, "data", "note"), EVIDENCE_LOOKUP_KP_NOTE);
});
test("size: contract + card stay under 12,000 characters without the checklist, annual and timing included", async () => {
// 原值: 契约 + 卡 ≤ 12,000;标题「under 12,000」
// 新值: 契约 + 卡 ≤ 12,500;标题「under 12,500」
// 原因: 产品 2026-10-03 决定全部领域卡都放 Rath 当前大运,卡片预算 12,000 → 12,500(BUG-1221,修改 BUG-1160/1161 的预算决定)
test("size: contract + card stay under 12,500 characters without the checklist, annual and timing included", async () => {
const serverChart = {
name: "public",
toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" },
@@ -357,7 +360,7 @@ test("size: contract + card stay under 12,000 characters without the checklist,
const view = await tools["run-jyotish-consultation"].execute!({ question: "q", domains: [domain] } as never, { writer: { custom: async () => {} } } as never) as Json;
const { methodology: _methodology, ...rest } = view;
void _methodology;
assert.ok(JSON.stringify(rest).length <= 12_000, `${chart.id}/${domain} contract + card ${JSON.stringify(rest).length}`);
assert.ok(JSON.stringify(rest).length <= 12_500, `${chart.id}/${domain} contract + card ${JSON.stringify(rest).length}`);
assert.deepEqual(dig(view, "evidence_card", "gaps") ?? [], [], `${chart.id}/${domain} gaps`);
}
}
@@ -1,6 +1,8 @@
// 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.
// TASK-astrologer-rulings-batch3-20261003 T4 (BUG-1218) and
// TASK-astrologer-rulings-batch4-20261003 T1 (BUG-1221): the Sanjay Rath
// Narayana rides beside the legacy one on every domain card, running MD only
// and in its short form (label, start sign, MD sign, start / end age); 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);
@@ -10,7 +12,7 @@ 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 { consultationDomainIds, type ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
import {
consultationWorkflowResponseSchema,
@@ -35,32 +37,66 @@ function engineRath(workflow: Json): Json {
return ((((workflow.chart as Json).modules as Json).narayana_dasha as Json).rath ?? {}) as Json;
}
function assertRathMd(workflow: Json, domain: ConsultationDomain, where: string) {
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, `${where} narayana_rath`);
// Short form only: label, start sign, running MD sign and ages (red line 2).
assert.deepEqual(Object.keys(shown), ["algorithm", "seed_sign", "md"], where);
assert.equal(shown.algorithm, rath.algorithm_label);
assert.equal(shown.algorithm, "Sanjay Rath 版(并列参考,未进打分与时间轴结论)");
assert.equal(shown.seed_sign, rath.seed_sign);
assert.deepEqual(shown.md, { sign: md.sign, start_age: md.start_age, end_age: md.end_age }, where);
// No sub-periods on the card.
assert.equal(JSON.stringify(shown).includes("antardasha"), false, where);
// The legacy Narayana is unchanged and still labelled.
assert.equal((timing.narayana as Json).algorithm, "旧算法(从上升起、一律顺行)", where);
assert.equal(typeof ((timing.narayana as Json).md as Json).sign, "string", where);
assert.equal(rath.used_for_scoring, false);
}
// 原值: 只有 timing / annual 卡带 Rath 大运,且带 lord / years / seed_level
// 新值: 全部领域卡都带,只放标签、起运宫、当前大运星座与起止年龄
// 原因: 产品 2026-10-03 决定全部领域卡都加 Rath 当前大运、字段尽量短(BUG-1221,任务书红线 2)
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);
assertRathMd(chart.timing_workflow, "timing", `${chart.id}/timing`);
assertRathMd(chart.annual_workflow, "annual", `${chart.id}/annual`);
}
});
// 原值: 「other domains leave the Rath MD to the report (12,000-character card budget)」:婚恋 / 事业 / 财富 / 健康卡不带 narayana_rath
// 新值: 「every domain card carries the Rath running MD」:全部 12 个领域卡都带
// 原因: 产品 2026-10-03 决定全部领域卡都加 Rath 当前大运,卡片预算 12,000 → 12,500(BUG-1221,修改 BUG-1160/1161)
test("every domain card carries the Rath running MD (12,500-character card budget)", () => {
for (const chart of golden.charts) {
for (const domain of consultationDomainIds) {
assertRathMd(chart.workflow, domain, `${chart.id}/${domain}`);
}
}
});
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");
// The nine public AA charts of the biography backtest golden
// (scripts/research/capture_consult_biography_backtest_golden.py): every
// captured route, every domain card carries the Rath running MD.
const backtest = JSON.parse(readFileSync(
new URL("./fixtures/consult-biography-backtest-golden.json", import.meta.url),
"utf8",
)) as { figures: Array<{ id: string; shared_chart: Json; routes: Record<string, Json & { chart?: Json; chart_modules?: Json }> }> };
test("nine public charts: every domain card carries the Rath running MD", () => {
assert.equal(backtest.figures.length, 9);
for (const figure of backtest.figures) {
for (const [route, captured] of Object.entries(figure.routes)) {
// Same overlay as frontend/scripts/research/consult-biography-backtest.mts routeWorkflow.
const { chart: chartOverrides, chart_modules: moduleOverrides, ...rest } = structuredClone(captured);
const shared = structuredClone(figure.shared_chart);
const workflow = { ...rest, chart: { ...shared, ...(chartOverrides ?? {}), modules: { ...(shared.modules as Json), ...(moduleOverrides ?? {}) } } };
for (const domain of consultationDomainIds) {
assertRathMd(workflow, domain, `${figure.id}/${route}/${domain}`);
}
}
}
});