fix(consult): project the running Narayana period and pratyantar dates to the model (BUG-1054)
timingKeys now admits current_dasha.md/ad/pd (sign, lord, years, start_age, end_age), remaining_years and pratyantar_dasha_timeline. Depth and item caps unchanged. Golden regression over three public AA engine captures asserts values, not key presence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c33701a4bd
commit
75a1844cdf
+6
-6
@@ -14189,15 +14189,15 @@
|
||||
|
||||
## BUG-1054 | 咨询投影把当前 Narayana 段和子运日期裁成空
|
||||
|
||||
- 状态:investigating
|
||||
- 状态:resolved(本地修复 `codex/consult-evidence-card-20260927`,待部署 staging;部署后补门禁 run 号)
|
||||
- 首次发现 / 最近更新:2026-09-27 / 2026-09-27
|
||||
- 影响面:`frontend/src/mastra/consultation-workflow.ts` 的 `toModelOutput` / `projectAllowlistedTree`;普通咨询写答案时看到的 timing 卡
|
||||
- 影响面:`frontend/src/mastra/consultation-workflow.ts` 的 `toModelOutput` / `projectAllowlistedTree`(`timingKeys`);普通咨询写答案时看到的 timing 卡
|
||||
- 现象:引擎已经算出当前 Narayana 段和 Vimshottari 子运(pratyantar)起止日期,投影给模型的 timing 卡里 `current_dasha` 是空对象,子运日期整段不在。
|
||||
- 触发条件:有出生分钟的本命咨询走 `toModelOutput`。2026-09-27 数据卡调研用 3 张公开 AA 盘 × 10 种问法,30/30 都是这个形状。
|
||||
- 根因:timing 投影只保留 allowlist 里的键。Narayana 当前段写在 `current_dasha.md` / `ad` / `pd` 下,`md` 不在 allowlist 里,于是对象被留成空。子运日期的键是 `pratyantar_dasha_timeline`,也不在 allowlist 里,整段被丢掉。大运和子运(antardasha)的日期键在 allowlist 里,所以那两段还在。
|
||||
- 修复:未修。本单是调研,不改投影。
|
||||
- 验证:调研脚本对 30 次投影做了结构化比对:上升、月亮星座与宫位、当前大运起止、D12 落点与引擎一致;Narayana 当前星座和子运起止在卡里是空。见 `docs/research/consult_evidence_card_research_2026_09_27.md`。
|
||||
- 防复发:实现单补投影时,要锁住「引擎当前 Narayana 星座和子运起止日期原样出现在模型 timing 卡里」,不能只断言 `narayana_dasha` 键存在。
|
||||
- 修复:`timingKeys` 放行 `md` / `ad` / `pd`、它们的嵌套标量 `years` / `start_age` / `end_age`、`remaining_years` 与 `pratyantar_dasha_timeline`;其余白名单、深度上限(4)与条数上限(24)不变。
|
||||
- 验证:新增 `frontend/tests/consult-projection-timing-20260927.test.ts`(4 条),fixture 是真实引擎对 3 张公开 AA 盘的 family 路由输出(`frontend/tests/fixtures/consult-evidence-card-golden.json`,由 `scripts/research/capture_consult_evidence_card_golden.py` 生成,只按键裁剪、不改值)。逐盘断言引擎的 Narayana 当前 md / ad / pd 的星座、主星、年数、起止年龄与 `remaining_years`,以及 pratyantar 当前段与下一段的主星和起止日期,原样出现在模型可见的 timing 卡里;大运、子运日期不变。修复前 2 / 4 条失败。
|
||||
- 防复发:timing 卡的回归断言值而不是键;数据卡(同一分支 T3)从这张投影复制当前 Narayana 段与 PD 起止,卡的逐字测试再锁一层。
|
||||
- 相关记录:BUG-287(同一条 allowlist 曾经把分盘和审计表整段挡住;这次是嵌套键还没放行,不是「没算」复发)
|
||||
- 复发自:无
|
||||
- 修复版本:无
|
||||
- 修复版本:`codex/consult-evidence-card-20260927`(本地提交,未推送)
|
||||
|
||||
@@ -334,6 +334,12 @@ const timingKeys = new Set([
|
||||
"charadasha", "transits", "sadesati", "triggers", "triggercount", "searchperiod", "window", "sequence", "durationyears",
|
||||
"fromage", "toage", "phase", "phasename", "moonsign", "saturnsign", "intensity", "active",
|
||||
"date", "target", "kind", "orb", "boundary", "claimboundary",
|
||||
// BUG-1054: the engine writes the running Narayana period under
|
||||
// `current_dasha.md` / `ad` / `pd` (sign, lord, years, start_age, end_age)
|
||||
// and the Vimshottari pratyantar window under `pratyantar_dasha_timeline`.
|
||||
// Without these keys the projection kept `current_dasha` as an empty object
|
||||
// and dropped the pratyantar dates, although the engine had both.
|
||||
"md", "ad", "pd", "years", "startage", "endage", "remainingyears", "pratyantardashatimeline",
|
||||
]);
|
||||
|
||||
const validationKeys = new Set([
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// BUG-1054: the model-visible timing projection dropped the running Narayana
|
||||
// period (`current_dasha.md/ad/pd`) and the Vimshottari pratyantar window
|
||||
// (`pratyantar_dasha_timeline`), although the engine computed both.
|
||||
//
|
||||
// The fixture is a real engine capture (AGENTS §7.4) of three public AA charts:
|
||||
// scripts/research/capture_consult_evidence_card_golden.py. Expected values are
|
||||
// read straight from the engine payload, and the assertion is on values, not on
|
||||
// the key being present.
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
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 }> };
|
||||
|
||||
function dig(value: unknown, ...keys: string[]): unknown {
|
||||
let current = value;
|
||||
for (const key of keys) {
|
||||
if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
|
||||
current = (current as Json)[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function timingCard(workflow: Json) {
|
||||
const parsed = consultationWorkflowResponseSchema.parse(structuredClone(workflow));
|
||||
const packet = toModelOutput(toAgentConsultationContext(parsed));
|
||||
const card = packet.claim_cards.find((item) => item.category === "timing");
|
||||
assert.ok(card, "the timing claim card is present");
|
||||
return card.evidence as Json;
|
||||
}
|
||||
|
||||
test("the fixture carries the engine values the projection used to drop", () => {
|
||||
assert.equal(golden.charts.length, 3);
|
||||
for (const chart of golden.charts) {
|
||||
const modules = dig(chart.workflow, "chart", "modules");
|
||||
assert.equal(typeof dig(modules, "narayana_dasha", "current_dasha", "md", "sign"), "string", chart.id);
|
||||
assert.match(String(dig(modules, "dasha_sub_periods", "pratyantar_dasha_timeline", "current", "start")), /^\d{4}-\d{2}-\d{2}$/, chart.id);
|
||||
}
|
||||
});
|
||||
|
||||
test("the running Narayana period reaches the model timing card with the engine's own values", () => {
|
||||
for (const chart of golden.charts) {
|
||||
const engine = dig(chart.workflow, "chart", "modules", "narayana_dasha", "current_dasha") as Json;
|
||||
const projected = dig(timingCard(chart.workflow), "narayana_dasha", "current_dasha") as Json;
|
||||
assert.ok(projected && Object.keys(projected).length > 0, `${chart.id}: current_dasha is no longer empty`);
|
||||
for (const level of ["md", "ad", "pd"] as const) {
|
||||
for (const key of ["sign", "lord", "years", "start_age", "end_age"] as const) {
|
||||
assert.equal(dig(projected, level, key), dig(engine, level, key), `${chart.id} ${level}.${key}`);
|
||||
}
|
||||
}
|
||||
assert.equal(projected.remaining_years, engine.remaining_years, `${chart.id} remaining_years`);
|
||||
}
|
||||
});
|
||||
|
||||
test("the Vimshottari pratyantar window reaches the model timing card with the engine's own dates", () => {
|
||||
for (const chart of golden.charts) {
|
||||
const engine = dig(chart.workflow, "chart", "modules", "dasha_sub_periods", "pratyantar_dasha_timeline") as Json;
|
||||
const projected = dig(timingCard(chart.workflow), "dasha_sub_periods", "pratyantar_dasha_timeline") as Json;
|
||||
for (const phase of ["current", "next"] as const) {
|
||||
for (const key of ["lord", "start", "end"] as const) {
|
||||
assert.equal(dig(projected, phase, key), dig(engine, phase, key), `${chart.id} pratyantar ${phase}.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("the mahadasha and antardasha dates the projection already carried are unchanged", () => {
|
||||
for (const chart of golden.charts) {
|
||||
const engine = dig(chart.workflow, "chart", "modules", "dasha_sub_periods", "current") as Json;
|
||||
const projected = dig(timingCard(chart.workflow), "dasha_sub_periods", "current") as Json;
|
||||
for (const level of ["mahadasha", "antardasha"] as const) {
|
||||
for (const key of ["lord", "start", "end"] as const) {
|
||||
assert.equal(dig(projected, level, key), dig(engine, level, key), `${chart.id} ${level}.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,98 @@
|
||||
"""Capture real engine consultation responses for the evidence-card tests.
|
||||
|
||||
Three public AA charts (Steve Jobs, Barack Obama, Elizabeth Taylor), the
|
||||
research reference date, raman ayanamsa, mean nodes, family route. External
|
||||
VedAstro is not called (same stand-in as the research runner). The response is
|
||||
trimmed by key only: every kept value is the engine's own value, unchanged.
|
||||
|
||||
PYTHONHASHSEED=0 python scripts/research/capture_consult_evidence_card_golden.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / "scripts"), str(ROOT / "scripts" / "research")]
|
||||
|
||||
from consult_evidence_card_lib import ( # noqa: E402
|
||||
AYANAMSA,
|
||||
NODE_MODE,
|
||||
QUESTIONS,
|
||||
REFERENCE_DATE,
|
||||
load_public_charts,
|
||||
)
|
||||
import consult_evidence_card_run as runner # noqa: E402
|
||||
|
||||
OUT = ROOT / "frontend" / "tests" / "fixtures" / "consult-evidence-card-golden.json"
|
||||
|
||||
TOP_KEEP = (
|
||||
"success", "question", "routing", "consumer_context", "thematic_report",
|
||||
"birth_time_sensitivity", "reference_transparency", "candidate_range",
|
||||
"range_boundary_contexts",
|
||||
)
|
||||
RECTIFICATION_KEEP = ("effective_accuracy", "lagna_boundary", "summary", "enabled_vargas")
|
||||
CHART_KEEP = ("success", "birth", "ascendant", "planets", "houses", "shadbala", "dasha", "yogas")
|
||||
MODULE_KEEP = (
|
||||
"varga_spectrum", "shadbala", "arudha_padas", "jaimini", "narayana_dasha",
|
||||
"ashtakavarga", "dasha_sub_periods", "kp_cusps", "gulika",
|
||||
"functional_benefic_malefic", "kakshya", "yogas", "chara_dasha", "transits",
|
||||
)
|
||||
NARAYANA_KEEP = ("lagna_sign", "current_dasha", "current_year", "current_age", "mahadasha_sequence")
|
||||
|
||||
|
||||
def _pick(value: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]:
|
||||
return {key: value[key] for key in keys if key in value}
|
||||
|
||||
|
||||
def trim(workflow: dict[str, Any]) -> dict[str, Any]:
|
||||
out = _pick(workflow, TOP_KEEP)
|
||||
out["rectification"] = _pick(workflow.get("rectification") or {}, RECTIFICATION_KEEP)
|
||||
chart = workflow.get("chart") or {}
|
||||
kept_chart = _pick(chart, CHART_KEEP)
|
||||
modules = _pick(chart.get("modules") or {}, MODULE_KEEP)
|
||||
if isinstance(modules.get("narayana_dasha"), dict):
|
||||
modules["narayana_dasha"] = _pick(modules["narayana_dasha"], NARAYANA_KEEP)
|
||||
kept_chart["modules"] = modules
|
||||
out["chart"] = kept_chart
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.environ.get("PYTHONHASHSEED") != "0":
|
||||
raise SystemExit("Set PYTHONHASHSEED=0 before starting this process (ERR-111).")
|
||||
runner._block_external_vedastro()
|
||||
question = next(item for item in QUESTIONS if item["id"] == "family")
|
||||
charts = []
|
||||
for chart in load_public_charts(ROOT):
|
||||
workflow = runner._run_workflow(runner._workflow_body(chart, question))
|
||||
charts.append({
|
||||
"id": chart["id"],
|
||||
"label": chart["label"],
|
||||
"source": chart["source"],
|
||||
"case_id": chart["case_id"],
|
||||
"rodden_rating": chart["rodden_rating"],
|
||||
"workflow": trim(workflow),
|
||||
})
|
||||
payload = {
|
||||
"source": "scripts/research/capture_consult_evidence_card_golden.py",
|
||||
"note": "Real engine consultation_workflow responses for three public AA charts, trimmed by key only.",
|
||||
"reference_date": REFERENCE_DATE,
|
||||
"ayanamsa": AYANAMSA,
|
||||
"node_mode": NODE_MODE,
|
||||
"route": question["domain"],
|
||||
"question": question["question"],
|
||||
"external_vedastro": "not_called",
|
||||
"charts": charts,
|
||||
}
|
||||
OUT.write_text(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user