TASK-consult-evidence-card-v2-20260927 T3. Card version evidence-card-v2. - Base section (every card): the engine's D9 summary (D9 lagna, each planet's D9 sign and dignity, Vargottama, D1/D9 reversals); D9 houses and aspects stay out. - Career: + AL (engine pada A1), 10H SAV, SAV of the Jupiter / Saturn transit signs. - Marriage: + 5H / 5L, day / night, Punarphoo (observation_only), Double Transit on 7H / 7L (house-7 run) and DK / UL, Vivah Saham, gender unknown. - Wealth: 8H / 12H named. - Annual: the annual Tajika chart verbatim (parameter_sensitive), or 年盘未接入 when the pack is blocked / not attached (never natal data); houses 1 + running / next AD lords' houses + the year's Jupiter / Saturn transit houses, with their basis; ingress / station dates only. - Timing: Rahu / Ketu with ingress dates, Jupiter / Saturn SAV and BAV, Double Transit conclusions (no degrees); vargas follow the turn's other domain, D9 when alone. - Chara Dasha leaves every card; KP is never on a card. The lookup enum adds karakamsha, dispositor_chains, inter_chart_linkage, argala, moon_transit; a KP lookup travels with its blocked note. - System prompt and tool descriptions name the D9 summary, the parameter_sensitive / observation_only tags, 年盘未接入 and the lookups. - Telemetry card version / agentVersion bumped to v2 (three-column notes in the touched tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
310 lines
17 KiB
TypeScript
310 lines
17 KiB
TypeScript
// TASK-consult-evidence-card-20260927 T3: the answer model reads the answer
|
|
// contract plus the evidence card instead of the whole projected result.
|
|
//
|
|
// Fixture: real engine consultation responses for three public AA charts
|
|
// (scripts/research/capture_consult_evidence_card_golden.py, AGENTS §7.4).
|
|
// Expected facts are read straight from the raw engine payload, so a card that
|
|
// rewrote, rounded or re-derived a value fails here.
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildEvidenceCard,
|
|
citedEvidenceCardFields,
|
|
EVIDENCE_CARD_BACKSTAGE_LINE,
|
|
EVIDENCE_CARD_LABELS,
|
|
EVIDENCE_CARD_SPECS,
|
|
EVIDENCE_CARD_VERSION,
|
|
evidenceCardFieldValues,
|
|
type EvidenceCard,
|
|
} from "../src/lib/consultation-evidence-card.ts";
|
|
import { consultationDomainIds, type ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
|
|
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
|
import { normalizeTechniqueAuditRows } from "../src/lib/consultation-technique-audit.ts";
|
|
import {
|
|
consultationWorkflowResponseSchema,
|
|
toAgentConsultationContext,
|
|
toModelOutput,
|
|
} from "../src/mastra/consultation-workflow.ts";
|
|
import {
|
|
CONSULTATION_MODEL_PACKET_VERSION,
|
|
createConsultationRuntimeState,
|
|
createConsultationTools,
|
|
} from "../src/mastra/consultation-tools.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 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 cardFor(workflow: Json, domain: ConsultationDomain): EvidenceCard {
|
|
return buildEvidenceCard([sources(workflow, domain)]);
|
|
}
|
|
|
|
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" },
|
|
truth: { birthTimeSource: "reported" },
|
|
};
|
|
|
|
async function runTool(workflow: Json, domains: ConsultationDomain[]) {
|
|
const state = createConsultationRuntimeState();
|
|
const tools = createConsultationTools({
|
|
userId: "u", sessionId: "s", requestId: `card-${domains.join("-")}`, consultationMode: "verified_chart",
|
|
serverChart: serverChart as never, state,
|
|
runWorkflow: async () => structuredClone(workflow) as never,
|
|
});
|
|
const view = await tools["run-jyotish-consultation"].execute!(
|
|
{ question: "测试问题", domains } as never,
|
|
{ writer: { custom: async () => {} } } as never,
|
|
) as Json;
|
|
return { view, state };
|
|
}
|
|
|
|
function engineFacts(workflow: Json) {
|
|
const chart = dig(workflow, "chart") as Json;
|
|
const modules = dig(chart, "modules") as Json;
|
|
const formal = dig(modules, "varga_spectrum", "formal") as Json;
|
|
return {
|
|
ascendantSign: dig(chart, "ascendant", "sign"),
|
|
ascendantDegree: dig(chart, "ascendant", "degree_in_sign"),
|
|
moonSign: dig(chart, "planets", "Moon", "sign"),
|
|
moonHouse: dig(chart, "planets", "Moon", "house"),
|
|
moonDegree: dig(chart, "planets", "Moon", "degree_in_sign"),
|
|
mahadasha: dig(modules, "dasha_sub_periods", "current", "mahadasha"),
|
|
antardasha: dig(modules, "dasha_sub_periods", "current", "antardasha"),
|
|
pratyantar: dig(modules, "dasha_sub_periods", "pratyantar_dasha_timeline", "current"),
|
|
narayanaMd: dig(modules, "narayana_dasha", "current_dasha", "md"),
|
|
functional: dig(modules, "functional_benefic_malefic") as Json,
|
|
varga: (code: string) => formal[code] as Json,
|
|
};
|
|
}
|
|
|
|
test("the card copies the engine's own facts verbatim (red line 2)", () => {
|
|
for (const chart of golden.charts) {
|
|
const facts = engineFacts(chart.workflow);
|
|
for (const domain of ["parents", "children", "marriage", "career"] as const) {
|
|
const card = cardFor(chart.workflow, domain);
|
|
const natal = card.base.natal as Json;
|
|
const timing = card.base.timing as Json;
|
|
const where = `${chart.id}/${domain}`;
|
|
assert.equal(dig(natal, "ascendant", "sign"), facts.ascendantSign, `${where} ascendant sign`);
|
|
assert.equal(dig(natal, "ascendant", "degree_in_sign"), facts.ascendantDegree, `${where} ascendant degree`);
|
|
assert.equal(dig(natal, "planets", "Moon", "sign"), facts.moonSign, `${where} Moon sign`);
|
|
assert.equal(dig(natal, "planets", "Moon", "house"), facts.moonHouse, `${where} Moon house`);
|
|
assert.equal(dig(natal, "planets", "Moon", "degree_in_sign"), facts.moonDegree, `${where} Moon degree`);
|
|
for (const [cardKey, engine] of [
|
|
["mahadasha", facts.mahadasha],
|
|
["antardasha", facts.antardasha],
|
|
["pratyantardasha", facts.pratyantar],
|
|
] as const) {
|
|
for (const key of ["lord", "start", "end"] as const) {
|
|
assert.equal(dig(timing, "vimshottari", cardKey, key), dig(engine, key), `${where} ${cardKey}.${key}`);
|
|
}
|
|
}
|
|
assert.equal(dig(timing, "narayana", "md", "sign"), dig(facts.narayanaMd, "sign"), `${where} Narayana sign`);
|
|
assert.equal(dig(timing, "narayana", "md", "start_age"), dig(facts.narayanaMd, "start_age"), `${where} Narayana start age`);
|
|
assert.deepEqual(dig(natal, "functional_benefic_malefic", "functional_benefics"), facts.functional.functional_benefics, `${where} functional benefics`);
|
|
assert.deepEqual(dig(natal, "functional_benefic_malefic", "functional_malefics"), facts.functional.functional_malefics, `${where} functional malefics`);
|
|
}
|
|
const varga = (domain: ConsultationDomain, code: string) => dig(cardFor(chart.workflow, domain).domain_sections, domain, "vargas", code) as Json;
|
|
for (const [domain, code] of [["parents", "D12"], ["family", "D12"], ["migration", "D12"], ["children", "D7"], ["family", "D7"], ["marriage", "D9"], ["general", "D9"], ["career", "D10"], ["general", "D10"]] as const) {
|
|
const engine = facts.varga(code);
|
|
assert.equal(varga(domain, code)?.lagna, engine.lagna, `${chart.id}/${domain} ${code} lagna`);
|
|
assert.equal(dig(varga(domain, code), "planets", "Moon"), dig(engine, "planets", "Moon"), `${chart.id}/${domain} ${code} Moon`);
|
|
assert.deepEqual(varga(domain, code)?.planets, engine.planets, `${chart.id}/${domain} ${code} planets`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("every card value is a value the projection or the engine already holds (nothing rewritten)", () => {
|
|
function scalars(value: unknown, into = new Set<string>()) {
|
|
if (value === null || typeof value !== "object") {
|
|
into.add(JSON.stringify(value));
|
|
return into;
|
|
}
|
|
for (const [key, item] of Object.entries(value as object)) {
|
|
if (!Array.isArray(value)) into.add(JSON.stringify(key));
|
|
scalars(item, into);
|
|
}
|
|
return into;
|
|
}
|
|
// 原值: every leaf must be an engine / projection value.
|
|
// 新值: the same, except the two labels the v2 card writes itself
|
|
// (EVIDENCE_CARD_LABELS: 性别未知, 年盘未接入).
|
|
// 原因: TASK-consult-evidence-card-v2-20260927 decision table mandates both labels.
|
|
const cardLabels = new Set(Object.values(EVIDENCE_CARD_LABELS).map((label) => JSON.stringify(label)));
|
|
for (const chart of golden.charts) {
|
|
for (const domain of consultationDomainIds) {
|
|
const input = sources(chart.workflow, domain);
|
|
const known = scalars({ packet: input.packet, context: input.context });
|
|
for (const label of cardLabels) known.add(label);
|
|
const card = buildEvidenceCard([input]);
|
|
for (const [fieldId, values] of evidenceCardFieldValues(card)) {
|
|
for (const value of values) {
|
|
assert.ok(known.has(JSON.stringify(value)), `${chart.id}/${domain} ${fieldId}=${JSON.stringify(value)} is not an engine value`);
|
|
}
|
|
}
|
|
assert.deepEqual(card.meta.gaps, [], `${chart.id}/${domain} gaps`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("every domain's card covers Part B: dual dasha, domain vargas, functional benefic/malefic, raw degrees and dates (red line 3)", () => {
|
|
const isoDate = /^\d{4}-\d{2}-\d{2}$/;
|
|
for (const chart of golden.charts) {
|
|
for (const domain of consultationDomainIds) {
|
|
const card = cardFor(chart.workflow, domain);
|
|
const where = `${chart.id}/${domain}`;
|
|
const timing = card.base.timing as Json;
|
|
// Dual dasha: Vimshottari MD/AD/PD with dates and the running Narayana period.
|
|
for (const level of ["mahadasha", "antardasha", "pratyantardasha"]) {
|
|
assert.match(String(dig(timing, "vimshottari", level, "start")), isoDate, `${where} ${level} start`);
|
|
assert.match(String(dig(timing, "vimshottari", level, "end")), isoDate, `${where} ${level} end`);
|
|
}
|
|
assert.equal(typeof dig(timing, "narayana", "md", "sign"), "string", `${where} Narayana md`);
|
|
// Domain vargas: every varga the domain's spec names.
|
|
const section = card.domain_sections[domain];
|
|
assert.ok(section, `${where} domain section`);
|
|
assert.ok(EVIDENCE_CARD_SPECS[domain].vargas.length > 0, `${where} spec names a varga`);
|
|
for (const code of EVIDENCE_CARD_SPECS[domain].vargas) {
|
|
assert.equal(typeof dig(section.vargas, code, "lagna"), "string", `${where} ${code}`);
|
|
}
|
|
// Functional benefics / malefics with house lordship.
|
|
const functional = card.base.natal.functional_benefic_malefic;
|
|
assert.ok(Array.isArray(functional.functional_benefics) && functional.functional_benefics.length > 0, `${where} benefics`);
|
|
assert.ok(Array.isArray(functional.functional_malefics) && functional.functional_malefics.length > 0, `${where} malefics`);
|
|
assert.ok(Object.keys(functional.owned_houses as object).length > 0, `${where} lordship`);
|
|
// Raw degrees.
|
|
assert.equal(typeof dig(card.base.natal, "ascendant", "degree_in_sign"), "number", `${where} ascendant degree`);
|
|
assert.equal(typeof dig(card.base.natal, "planets", "Sun", "degree_in_sign"), "number", `${where} Sun degree`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("the model sees the answer contract and the card, not the full result (red line 1, D1, D3)", async () => {
|
|
const workflow = golden.charts[0]!.workflow;
|
|
const { view } = await runTool(workflow, ["parents"]);
|
|
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
|
|
const full = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: "parents" }), "parents");
|
|
|
|
// The keys the system prompt states its rules against, with the same values
|
|
// the full result carried.
|
|
assert.equal(view.packet_version, CONSULTATION_MODEL_PACKET_VERSION);
|
|
assert.equal(view.status, full.status);
|
|
assert.equal(view.route, full.route);
|
|
assert.deepEqual(dig(view, "evidence_contract", "answer_policy"), full.evidence_contract.answer_policy);
|
|
assert.equal(dig(view, "evidence_contract", "answer_policy", "can_answer_direction"), true);
|
|
assert.equal(typeof dig(view, "evidence_contract", "answer_policy", "can_answer_precise_timing"), "boolean");
|
|
assert.deepEqual(dig(view, "evidence_contract", "hard_blockers"), full.evidence_contract.hard_blockers);
|
|
assert.deepEqual(dig(view, "evidence_contract", "missing_route_layers"), full.evidence_contract.missing_route_layers);
|
|
assert.deepEqual(dig(view, "evidence_contract", "available_layers"), full.evidence_contract.available_layers);
|
|
assert.equal(dig(view, "evidence_contract", "user_facing_limitation"), full.evidence_contract.user_facing_limitation);
|
|
assert.deepEqual(view.rectification, full.rectification);
|
|
assert.equal("presentation" in view, false, "required_blocks names the audit table the body must not carry");
|
|
assert.deepEqual((view.claim_cards as Array<{ category: string }>).map((card) => card.category), ["natal_foundation", "timing", "domain"]);
|
|
assert.deepEqual(view.domains, ["parents"]);
|
|
assert.deepEqual(view.omitted_domains, []);
|
|
assert.equal(view.success, true);
|
|
assert.ok(dig(view, "methodology", "sections"), "the domain checklist still travels with the result");
|
|
assert.equal(dig(view, "evidence_card", "card_version"), EVIDENCE_CARD_VERSION);
|
|
assert.equal(dig(view, "evidence_card", "backstage"), EVIDENCE_CARD_BACKSTAGE_LINE);
|
|
// No duplicate package, no audit table, no spectra, no status layers.
|
|
assert.equal("consultations" in view, false);
|
|
const { methodology: _methodology, ...withoutMethod } = view;
|
|
void _methodology;
|
|
const text = JSON.stringify(withoutMethod);
|
|
// As keys: available_layers may still name a layer ("varga_spectrum") as available.
|
|
for (const absent of ["technique_audit_table", "varga_spectrum", "western_spectrum", "must_use_layers", "research_dn", "extended", "technique_truth", "reference_transparency", "vedastro_cross_check", "presentation"]) {
|
|
assert.equal(text.includes(`"${absent}":`), false, absent);
|
|
}
|
|
});
|
|
|
|
test("a two-domain turn carries one base, both domain sections and each domain's own contract", async () => {
|
|
const workflow = golden.charts[1]!.workflow;
|
|
const { view } = await runTool(workflow, ["children", "education"]);
|
|
const claimCards = view.claim_cards as Array<{ category: string; evidence: Json }>;
|
|
assert.deepEqual(claimCards.map((card) => card.category), ["natal_foundation", "timing", "domain"]);
|
|
assert.deepEqual(Object.keys(claimCards[2]!.evidence), ["children", "education"]);
|
|
const consultations = view.consultations as Array<Json>;
|
|
assert.deepEqual(consultations.map((item) => item.domain), ["children", "education"]);
|
|
for (const item of consultations) {
|
|
assert.deepEqual(Object.keys(item).sort(), ["domain", "evidence_contract", "rectification", "route", "status"]);
|
|
assert.equal(typeof dig(item, "evidence_contract", "answer_policy", "can_answer_direction"), "boolean");
|
|
}
|
|
assert.equal(typeof dig(view, "evidence_contract", "answer_policy", "can_answer_direction"), "boolean");
|
|
});
|
|
|
|
test("the 「本轮技法」 rows are exactly the rows the full result produced before the card", async () => {
|
|
for (const chart of golden.charts) {
|
|
for (const domains of [["parents"], ["career"], ["children", "education"]] as ConsultationDomain[][]) {
|
|
const { state } = await runTool(chart.workflow, domains);
|
|
// Before the card: single domain read the domain packet's audit table;
|
|
// several domains read the first packet's (mergeConsultationEvidencePackets).
|
|
const first = sources(chart.workflow, domains[0]!).packet;
|
|
const expected = normalizeTechniqueAuditRows(first.evidence_contract.technique_audit_table);
|
|
assert.ok((expected?.length ?? 0) > 30, "the audit table is the full 40-odd rows");
|
|
assert.deepEqual(state.techniqueAuditTable, expected, `${chart.id}/${domains.join("+")}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("model-visible size falls to card scale; only the method checklist can push it past 12,000", async () => {
|
|
const methodHeavy = new Set(["career", "marriage", "wealth"]);
|
|
for (const chart of golden.charts) {
|
|
for (const domain of consultationDomainIds) {
|
|
const { view, state } = await runTool(chart.workflow, [domain]);
|
|
const total = JSON.stringify(view).length;
|
|
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})`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("the card names what else this calculation holds, without the sections it already carries", () => {
|
|
const card = cardFor(golden.charts[0]!.workflow, "parents");
|
|
assert.equal(card.meta.backstage, EVIDENCE_CARD_BACKSTAGE_LINE);
|
|
assert.ok(card.meta.supplementable_sections.includes("varga:D60"));
|
|
assert.ok(card.meta.supplementable_sections.includes("western:solar_return"));
|
|
assert.ok(card.meta.supplementable_sections.includes("yogas"));
|
|
assert.equal(card.meta.supplementable_sections.includes("varga:D12"), false);
|
|
const career = cardFor(golden.charts[0]!.workflow, "career");
|
|
assert.equal(career.meta.supplementable_sections.includes("yogas"), false);
|
|
assert.equal(career.meta.supplementable_sections.includes("varga:D10"), false);
|
|
});
|
|
|
|
test("citation counting keeps field ids only and needs a date, degree or planet-in-sign phrase", () => {
|
|
const card = cardFor(golden.charts[0]!.workflow, "parents");
|
|
const start = String(dig(card.base.timing, "vimshottari", "pratyantardasha", "start"));
|
|
const moonSign = String(dig(card.base.natal, "planets", "Moon", "sign"));
|
|
const answer = `子运从 ${start} 开始;Moon in ${moonSign}。月亮是 ${moonSign}。`;
|
|
const cited = citedEvidenceCardFields(card, answer);
|
|
assert.ok(cited.includes("base.timing.vimshottari.pratyantardasha.start"));
|
|
assert.ok(cited.includes("base.natal.planets.Moon.sign"));
|
|
for (const id of cited) assert.match(id, /^[A-Za-z][A-Za-z0-9._-]*$/);
|
|
// A bare sign name is not a citation.
|
|
assert.deepEqual(citedEvidenceCardFields(card, `月亮 ${moonSign}`), []);
|
|
assert.deepEqual(citedEvidenceCardFields(card, ""), []);
|
|
});
|