fix(consult): card carries the engine's short Narayana label; AD/PD drop derivable years (BUG-1215)

The full note pushed steve_jobs/marriage to 12,028 characters (budget
12,000), and cutting it in the card broke the 'nothing rewritten' contract.
The engine now also writes algorithm_label 旧算法(从上升起、一律顺行), the card
copies it verbatim, and Narayana AD/PD rows drop years (end_age - start_age).
Largest card 11,979 (baseline 11,983). Goldens re-captured; the biography
golden now keeps the Narayana label keys too.

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 16:48:19 +08:00
co-authored by Claude Opus 5.5
parent 921b336ba5
commit 693467bf0c
8 changed files with 20 additions and 13 deletions
@@ -426,6 +426,9 @@ const FUNCTIONAL_KEYS = [
] as const;
const PERIOD_KEYS = ["lord", "start", "end"] as const;
const NARAYANA_PERIOD_KEYS = ["sign", "lord", "years", "start_age", "end_age"] as const;
// BUG-1215: AD / PD drop `years` (it is end_age − start_age) so the legacy
// algorithm label fits the 12,000-character card budget.
const NARAYANA_SUB_PERIOD_KEYS = ["sign", "lord", "start_age", "end_age"] as const;
const KARAKA_KEYS = ["planet", "degree_in_sign", "rank"] as const;
const ZODIAC_SIGNS = [
@@ -610,14 +613,15 @@ function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
const currentNarayana = record(record(timing.narayana_dasha).current_dasha);
const narayana: JsonRecord = {};
for (const level of ["md", "ad", "pd"] as const) {
const picked = pickKeys(currentNarayana[level], NARAYANA_PERIOD_KEYS);
const picked = pickKeys(currentNarayana[level], level === "md" ? NARAYANA_PERIOD_KEYS : NARAYANA_SUB_PERIOD_KEYS);
if (nonEmpty(picked)) narayana[level] = picked;
}
const remaining = copy(currentNarayana.remaining_years);
if (remaining !== undefined) narayana.remaining_years = remaining;
// BUG-1215: say which Narayana this is (legacy: from the Lagna, always zodiacal).
const algorithmNote = record(timing.narayana_dasha).algorithm_note;
if (narayana.md && typeof algorithmNote === "string" && algorithmNote) narayana.algorithm = algorithmNote;
// BUG-1215: say which Narayana this is (the engine's short label,
// 旧算法(从上升起、一律顺行); the full note stays in the data).
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");
return { vimshottari, narayana };
}
+1 -1
View File
@@ -347,7 +347,7 @@ const timingKeys = new Set([
// and dropped the pratyantar dates, although the engine had both.
"md", "ad", "pd", "years", "startage", "endage", "remainingyears", "pratyantardashatimeline",
// BUG-1215: the running Narayana is the legacy algorithm; its label travels with it.
"algorithmnote",
"algorithmlabel",
]);
const validationKeys = new Set([
@@ -188,7 +188,7 @@ test("every domain's card covers Part B: dual dasha, domain vargas, functional b
}
assert.equal(typeof dig(timing, "narayana", "md", "sign"), "string", `${where} Narayana md`);
// BUG-1215: the running Narayana is labelled as the legacy algorithm.
assert.equal(dig(timing, "narayana", "algorithm"), "旧算法(从上升起、一律顺行),待替换为 Sanjay Rath 版", `${where} Narayana algorithm`);
assert.equal(dig(timing, "narayana", "algorithm"), "旧算法(从上升起、一律顺行)", `${where} Narayana algorithm`);
// Domain vargas: every varga the domain's spec names.
const section = card.domain_sections[domain];
assert.ok(section, `${where} domain section`);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
@@ -17,6 +17,8 @@ from typing import Any, Dict
LEGACY_PROFILE = "legacy_reference_only"
LEGACY_NOTE_ZH = "旧算法(从上升起、一律顺行),待替换为 Sanjay Rath 版"
LEGACY_NOTE_EN = "Legacy algorithm (starts from the Lagna, always zodiacal); to be replaced by Sanjay Rath's version"
# Short form for the consultation evidence card (12,000-character budget).
LEGACY_LABEL_ZH = "旧算法(从上升起、一律顺行)"
def label_legacy_narayana(result: Any) -> Any:
@@ -25,4 +27,5 @@ def label_legacy_narayana(result: Any) -> Any:
result["profile"] = LEGACY_PROFILE
result["algorithm_note"] = LEGACY_NOTE_ZH
result["algorithm_note_en"] = LEGACY_NOTE_EN
result["algorithm_label"] = LEGACY_LABEL_ZH
return result
@@ -59,9 +59,9 @@ MODULE_KEEP = (
"ashtakavarga", "dasha_sub_periods", "kp_cusps", "gulika",
"functional_benefic_malefic", "kakshya", "yogas", "chara_dasha", "transits",
)
# BUG-1215: + profile / algorithm_note (the legacy-algorithm label the card carries).
# BUG-1215: + profile / algorithm_note / algorithm_label (the legacy-algorithm label the card carries).
NARAYANA_KEEP = ("lagna_sign", "current_dasha", "current_year", "current_age", "mahadasha_sequence",
"profile", "algorithm_note")
"profile", "algorithm_note", "algorithm_label")
def _pick(value: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]:
+3 -3
View File
@@ -46,11 +46,11 @@ def test_consultation_modules_carry_the_label() -> None:
for chart in golden["charts"]:
for route in ("workflow", "annual_workflow", "timing_workflow"):
module = chart[route]["chart"]["modules"]["narayana_dasha"]
assert (module["profile"], module["algorithm_note"]) == (LEGACY_PROFILE, LEGACY_NOTE_ZH)
assert (module["profile"], module["algorithm_note"], module["algorithm_label"]) == (LEGACY_PROFILE, LEGACY_NOTE_ZH, "旧算法(从上升起、一律顺行)")
card = (ROOT / "frontend/src/lib/consultation-evidence-card.ts").read_text(encoding="utf-8")
assert "narayana.algorithm = algorithmNote" in card
assert "narayana.algorithm = algorithmLabel" in card and "NARAYANA_SUB_PERIOD_KEYS" in card
workflow = (ROOT / "frontend/src/mastra/consultation-workflow.ts").read_text(encoding="utf-8")
assert '"algorithmnote"' in workflow
assert '"algorithmlabel"' in workflow
def test_kn_rao_never_names_narayana_on_user_visible_surfaces() -> None: