- Evidence-card and biography-backtest goldens regenerated with the capture
scripts (PYTHONHASHSEED=0, cache TTL 0; byte-identical twice); only the
functional-role fields change.
- CAREER_FIELD_ASK_RULE: A10 / AL / Sun / 10th lord in 4/6/8/12 say
resistance or cost only, never "not on stage / go deep", and houses are not
translated into fields.
- AFFLICTION_RANGE_RULE: no portrait of an afflicted person from the lord's
house or a benefic occupant ("managing / arranging / cares about your
future"), actions do not presuppose reaching them, one ask per answer.
- Examples that paired "4th lord in 10th" with "her attention is on your
future / close" now state the line is unafflicted (Monroe's chart had that
exact placement and copied it). Retrograde line also bans "drags on".
- Backtest script: several follow-up domains in one run.
Changed assertions carry 原值 / 新值 / 原因 in the test comments.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
239 lines
12 KiB
TypeScript
239 lines
12 KiB
TypeScript
// TASK-consult-card-affliction-data-20261001 T2–T4 (BUG-1155~1157): the card
|
||
// carries who aspects whom, co-tenants and combustion; the parents roles carry
|
||
// full rows for their house lords and significators; health / education take
|
||
// the houses they were missing; the annual card names transit Saturn's house
|
||
// from the natal Moon; yogas are taken by domain tag. Expected values are read
|
||
// from the real engine's own layers in the golden fixture
|
||
// (scripts/research/capture_consult_evidence_card_golden.py, AGENTS §7.4).
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
buildEvidenceCard,
|
||
EVIDENCE_CARD_SPECS,
|
||
type EvidenceCard,
|
||
} 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>;
|
||
type GoldenChart = { id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json };
|
||
const golden = JSON.parse(readFileSync(
|
||
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
|
||
"utf8",
|
||
)) as { charts: GoldenChart[] };
|
||
|
||
const chartById = (id: string) => golden.charts.find((chart) => chart.id === id)!;
|
||
const jobs = chartById("steve_jobs");
|
||
const obama = chartById("barack_obama");
|
||
const taylor = chartById("elizabeth_taylor");
|
||
|
||
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 sources(workflow: Json, domain: ConsultationDomain) {
|
||
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
|
||
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: domain }), domain);
|
||
return { domain, packet, context };
|
||
}
|
||
|
||
function card(workflow: Json, domain: ConsultationDomain): EvidenceCard {
|
||
return buildEvidenceCard([sources(workflow, domain)]);
|
||
}
|
||
|
||
function layer(workflow: Json, name: string): Json {
|
||
return dig(workflow, "chart", "consultation_native_layers", name) as Json;
|
||
}
|
||
|
||
function enginePlanets(workflow: Json): Record<string, Json> {
|
||
return dig(workflow, "chart", "planets") as Record<string, Json>;
|
||
}
|
||
|
||
test("base planets carry the engine's aspected_by, combustion (true only) and co-tenants (selection)", () => {
|
||
for (const chart of golden.charts) {
|
||
const built = card(chart.workflow, "general");
|
||
const drishti = layer(chart.workflow, "graha_drishti");
|
||
assert.equal(drishti.status, "executed", chart.id);
|
||
const planets = built.base.natal.planets as Record<string, Json>;
|
||
const engine = enginePlanets(chart.workflow);
|
||
for (const [name, row] of Object.entries(planets)) {
|
||
const source = dig(drishti, "planets", name) as Json;
|
||
const by = source.aspected_by as string[];
|
||
if (by.length) assert.deepEqual(row.aspected_by, by, `${chart.id} ${name} aspected_by`);
|
||
else assert.equal("aspected_by" in row, false, `${chart.id} ${name} empty aspected_by is left out`);
|
||
if (source.combust === true) assert.equal(row.combust, true, `${chart.id} ${name} combust`);
|
||
else assert.equal("combust" in row, false, `${chart.id} ${name} combust only when true`);
|
||
const coTenants = Object.entries(engine)
|
||
.filter(([other, value]) => other !== name && value.house === engine[name]!.house)
|
||
.map(([other]) => other);
|
||
if (coTenants.length) assert.deepEqual(row.conjunct_with, coTenants, `${chart.id} ${name} conjunct_with`);
|
||
else assert.equal("conjunct_with" in row, false, `${chart.id} ${name} alone`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test("Obama: Saturn in the 1st aspects the Sun in the 7th; Mercury (9th lord) is combust", () => {
|
||
const planets = card(obama.workflow, "parents").base.natal.planets as Record<string, Json>;
|
||
assert.equal(planets.Saturn!.house, 1);
|
||
assert.equal(planets.Sun!.house, 7);
|
||
assert.ok((planets.Sun!.aspected_by as string[]).includes("Saturn"));
|
||
assert.ok((planets.Mercury!.aspected_by as string[]).includes("Saturn"));
|
||
assert.equal(planets.Mercury!.combust, true);
|
||
assert.deepEqual(dig(layer(obama.workflow, "graha_drishti"), "planets", "Saturn", "aspects_houses"), [3, 7, 10]);
|
||
});
|
||
|
||
test("a house lord sharing a house with Rahu shows it (Jobs: 9th lord Venus with Rahu in the 4th)", () => {
|
||
const planets = card(jobs.workflow, "general").base.natal.planets as Record<string, Json>;
|
||
assert.equal(planets.Venus!.house, 4);
|
||
assert.ok((planets.Venus!.conjunct_with as string[]).includes("Rahu"));
|
||
assert.ok((planets.Rahu!.conjunct_with as string[]).includes("Venus"));
|
||
});
|
||
|
||
test("domain house rows carry the engine's aspected_by for each selected house", () => {
|
||
for (const chart of golden.charts) {
|
||
for (const domain of ["marriage", "parents", "health"] as const) {
|
||
const section = card(chart.workflow, domain).domain_sections[domain]!;
|
||
for (const [number, row] of Object.entries(section.houses)) {
|
||
assert.deepEqual(
|
||
(row as Json).aspected_by,
|
||
dig(layer(chart.workflow, "graha_drishti"), "houses", number, "aspected_by"),
|
||
`${chart.id}/${domain} house ${number}`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
const seventh = card(obama.workflow, "marriage").domain_sections.marriage!.houses["7"] as Json;
|
||
assert.deepEqual(seventh.aspected_by, ["Jupiter", "Saturn"]);
|
||
});
|
||
|
||
test("parents roles carry full rows for the house lords and significators, functional class selected", () => {
|
||
for (const chart of golden.charts) {
|
||
const built = card(chart.workflow, "parents");
|
||
const functional = built.base.natal.functional_benefic_malefic;
|
||
const roles = built.domain_sections.parents!.roles as Record<string, Json>;
|
||
for (const [key, role] of Object.entries(roles)) {
|
||
const lordRows = role.lord_rows as Record<string, Json>;
|
||
const significatorRows = role.significator_rows as Record<string, Json>;
|
||
assert.deepEqual(Object.keys(lordRows), role.house_lords, `${chart.id} ${key} lords`);
|
||
assert.deepEqual(Object.keys(significatorRows), role.significators, `${chart.id} ${key} significators`);
|
||
for (const [name, row] of [...Object.entries(lordRows), ...Object.entries(significatorRows)]) {
|
||
const base = (built.base.natal.planets as Record<string, Json>)[name]!;
|
||
for (const field of ["house", "sign", "status", "conjunct_with", "aspected_by", "combust"]) {
|
||
assert.deepEqual(row[field], base[field], `${chart.id} ${key} ${name}.${field}`);
|
||
}
|
||
const expected = (functional.functional_benefics as string[]).includes(name) ? "benefic"
|
||
: (functional.functional_malefics as string[]).includes(name) ? "malefic"
|
||
: (functional.functional_neutrals as string[]).includes(name) ? "neutral" : undefined;
|
||
assert.equal(row.functional, expected, `${chart.id} ${key} ${name}.functional`);
|
||
}
|
||
const d12 = role.d12_house as Json;
|
||
const d12Planets = dig(built.domain_sections.parents!.vargas, "D12", "planets") as Json;
|
||
assert.equal(typeof d12.lord, "string", `${chart.id} ${key} D12 lord`);
|
||
assert.equal(d12.lord_sign, d12Planets[d12.lord as string], `${chart.id} ${key} D12 lord sign`);
|
||
}
|
||
}
|
||
const father = dig(card(jobs.workflow, "parents").domain_sections.parents, "roles", "father") as Json;
|
||
assert.ok((dig(father, "lord_rows", "Venus", "conjunct_with") as string[]).includes("Rahu"));
|
||
// 原值: "malefic"(v1 公式:12 宫主一律判凶)
|
||
// 新值: "neutral"(profile bphs_ch34_general_with_sign_exceptions_v2:单管 12 宫的星是条件性的,不自动判凶)
|
||
// 原因: 产品 2026-10-02 采纳上游功能吉凶 v2(57782aea,BUG-1158);golden 由 capture 脚本重新生成
|
||
assert.equal(dig(father, "significator_rows", "Sun", "functional"), "neutral");
|
||
});
|
||
|
||
test("health takes the 1st and 12th houses, education the 4th", () => {
|
||
assert.deepEqual(EVIDENCE_CARD_SPECS.health.houses, [1, 6, 8, 12]);
|
||
assert.deepEqual(EVIDENCE_CARD_SPECS.education.houses, [4, 5, 9]);
|
||
const health = card(taylor.workflow, "health").domain_sections.health!;
|
||
assert.deepEqual(Object.keys(health.houses).sort((a, b) => Number(a) - Number(b)), ["1", "6", "8", "12"]);
|
||
const education = card(jobs.workflow, "education").domain_sections.education!;
|
||
assert.ok((dig(education.houses, "4", "occupants") as string[]).includes("Rahu"), "Jobs: Rahu in the 4th is now on the card");
|
||
});
|
||
|
||
test("the annual card copies transit Saturn's house from the natal Moon (Jobs: Saturn over the Pisces Moon)", () => {
|
||
for (const chart of golden.charts) {
|
||
const section = card(chart.annual_workflow, "annual").domain_sections.annual!;
|
||
assert.equal(
|
||
dig(section.slow_transits, "saturn_from_moon_house"),
|
||
dig(layer(chart.annual_workflow, "slow_transits"), "saturn_from_moon_house"),
|
||
chart.id,
|
||
);
|
||
}
|
||
const section = card(jobs.annual_workflow, "annual").domain_sections.annual!;
|
||
assert.equal(dig(section.slow_transits, "current", "Saturn", "sign"), "Pisces");
|
||
assert.equal(dig(enginePlanets(jobs.annual_workflow), "Moon", "sign"), "Pisces");
|
||
assert.equal(dig(section.slow_transits, "saturn_from_moon_house"), 1);
|
||
});
|
||
|
||
test("yogas are taken by domain tag; hits are the sieve's own matches", () => {
|
||
const tagged = (workflow: Json) => (layer(workflow, "yoga_domains").rows as Json[]);
|
||
// The annual route sieves every packet (20 rules), so every tag is present.
|
||
for (const chart of golden.charts) {
|
||
for (const domain of ["career", "wealth", "general", "parents", "children"] as const) {
|
||
const yogas = card(chart.annual_workflow, domain).domain_sections[domain]!.yogas as Json;
|
||
const wanted = EVIDENCE_CARD_SPECS[domain].yogaTags;
|
||
const own = tagged(chart.annual_workflow).filter((row) => wanted.includes(row.domain as string));
|
||
assert.deepEqual(yogas.names, own.map((row) => row.name), `${chart.id}/${domain} names`);
|
||
assert.deepEqual(yogas.hits, own.filter((row) => row.hit === true).map((row) => row.name), `${chart.id}/${domain} hits`);
|
||
}
|
||
}
|
||
const names = (domain: ConsultationDomain) => (
|
||
card(obama.annual_workflow, domain).domain_sections[domain]!.yogas as Json
|
||
).names as string[];
|
||
for (const domain of ["wealth", "general"] as const) {
|
||
assert.equal(names(domain).some((name) => /^Matru(nasa|\s|deer|S)/.test(name) || /Puthra/.test(name)), false, `Obama ${domain}`);
|
||
}
|
||
assert.ok(names("parents").includes("Matrunasa Yoga"));
|
||
assert.ok(names("parents").every((name) => name.startsWith("Matru")));
|
||
assert.ok(names("children").every((name) => name.includes("Puthra")) && names("children").length > 0);
|
||
assert.deepEqual(
|
||
(card(obama.annual_workflow, "parents").domain_sections.parents!.yogas as Json).hits,
|
||
["Matrunasa Yoga"],
|
||
);
|
||
});
|
||
|
||
test("missing engine layers are gaps, never filled in", () => {
|
||
const workflow = structuredClone(obama.workflow);
|
||
const layers = dig(workflow, "chart", "consultation_native_layers") as Json;
|
||
layers.graha_drishti = { status: "blocked", reason: "fixture" };
|
||
layers.yoga_domains = { status: "blocked", reason: "fixture" };
|
||
const built = card(workflow, "parents");
|
||
assert.ok(built.meta.gaps.includes("base.aspects"));
|
||
assert.ok(built.meta.gaps.includes("parents.yoga_domains"));
|
||
for (const row of Object.values(built.base.natal.planets as Record<string, Json>)) {
|
||
assert.equal("aspected_by" in row, false);
|
||
assert.equal("combust" in row, false);
|
||
}
|
||
for (const row of Object.values(built.domain_sections.parents!.houses)) {
|
||
assert.equal("aspected_by" in (row as Json), false);
|
||
}
|
||
});
|
||
|
||
test("budget compression keeps every value: antardashas as [lord, start, end], vargottama only when true", () => {
|
||
for (const chart of golden.charts) {
|
||
const built = card(chart.annual_workflow, "annual");
|
||
const boundaries = dig(chart.annual_workflow, "chart", "modules", "dasha_sub_periods", "boundaries") as Json[];
|
||
assert.deepEqual(
|
||
built.base.timing.vimshottari.antardashas_in_mahadasha,
|
||
boundaries.map((row) => [row.lord, row.start, row.end]),
|
||
chart.id,
|
||
);
|
||
const summary = layer(chart.annual_workflow, "d9_summary").planets as Record<string, Json>;
|
||
for (const [name, row] of Object.entries(built.base.natal.d9.planets as Record<string, Json>)) {
|
||
assert.equal(row.vargottama === true, summary[name]!.vargottama === true, `${chart.id} ${name}`);
|
||
assert.notEqual(row.vargottama, false, `${chart.id} ${name}: false is left out`);
|
||
}
|
||
}
|
||
});
|