fix(consult): put locally computed techniques in the model packet
Web consult already computed Yoga, Arudha, KP, and related layers, then stripped them before the spoken model. Keep those executed structures in toModelOutput so answers cannot invent degrees from parametric knowledge. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4490,3 +4490,19 @@
|
||||
- 相关记录:BUG-265、BUG-269、BUG-283
|
||||
- 复发自:BUG-283(不再等模型,但仍把完整排盘当首页可用性)
|
||||
- 修复版本:待提交
|
||||
|
||||
## BUG-300 | 网页咨询把已算技法从模型包裁掉,模型用参数知识填洞
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-19
|
||||
- 最近更新:2026-08-19
|
||||
- 影响面:普通咨询 `toModelOutput`、`_attach_local_consultation_layers`、口头回答密度
|
||||
- 用户现象:线上咨询缺 Yoga、Arudha/UL/A10、Chara、行运等本地 skill 会用的结构,模型用常识补度数和宫位。
|
||||
- 触发条件:有出生分钟的个人盘咨询。计算工作流已挂上分盘谱、Narayana、功能性吉凶,但发给模型的证据包没有这些层。
|
||||
- 根因:`toModelOutput` 的本命投影只保留升一/行星/宫位/Shadbala/AV/功能性吉凶/分盘谱。Yoga、Arudha、Gulika、Kakshya、KP、Chara、行运留在 `toAgentConsultationContext` 里却不进模型包。咨询路径也未把 Yoga/Chara/Sade Sati 写入 modules。
|
||||
- 修复:咨询 attach 补 Yoga、路线相关 Chara、Sade Sati 行运。模型包投影这些层,并给出 `must_use_layers` 已执行短名单。未交付层禁止用常识补算。MEVG / VedAstro 仍诚实 `blocked`。不绑 AGENTS.md 或整份 SKILL。
|
||||
- 验证:`frontend/tests/consultation-context.test.ts`、`consultation-spectrum-parity.test.ts`、`consultation-voice-contract.test.ts`;`tests/test_consultation_consumer_context.py`。
|
||||
- 防复发:咨询 `toModelOutput` 必须包含 skill 共同基础里本地已执行层;`local_layers` 不得再读引擎未写入的 module 键。
|
||||
- 相关记录:BUG-279、BUG-287、BUG-298
|
||||
- 复发自:BUG-279(算了却不交给模型)
|
||||
- 修复版本:待提交
|
||||
|
||||
@@ -34,6 +34,9 @@ const TECHNIQUE_LABELS: Readonly<Record<string, string>> = {
|
||||
"Arudha / UL / A10": "Arudha / UL / A10",
|
||||
"Narayana Dasha": "Narayana Dasha",
|
||||
"Vimshottari sub-periods": "Vimshottari 小运",
|
||||
Yogas: "格局 Yoga",
|
||||
"Chara Dasha": "Chara Dasha",
|
||||
"Transits / Sade Sati": "行运 / 土星过月",
|
||||
Ashtakavarga: "Ashtakavarga",
|
||||
"KP cusps": "KP 宫头",
|
||||
"Gulika / Mandi": "Gulika / Mandi",
|
||||
@@ -81,6 +84,8 @@ const NOTE_LABELS: Readonly<Record<string, string>> = {
|
||||
observation_only: "仅观察层",
|
||||
"Chesta/Sthana/Dig/Drik/Kala/Naisargika as components; absolute Virupa truth is not closed": "绝对 Virupa 真值未闭环",
|
||||
"observation_only; not verified day/month timing": "仅观察层,不作已核验日期",
|
||||
"observation windows, not guaranteed events": "观察窗口,不是保证事件",
|
||||
"commercial claims stay non-deterministic; not a majority-vote truth": "商业断语保持非确定,不以多数引擎表决",
|
||||
"this run is a natal consultation": "本轮为本命咨询",
|
||||
horary_path: "问卦路径",
|
||||
"requires a partner chart": "需要伴侣盘",
|
||||
|
||||
@@ -434,6 +434,7 @@ function mergeConsultationEvidencePackets(
|
||||
...(audit !== undefined ? { technique_audit_table: audit } : {}),
|
||||
...(varga !== undefined ? { varga_spectrum: varga } : {}),
|
||||
...(western !== undefined ? { western_spectrum: western } : {}),
|
||||
must_use_layers: unionStringList(contracts.map((contract) => contract.must_use_layers)),
|
||||
},
|
||||
claim_cards: options.claimCards,
|
||||
// `not_auto_rectified` is the restrictive boundary, so one domain reporting
|
||||
|
||||
@@ -176,6 +176,7 @@ export const consultationEvidencePacketSchema = z.object({
|
||||
technique_audit_table: modelOutputEvidenceSchema.optional(),
|
||||
varga_spectrum: modelOutputEvidenceSchema.optional(),
|
||||
western_spectrum: modelOutputEvidenceSchema.optional(),
|
||||
must_use_layers: modelOutputEvidenceSchema.optional(),
|
||||
}),
|
||||
claim_cards: z.array(z.object({
|
||||
category: z.enum(consultationEvidenceCategoryValues),
|
||||
@@ -247,6 +248,8 @@ const natalFoundationKeys = new Set([
|
||||
"components", "matrix", "matrixshape", "signs", "sources", "source", "method", "boundary",
|
||||
"functionalbeneficmalefic", "functionalbenefics", "functionalmalefics", "functionalneutrals",
|
||||
"yogakarakas", "ownedhouses", "ascendantsign", "effectonconfidence",
|
||||
"yogas", "category", "arudhapadas", "padas", "a10", "a7", "ul", "al", "gulika", "mandi", "kakshya",
|
||||
"kpcusps", "cusps", "sublord", "star", "starlord", "claimboundary", "reason",
|
||||
]);
|
||||
|
||||
const timingKeys = new Set([
|
||||
@@ -255,6 +258,9 @@ const timingKeys = new Set([
|
||||
"boundaries", "boundarycount", "start", "end", "startyear", "endyear", "iscurrent",
|
||||
"activationdescription", "sign", "lord", "planet", "name", "strength", "score", "source",
|
||||
"method", "confidence", "confidencecap", "summary", "conclusion", "fingerprint",
|
||||
"charadasha", "transits", "sadesati", "triggers", "window", "sequence", "durationyears",
|
||||
"fromage", "toage", "phase", "phasename", "moonsign", "saturnsign", "intensity", "active",
|
||||
"date", "target", "kind", "orb", "boundary", "claimboundary",
|
||||
]);
|
||||
|
||||
const validationKeys = new Set([
|
||||
@@ -334,10 +340,17 @@ function projectTechniqueAudit(value: unknown): ModelOutputValue | undefined {
|
||||
const rows = value.slice(0, 80).map((item) => {
|
||||
const row = record(item);
|
||||
const projected: { [key: string]: ModelOutputValue } = {};
|
||||
for (const key of ["technique", "status", "system", "boundary"] as const) {
|
||||
for (const key of ["technique", "status", "system", "boundary", "effect_on_confidence"] as const) {
|
||||
const scalar = boundedPrimitive(row[key]);
|
||||
if (scalar !== undefined) projected[key] = scalar;
|
||||
}
|
||||
for (const key of ["key_functional_benefics", "key_functional_malefics"] as const) {
|
||||
if (!Array.isArray(row[key])) continue;
|
||||
const names = row[key]
|
||||
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
||||
.slice(0, 9);
|
||||
if (names.length) projected[key] = names;
|
||||
}
|
||||
return projected;
|
||||
}).filter((row) => Object.keys(row).length > 0);
|
||||
return rows.length ? rows : undefined;
|
||||
@@ -385,6 +398,11 @@ function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationCo
|
||||
shadbala: context.chart.shadbala,
|
||||
ashtakavarga: context.chart.ashtakavarga,
|
||||
functional_benefic_malefic: context.local_layers.functional_benefic_malefic,
|
||||
yogas: context.local_layers.yogas,
|
||||
arudha_padas: context.local_layers.arudha_padas,
|
||||
gulika: context.local_layers.gulika,
|
||||
kakshya: context.local_layers.kakshya,
|
||||
kp_cusps: context.local_layers.kp_cusps,
|
||||
}, natalFoundationKeys, { allowAstrologyEntityKeys: true });
|
||||
const output = record(foundation);
|
||||
const varga = projectVargaSpectrum(context.local_layers.varga_spectrum);
|
||||
@@ -433,6 +451,8 @@ function projectTimingEvidence(context: ReturnType<typeof toAgentConsultationCon
|
||||
dasha: context.chart.dasha,
|
||||
dasha_sub_periods: context.local_layers.dasha_sub_periods,
|
||||
narayana_dasha: context.local_layers.narayana_dasha,
|
||||
chara_dasha: context.local_layers.chara_dasha,
|
||||
transits: context.local_layers.transits,
|
||||
}, timingKeys, { allowAstrologyEntityKeys: true });
|
||||
const output = record(timing);
|
||||
const western = projectWesternSpectrum(context.evidence_contract.western_spectrum);
|
||||
@@ -456,11 +476,53 @@ function stringList(value: unknown, limit = 24) {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, limit) : [];
|
||||
}
|
||||
|
||||
const SHARED_MUST_USE_LAYERS = [
|
||||
"Formal Vargas D1–D60",
|
||||
"Functional Benefic/Malefic",
|
||||
"Yogas",
|
||||
"Vimshottari sub-periods",
|
||||
"Narayana Dasha",
|
||||
"Shadbala components",
|
||||
"Ashtakavarga",
|
||||
"Arudha / UL / A10",
|
||||
"Western natal (tropical)",
|
||||
"Transits / Sade Sati",
|
||||
] as const;
|
||||
|
||||
const DOMAIN_MUST_USE_LAYERS: Readonly<Record<string, readonly string[]>> = {
|
||||
career: ["Formal Vargas D1–D60", "Arudha / UL / A10", "Narayana Dasha", "Chara Dasha"],
|
||||
marriage: ["Formal Vargas D1–D60", "Arudha / UL / A10"],
|
||||
wealth: ["Formal Vargas D1–D60", "Ashtakavarga"],
|
||||
timing: ["Narayana Dasha", "Vimshottari sub-periods", "Transits / Sade Sati", "Chara Dasha"],
|
||||
annual: ["Narayana Dasha", "Vimshottari sub-periods", "Chara Dasha"],
|
||||
health: ["Formal Vargas D1–D60", "Shadbala components"],
|
||||
};
|
||||
|
||||
function mustUseExecutedLayers(route: string, audit: unknown): string[] {
|
||||
const rows = Array.isArray(audit) ? audit : [];
|
||||
const executed = new Set(
|
||||
rows
|
||||
.map((item) => record(item))
|
||||
.filter((row) => row.status === "executed")
|
||||
.map((row) => String(row.technique || "")),
|
||||
);
|
||||
const wanted = [...SHARED_MUST_USE_LAYERS, ...(DOMAIN_MUST_USE_LAYERS[route] ?? [])];
|
||||
const output: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const name of wanted) {
|
||||
if (!executed.has(name) || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
output.push(name);
|
||||
}
|
||||
return output.slice(0, 24);
|
||||
}
|
||||
|
||||
function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultationContext>["evidence_contract"]) {
|
||||
const policy = record(contract.answer_policy);
|
||||
const audit = projectTechniqueAudit(contract.technique_audit_table);
|
||||
const varga = projectVargaSpectrum(contract.varga_spectrum);
|
||||
const western = projectWesternSpectrum(contract.western_spectrum);
|
||||
const mustUse = mustUseExecutedLayers(String(contract.route || ""), contract.technique_audit_table);
|
||||
return {
|
||||
available_layers: stringList(contract.available_layers, 80),
|
||||
missing_route_layers: stringList(contract.missing_route_layers, 40),
|
||||
@@ -478,6 +540,7 @@ function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultation
|
||||
...(audit !== undefined ? { technique_audit_table: audit } : {}),
|
||||
...(varga !== undefined ? { varga_spectrum: varga } : {}),
|
||||
...(western !== undefined ? { western_spectrum: western } : {}),
|
||||
...(mustUse.length > 0 ? { must_use_layers: mustUse } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -570,6 +633,10 @@ export function toAgentConsultationContext(data: JsonRecord) {
|
||||
// precise timing as allowed. The field now names the layer the engine actually attaches.
|
||||
dasha_sub_periods: modules.dasha_sub_periods, narayana_dasha: modules.narayana_dasha, gulika: modules.gulika,
|
||||
kakshya: modules.kakshya,
|
||||
yogas: modules.yogas || chart.yogas,
|
||||
kp_cusps: modules.kp_cusps,
|
||||
chara_dasha: modules.chara_dasha,
|
||||
transits: modules.transits,
|
||||
functional_benefic_malefic: modules.functional_benefic_malefic
|
||||
|| record(data.machine_evidence_packet).functional_benefic_malefic,
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@ When reference_transparency is present:
|
||||
- Only say the chart calculation failed when evidence_contract.hard_blockers is non-empty.
|
||||
- Never claim D2, D11, D9, D10, A10, UL, Narayana Dasha, a delivered Varga, or a delivered Western layer is missing when it appears in evidence_contract.available_layers, evidence_contract.varga_spectrum, evidence_contract.western_spectrum, chart, or local_layers.
|
||||
- evidence_contract.technique_audit_table is the invocation record for this run. Use every executed layer that is relevant to the question. Status executed means the server ran it; blocked means it could not; not_applicable means it does not apply. Never treat an omitted row as permission to invent the technique, and never silently skip an executed layer the question needs.
|
||||
- evidence_contract.must_use_layers is the executed shortlist for this answer. A placement, yoga, dasha boundary, or transit that is not in this packet was not delivered: do not invent it from model knowledge.
|
||||
- Do not paste that Technique Audit Table into the spoken answer. The product UI renders it collapsed from the delivered rows. This is the one audit surface; do not also dump JSON.
|
||||
- local_layers.dasha_sub_periods carries the antardasha boundaries inside the running mahadasha. When it is present, use those boundaries and never say sub-periods were not calculated; when it is absent, say so once instead of implying the calculation broke.
|
||||
- Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome.
|
||||
|
||||
@@ -93,6 +93,25 @@ test("projects only bounded server-selected evidence to the model", () => {
|
||||
functional_malefics: ["Mercury"],
|
||||
},
|
||||
kakshya: { status: "observation_only", boundary: "not verified day/month timing" },
|
||||
yogas: { status: "executed", count: 1, yogas: [{ name: "Gaja Kesari", planets: ["Jupiter", "Moon"] }] },
|
||||
arudha_padas: {
|
||||
padas: {
|
||||
A10: { name: "Karma Pada (A10)", sign: "Capricorn" },
|
||||
UL: { name: "Upapada (UL)", sign: "Libra" },
|
||||
},
|
||||
},
|
||||
gulika: { sign: "Scorpio", claim_boundary: "observation_only" },
|
||||
kp_cusps: { houses: { "10": { sign: "Taurus", sublord: "Saturn" } } },
|
||||
chara_dasha: {
|
||||
status: "executed",
|
||||
current: { sign: "Libra", lord: "Venus", duration_years: 8 },
|
||||
sequence: [{ sign: "Libra", lord: "Venus", duration_years: 8 }],
|
||||
},
|
||||
transits: {
|
||||
status: "executed",
|
||||
sade_sati: { phase: "none", moon_sign: "Taurus", saturn_sign: "Pisces" },
|
||||
boundary: "observation windows, not guaranteed events",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: { primary_theme: "career" },
|
||||
@@ -133,7 +152,12 @@ test("projects only bounded server-selected evidence to the model", () => {
|
||||
user_facing_limitation: "精确月份暂不可用",
|
||||
technique_audit_table: [
|
||||
{ technique: "Formal Vargas D1–D60", status: "executed", system: "jyotish", boundary: "20/20 named traditional charts" },
|
||||
{ technique: "Yogas", status: "executed", system: "jyotish" },
|
||||
{ technique: "Arudha / UL / A10", status: "executed", system: "jyotish" },
|
||||
{ technique: "Narayana Dasha", status: "executed", system: "jyotish" },
|
||||
{ technique: "Functional Benefic/Malefic", status: "executed", key_functional_benefics: ["Jupiter"] },
|
||||
{ technique: "Western natal (tropical)", status: "executed", system: "western" },
|
||||
{ technique: "MEVG / Global Web Evidence", status: "blocked", boundary: "caps_claims_until_global_web_evidence_runs" },
|
||||
],
|
||||
varga_spectrum: {
|
||||
status: "used",
|
||||
@@ -197,4 +221,16 @@ test("projects only bounded server-selected evidence to the model", () => {
|
||||
assert.equal(serialized.includes("technique_audit_table"), true);
|
||||
assert.equal(serialized.includes("functional_benefics"), true);
|
||||
assert.equal(serialized.includes("Jupiter"), true);
|
||||
assert.equal(serialized.includes("Gaja Kesari"), true);
|
||||
assert.equal(serialized.includes("Karma Pada (A10)"), true);
|
||||
assert.equal(serialized.includes("chara_dasha"), true);
|
||||
assert.equal(serialized.includes("Venus"), true);
|
||||
assert.equal(serialized.includes("sade_sati"), true);
|
||||
assert.equal(serialized.includes("Taurus"), true);
|
||||
const mustUse = output.evidence_contract.must_use_layers;
|
||||
assert.equal(Array.isArray(mustUse), true);
|
||||
assert.equal((mustUse as string[]).includes("Yogas"), true);
|
||||
assert.equal((mustUse as string[]).includes("Arudha / UL / A10"), true);
|
||||
assert.equal((mustUse as string[]).includes("MEVG / Global Web Evidence"), false);
|
||||
assert.equal(serialized.includes("https://"), false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
||||
|
||||
test("web consult compute and projection keep the live skill's locally computed spectrum", () => {
|
||||
const skill = readFileSync(resolve(root, "SKILL.md"), "utf8");
|
||||
const attach = readFileSync(resolve(root, "scripts/jyotish_api_server.py"), "utf8");
|
||||
const workflow = readFileSync(resolve(root, "frontend/src/mastra/consultation-workflow.ts"), "utf8");
|
||||
const instructions = readFileSync(resolve(root, "frontend/src/mastra/index.ts"), "utf8");
|
||||
|
||||
assert.match(skill, /共同基础:D1、功能性吉凶星、相关 Yoga、Vimshottari 与 Narayana Dasha、行运、Shadbala、Ashtakavarga/);
|
||||
assert.match(skill, /事业至少 `D10 \+ A10`/);
|
||||
assert.match(skill, /婚恋至少 `D9 \+ UL`/);
|
||||
|
||||
for (const layer of ["yogas", "chara_dasha", "transits", "arudha_padas", "narayana_dasha"]) {
|
||||
assert.match(attach, new RegExp(`modules\\['${layer}'\\]`), layer);
|
||||
}
|
||||
assert.match(attach, /_compact_yoga_entry/);
|
||||
assert.match(attach, /_compact_chara_dasha/);
|
||||
assert.doesNotMatch(attach, /defer_optional_external_evidence = False/);
|
||||
|
||||
assert.match(workflow, /yogas: context\.local_layers\.yogas/);
|
||||
assert.match(workflow, /arudha_padas: context\.local_layers\.arudha_padas/);
|
||||
assert.match(workflow, /chara_dasha: context\.local_layers\.chara_dasha/);
|
||||
assert.match(workflow, /transits: context\.local_layers\.transits/);
|
||||
assert.match(workflow, /must_use_layers/);
|
||||
assert.match(instructions, /must_use_layers is the executed shortlist/);
|
||||
assert.doesNotMatch(instructions, /AGENTS\.md/);
|
||||
});
|
||||
@@ -27,6 +27,8 @@ test("product voice keeps skill technique density without dumping JSON", () => {
|
||||
assert.match(mastra, /The bound skill method is this product's answering contract/);
|
||||
assert.match(mastra, /Do not use a restricted technique/);
|
||||
assert.match(mastra, /Do not paste that Technique Audit Table into the spoken answer/);
|
||||
assert.match(mastra, /must_use_layers is the executed shortlist/);
|
||||
assert.match(mastra, /do not invent it from model knowledge/);
|
||||
assert.match(mastra, /Do not announce a skipped-domain inventory/);
|
||||
assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/);
|
||||
assert.doesNotMatch(mastra, /2-5 short paragraphs/);
|
||||
|
||||
@@ -613,6 +613,92 @@ def _consultation_layer_present(value) -> bool:
|
||||
return status not in {'blocked', 'unavailable', 'missing'}
|
||||
|
||||
|
||||
def _consultation_audit_status(value) -> str:
|
||||
if isinstance(value, dict):
|
||||
raw = str(value.get('status') or '').lower()
|
||||
if raw == 'not_applicable':
|
||||
return 'not_applicable'
|
||||
if raw in {'blocked', 'unavailable', 'missing'}:
|
||||
return 'blocked'
|
||||
if _consultation_layer_present(value):
|
||||
return 'executed'
|
||||
if isinstance(value, list) and value:
|
||||
return 'executed'
|
||||
return 'blocked'
|
||||
|
||||
|
||||
_CHARA_DASHA_ROUTES = frozenset({'career', 'timing', 'annual', 'marriage'})
|
||||
|
||||
|
||||
def _consultation_declared_routes(body: dict) -> set[str]:
|
||||
values: list[str] = []
|
||||
for key in ('strict_workflow_route', 'theme'):
|
||||
raw = body.get(key)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
values.append(raw.strip().lower())
|
||||
elif isinstance(raw, list):
|
||||
values.extend(str(item).strip().lower() for item in raw if item)
|
||||
themes = body.get('themes')
|
||||
if isinstance(themes, list):
|
||||
values.extend(str(item).strip().lower() for item in themes if item)
|
||||
return {item for item in values if item}
|
||||
|
||||
|
||||
def _compact_yoga_entry(value) -> dict | None:
|
||||
if isinstance(value, str) and value.strip():
|
||||
return {'name': value.strip()[:80]}
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
name = str(value.get('name') or value.get('yoga') or '').strip()
|
||||
if not name:
|
||||
return None
|
||||
compact: dict = {'name': name[:80]}
|
||||
planets = value.get('planets')
|
||||
if isinstance(planets, list):
|
||||
names = [str(item) for item in planets[:6] if item]
|
||||
if names:
|
||||
compact['planets'] = names
|
||||
elif isinstance(planets, str) and planets.strip():
|
||||
compact['planets'] = [planets.strip()]
|
||||
category = value.get('category') or value.get('cat')
|
||||
if category:
|
||||
compact['category'] = str(category)[:40]
|
||||
return compact
|
||||
|
||||
|
||||
def _compact_chara_dasha(raw: dict, age_years: float) -> dict:
|
||||
sequence = raw.get('dasha_sequence') if isinstance(raw.get('dasha_sequence'), list) else []
|
||||
cursor = 0.0
|
||||
current = None
|
||||
compact_sequence = []
|
||||
for period in sequence:
|
||||
if not isinstance(period, dict):
|
||||
continue
|
||||
try:
|
||||
duration = float(period.get('duration_years') or 0)
|
||||
except (TypeError, ValueError):
|
||||
duration = 0.0
|
||||
start = cursor
|
||||
end = cursor + duration
|
||||
row = {
|
||||
'sign': period.get('sign'),
|
||||
'lord': period.get('lord'),
|
||||
'duration_years': duration,
|
||||
}
|
||||
if current is None and (start <= age_years < end or (age_years >= end and period is sequence[-1])):
|
||||
current = {**row, 'from_age': round(start, 2), 'to_age': round(end, 2)}
|
||||
if len(compact_sequence) < 6:
|
||||
compact_sequence.append(row)
|
||||
cursor = end
|
||||
return {
|
||||
'status': 'executed',
|
||||
'method': 'kn_rao',
|
||||
'current': current,
|
||||
'sequence': compact_sequence,
|
||||
'boundary': 'commercial claims stay non-deterministic; not a majority-vote truth',
|
||||
}
|
||||
|
||||
|
||||
def _planet_longitudes(planets: dict) -> dict[str, float]:
|
||||
longitudes = {}
|
||||
for name, data in planets.items():
|
||||
@@ -855,12 +941,17 @@ def _consultation_technique_audit_table(
|
||||
for raw in quality_gate.get('technique_audit_table') or []:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
rows.append(_audit_row(
|
||||
row = _audit_row(
|
||||
str(raw.get('technique') or raw.get('name') or 'Technique'),
|
||||
raw.get('status'),
|
||||
system=str(raw.get('system') or 'cross_system'),
|
||||
boundary=str(raw.get('effect_on_confidence') or raw.get('boundary') or ''),
|
||||
))
|
||||
)
|
||||
for key in ('key_functional_benefics', 'key_functional_malefics'):
|
||||
names = raw.get(key)
|
||||
if isinstance(names, list) and names:
|
||||
row[key] = [str(item) for item in names[:9] if item]
|
||||
rows.append(row)
|
||||
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
|
||||
spectrum = modules.get('varga_spectrum') if isinstance(modules.get('varga_spectrum'), dict) else {}
|
||||
counts = spectrum.get('counts') if isinstance(spectrum.get('counts'), dict) else {}
|
||||
@@ -885,12 +976,13 @@ def _consultation_technique_audit_table(
|
||||
('narayana_dasha', 'Narayana Dasha', None),
|
||||
('dasha_sub_periods', 'Vimshottari sub-periods', None),
|
||||
('ashtakavarga', 'Ashtakavarga', 'component parity remains partial'),
|
||||
('yogas', 'Yogas', None),
|
||||
('chara_dasha', 'Chara Dasha', 'commercial claims stay non-deterministic; not a majority-vote truth'),
|
||||
('transits', 'Transits / Sade Sati', 'observation windows, not guaranteed events'),
|
||||
('kp_cusps', 'KP cusps', 'KP remains non-deterministic in this product'),
|
||||
('gulika', 'Gulika / Mandi', 'observation_only'),
|
||||
):
|
||||
value = modules.get(name)
|
||||
status = 'executed' if _consultation_layer_present(value) else 'blocked'
|
||||
rows.append(_audit_row(label, status, boundary=boundary))
|
||||
rows.append(_audit_row(label, _consultation_audit_status(modules.get(name)), boundary=boundary))
|
||||
shadbala = modules.get('shadbala') or chart.get('shadbala')
|
||||
rows.append(_audit_row(
|
||||
'Shadbala components',
|
||||
@@ -905,11 +997,17 @@ def _consultation_technique_audit_table(
|
||||
))
|
||||
if not any(row.get('technique') == 'Functional Benefic/Malefic' for row in rows):
|
||||
functional = modules.get('functional_benefic_malefic')
|
||||
rows.append(_audit_row(
|
||||
functional_row = _audit_row(
|
||||
'Functional Benefic/Malefic',
|
||||
'executed' if _consultation_layer_present(functional) and str(functional.get('status') or '') != 'blocked' else 'blocked',
|
||||
boundary=str((functional or {}).get('effect_on_confidence') or '') if isinstance(functional, dict) else '',
|
||||
))
|
||||
)
|
||||
if isinstance(functional, dict):
|
||||
for key in ('functional_benefics', 'functional_malefics'):
|
||||
names = functional.get(key)
|
||||
if isinstance(names, list) and names:
|
||||
functional_row[f'key_{key}'] = [str(item) for item in names[:9] if item]
|
||||
rows.append(functional_row)
|
||||
western = _compact_western_spectrum(western_evidence_packet)
|
||||
rows.append(_audit_row(
|
||||
'Western natal (tropical)',
|
||||
@@ -1163,6 +1261,122 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
except Exception as exc:
|
||||
diagnostics.append({'layer': 'kakshya', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
for planet in planets.values():
|
||||
if isinstance(planet, dict) and not planet.get('sign') and planet.get('sign_idx') is not None:
|
||||
try:
|
||||
planet['sign'] = SIGNS[int(planet['sign_idx']) % 12]
|
||||
except (TypeError, ValueError, IndexError):
|
||||
pass
|
||||
|
||||
if not _consultation_layer_present(modules.get('yogas')):
|
||||
try:
|
||||
raw_yogas = chart.get('yogas') if isinstance(chart.get('yogas'), list) else []
|
||||
compact = [entry for entry in (_compact_yoga_entry(item) for item in raw_yogas) if entry]
|
||||
if not compact:
|
||||
api = handler._compute_yogas_api({
|
||||
**birth_payload,
|
||||
'planets': planets,
|
||||
'ascendant': ascendant,
|
||||
})
|
||||
result = api.get('result') if isinstance(api, dict) else {}
|
||||
extended = result.get('extended_yogas') if isinstance(result, dict) else []
|
||||
engine = result.get('rule_engine_yogas') if isinstance(result, dict) else []
|
||||
compact = [
|
||||
entry for entry in (
|
||||
_compact_yoga_entry(item)
|
||||
for item in (extended or []) + (engine or [])
|
||||
)
|
||||
if entry
|
||||
][:12]
|
||||
modules['yogas'] = {
|
||||
'status': 'executed' if compact else 'blocked',
|
||||
'count': len(compact),
|
||||
'yogas': compact[:12],
|
||||
}
|
||||
if compact and not isinstance(chart.get('yogas'), list):
|
||||
chart['yogas'] = compact[:12]
|
||||
except Exception as exc:
|
||||
modules['yogas'] = {'status': 'blocked', 'reason': exc.__class__.__name__, 'yogas': []}
|
||||
diagnostics.append({'layer': 'yogas', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
if not isinstance(modules.get('chara_dasha'), dict):
|
||||
if _consultation_declared_routes(body) & _CHARA_DASHA_ROUTES:
|
||||
try:
|
||||
jaimini = _load_local_module('jaimini')
|
||||
planet_lons = _planet_longitudes(planets)
|
||||
asc_sign_idx = int(ascendant.get('sign_idx', int(float(ascendant.get('lon', 0))) // 30)) % 12
|
||||
raw = jaimini.calc_chara_dasha(
|
||||
asc_sign_idx,
|
||||
planet_lons,
|
||||
int(birth_payload.get('year', 0) or 0),
|
||||
int(birth_payload.get('month', 1) or 1),
|
||||
int(birth_payload.get('day', 1) or 1),
|
||||
)
|
||||
modules['chara_dasha'] = _compact_chara_dasha(raw, _consultation_current_age(birth_payload, body))
|
||||
except Exception as exc:
|
||||
modules['chara_dasha'] = {
|
||||
'status': 'blocked',
|
||||
'reason': exc.__class__.__name__,
|
||||
'boundary': 'commercial claims stay non-deterministic; not a majority-vote truth',
|
||||
}
|
||||
diagnostics.append({'layer': 'chara_dasha', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
else:
|
||||
modules['chara_dasha'] = {
|
||||
'status': 'not_applicable',
|
||||
'boundary': 'Chara Dasha is attached for career, timing, annual, and marriage routes',
|
||||
}
|
||||
|
||||
if not isinstance(modules.get('transits'), dict):
|
||||
try:
|
||||
sade_sati = chart.get('sade_sati') if isinstance(chart.get('sade_sati'), dict) else None
|
||||
if not sade_sati:
|
||||
moon = planets.get('Moon') if isinstance(planets.get('Moon'), dict) else {}
|
||||
calculation_service = _load_local_module('domain_calculation_service')
|
||||
sade_sati = calculation_service.compute_sade_sati(
|
||||
moon_degree=float(moon.get('lon') or 0),
|
||||
asc_degree=float(ascendant.get('lon') or 0),
|
||||
reference_date=_consultation_reference_date(body).strftime('%Y-%m-%d'),
|
||||
tz=float(birth_payload.get('tz') or birth_payload.get('timezone') or 0),
|
||||
ayanamsa=str(body.get('ayanamsa') or birth_payload.get('ayanamsa') or 'lahiri'),
|
||||
)
|
||||
compact_sade = None
|
||||
if isinstance(sade_sati, dict):
|
||||
inner = sade_sati.get('sade_sati') if isinstance(sade_sati.get('sade_sati'), dict) else sade_sati
|
||||
compact_sade = {
|
||||
key: inner[key]
|
||||
for key in ('active', 'phase', 'phase_name', 'moon_sign', 'saturn_sign', 'intensity')
|
||||
if isinstance(inner, dict) and key in inner
|
||||
}
|
||||
for key in ('moon_sign', 'saturn_sign'):
|
||||
if key in sade_sati and key not in compact_sade:
|
||||
compact_sade[key] = sade_sati[key]
|
||||
if not compact_sade:
|
||||
compact_sade = None
|
||||
triggers = []
|
||||
raw_triggers = chart.get('transit_triggers') or (modules.get('transit_triggers') if isinstance(modules.get('transit_triggers'), list) else [])
|
||||
if isinstance(raw_triggers, list):
|
||||
for item in raw_triggers[:6]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
triggers.append({
|
||||
key: item[key]
|
||||
for key in ('date', 'planet', 'target', 'kind', 'orb')
|
||||
if key in item and item[key] is not None
|
||||
})
|
||||
modules['transits'] = {
|
||||
'status': 'executed' if compact_sade or triggers else 'blocked',
|
||||
'sade_sati': compact_sade,
|
||||
'triggers': triggers,
|
||||
'boundary': 'observation windows, not guaranteed events',
|
||||
}
|
||||
except Exception as exc:
|
||||
modules['transits'] = {
|
||||
'status': 'blocked',
|
||||
'reason': exc.__class__.__name__,
|
||||
'boundary': 'observation windows, not guaranteed events',
|
||||
}
|
||||
diagnostics.append({'layer': 'transits', 'status': 'unavailable', 'reason': exc.__class__.__name__})
|
||||
|
||||
chart['local_consultation_layers'] = {
|
||||
'status': 'ready' if not diagnostics else 'partial',
|
||||
'source': 'repository_local_engines',
|
||||
@@ -1172,8 +1386,9 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
'varga_full', 'varga_spectrum', 'arudha_padas', 'narayana_dasha',
|
||||
'dasha_sub_periods', 'ashtakavarga', 'kp_cusps', 'gulika',
|
||||
'functional_benefic_malefic', 'shadbala', 'kakshya',
|
||||
'yogas', 'chara_dasha', 'transits',
|
||||
)
|
||||
if _consultation_layer_present(modules.get(name))
|
||||
if _consultation_audit_status(modules.get(name)) == 'executed'
|
||||
],
|
||||
'diagnostics': diagnostics,
|
||||
}
|
||||
|
||||
@@ -122,6 +122,39 @@ def test_local_consultation_layers_supply_d10_a10_and_narayana_without_vedastro(
|
||||
assert 'kakshya' in chart['local_consultation_layers']['available']
|
||||
|
||||
|
||||
def test_local_consultation_layers_include_yogas_transits_and_route_chara() -> None:
|
||||
career = _attach_local_consultation_layers(
|
||||
_handler(),
|
||||
_base_chart(),
|
||||
dict(_BIRTH),
|
||||
{'current_date': _REFERENCE_DATE, 'strict_workflow_route': 'career'},
|
||||
)
|
||||
yogas = career['modules']['yogas']
|
||||
assert yogas['status'] == 'executed'
|
||||
assert yogas['yogas'][0]['name']
|
||||
chara = career['modules']['chara_dasha']
|
||||
assert chara['status'] == 'executed'
|
||||
assert chara['current']['sign']
|
||||
assert chara['sequence'][0]['lord']
|
||||
transits = career['modules']['transits']
|
||||
assert transits['status'] == 'executed'
|
||||
assert transits['sade_sati']['moon_sign']
|
||||
assert transits['sade_sati']['saturn_sign']
|
||||
assert transits['sade_sati']['phase'] in {'none', 'rising', 'peak', 'setting'}
|
||||
assert 'yogas' in career['local_consultation_layers']['available']
|
||||
assert 'chara_dasha' in career['local_consultation_layers']['available']
|
||||
assert 'transits' in career['local_consultation_layers']['available']
|
||||
|
||||
wealth = _attach_local_consultation_layers(
|
||||
_handler(),
|
||||
_base_chart(),
|
||||
dict(_BIRTH),
|
||||
{'current_date': _REFERENCE_DATE, 'strict_workflow_route': 'wealth'},
|
||||
)
|
||||
assert wealth['modules']['chara_dasha']['status'] == 'not_applicable'
|
||||
assert 'chara_dasha' not in wealth['local_consultation_layers']['available']
|
||||
|
||||
|
||||
def test_consultation_computes_formal_and_research_varga_spectrum() -> None:
|
||||
chart = _attach_local_consultation_layers(
|
||||
_handler(),
|
||||
|
||||
Reference in New Issue
Block a user