diff --git a/jyotish-app/jyotish-engine.js b/jyotish-app/jyotish-engine.js
index c34be483..305eb478 100644
--- a/jyotish-app/jyotish-engine.js
+++ b/jyotish-app/jyotish-engine.js
@@ -94,6 +94,17 @@ export const PLANET_RELATIONS = {
Saturn: { friends: ['Mercury','Venus'], enemies: ['Sun','Moon','Mars'] },
};
+export function getPlanetStatus(planet, sign) {
+ if (EXALTATION[planet] === sign) return "入旺";
+ if (DEBILITATION[planet] === sign) return "落陷";
+ if (SIGN_LORDS[sign] === planet) return "入庙";
+ const lord = SIGN_LORDS[sign];
+ const rel = PLANET_RELATIONS[planet];
+ if (rel?.friends?.includes(lord)) return "入友";
+ if (rel?.enemies?.includes(lord)) return "入敌";
+ return "中性";
+}
+
// Swiss Ephemeris constants
const SE_SUN = 0, SE_MOON = 1, SE_MARS = 4, SE_MERCURY = 2, SE_JUPITER = 5, SE_VENUS = 3, SE_SATURN = 6, SE_MEAN_NODE = 10;
const SE_SIDM_LAHIRI = 1;
@@ -312,8 +323,8 @@ export async function initEngine() {
*/
export async function computeChart(birth) {
const swe = await initEngine();
- const { year, month, day, hour, minute, lat, lon, tz } = birth;
- const hourDecimal = hour + minute / 60.0 - tz;
+ const { year, month, day, hour, minute, second = 0, lat, lon, tz } = birth;
+ const hourDecimal = hour + minute / 60.0 + second / 3600.0 - tz;
// Julian Day (swe.julday 第5参数默认1=Gregorian)
const jd = swe.julday(year, month, day, hourDecimal);
@@ -345,7 +356,12 @@ export async function computeChart(birth) {
const result = {
birth_info: {
date: `${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`,
- time: `${String(hour).padStart(2,'0')}:${String(minute).padStart(2,'0')}`,
+ time: second
+ ? `${String(hour).padStart(2,'0')}:${String(minute).padStart(2,'0')}:${String(second).padStart(2,'0')}`
+ : `${String(hour).padStart(2,'0')}:${String(minute).padStart(2,'0')}`,
+ hour,
+ minute,
+ second,
tz: `UTC${tz >= 0 ? '+' : ''}${tz}`,
lat, lon,
julian_day: Math.round(jd * 1e6) / 1e6,
@@ -401,10 +417,7 @@ export async function computeChart(birth) {
const retro = spd < 0;
const house = ((si - ascIdx + 12) % 12) + 1;
- let status = "中性";
- if (EXALTATION[pname] === sign) status = "入旺";
- else if (DEBILITATION[pname] === sign) status = "落陷";
- else if (SIGN_LORDS[sign] === pname) status = "入庙";
+ const status = getPlanetStatus(pname, sign);
const ni = Math.floor(lonSidereal / nakSpan);
const pada = Math.floor((lonSidereal % nakSpan) / (nakSpan / 4)) + 1;
diff --git a/jyotish-app/main.js b/jyotish-app/main.js
index fcbfecb3..a120e605 100644
--- a/jyotish-app/main.js
+++ b/jyotish-app/main.js
@@ -181,12 +181,12 @@ const TERMINOLOGY_MODE_OPTIONS = {
const CALCULATION_SETTING_OPTIONS = {
ayanamsa: [
['lahiri', 'Lahiri / Chitrapaksha'],
- ['raman', 'Raman(后续统一引擎切换)'],
- ['kp', 'KP / Krishnamurti(后续统一引擎切换)'],
+ ['raman', 'Raman'],
+ ['kp', 'KP / Krishnamurti'],
],
nodeMode: [
['mean', 'Mean node / 平均罗喉'],
- ['true', 'True node / 真罗喉(后续统一引擎切换)'],
+ ['true', 'True node / 真罗喉'],
],
houseSystem: [
['whole_sign', 'Whole Sign / Rashi'],
@@ -449,17 +449,25 @@ function initDateSelects() {
// 表单提交
// ============================================================================
async function computeChartForBirth(birth) {
- const { year, month, day, hour, minute, lat, lon, tz } = birth;
+ const { year, month, day, hour, minute, second = 0, lat, lon, tz } = birth;
const settings = readCalculationSettings();
- const payload = applyCalculationSettingsToPayload({ year, month, day, hour, minute, lat, lon, tz });
+ const payload = applyCalculationSettingsToPayload({ year, month, day, hour, minute, second, lat, lon, tz });
+ if (!payload.ayanamsa) payload.ayanamsa = settings.ayanamsa;
const apiChart = await window.JyotishAPI?.computeWithPython(payload);
if (apiChart?.success) {
console.log('[Jyotish] ✅ 本地 API 服务 —', apiChart.dasha_count, 'Dasha');
return attachCalculationSettings(apiChart, settings);
}
await initEngine();
- const fallbackChart = await computeChart({ year, month, day, hour, minute, lat, lon, tz });
+ const fallbackChart = await computeChart({ year, month, day, hour, minute, second, lat, lon, tz });
fallbackChart._fallback = true;
+ fallbackChart._calculation_boundary = {
+ ayanamsa: settings.ayanamsa,
+ appliedAyanamsa: 'lahiri',
+ note: settings.ayanamsa === 'lahiri'
+ ? 'Browser fallback uses Lahiri-compatible SwissEph WASM path.'
+ : `Browser fallback cannot apply ${settings.ayanamsa}; start the local API service to calculate this ayanamsa.`,
+ };
attachCalculationSettings(fallbackChart, settings);
console.log('[Jyotish] ⚠️ JS引擎计算完成');
return fallbackChart;
@@ -501,12 +509,12 @@ function setupForm() {
const tz = resolveTimezoneValue($('birth-tz').value);
if (!year || !month || !day || !timeVal) { alert(t('alert.date')); return; }
if (isNaN(lat) || isNaN(lon)) { alert(t('alert.city')); return; }
- const [hour, minute] = timeVal.split(':').map(Number);
+ const [hour, minute, second = 0] = timeVal.split(':').map(Number);
btnText.classList.add('hidden'); btnLoading.classList.remove('hidden'); btn.disabled = true;
setChartComputeStatus('正在计算星盘...', 'warn');
try {
// v6.9.4: 计算层 — 优先本地 API 服务, 回退JS引擎
- chartData = await computeChartForBirth({ year, month, day, hour, minute, lat, lon, tz });
+ chartData = await computeChartForBirth({ year, month, day, hour, minute, second, lat, lon, tz });
} catch (e) {
console.error('[Jyotish] 计算失败:', e);
setChartComputeStatus(buildChartComputeRecoveryMessage(e), 'error');
@@ -514,7 +522,7 @@ function setupForm() {
return;
}
try {
- window.__jyotishBirth = { year, month, day, hour, minute, lat, lon, tz };
+ window.__jyotishBirth = { year, month, day, hour, minute, second, lat, lon, tz };
renderAll();
showPage('chart');
aiChatSetChartData(chartData);
@@ -539,7 +547,10 @@ function fillBirthFormFromData(birth) {
monthEl.value = String(birth.month);
monthEl.dispatchEvent(new Event('change'));
dayEl.value = String(birth.day);
- $('birth-time').value = `${String(birth.hour).padStart(2, '0')}:${String(birth.minute).padStart(2, '0')}`;
+ const second = Number.isFinite(Number(birth.second)) ? Number(birth.second) : 0;
+ $('birth-time').value = second
+ ? `${String(birth.hour).padStart(2, '0')}:${String(birth.minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`
+ : `${String(birth.hour).padStart(2, '0')}:${String(birth.minute).padStart(2, '0')}`;
$('birth-lat').value = birth.lat;
$('birth-lon').value = birth.lon;
$('birth-tz').value = String(birth.tz);
@@ -817,6 +828,7 @@ function renderAll() {
chartData._client_audit = { validation, audit, actionableContext: actionableCtx, provenance };
renderSpecialLagnaReport(arudha, ascendant, birth_info, chartData.special_lagnas);
renderCompleteReadingTab({ ascendant, moonP, allYogas, dashaData, extraDasas, chartData, validation, audit });
+ renderAIPromptPackPanel(chartData);
renderProvenancePanel({
chartData,
panchanga: ty,
@@ -863,6 +875,129 @@ function renderCompleteReadingTab({ ascendant, moonP, allYogas, dashaData, extra
renderMEVGAudit($('mevg-audit-section'), mevg);
}
+function renderAIPromptPackPanel(cd) {
+ const host = $('ai-prompt-pack-panel');
+ if (!host) return;
+ const pack = normalizeAIPromptPack(cd);
+ const evidence = pack.evidence_snapshot || {};
+ const ayanamsa = evidence.ayanamsa || {};
+ const core = evidence.core || {};
+ const timing = evidence.timing || {};
+ const strength = evidence.strength || {};
+ const docs = pack.retrieval_plan?.local_reference_docs || [];
+ const tags = pack.retrieval_plan?.retrieval_tags || [];
+ const ranking = Array.isArray(strength.shadbala_ranking) ? strength.shadbala_ranking : [];
+ host.innerHTML = `
+
+
+
+ AI Prompt Pack
+ ${escapeHtml(pack.mode || 'jyotish_structured_prompt_pack')}
+
+
schema v${escapeHtml(String(pack.schema_version || 1))}
+
+
+
+ Ayanamsa
+ ${escapeHtml(ayanamsa.display || getCalculationSettingLabel('ayanamsa', readCalculationSettings().ayanamsa))}
+ ${escapeHtml(`node=${ayanamsa.node_mode || 'mean'} · value=${ayanamsa.value ?? '-'}`)}
+
+
+ Lagna / Moon
+ ${escapeHtml(core.ascendant?.sign || cd?.ascendant?.sign || '-')} / ${escapeHtml(core.Moon?.sign || cd?.planets?.Moon?.sign || '-')}
+ D1 evidence snapshot
+
+
+ Dasha
+ ${escapeHtml(timing.current_mahadasha || cd?.dasha?.current_md || '-')}
+ ${escapeHtml(timing.current_antardasha ? `AD ${timing.current_antardasha}` : timing.start_date || '')}
+
+
+
+
+
Prompt
+
${escapeHtml(pack.prompt_zh || '')}
+
+
+
Evidence
+
+ ${['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'].map(planet => {
+ const pdata = core[planet] || cd?.planets?.[planet] || {};
+ return `${escapeHtml(planet)} · ${escapeHtml(pdata.sign || '-')} ${escapeHtml(formatCompactDegree(pdata.degree))}`;
+ }).join('')}
+
+ ${ranking.length ? `
+
+ ${ranking.slice(0, 5).map(item => `${escapeHtml(item.planet || '-')} ${escapeHtml(String(item.total_rupas ?? item.rupas ?? '-'))}`).join('')}
+
+ ` : ''}
+
+
+
+
+ `;
+}
+
+function normalizeAIPromptPack(cd = {}) {
+ if (cd?.ai_prompt_pack?.evidence_snapshot) return cd.ai_prompt_pack;
+ const settings = normalizeCalculationSettings(cd?._calculation_settings || readCalculationSettings());
+ const birth = cd.birth || cd.birth_info || {};
+ const planets = cd.planets || {};
+ return {
+ schema_version: 1,
+ mode: 'jyotish_structured_prompt_pack',
+ prompt_zh: [
+ '你是一个审慎的 AI Native 印度/吠陀占星分析助手。',
+ '请只基于 evidence_snapshot 中的计算证据生成解读,不要编造星盘不存在的配置。',
+ `本盘使用 ${getCalculationSettingLabel('ayanamsa', settings.ayanamsa)} ayanamsa,节点口径为 ${getCalculationSettingLabel('nodeMode', settings.nodeMode)}。`,
+ '不要仅凭单一配置下结论;核心判断至少交叉 D1、D9、Dasha、Shadbala/Ashtakavarga 或 Transit 中的两个证据层。',
+ ].join('\n'),
+ evidence_snapshot: {
+ birth,
+ ayanamsa: {
+ name: settings.ayanamsa,
+ display: getCalculationSettingLabel('ayanamsa', settings.ayanamsa),
+ value: birth.ayanamsa ?? cd.ayanamsa,
+ node_mode: settings.nodeMode,
+ },
+ core: {
+ ascendant: cd.ascendant || {},
+ ...Object.fromEntries(Object.entries(planets).filter(([planet]) => ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'].includes(planet))),
+ },
+ timing: {
+ current_mahadasha: cd.dasha?.current_md,
+ remaining_years: cd.dasha?.remaining_years,
+ start_date: cd.dasha?.start_date,
+ },
+ strength: {
+ shadbala_ranking: Object.entries(cd.shadbala || {}).map(([planet, pdata]) => ({ planet, rupas: pdata?.rupas, level: pdata?.level })),
+ },
+ },
+ retrieval_plan: {
+ local_reference_docs: [
+ 'references/ai-reading-workflow-prompt.md',
+ 'references/comprehensive-reading-workflow.md',
+ 'references/prediction-boundary-protocol.md',
+ ],
+ retrieval_tags: ['no_single_factor_conclusion', 'oracle_boundary_visible', 'confidence_labeled_reading'],
+ },
+ };
+}
+
+function formatCompactDegree(value) {
+ const num = Number(value);
+ return Number.isFinite(num) ? `${num.toFixed(2)}°` : '-';
+}
+
function readCalculationSettings() {
try {
const saved = JSON.parse(localStorage.getItem(CALCULATION_SETTINGS_KEY) || '{}');
@@ -929,9 +1064,10 @@ function buildCalculationProvenance(chartData, panchanga, context = {}) {
const terminologyMode = readTerminologyMode(chartData?._terminology_mode);
const terminology = getTerminologyModeOption(terminologyMode);
const ayanamsaValue = birth.ayanamsa ?? chartData?.birth?.ayanamsa ?? chartData?.ayanamsa;
+ const ayanamsaLabel = birth.ayanamsa_display || chartData?.birth?.ayanamsa_display || getCalculationSettingLabel('ayanamsa', settings.ayanamsa);
const ayanamsa = ayanamsaValue != null
- ? `${getCalculationSettingLabel('ayanamsa', settings.ayanamsa)} (${Number(ayanamsaValue).toFixed(4)} deg)`
- : getCalculationSettingLabel('ayanamsa', settings.ayanamsa);
+ ? `${ayanamsaLabel} (${Number(ayanamsaValue).toFixed(4)} deg)`
+ : ayanamsaLabel;
const hasPython = chartData?.success && !chartData?._fallback;
return {
engine: hasPython ? `本地 API 服务 ${chartData?.version || ''}`.trim() : 'Browser SwissEph fallback',
@@ -939,7 +1075,7 @@ function buildCalculationProvenance(chartData, panchanga, context = {}) {
ephemerisBackend: getCalculationSettingLabel('ephemerisBackend', settings.ephemerisBackend),
terminologyMode: getCalculationSettingLabel('terminologyMode', settings.terminologyMode),
ayanamsa,
- nodeMode: `${getCalculationSettingLabel('nodeMode', settings.nodeMode)};Ketu derived 180 deg opposite`,
+ nodeMode: `${birth.node_mode || getCalculationSettingLabel('nodeMode', settings.nodeMode)};Ketu derived 180 deg opposite`,
houseSystem: `${getCalculationSettingLabel('houseSystem', settings.houseSystem)};Bhava Chalit 可在专项接口复核`,
sunrisePolicy: getCalculationSettingLabel('sunrisePolicy', settings.sunrisePolicy),
geocoderPolicy: getCalculationSettingLabel('geocoderPolicy', settings.geocoderPolicy),
@@ -954,7 +1090,7 @@ function buildCalculationProvenance(chartData, panchanga, context = {}) {
terminologyNote: terminology.note,
calculationSettings: settings,
ruleVariantStatus: 'Rule variants are saved/exported as interpretive policy; non-current variants are staged until each engine path supports live switching.',
- calculationSettingsStatus: 'Settings are saved and exported; ayanamsa/node/rule/ephemeris switching beyond the current path is staged for unified engine support.',
+ calculationSettingsStatus: 'Settings are saved and exported; ayanamsa and node mode are live API parameters. Rule variants remain interpretive policy unless the target endpoint returns live variant metadata.',
chartStyle: context.chartStyle === 'north' ? 'North Indian' : 'South Indian',
timezone: birth.tz || 'UTC+8',
coordinates: `${safeNumber(birth.lat ?? 0).toFixed(4)}, ${safeNumber(birth.lon ?? 0).toFixed(4)}`,
@@ -1233,13 +1369,15 @@ function openSavedChartFromPanel(id) {
function normalizeSavedBirth(cd) {
const birth = cd?.birth_info || cd?.birth || {};
const [year, month, day] = String(birth.date || '').split('-').map(Number);
- const [hour, minute] = String(birth.time || '').split(':').map(Number);
+ const [hour, minute, secondRaw] = String(birth.time || '').split(':').map(Number);
+ const second = Number.isFinite(secondRaw) ? secondRaw : Number(birth.second) || 0;
return {
year: year || 2000,
month: month || 1,
day: day || 1,
hour: hour || 12,
minute: minute || 0,
+ second,
lat: Number(birth.lat) || 0,
lon: Number(birth.lon) || 0,
tz: resolveTimezoneValue(birth.tz),
@@ -1574,6 +1712,7 @@ function renderTrustCenterPanel() {
${renderRuntimeHealthPanel(runtime)}
${renderValidationTransparencyPanel()}
+ ${renderRealCaseRevalidationPanel()}
${renderTerminologyModePanel()}
Packaging preflight: python3 scripts/desktop_packaging_preflight.py。Pake 适合快速 URL 壳;Tauri shell with sidecar 适合后续一键启动本地 API 服务。
`;
}
+function renderStaticDemoBoundary() {
+ return `
+