From 11225171ee8d97c2c05d2bd991bb869291c97d4b Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 3 Oct 2026 22:49:30 +0800 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- docs/BUG_HISTORY.md | 17 ++++ .../src/lib/consultation-evidence-card.ts | 39 +++++---- frontend/src/lib/consultation-methodology.ts | 2 + ...nsult-condensed-checklist-20260927.test.ts | 11 ++- .../consult-evidence-card-20260927.test.ts | 11 ++- .../consult-evidence-card-v2-20260927.test.ts | 7 +- ...onsult-narayana-rath-card-20261003.test.ts | 84 +++++++++++++------ 7 files changed, 117 insertions(+), 54 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 6ddc68b2..7c9eef20 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16467,3 +16467,20 @@ - 相关记录:BUG-1095、BUG-1026~1028、ERR-109。 - 复发自:无(ERR-109 同类漏同步) - 修复版本:staging `01929bae`(生产待提升) + +## BUG-1221 | Rath 版 Narayana 当前大运只上年运、时运卡,婚恋、事业等领域卡看不到 + +- 状态:resolved(分支 `codex/astrologer-rulings-batch4-20261003`,未推送;待验收) +- 首次发现 / 最近更新:2026-10-03 / 2026-10-03 +- 来源:第三批进度记录「下轮问占星师」第 9 条(产品问题);产品负责人 2026-10-03 决定;任务书 `TASK-astrologer-rulings-batch4-20261003` T1。 +- 影响面:`frontend/src/lib/consultation-evidence-card.ts` `baseTiming`(`base.timing.narayana_rath`);三份字数断言(`consult-evidence-card-20260927`、`consult-evidence-card-v2-20260927`、`consult-condensed-checklist-20260927`)。 +- 现象:BUG-1218 T4 把 Rath 当前大运只放进 timing / annual 卡(`RATH_NARAYANA_CARD_DOMAINS`),婚恋、事业、财富、健康、父母等单领域卡只有旧算法 Narayana。 +- 触发条件:任何非年运 / 时运的单领域或双领域对话。 +- 根因:字数预算。单领域卡最大 11,979 / 12,000(乔布斯婚恋,含清单),Rath 大运约 +174 字会超(BUG-1160/1161 的 12,000 / 18,000 口径)。 +- 决策(产品负责人 2026-10-03,修改 BUG-1160/1161 的预算决定):全部领域卡都加 Rath 当前大运;卡片预算 12,000 → 12,500,加方法清单的总量 18,000 → 18,500。 +- 修复:删掉 `RATH_NARAYANA_CARD_DOMAINS` 与 `withRath`,`baseTiming` 对所有卡都放 Rath;字段按任务书红线 2 收短为 `algorithm`(引擎短标签「Sanjay Rath 版(并列参考,未进打分与时间轴结论)」)、`seed_sign`、`md: {sign, start_age, end_age}`;大运宫主、年数(= 止 − 起)、起运宫第几级、子运留在报告。旧算法 `narayana` 与「旧算法」标签不变。不改提示词。三份字数断言改为 12,500 / 18,500(三栏写在测试注释与进度记录)。 +- 验证:`frontend/tests/consult-narayana-rath-card-20261003.test.ts` 三条:timing / annual 卡、三张 golden 盘 × 12 个领域、9 位公开名人回测 golden × 4 条路由 × 12 个领域,每张卡都带 Rath 当前大运、字段只有上述三项且取自引擎原值、不含子运、旧算法标签仍在。9 盘 × 12 领域卡长度表(改前 / 改后 / 余量)见进度记录:契约 + 卡最大 10,284 → 10,244(年运卡因收短字段反而少 40),含清单 total 最大 13,734 → 13,694;单领域非清单重口径最大 11,913(巴拉克·奥巴马综合);乔布斯婚恋 total 11,979 → 12,113。 +- 防复发:上述测试;预算断言 12,500 / 18,500。 +- 相关记录:BUG-1160、BUG-1161、BUG-1218。 +- 复发自:无 +- 修复版本:待发布 diff --git a/frontend/src/lib/consultation-evidence-card.ts b/frontend/src/lib/consultation-evidence-card.ts index 47d7be85..4b4ecd86 100644 --- a/frontend/src/lib/consultation-evidence-card.ts +++ b/frontend/src/lib/consultation-evidence-card.ts @@ -585,15 +585,16 @@ function baseNatal(natal: Record, 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. + * BUG-1218 (T4) / BUG-1221: every domain card carries the Sanjay Rath + * Narayana beside the legacy one, running MD only and in its short form: + * label, start sign, MD sign and its start / end age. The MD lord, its years + * (end_age − start_age), the deciding level and the sub-periods stay in the + * report. Product 2026-10-03: card budget 12,500, with the method checklist + * 18,500 (was 12,000 / 18,000, BUG-1160/1161). */ -const RATH_NARAYANA_CARD_DOMAINS: ReadonlySet = new Set(["timing", "annual"]); +const RATH_MD_CARD_KEYS = ["sign", "start_age", "end_age"] as const; -function baseTiming(timing: Record, gaps: string[], withRath = false) { +function baseTiming(timing: Record, gaps: string[]) { const sub = record(timing.dasha_sub_periods); const current = record(sub.current); const pratyantar = record(sub.pratyantar_dasha_timeline); @@ -634,17 +635,16 @@ function baseTiming(timing: Record, gaps: string[], withRath = 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). + // BUG-1218 (T4) / BUG-1221: the Sanjay Rath version rides beside the + // legacy one on every card — its label, start sign and the running MD only + // (sub-periods stay in the report; 12,500-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; - } + const rathMd = pickKeys(record(rath.current_dasha).md, RATH_MD_CARD_KEYS); + if (nonEmpty(rathMd) && typeof rath.algorithm_label === "string") { + const narayanaRath: JsonRecord = { algorithm: rath.algorithm_label }; + const seedSign = copy(rath.seed_sign); + if (seedSign !== undefined && seedSign !== null) narayanaRath.seed_sign = seedSign; + narayanaRath.md = rathMd; return { vimshottari, narayana, narayana_rath: narayanaRath }; } return { vimshottari, narayana }; @@ -1021,10 +1021,9 @@ 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, withRath), + timing: baseTiming(claimEvidence(first.packet, "timing"), gaps), }; const domainSections: EvidenceCard["domain_sections"] = {}; const domains: ConsultationDomain[] = []; @@ -1049,7 +1048,7 @@ export function buildEvidenceCard( const ownGaps: string[] = []; const candidate = { natal: baseNatal(claimEvidence(source.packet, "natal_foundation"), ownGaps), - timing: baseTiming(claimEvidence(source.packet, "timing"), ownGaps, withRath), + timing: baseTiming(claimEvidence(source.packet, "timing"), ownGaps), }; if (JSON.stringify(candidate) !== JSON.stringify(base)) { own = candidate; diff --git a/frontend/src/lib/consultation-methodology.ts b/frontend/src/lib/consultation-methodology.ts index d3bb760f..4ed01aed 100644 --- a/frontend/src/lib/consultation-methodology.ts +++ b/frontend/src/lib/consultation-methodology.ts @@ -69,6 +69,8 @@ const domainMethodology: Readonly = new Set([ diff --git a/frontend/tests/consult-condensed-checklist-20260927.test.ts b/frontend/tests/consult-condensed-checklist-20260927.test.ts index dd9838a8..1418cd10 100644 --- a/frontend/tests/consult-condensed-checklist-20260927.test.ts +++ b/frontend/tests/consult-condensed-checklist-20260927.test.ts @@ -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}`); } } } diff --git a/frontend/tests/consult-evidence-card-20260927.test.ts b/frontend/tests/consult-evidence-card-20260927.test.ts index a50c83cc..2c77ae37 100644 --- a/frontend/tests/consult-evidence-card-20260927.test.ts +++ b/frontend/tests/consult-evidence-card-20260927.test.ts @@ -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})`); } } }); diff --git a/frontend/tests/consult-evidence-card-v2-20260927.test.ts b/frontend/tests/consult-evidence-card-v2-20260927.test.ts index f3fb4754..ce8cdf42 100644 --- a/frontend/tests/consult-evidence-card-v2-20260927.test.ts +++ b/frontend/tests/consult-evidence-card-v2-20260927.test.ts @@ -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`); } } diff --git a/frontend/tests/consult-narayana-rath-card-20261003.test.ts b/frontend/tests/consult-narayana-rath-card-20261003.test.ts index bcea87cd..64362cd9 100644 --- a/frontend/tests/consult-narayana-rath-card-20261003.test.ts +++ b/frontend/tests/consult-narayana-rath-card-20261003.test.ts @@ -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 }> }; + +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}`); + } } } });