From 96caaeae2c97da48798e9664265e65d077685e36 Mon Sep 17 00:00:00 2001
From: 732642856 <732642856@qq.com>
Date: Thu, 25 Jun 2026 23:23:56 +0800
Subject: [PATCH] feat: add oracle evidence safeguards
---
.gitignore | 2 +
README.md | 14 ++
docs/user_jhora_capture_guide.md | 199 ++++++++++++++++
jyotish-app/main.js | 133 ++++++++++-
jyotish-app/public/api-bridge.js | 5 +
jyotish-app/style.css | 82 ++++++-
progress.md | 22 ++
references/oracle/artifacts/.gitkeep | 1 +
references/oracle/artifacts/README.md | 34 +++
references/oracle/ashtakoot_oracle_cases.json | 195 +++++++++++++++
.../jhora_steve_jobs_lahiri_first_packet.json | 115 +++++++++
scripts/jyotish_api_server.py | 13 +
scripts/jyotish_engine.py | 16 ++
scripts/oracle_collection_queue.py | 23 ++
scripts/oracle_evidence_validator.py | 82 +++++++
task_plan.md | 6 +-
tests/test_cli_smoke.py | 7 +
tests/test_frontend_productization.py | 127 ++++++++++
tests/test_oracle_collection_queue.py | 61 +++++
tests/test_oracle_evidence_validator.py | 225 ++++++++++++++++--
20 files changed, 1337 insertions(+), 25 deletions(-)
create mode 100644 docs/user_jhora_capture_guide.md
create mode 100644 references/oracle/artifacts/.gitkeep
create mode 100644 references/oracle/artifacts/README.md
create mode 100644 references/oracle/ashtakoot_oracle_cases.json
create mode 100644 references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json
diff --git a/.gitignore b/.gitignore
index 963df2af..8981cbd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@ htmlcov/
benchmarks/jyotish/outputs/
output_report.txt
results_extracted.md
+runtime-smoke-report-*.html
+jyotish-app/runtime-smoke-report-*.html
diff --git a/README.md b/README.md
index d1ebc066..7554966b 100644
--- a/README.md
+++ b/README.md
@@ -131,8 +131,22 @@ python3 scripts/oracle_collection_queue.py \
该 JSON 的 scope 是 `external_oracle_collection_queue`。当前队列有 5 个 `template_only` 任务、`ready_for_calibration: 0`、`production_tuning_allowed: false`,说明只能继续采集 JHora/PyJHora/VedAstro 等外部黑盒目标值;在模板字段未填充、状态未升为 `external_verified` 前,不能用这些样本做 Dasha/Shadbala 生产调参。
+Ashtakoot 外部合婚 oracle 使用同一个队列生成器,但独立样本文件是 `references/oracle/ashtakoot_oracle_cases.json`:
+
+```bash
+python3 scripts/oracle_collection_queue.py \
+ --oracle-file references/oracle/ashtakoot_oracle_cases.json \
+ --format json
+```
+
+该队列同样保持 `ready_for_calibration: 0`,用于采集 `ashtakoot_36_point` 外部合婚目标值,而不是重写现有 `scripts/ashtakoot.py` 算法。每条样本要补齐 `target.total_score`、`target.varna`、`target.vashya`、`target.tara`、`target.yoni`、`target.graha_maitri`、`target.gana`、`target.bhakoot`、`target.nadi`、`target.kuja_status`,并保留 JHora/VedAstro/AstroSage 等外部截图或 API artifact。
+
每个队列任务还包含 `evidence_packet.capture_id` 草稿证据包。人工或副手录入外部真值时,必须至少填写 `tool_name`、`tool_version_or_url`、`capture_date`、`source_artifact`、`ayanamsa`、`node_mode`、`timezone`、`operator_note`,并保留截图、API 响应或 stdout 等外部 artifact;不得把本仓库本地计算输出当作 `source_artifact`。
+外部截图和 stdout 片段统一存入 `references/oracle/artifacts/`,证据包里的 `source_artifact` 必须使用该目录下的 repo-relative 路径或明确标注的外部审阅位置。所有私人截图必须打码;不得提交私人 PDF 原件、不得提交完整出生报告,也不得提交浏览器 scratch 目录或含账号会话/cookie/token/桌面通知的截图。
+
+第一条 JHora/PyJHora 黑盒证据采集按 `docs/user_jhora_capture_guide.md` 执行:优先使用 Steve Jobs 或合成样本,采集 Moon sidereal longitude、Vimshottari start date 与 Shadbala 七曜六分量,保存到 `references/oracle/artifacts/` 后再运行 evidence validator。
+
外部目标字段采用 `target_fields` + `target_placeholders` 双层结构:`target_fields` 固定记录该案例需要校验的目标,例如 `target.moon_sidereal_longitude_deg`、`target.vimshottari_start_date`、`target.shadbala_components`;当这些字段被真实外部来源填入并且证据包状态升为 `external_verified` 后,队列生成器会保留这些值,不会再把它们降级成 `draft`。这保证了“人工/JHora/PyJHora/VedAstro 采集 → JSON 填写 → 队列生成 → validator 复核”的路径可复验。
当外部证据包被填写回队列 JSON 后,用证据验证器做第二层防线:
diff --git a/docs/user_jhora_capture_guide.md b/docs/user_jhora_capture_guide.md
new file mode 100644
index 00000000..ad83cb6d
--- /dev/null
+++ b/docs/user_jhora_capture_guide.md
@@ -0,0 +1,199 @@
+# JHora/PyJHora External Evidence Capture Guide
+
+This guide turns the first Dasha/Shadbala oracle sample from a draft into a
+reviewable external evidence packet. It is for manual black-box capture only:
+do not copy JHora/PyJHora implementation code, formulas, constants, or private
+reports into this repository.
+
+## Safe Case Choice
+
+Start with one of these public or synthetic tasks:
+
+- `template_steve_jobs_dasha_lahiri`: public-figure Dasha boundary sample.
+- `template_user_REDACTED_YEAR_moon_longitude_lahiri`: user-template sample; use only if
+ the screenshot is heavily redacted.
+
+Avoid private client material for the first pass. If a private screenshot is
+unavoidable, it 必须打码 before it enters `references/oracle/artifacts/`.
+
+## Privacy Rules
+
+- Store evidence only under `references/oracle/artifacts/`.
+- `source_artifact` must be a repo-relative path such as
+ `references/oracle/artifacts/jhora_steve_jobs_dasha_lahiri_v1.png`.
+- 必须打码 names, addresses, account names, notifications, file paths, browser
+ tabs, and any personally identifying text.
+- 不得提交私人 PDF 原件.
+- 不得提交完整出生报告.
+- 不得提交浏览器 scratch folders, cookies, account sessions, tokens, desktop
+ notifications, or downloaded temp folders.
+
+## External Tool Setup
+
+For JHora:
+
+1. Open a clean chart in JHora.
+2. Enter the exact date, time, timezone, latitude, and longitude from
+ `references/oracle/dasha_shadbala_oracle_cases.json`.
+3. Set ayanamsa explicitly. The first pass should use Lahiri; later samples
+ should repeat with Raman and KP.
+4. Record whether the tool is using mean node or true node. The packet metadata
+ must write this exactly as `mean node` or `true node`.
+5. Keep the tool version, build date, or download URL visible in notes if
+ possible.
+
+For PyJHora:
+
+1. Use an isolated environment and treat output as a black-box external
+ reference.
+2. Record the package version and command or script path in operator notes.
+3. Capture stdout as a text artifact only if it contains the requested target
+ values and no private data.
+4. Because PyJHora license metadata can vary by source, use it as an external
+ behavior oracle unless a separate license review approves direct reuse.
+
+## Required Screenshots Or Stdout
+
+Capture enough external evidence to fill every requested target field.
+
+### Moon sidereal longitude
+
+Required for `template_user_REDACTED_YEAR_moon_longitude_lahiri`.
+
+- Capture Moon sidereal longitude in absolute 0-360 degrees if available.
+- Also capture sign-local degrees/minutes/seconds when the tool shows it.
+- Record ayanamsa, timezone, and node mode beside the value.
+
+### Vimshottari start date
+
+Required for `template_user_REDACTED_YEAR_moon_longitude_lahiri`,
+`template_steve_jobs_dasha_lahiri`, and
+`template_historical_epoch_lahiri`.
+
+- Capture the Vimshottari start date or first Mahadasha boundary shown by the
+ external tool.
+- Write it as ISO date in `target.vimshottari_start_date`, for example
+ `1986-05-18`.
+- If the tool uses a visible year length, DST, or local sunrise policy, record
+ that in `operator_note`.
+
+### Shadbala 七曜六分量
+
+Required for every task whose target includes `shadbala_components`.
+
+The packet must include all seven planets:
+
+- Sun
+- Moon
+- Mars
+- Mercury
+- Jupiter
+- Venus
+- Saturn
+
+Each planet must include all six components:
+
+- `sthana`
+- `dig`
+- `kala`
+- `chesta`
+- `naisargika`
+- `drik`
+
+Do not fill only a total. Do not apply a global scaling factor. If the tool
+shows Rupa and Virupa, record the unit in `operator_note` and preserve the
+component numbers exactly as read from the screenshot or stdout.
+
+## Artifact Naming
+
+Use short, stable names:
+
+- `references/oracle/artifacts/jhora_steve_jobs_lahiri_dasha_v1.png`
+- `references/oracle/artifacts/jhora_steve_jobs_lahiri_shadbala_v1.png`
+- `references/oracle/artifacts/pyjhora_steve_jobs_lahiri_stdout_v1.txt`
+- `references/oracle/artifacts/jhora_user_REDACTED_YEAR_lahiri_redacted_v1.png`
+
+If a screenshot is corrected, keep the old artifact only if it is already
+referenced by a packet; otherwise replace the draft before review.
+
+## Packet Fill Procedure
+
+Generate the current queue:
+
+```bash
+python3 scripts/oracle_collection_queue.py \
+ --oracle-file references/oracle/dasha_shadbala_oracle_cases.json \
+ --format json > /tmp/jyotish_oracle_queue_capture.json
+```
+
+Choose the task matching the external tool output, then fill:
+
+- `evidence_packet.status`: `external_verified`
+- `evidence_packet.metadata.tool_name`: `JHora` or `PyJHora`
+- `evidence_packet.metadata.tool_version_or_url`
+- `evidence_packet.metadata.capture_date`
+- `evidence_packet.metadata.source_artifact`
+- `evidence_packet.metadata.ayanamsa`: `Lahiri`, `Raman`, or `KP`
+- `evidence_packet.metadata.node_mode`: `mean node` or `true node`
+- `evidence_packet.metadata.timezone`
+- `evidence_packet.metadata.operator_note`
+- `evidence_packet.target_placeholders.target.moon_sidereal_longitude_deg`
+- `evidence_packet.target_placeholders.target.vimshottari_start_date`
+- `evidence_packet.target_placeholders.target.shadbala_components`
+
+For a valid first Shadbala row, the component object must look like this shape:
+
+```json
+{
+ "Sun": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Moon": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Mars": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Mercury": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Jupiter": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Venus": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0},
+ "Saturn": {"sthana": 0, "dig": 0, "kala": 0, "chesta": 0, "naisargika": 0, "drik": 0}
+}
+```
+
+Replace every `0` with the external value. A zero is valid only if the external
+tool literally shows zero.
+
+## Validation
+
+Run the validator against the filled queue:
+
+```bash
+python3 scripts/oracle_evidence_validator.py \
+ --queue-file /tmp/jyotish_oracle_queue_capture.json
+```
+
+The first successful sample should move the summary from `valid_packets: 0` to
+`valid_packets: 1`, and from `ready_for_calibration: 0` to
+`ready_for_calibration: 1`. This does not allow production tuning by itself; it
+only means one packet is review-ready.
+
+## Reject And Recapture Cases
+
+Reject the packet and recapture if any of these occur:
+
+- `missing_external_artifact`
+- `status_not_external_verified:draft`
+- `local_engine_artifact_rejected`
+- `missing_shadbala_component:all_planets`
+- `missing_shadbala_component:Sun.kala` or any other component-level error
+- Screenshot does not show ayanamsa.
+- Screenshot does not show whether mean node or true node was used.
+- Source artifact contains unredacted private data.
+- Source artifact is a private PDF original or complete birth report.
+- Values came from this repository's local engine instead of JHora/PyJHora or
+ another external oracle.
+
+## Minimal Recapture Checklist
+
+If the screenshot is not enough, recapture only the missing evidence:
+
+1. Tool/version and settings screen.
+2. Moon sidereal longitude screen.
+3. Vimshottari start date screen.
+4. Shadbala component table with all seven planets and six components.
+5. A short operator note explaining ayanamsa, node mode, timezone, and unit.
diff --git a/jyotish-app/main.js b/jyotish-app/main.js
index 7a8b9756..87eab203 100644
--- a/jyotish-app/main.js
+++ b/jyotish-app/main.js
@@ -894,8 +894,13 @@ function renderAIPromptPackPanel(cd) {
AI Prompt Pack
${escapeHtml(pack.mode || 'jyotish_structured_prompt_pack')}
- schema v${escapeHtml(String(pack.schema_version || 1))}
+
+ 复制 Prompt
+ 复制 Evidence
+ schema v${escapeHtml(String(pack.schema_version || 1))}
+
+ ${renderAyanamsaRuntimeStatus(cd, pack)}
Ayanamsa
@@ -945,6 +950,79 @@ function renderAIPromptPackPanel(cd) {
`;
+ setupAIPromptPackActions(host, cd, pack);
+}
+
+function renderAyanamsaRuntimeStatus(cd, pack) {
+ const settings = readCalculationSettings();
+ const boundary = cd?._calculation_boundary || {};
+ const evidenceAyanamsa = pack?.evidence_snapshot?.ayanamsa || {};
+ const requested = evidenceAyanamsa.name || settings.ayanamsa;
+ const applied = boundary.appliedAyanamsa || evidenceAyanamsa.name || requested;
+ const status = cd?._fallback ? 'browser_fallback' : 'backend_computed';
+ const label = status === 'backend_computed' ? '后端实算' : '浏览器降级';
+ const note = boundary.note || (
+ status === 'backend_computed'
+ ? '当前 Ayanamsa 来自本地 API 服务返回,可随导出和 AI Prompt Pack 追溯。'
+ : '浏览器 fallback 只能使用 Lahiri 兼容路径;Raman/KP 等需启动本地 API 服务。'
+ );
+ return `
+
+ ${escapeHtml(label)}
+ ${escapeHtml(getCalculationSettingLabel('ayanamsa', requested))}
+ ${escapeHtml(`applied=${applied} · status=${status}`)}
+ ${escapeHtml(note)}
+
+ `;
+}
+
+function setupAIPromptPackActions(host, cd, pack) {
+ host.querySelectorAll('[data-ai-prompt-action]').forEach(button => {
+ button.addEventListener('click', async () => {
+ const action = button.getAttribute('data-ai-prompt-action');
+ const copied = await copyAIPromptPackSection(action, cd, pack);
+ button.textContent = copied ? '已复制' : '复制失败';
+ setTimeout(() => {
+ button.textContent = action === 'copy-evidence' ? '复制 Evidence' : '复制 Prompt';
+ }, 1400);
+ });
+ });
+}
+
+async function copyAIPromptPackSection(action, cd, pack) {
+ const payload = action === 'copy-evidence'
+ ? {
+ evidence_snapshot: pack?.evidence_snapshot || {},
+ retrieval_plan: pack?.retrieval_plan || {},
+ calculation_boundary: cd?._calculation_boundary || null,
+ }
+ : {
+ prompt_zh: pack?.prompt_zh || '',
+ mode: pack?.mode || 'jyotish_structured_prompt_pack',
+ schema_version: pack?.schema_version || 1,
+ };
+ const text = [
+ 'AI Prompt Pack 审计上下文已复制',
+ JSON.stringify(payload, null, 2),
+ ].join('\n');
+ try {
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(text);
+ return true;
+ }
+ const area = document.createElement('textarea');
+ area.value = text;
+ area.setAttribute('readonly', 'true');
+ area.style.position = 'fixed';
+ area.style.left = '-9999px';
+ document.body.appendChild(area);
+ area.select();
+ const ok = document.execCommand('copy');
+ document.body.removeChild(area);
+ return ok;
+ } catch {
+ return false;
+ }
}
function normalizeAIPromptPack(cd = {}) {
@@ -981,6 +1059,7 @@ function normalizeAIPromptPack(cd = {}) {
strength: {
shadbala_ranking: Object.entries(cd.shadbala || {}).map(([planet, pdata]) => ({ planet, rupas: pdata?.rupas, level: pdata?.level })),
},
+ oracle_progress: oracleProgressSnapshot(),
},
retrieval_plan: {
local_reference_docs: [
@@ -993,6 +1072,20 @@ function normalizeAIPromptPack(cd = {}) {
};
}
+function oracleProgressSnapshot() {
+ return {
+ scope: 'external_oracle_evidence_validation',
+ collection_queue: 'external_oracle_collection_queue',
+ total_packets: 5,
+ valid_packets: 0,
+ ready_for_calibration: 0,
+ production_tuning_allowed: false,
+ artifact_policy: 'references/oracle/artifacts/',
+ promotion_rule: 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
+ boundary: 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.',
+ };
+}
+
function formatCompactDegree(value) {
const num = Number(value);
return Number.isFinite(num) ? `${num.toFixed(2)}°` : '-';
@@ -1827,6 +1920,7 @@ function renderOracleEvidenceIntakePanel() {
Oracle Evidence Intake
下载 5 个外部真值空白包;只接受 JHora/PyJHora/VedAstro 黑盒证据,禁止本地输出冒充 external_verified。
+ ${renderOracleEvidenceProgressDashboard()}
${ORACLE_EVIDENCE_INTAKE_TASKS.map(task => `
@@ -1859,6 +1953,38 @@ function renderOracleEvidenceIntakePanel() {
`;
}
+function renderOracleEvidenceProgressDashboard() {
+ const totalPackets = ORACLE_EVIDENCE_INTAKE_TASKS.length;
+ const validPackets = 0;
+ const readyPackets = 0;
+ const shadbalaTargets = ORACLE_EVIDENCE_INTAKE_TASKS
+ .filter(task => task.targetFields.includes('shadbala_components')).length;
+ const dashaTargets = ORACLE_EVIDENCE_INTAKE_TASKS
+ .filter(task => task.targetFields.includes('vimshottari_start_date')).length;
+ const progress = totalPackets ? Math.round((validPackets / totalPackets) * 100) : 0;
+ return `
+
+
+ Dasha/Shadbala 真实进度
+ ${validPackets} / ${totalPackets} valid_packets · ${readyPackets} ready_for_calibration · production_tuning_allowed=false
+
+
+
+
+
+ ${renderValidationTransparencyMetric('Evidence', `${validPackets} / ${totalPackets}`, '0 / 5 外部证据包通过前,生产调参保持关闭。')}
+ ${renderValidationTransparencyMetric('Dasha', `${dashaTargets} targets`, 'vimshottari_start_date 需要 JHora/PyJHora 黑盒对照。')}
+ ${renderValidationTransparencyMetric('Shadbala', `${shadbalaTargets} targets`, 'missing_shadbala_component 会拦截缺 sthana/dig/kala/chesta/naisargika/drik 的七曜分量。')}
+ ${renderValidationTransparencyMetric('Artifacts', 'references/oracle/artifacts/', 'source_artifact 必须指向脱敏后的外部截图或 stdout。')}
+
+
+ 隐私
+ 私人截图必须打码;不得上传私人 PDF 原件、完整出生报告或浏览器 scratch 会话文件。
+
+
+ `;
+}
+
function buildOracleEvidencePacket(task) {
const target = {};
task.targetFields.forEach(field => {
@@ -1895,7 +2021,8 @@ function buildOracleEvidencePacket(task) {
},
operator_checklist: [
'Fill tool_name and tool_version_or_url from the external product.',
- 'Attach source_artifact as a screenshot/export path outside this local engine.',
+ 'Attach source_artifact as a repo-relative references/oracle/artifacts/... path after 必须打码.',
+ 'Do not attach private PDF originals, complete birth reports, browser scratch folders, account sessions, cookies, tokens, or desktop notifications.',
'Fill every target value from black-box output before requesting review.',
],
};
@@ -1911,7 +2038,7 @@ function downloadOracleEvidencePacket(caseId) {
const packet = buildOracleEvidencePacket(task);
downloadText(JSON.stringify(packet, null, 2), `jyotish-oracle-evidence-${task.caseId}.json`, 'application/json;charset=utf-8');
if (status) {
- status.textContent = `已下载 ${task.caseId} 空白证据包;填入外部截图和数值后再运行 evidence validator。`;
+ status.textContent = `已下载 ${task.caseId} 空白证据包;source_artifact 请指向 references/oracle/artifacts/ 下已打码文件,再运行 evidence validator。`;
}
}
diff --git a/jyotish-app/public/api-bridge.js b/jyotish-app/public/api-bridge.js
index cc3c1264..e6fe9b50 100644
--- a/jyotish-app/public/api-bridge.js
+++ b/jyotish-app/public/api-bridge.js
@@ -172,6 +172,10 @@ async function generateReportArtifact(payload) {
return postJson('/api/report_artifact', payload);
}
+async function validateOracleEvidence(payload) {
+ return postJson('/api/oracle_evidence', payload);
+}
+
async function computeThematicReport(payload) {
return postJson('/api/thematic_report', payload);
}
@@ -437,6 +441,7 @@ window.JyotishAPI = {
computeRelationship,
importChart,
generateReportArtifact,
+ validateOracleEvidence,
computeThematicReport,
getAPIHealth,
getCapabilityAudit,
diff --git a/jyotish-app/style.css b/jyotish-app/style.css
index 125c15d8..8d81fdb6 100644
--- a/jyotish-app/style.css
+++ b/jyotish-app/style.css
@@ -5217,6 +5217,64 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t
font-size: 12px;
font-style: normal;
}
+.ai-prompt-pack-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 6px;
+ min-width: 0;
+}
+.ai-prompt-pack-actions button {
+ min-height: 30px;
+ padding: 5px 9px;
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-sm);
+ background: var(--bg-surface);
+ color: var(--text-body);
+ font-size: 12px;
+ cursor: pointer;
+}
+.ai-prompt-pack-actions button:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+.ayanamsa-runtime-status {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 4px 10px;
+ align-items: center;
+ margin: 12px 0;
+ padding: 10px 12px;
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-sm);
+ background: var(--bg-surface);
+}
+.ayanamsa-runtime-status span {
+ padding: 3px 7px;
+ border-radius: var(--radius-sm);
+ background: rgba(32, 111, 83, 0.12);
+ color: var(--accent);
+ font-size: 11px;
+ font-weight: 800;
+}
+.ayanamsa-runtime-status-browser_fallback span {
+ background: rgba(160, 98, 0, 0.13);
+ color: #8a5600;
+}
+.ayanamsa-runtime-status strong {
+ min-width: 0;
+ color: var(--text-heading);
+ font-size: 13px;
+ overflow-wrap: anywhere;
+}
+.ayanamsa-runtime-status small,
+.ayanamsa-runtime-status em {
+ grid-column: 1 / -1;
+ color: var(--text-muted);
+ font-size: 11px;
+ font-style: normal;
+ overflow-wrap: anywhere;
+}
.ai-prompt-pack-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -5884,6 +5942,28 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t
border-radius: var(--radius-sm);
background: var(--bg-surface);
}
+.oracle-evidence-progress-dashboard {
+ display: grid;
+ gap: 10px;
+ margin-top: 8px;
+ padding: 10px;
+ border: 1px solid rgba(21, 94, 117, 0.18);
+ border-radius: var(--radius-sm);
+ background: rgba(236, 254, 255, 0.74);
+}
+.oracle-evidence-progress-bar {
+ position: relative;
+ overflow: hidden;
+ height: 8px;
+ border-radius: 999px;
+ background: rgba(148, 163, 184, 0.22);
+}
+.oracle-evidence-progress-bar span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--blue), var(--green));
+}
.oracle-evidence-intake-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -6522,9 +6602,9 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t
.first-use-grid,
.runtime-health-grid,
.trust-status-grid,
- .oracle-evidence-intake-grid,
.terminology-mode-options,
.case-workspace-controls { grid-template-columns: 1fr; }
+ .oracle-evidence-intake-grid { grid-template-columns: 1fr; }
.static-demo-boundary-grid { grid-template-columns: 1fr; }
.validation-transparency-grid { grid-template-columns: 1fr; }
.case-workspace-counts,
diff --git a/progress.md b/progress.md
index 0543093c..9e608e6b 100644
--- a/progress.md
+++ b/progress.md
@@ -356,3 +356,25 @@
- 完成副手任务升级:新增 Round 13/14/15 工作单;Round 15 改为 6 个并行包,覆盖 Evidence 判卷闭环、Shadbala 六分量、Dasha 边界日期、高需求技法、外部截图存档规范与下一轮 Codex 任务建议。
- 验证完成:`python3 -B -m pytest tests/test_frontend_productization.py::test_trust_center_exposes_oracle_evidence_intake_cards tests/test_frontend_productization.py::test_trust_center_and_ai_expose_dasha_shadbala_calibration_status tests/test_frontend_productization.py::test_provenance_panchanga_workspace_panel_is_productized tests/test_api_server_security.py::test_oracle_evidence_api_validates_uploaded_packets tests/test_oracle_collection_queue.py tests/test_oracle_evidence_validator.py -q` 通过;`python3 scripts/oracle_collection_queue.py ...` + `python3 scripts/oracle_evidence_validator.py ...` 仍显示 `valid_packets: 0` / `ready_for_calibration: 0`;`npm run build --prefix jyotish-app` 通过;`git diff --check` 通过。
- 当前下一最高优先级:创建 `references/oracle/artifacts/` 存档规范和第一条真实 JHora/PyJHora 黑盒证据包,优先推动 Shadbala 六分量与 Vimshottari Dasha 边界日期从 0/5 进入至少 1/5 可人工复核状态。
+- 完成前端 Multi-Ayanamsa 与 AI Prompt Pack 可视化补强:完整解盘页 `AI Prompt Pack` 面板新增 Ayanamsa runtime status,区分 `backend_computed` 与 `browser_fallback`,显示 requested/applied ayanamsa 与降级说明;新增“复制 Prompt / 复制 Evidence”按钮,复制内容包含 prompt、evidence_snapshot、retrieval_plan 与 calculation_boundary。
+- TDD/验证完成:先让 `test_frontend_ayanamsa_settings_are_live_api_parameters` 和 `test_ai_prompt_pack_panel_exposes_copyable_audit_context` 红灯,随后实现转绿;`python3 -m pytest -q tests/test_frontend_productization.py -k "ayanamsa_settings or ai_prompt_pack_panel or ai_chat_prefers"` 通过;`npm run build --prefix jyotish-app` 通过;`git diff --check` 通过。
+- 当前下一最高优先级:继续 `references/oracle/artifacts/` 存档规范与第一条 JHora/PyJHora 黑盒证据包采集说明,和 Antigravity Round 16 的 artifact storage / Shadbala component validator / first JHora sample checklist 对齐。
+- 完成 Antigravity Round 18 重型副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round18_2026_06_25.md`,要求副手至少产出 12 份报告,覆盖当前补丁复核、全网开源许可证矩阵、功能缺口重排、用户黑盒流程、隐私仓库卫生、测试债和 AI Prompt Pack/Skill 同步,明确只允许宽松许可证代码进入可复用候选。
+- 完成 Oracle artifact 存档规范:新增 `references/oracle/artifacts/.gitkeep` 与 `references/oracle/artifacts/README.md`,README 同步 `references/oracle/artifacts/`、`source_artifact`、`external_oracle_artifact`、必须打码、不得提交私人 PDF 原件、不得提交完整出生报告、不得提交浏览器 scratch 的规则;前端 Evidence Packet 下载文案同步相对路径与脱敏要求。
+- 完成 Shadbala evidence 强校验:`scripts/oracle_evidence_validator.py` 新增 `SHADBALA_REQUIRED_PLANETS` 与 `SHADBALA_REQUIRED_COMPONENTS`,对 Sun/Moon/Mars/Mercury/Jupiter/Venus/Saturn 七曜的 sthana/dig/kala/chesta/naisargika/drik 六分量逐项拦截;测试覆盖空 dict、只填 Sun、完整七曜六分量和 API 上传复用 validator。
+- 完成 Trust Center 真实采集进度面板:`renderOracleEvidenceProgressDashboard()` 在 Oracle Evidence Intake 顶部展示 `Dasha/Shadbala 真实进度`、`0 / 5`、`valid_packets`、`ready_for_calibration`、`production_tuning_allowed=false`、`references/oracle/artifacts/` 与 `missing_shadbala_component` 边界,避免用户误以为 Dasha/Shadbala 已完成外部绝对值校准。
+- 修复 Round 17 暴露的门禁失败:移动端布局测试失败根因是 CSS selector 组被新增 `.oracle-evidence-intake-grid` 打断,已恢复原断言组并单独声明 oracle intake 移动单列;`jyotish-app/public/api-bridge.js` 已同步 `validateOracleEvidence`,确保 public/static bridge 与源码 bridge 一致。
+- 验证完成:`python3 -m pytest -q tests/test_frontend_productization.py::test_mobile_layout_keeps_dense_sections_single_column tests/test_frontend_productization.py::test_trust_center_exposes_oracle_evidence_intake_cards tests/test_frontend_productization.py::test_oracle_artifact_storage_policy_is_documented` 通过;`python3 -m pytest -q tests/test_api_server_security.py::test_oracle_evidence_api_validates_uploaded_packets tests/test_oracle_evidence_validator.py` 通过;`python3 -m pytest tests/test_frontend_productization.py tests/test_cli_smoke.py tests/test_api_server_security.py tests/test_jaimini.py tests/test_shadbala_complete.py tests/test_transit_trigger.py tests/test_oracle_collection_queue.py tests/test_oracle_evidence_validator.py -q` 238 项通过;`npm run build --prefix jyotish-app` 通过;`python3 scripts/run_quality_gate.py --profile quick --skip-yoga-logic` 通过,包含 runtime smoke。
+- 当前下一最高优先级:把第一条 JHora/PyJHora 黑盒 evidence packet 教程/模板落成可交给人工执行的文档,并等待外部截图或 stdout 后把 `valid_packets` 从 0/5 推到 1/5;同时接收 Round 18 副手报告,优先吸收宽松许可证可复用候选和真实用户流程缺口。
+- 完成 Antigravity Round 19 副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round19_2026_06_25.md`,要求副手以当前工作树重新复核 Round 18 过期结论、JHora/PyJHora 教程、第一条 evidence packet 模板、1/5 人工 runbook、runtime artifact 卫生、progress dashboard DOM/UX、Top 30 开源许可证复筛、Ashtakoot 破冰、Shadbala 单位校验、AI Prompt Pack oracle progress 和 Git tracking 策略。
+- 完成第一条外部证据包执行资产:新增 `docs/user_jhora_capture_guide.md` 与 `references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json`;模板保持 `status=draft`,只预留 Steve Jobs Lahiri/true node 的 metadata、Vimshottari start date 和 Shadbala 七曜六分量空位,不伪造任何外部数值。
+- 验证完成:`python3 -m pytest -q tests/test_oracle_collection_queue.py::test_first_jhora_evidence_packet_template_is_safe_and_fillable tests/test_frontend_productization.py::test_first_jhora_capture_guide_is_actionable` 通过;`python3 -m json.tool references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json` 通过。
+- 当前下一最高优先级:接收 Round 19 副手报告并继续修正其发现;并在没有人工 JHora/PyJHora 截图前,优先补 `.gitignore` runtime-smoke HTML 卫生、Shadbala 数值单位/类型校验和 AI Prompt Pack 的 oracle progress 摘要。
+- 完成 Round 19 反馈的无需人工 P0/P1 修复:`.gitignore` 新增 `runtime-smoke-report-*.html` 与 `jyotish-app/runtime-smoke-report-*.html`,避免 runtime smoke HTML 上云;`oracle_evidence_validator.py` 对 Shadbala 七曜六分量增加 int/float 且非 bool、非负校验,拒绝字符串数字和负数;`jyotish_engine.py`、`jyotish_api_server.py` 与前端 fallback `normalizeAIPromptPack()` 的 `evidence_snapshot.oracle_progress` 均携带 `valid_packets: 0`、`ready_for_calibration: 0`、`production_tuning_allowed: false`、`references/oracle/artifacts/` 与 `external_verified` 晋级规则。
+- TDD/验证完成:新增红灯测试 `test_runtime_smoke_html_artifacts_are_ignored`、`test_validator_rejects_non_numeric_or_negative_shadbala_components`、CLI prompt pack `oracle_progress` 断言和前端 fallback 静态断言;修复后 `python3 -m pytest -q tests/test_frontend_productization.py::test_frontend_branded_avatar_and_prompt_pack_are_productized tests/test_frontend_productization.py::test_api_bridge_variants_prefer_backend_prompt_pack_context tests/test_frontend_productization.py::test_runtime_smoke_html_artifacts_are_ignored` 通过;`python3 -m pytest -q tests/test_cli_smoke.py::test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack tests/test_oracle_evidence_validator.py tests/test_api_server_security.py::test_oracle_evidence_api_validates_uploaded_packets` 通过;`npm run build --prefix jyotish-app` 通过;`python3 scripts/run_quality_gate.py --profile quick --skip-yoga-logic` 通过,当前 quick 集合 242 项 pytest、build 与 runtime smoke 均通过。
+- 当前下一最高优先级:在无法本机生成真实 JHora/PyJHora 外部截图的前提下,推进 Ashtakoot 合婚破冰与外部 oracle template 设计;同时准备把 Round 16-19 副手报告、work orders、artifact policy、capture guide 与 first packet template 纳入 Git 跟踪,避免不同窗口再次遗漏。
+- 完成 Antigravity Round 20 副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round20_2026_06_25.md`,要求副手纠正 Round 19 “Ashtakoot 完全缺失”的过期结论,复核已有 `scripts/ashtakoot.py`、`tests/test_ashtakoot.py`、`jyotish_engine.py ashtakoot`、`/api/synastry` 与前端合盘入口;同时继续审计 Round 19 修复、Ashtakoot oracle case 设计、UI/E2E 流程、JHora 1/5 人工监督、Git 纳入策略、开源 Ashtakoot 许可证矩阵、Shadbala 单位/总分设计和 AI Prompt Pack oracle progress。
+- 当前下一最高优先级:先不重写 Ashtakoot 已有算法,转为补 Ashtakoot 外部 oracle cases、E2E 用户流程和 Git tracking 收口;真实 JHora/PyJHora 1/5 仍等待人工外部工具。
+- 完成 Antigravity Round 21 重型副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round21_2026_06_25.md`,把副手工作量扩到 18 个报告包、至少 20 个联网开源/产品复核对象、Top 40 ROI 任务,并明确 Ashtakoot oracle、VedAstro/RaviKarrii/pyhora2 许可证深挖、Trust Center UX、Git 入库、Shadbala 二期校验和 Round 22 执行计划。
+- 完成 Ashtakoot 外部 oracle 第一层队列与 validator 强化:新增 `references/oracle/ashtakoot_oracle_cases.json` 5 条 draft cases,`oracle_collection_queue.py` 可输出 `target_modules=["ashtakoot"]`,README 记录 `Ashtakoot 外部合婚 oracle` 命令;`oracle_evidence_validator.py` 增加 36 分制范围、8 Kuta 分项范围和总分求和一致性拦截,拒绝 99 分、字符串/非法范围与 sum mismatch 的假外部证据包。
+- 验证完成:`python3 -m pytest -q tests/test_oracle_evidence_validator.py tests/test_oracle_collection_queue.py` 14 项通过;`python3 -m pytest -q tests/test_ashtakoot.py tests/test_frontend_productization.py::test_dasha_reference_audit_is_documented_and_gated` 53 项通过;`npm run build --prefix jyotish-app` 通过;Ashtakoot queue 输出 5 条 template-only 样本且 `production_tuning_allowed=false`。
diff --git a/references/oracle/artifacts/.gitkeep b/references/oracle/artifacts/.gitkeep
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/references/oracle/artifacts/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/references/oracle/artifacts/README.md b/references/oracle/artifacts/README.md
new file mode 100644
index 00000000..1f5204ae
--- /dev/null
+++ b/references/oracle/artifacts/README.md
@@ -0,0 +1,34 @@
+# External Oracle Artifacts
+
+This directory stores sanitized evidence artifacts referenced by `source_artifact`
+inside external oracle evidence packets.
+
+## Required Path Policy
+
+- Use repo-relative paths such as `references/oracle/artifacts/jhora_REDACTED_YEAR_moon_lahiri_v1.png`.
+- `source_artifact` must point to a file in `references/oracle/artifacts/` or to a documented external review location.
+- Mark the artifact kind as `external_oracle_artifact` in operator notes when possible.
+
+## Privacy Rules
+
+- 必须打码 any real person's full name, contact details, exact street address, hospital, account name, or other identifying text.
+- 不得提交私人 PDF 原件;only sanitized screenshots or typed stdout snippets belong here.
+- 不得提交完整出生报告 or full private reading transcripts.
+- 不得提交浏览器 scratch folders, screenshots containing account sessions, cookies, tokens, or desktop notifications.
+- Prefer synthetic fixtures, public figures, or heavily redacted screenshots.
+
+## Allowed Examples
+
+- Redacted JHora screenshot showing settings, Moon longitude, Dasha boundary, or Shadbala component table.
+- Redacted PyJHora stdout captured in a plain text file with command metadata.
+- Public-figure sample screenshot where source settings are visible and no private account data appears.
+
+## Not Allowed
+
+- Unredacted volunteer/client birth data.
+- `output_report.txt`, `results_extracted.md`, private PDFs, or full generated readings.
+- Local engine output from this repository pretending to be an external oracle.
+
+Evidence packets remain `draft` until their target values are filled, their
+artifact is reviewed, and their status is explicitly promoted to
+`external_verified`.
diff --git a/references/oracle/ashtakoot_oracle_cases.json b/references/oracle/ashtakoot_oracle_cases.json
new file mode 100644
index 00000000..4b99b774
--- /dev/null
+++ b/references/oracle/ashtakoot_oracle_cases.json
@@ -0,0 +1,195 @@
+{
+ "schema_version": 1,
+ "scope": "external_ashtakoot_oracle_cases",
+ "notes": [
+ "These rows are external compatibility evidence templates, not production tuning constants.",
+ "Do not promote any row to external_verified until all target values come from JHora/VedAstro/AstroSage or another documented external source.",
+ "Do not copy AGPL/GPL/commercial implementation code or proprietary screenshots beyond redacted evidence artifacts."
+ ],
+ "template_cases": [
+ {
+ "id": "ashtakoot_public_couple_lahiri_01",
+ "status": "template_only",
+ "source": "JHora/VedAstro/AstroSage compatibility screenshot",
+ "privacy": "public_or_synthetic_relationship_template",
+ "birth": {
+ "male": {
+ "year": 1955,
+ "month": 2,
+ "day": 24,
+ "hour": 19,
+ "minute": 15,
+ "second": 0,
+ "lat": 37.7749,
+ "lon": -122.4194,
+ "tz": -8
+ },
+ "female": {
+ "year": 1958,
+ "month": 8,
+ "day": 14,
+ "hour": 12,
+ "minute": 0,
+ "second": 0,
+ "lat": 34.0522,
+ "lon": -118.2437,
+ "tz": -8
+ }
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "true",
+ "matching_system": "ashtakoot_36_point"
+ },
+ "target": {
+ "total_score": null,
+ "varna": null,
+ "vashya": null,
+ "tara": null,
+ "yoni": null,
+ "graha_maitri": null,
+ "gana": null,
+ "bhakoot": null,
+ "nadi": null,
+ "kuja_status": null
+ },
+ "verification_note": "Use public/synthetic relationship data first; fill targets only from external compatibility output."
+ },
+ {
+ "id": "ashtakoot_synthetic_high_score_lahiri_02",
+ "status": "template_only",
+ "source": "VedAstro/JHora compatibility screenshot",
+ "privacy": "synthetic_relationship_template",
+ "birth": {
+ "male_moon_longitude_deg": 45.0,
+ "female_moon_longitude_deg": 48.0
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "matching_system": "ashtakoot_36_point",
+ "input_mode": "moon_longitude_only"
+ },
+ "target": {
+ "total_score": null,
+ "varna": null,
+ "vashya": null,
+ "tara": null,
+ "yoni": null,
+ "graha_maitri": null,
+ "gana": null,
+ "bhakoot": null,
+ "nadi": null,
+ "kuja_status": null
+ },
+ "verification_note": "Synthetic high-score style case; use an external tool that accepts Moon longitude or matching birth data."
+ },
+ {
+ "id": "ashtakoot_synthetic_nadi_dosha_03",
+ "status": "template_only",
+ "source": "JHora/AstroSage compatibility screenshot",
+ "privacy": "synthetic_relationship_template",
+ "birth": {
+ "male_moon_longitude_deg": 0.0,
+ "female_moon_longitude_deg": 180.0
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "matching_system": "ashtakoot_36_point",
+ "input_mode": "moon_longitude_only"
+ },
+ "target": {
+ "total_score": null,
+ "varna": null,
+ "vashya": null,
+ "tara": null,
+ "yoni": null,
+ "graha_maitri": null,
+ "gana": null,
+ "bhakoot": null,
+ "nadi": null,
+ "kuja_status": null
+ },
+ "verification_note": "Nadi-dosha boundary case; capture any exception or mitigation note shown by the external source."
+ },
+ {
+ "id": "ashtakoot_synthetic_bhakoot_boundary_04",
+ "status": "template_only",
+ "source": "VedAstro/JHora compatibility screenshot",
+ "privacy": "synthetic_relationship_template",
+ "birth": {
+ "male_moon_longitude_deg": 15.5,
+ "female_moon_longitude_deg": 120.0
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "matching_system": "ashtakoot_36_point",
+ "input_mode": "moon_longitude_only"
+ },
+ "target": {
+ "total_score": null,
+ "varna": null,
+ "vashya": null,
+ "tara": null,
+ "yoni": null,
+ "graha_maitri": null,
+ "gana": null,
+ "bhakoot": null,
+ "nadi": null,
+ "kuja_status": null
+ },
+ "verification_note": "Bhakoot/rashi distance boundary case; capture component score and external explanation."
+ },
+ {
+ "id": "ashtakoot_kuja_dosha_chart_05",
+ "status": "template_only",
+ "source": "JHora/AstroSage compatibility screenshot",
+ "privacy": "synthetic_relationship_template",
+ "birth": {
+ "male": {
+ "year": 1990,
+ "month": 1,
+ "day": 15,
+ "hour": 6,
+ "minute": 30,
+ "second": 0,
+ "lat": 28.6139,
+ "lon": 77.209,
+ "tz": 5.5
+ },
+ "female": {
+ "year": 1992,
+ "month": 7,
+ "day": 20,
+ "hour": 18,
+ "minute": 45,
+ "second": 0,
+ "lat": 19.076,
+ "lon": 72.8777,
+ "tz": 5.5
+ }
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "matching_system": "ashtakoot_36_point",
+ "requires_full_chart": true
+ },
+ "target": {
+ "total_score": null,
+ "varna": null,
+ "vashya": null,
+ "tara": null,
+ "yoni": null,
+ "graha_maitri": null,
+ "gana": null,
+ "bhakoot": null,
+ "nadi": null,
+ "kuja_status": null
+ },
+ "verification_note": "Full chart compatibility case; external artifact must show Kuja/Manglik status or its absence."
+ }
+ ]
+}
diff --git a/references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json b/references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json
new file mode 100644
index 00000000..673867af
--- /dev/null
+++ b/references/oracle/evidence_packet_templates/jhora_steve_jobs_lahiri_first_packet.json
@@ -0,0 +1,115 @@
+{
+ "capture_id": "external_template_steve_jobs_dasha_lahiri",
+ "status": "draft",
+ "case_id": "template_steve_jobs_dasha_lahiri",
+ "birth": {
+ "year": 1955,
+ "month": 2,
+ "day": 24,
+ "hour": 19,
+ "minute": 15,
+ "second": 0,
+ "lat": 37.7749,
+ "lon": -122.4194,
+ "tz": -8
+ },
+ "settings": {
+ "ayanamsa": "lahiri",
+ "node_mode": "true"
+ },
+ "required_metadata_fields": [
+ "tool_name",
+ "tool_version_or_url",
+ "capture_date",
+ "source_artifact",
+ "ayanamsa",
+ "node_mode",
+ "timezone",
+ "operator_note"
+ ],
+ "metadata": {
+ "tool_name": "",
+ "tool_version_or_url": "",
+ "capture_date": "",
+ "source_artifact": "references/oracle/artifacts/",
+ "ayanamsa": "Lahiri",
+ "node_mode": "true node",
+ "timezone": "UTC-08:00",
+ "operator_note": ""
+ },
+ "target_placeholders": {
+ "target.vimshottari_start_date": null,
+ "target.shadbala_components": {
+ "Sun": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Moon": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Mars": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Mercury": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Jupiter": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Venus": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ },
+ "Saturn": {
+ "sthana": null,
+ "dig": null,
+ "kala": null,
+ "chesta": null,
+ "naisargika": null,
+ "drik": null
+ }
+ }
+ },
+ "integrity_checks": {
+ "must_not_come_from_local_engine": true,
+ "requires_external_artifact": true,
+ "requires_status_external_verified_before_calibration": true,
+ "reject_global_shadbala_scaling": true
+ },
+ "promotion_status_after_fill": "external_verified",
+ "operator_checklist": [
+ "Open JHora or PyJHora as an external black-box oracle.",
+ "Capture a redacted source artifact under references/oracle/artifacts/.",
+ "Fill Vimshottari start date from the external tool.",
+ "Fill all seven Shadbala planets and all six components from the external tool.",
+ "Promote status to external_verified only after source_artifact and target values are filled."
+ ]
+}
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index 56928ea6..42d10be5 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -1817,6 +1817,17 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'不要仅凭单一配置下结论;核心判断至少交叉 D1、D9、Dasha、Shadbala/Ashtakavarga 或 Transit 中的两个证据层。',
'必须显式标注置信度和边界:Dasha/PDF 起点差异、Shadbala 外部绝对值 oracle 尚未完成时,不得声称已经完全校准。',
]
+ oracle_progress = {
+ 'scope': 'external_oracle_evidence_validation',
+ 'collection_queue': 'external_oracle_collection_queue',
+ 'total_packets': 5,
+ 'valid_packets': 0,
+ 'ready_for_calibration': 0,
+ 'production_tuning_allowed': False,
+ 'artifact_policy': 'references/oracle/artifacts/',
+ 'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
+ 'boundary': 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.',
+ }
return {
'schema_version': 1,
'mode': 'jyotish_structured_prompt_pack',
@@ -1844,6 +1855,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'quality_boundary': {
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
+ 'oracle_progress': oracle_progress,
},
'retrieval_plan': {
'local_reference_docs': [
@@ -1858,6 +1870,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'no_single_factor_conclusion',
'd1_d9_dasha_cross_validation',
'oracle_boundary_visible',
+ 'external_oracle_evidence_validation',
'confidence_labeled_reading',
],
},
diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py
index 2ab93555..9624f0b3 100644
--- a/scripts/jyotish_engine.py
+++ b/scripts/jyotish_engine.py
@@ -567,6 +567,20 @@ def _planet_snapshot(planets, planet_name):
}
+def _oracle_progress_snapshot():
+ return {
+ 'scope': 'external_oracle_evidence_validation',
+ 'collection_queue': 'external_oracle_collection_queue',
+ 'total_packets': 5,
+ 'valid_packets': 0,
+ 'ready_for_calibration': 0,
+ 'production_tuning_allowed': False,
+ 'artifact_policy': 'references/oracle/artifacts/',
+ 'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
+ 'boundary': 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.',
+ }
+
+
def _build_ai_prompt_pack(report):
"""Build a compact, evidence-first prompt pack for downstream AI/RAG reading."""
modules = report.get('modules', {}) if isinstance(report, dict) else {}
@@ -640,6 +654,7 @@ def _build_ai_prompt_pack(report):
'warnings': report.get('warnings', []),
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
+ 'oracle_progress': _oracle_progress_snapshot(),
}
prompt_lines = [
@@ -670,6 +685,7 @@ def _build_ai_prompt_pack(report):
'no_single_factor_conclusion',
'd1_d9_dasha_cross_validation',
'oracle_boundary_visible',
+ 'external_oracle_evidence_validation',
'confidence_labeled_reading',
],
},
diff --git a/scripts/oracle_collection_queue.py b/scripts/oracle_collection_queue.py
index 90275c94..9786e0b7 100644
--- a/scripts/oracle_collection_queue.py
+++ b/scripts/oracle_collection_queue.py
@@ -58,6 +58,19 @@ SOURCE_GUIDANCE = {
"The row is not derived from this repository's local Shadbala output.",
],
},
+ "ashtakoot": {
+ "preferred_sources": ["JHora compatibility screenshot", "VedAstro HTTP/API output", "AstroSage public compatibility screen"],
+ "steps": [
+ "Set both birth records or both Moon longitudes in the external compatibility tool.",
+ "Record the 36-point total score and all eight Kuta component scores.",
+ "Capture Kuja/Manglik status and any visible ayanamsa, node mode or matching settings.",
+ ],
+ "promotion_criteria": [
+ "All eight Ashtakoot component targets are filled from an external compatibility source.",
+ "The 36-point total is traceable to component scores.",
+ "The row is not derived from this repository's local synastry/ashtakoot output.",
+ ],
+ },
}
@@ -67,6 +80,16 @@ FIELD_TO_MODULE = {
"ascendant_longitude_deg": "longitude",
"vimshottari_start_date": "dasha",
"shadbala_components": "shadbala",
+ "total_score": "ashtakoot",
+ "varna": "ashtakoot",
+ "vashya": "ashtakoot",
+ "tara": "ashtakoot",
+ "yoni": "ashtakoot",
+ "graha_maitri": "ashtakoot",
+ "gana": "ashtakoot",
+ "bhakoot": "ashtakoot",
+ "nadi": "ashtakoot",
+ "kuja_status": "ashtakoot",
}
REQUIRED_EVIDENCE_METADATA_FIELDS = [
diff --git a/scripts/oracle_evidence_validator.py b/scripts/oracle_evidence_validator.py
index 49649561..78d2abd8 100644
--- a/scripts/oracle_evidence_validator.py
+++ b/scripts/oracle_evidence_validator.py
@@ -24,6 +24,29 @@ LOCAL_ENGINE_MARKERS = [
"oracle_collection_queue.py",
"oracle_boundary_audit.py",
]
+SHADBALA_REQUIRED_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
+SHADBALA_REQUIRED_COMPONENTS = ["sthana", "dig", "kala", "chesta", "naisargika", "drik"]
+ASHTAKOOT_SCORE_RANGES = {
+ "target.total_score": (0.0, 36.0),
+ "target.varna": (0.0, 1.0),
+ "target.vashya": (0.0, 2.0),
+ "target.tara": (0.0, 3.0),
+ "target.yoni": (0.0, 4.0),
+ "target.graha_maitri": (0.0, 5.0),
+ "target.gana": (0.0, 6.0),
+ "target.bhakoot": (0.0, 7.0),
+ "target.nadi": (0.0, 8.0),
+}
+ASHTAKOOT_COMPONENT_FIELDS = [
+ "target.varna",
+ "target.vashya",
+ "target.tara",
+ "target.yoni",
+ "target.graha_maitri",
+ "target.gana",
+ "target.bhakoot",
+ "target.nadi",
+]
def _resolve_path(path: str) -> str:
@@ -72,6 +95,13 @@ def _validate_packet(task: dict[str, Any]) -> dict[str, Any]:
if _is_blank(value):
problems.append(f"placeholder_unfilled:{field}")
+ shadbala_components = target_placeholders.get("target.shadbala_components")
+ if "target.shadbala_components" in expected_fields:
+ problems.extend(_validate_shadbala_components(shadbala_components))
+
+ if "target.total_score" in expected_fields:
+ problems.extend(_validate_ashtakoot_scores(target_placeholders))
+
integrity = packet.get("integrity_checks", {})
if integrity.get("must_not_come_from_local_engine") and _is_local_engine_artifact(packet):
problems.append("local_engine_artifact_rejected")
@@ -95,6 +125,58 @@ def _validate_packet(task: dict[str, Any]) -> dict[str, Any]:
}
+def _validate_shadbala_components(value: Any) -> list[str]:
+ problems: list[str] = []
+ if _is_blank(value) or not isinstance(value, dict):
+ return ["missing_shadbala_component:all_planets"]
+ for planet in SHADBALA_REQUIRED_PLANETS:
+ row = value.get(planet)
+ if not isinstance(row, dict) or _is_blank(row):
+ problems.append(f"missing_shadbala_component:{planet}")
+ continue
+ for component in SHADBALA_REQUIRED_COMPONENTS:
+ component_value = row.get(component)
+ if _is_blank(component_value):
+ problems.append(f"missing_shadbala_component:{planet}.{component}")
+ continue
+ if not isinstance(component_value, (int, float)) or isinstance(component_value, bool):
+ problems.append(f"invalid_shadbala_component_type:{planet}.{component}")
+ continue
+ if component_value < 0:
+ problems.append(f"invalid_shadbala_component_negative:{planet}.{component}")
+ return problems
+
+
+def _is_number(value: Any) -> bool:
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
+
+
+def _validate_ashtakoot_scores(target_placeholders: dict[str, Any]) -> list[str]:
+ problems: list[str] = []
+ numeric_values: dict[str, float] = {}
+ for field, (minimum, maximum) in ASHTAKOOT_SCORE_RANGES.items():
+ if field not in target_placeholders:
+ continue
+ value = target_placeholders.get(field)
+ if _is_blank(value):
+ continue
+ if not _is_number(value):
+ problems.append(f"invalid_ashtakoot_score_type:{field}")
+ continue
+ numeric_value = float(value)
+ numeric_values[field] = numeric_value
+ if numeric_value < minimum or numeric_value > maximum:
+ problems.append(f"invalid_ashtakoot_score_range:{field}")
+
+ if "target.total_score" in numeric_values and all(
+ field in numeric_values for field in ASHTAKOOT_COMPONENT_FIELDS
+ ):
+ component_sum = sum(numeric_values[field] for field in ASHTAKOOT_COMPONENT_FIELDS)
+ if abs(component_sum - numeric_values["target.total_score"]) > 0.01:
+ problems.append("ashtakoot_score_sum_mismatch")
+ return problems
+
+
def build_report(queue: dict[str, Any]) -> dict[str, Any]:
packets = [_validate_packet(task) for task in queue.get("tasks", [])]
ready_for_calibration = sum(1 for packet in packets if packet["ready_for_calibration"])
diff --git a/task_plan.md b/task_plan.md
index 3b200baf..988648b8 100644
--- a/task_plan.md
+++ b/task_plan.md
@@ -112,4 +112,8 @@
- [x] AI Native Prompt/RAG 承载层第一步:`full-reading.ai_prompt_pack` 输出证据快照、检索文档、边界约束和结构化中文提示词,供网页/app 或 skill 后端 AI 代理生成高阶解读。
- [x] Antigravity AI 副手工作单:新增 `docs/research/antigravity_sidecar_work_order_2026_06_25.md`,把 Antigravity 限定为外部 oracle 样本采集、网页/app 审计、skill 同步审计和浏览器用户流验证,避免与核心计算修改冲突。
- [x] Standalone ayanamsa 全局状态修复:新增 `scripts/ayanamsa_utils.py`,让 Transit、Solar Return、Muhurta、cmd_muhurta、Yoga 验证脚本在 `FLG_SIDEREAL` 前显式设置 ayanamsa;默认 Lahiri,调用方可显式传入 Raman/KP 等。
-- [ ] 下一步:继续扩充多来源 oracle 样本,补 JHora/PyJHora 的 Moon sidereal longitude、ayanamsa、Vimshottari 起点和 Shadbala 六分量目标值;同时把前端 Multi-Ayanamsa 设置与 `ai_prompt_pack` 可视化。
+- [x] 前端 Multi-Ayanamsa 设置与 `ai_prompt_pack` 可视化:完整解盘页显示 Ayanamsa 后端实算/浏览器降级状态,AI Prompt Pack 支持复制 Prompt 与 Evidence 审计上下文。
+- [x] Oracle artifact 存档规范、Shadbala evidence 强校验与真实进度面板:`references/oracle/artifacts/` 已有脱敏规范与占位,Trust Center 显示 `0 / 5` 真实采集进度,validator 强制 Shadbala 七曜六分量,quick quality gate 已通过。
+- [x] Ashtakoot 外部 oracle 样本队列与第一层 validator:新增 5 条外部合婚 oracle draft cases,README/队列生成器/测试同步;validator 拦截 36 分制范围、8 Kuta 分项范围和总分不一致,防止假外部证据污染。
+- [x] Antigravity Round 21 重型副手任务单:把副手任务扩展到 18 个报告包、至少 20 个联网对标对象、Top 40 ROI 拆解,继续承接 Ashtakoot、开源复用、Trust Center UX、Git 入库和 Round 22 规划。
+- [ ] 第一条 JHora/PyJHora 黑盒证据包:等待人工外部工具截图/输出,补 Moon sidereal longitude、ayanamsa、Vimshottari 起点和 Shadbala 七曜六分量目标值,把 `valid_packets` 从 0/5 推到 1/5。
diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py
index 4f835962..72aa1c96 100644
--- a/tests/test_cli_smoke.py
+++ b/tests/test_cli_smoke.py
@@ -186,6 +186,13 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert "references/ai-reading-workflow-prompt.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
assert prompt_pack["evidence_snapshot"]["ayanamsa"]["name"] == "raman"
assert prompt_pack["evidence_snapshot"]["core"]["ascendant"]["sign"] == result["chart"]["ascendant"]["sign"]
+ oracle_progress = prompt_pack["evidence_snapshot"]["oracle_progress"]
+ assert oracle_progress["scope"] == "external_oracle_evidence_validation"
+ assert oracle_progress["valid_packets"] == 0
+ assert oracle_progress["ready_for_calibration"] == 0
+ assert oracle_progress["production_tuning_allowed"] is False
+ assert oracle_progress["artifact_policy"] == "references/oracle/artifacts/"
+ assert "external_verified" in oracle_progress["promotion_rule"]
def test_varga_cli_outputs_d9_and_d10() -> None:
diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py
index b57dbb2b..304a4aa5 100644
--- a/tests/test_frontend_productization.py
+++ b/tests/test_frontend_productization.py
@@ -253,6 +253,8 @@ def test_frontend_branded_avatar_and_prompt_pack_are_productized() -> None:
assert "function renderAIPromptPackPanel" in main
assert "ai-prompt-pack-panel" in main
assert "evidence_snapshot" in main
+ assert "oracle_progress" in main
+ assert "artifact_policy: 'references/oracle/artifacts/'" in main
assert "retrieval_plan" in main
assert ".logo-avatar" in style
assert ".ai-prompt-pack-panel" in style
@@ -273,6 +275,11 @@ def test_frontend_ayanamsa_settings_are_live_api_parameters() -> None:
assert "computeChart({ year, month, day, hour, minute, second, lat, lon, tz })" in main
assert "fallbackChart._calculation_boundary" in main
assert "payload.ayanamsa" in main
+ assert "renderAyanamsaRuntimeStatus(cd, pack)" in main
+ assert "function renderAyanamsaRuntimeStatus" in main
+ assert "backend_computed" in main
+ assert "browser_fallback" in main
+ assert "ayanamsa-runtime-status" in main
def test_ai_chat_prefers_backend_prompt_pack_context() -> None:
@@ -285,6 +292,20 @@ def test_ai_chat_prefers_backend_prompt_pack_context() -> None:
assert "AI Prompt Pack 已作为上下文入口" in ai_chat
+def test_ai_prompt_pack_panel_exposes_copyable_audit_context() -> None:
+ main = read("main.js")
+ style = read("style.css")
+
+ assert "ai-prompt-pack-actions" in main
+ assert "data-ai-prompt-action=\"copy-prompt\"" in main
+ assert "data-ai-prompt-action=\"copy-evidence\"" in main
+ assert "setupAIPromptPackActions" in main
+ assert "copyAIPromptPackSection" in main
+ assert "AI Prompt Pack 审计上下文已复制" in main
+ assert ".ai-prompt-pack-actions" in style
+ assert ".ayanamsa-runtime-status" in style
+
+
def test_api_bridge_variants_prefer_backend_prompt_pack_context() -> None:
for rel_path in ["api-bridge.js", "public/api-bridge.js"]:
bridge = read(rel_path)
@@ -295,6 +316,7 @@ def test_api_bridge_variants_prefer_backend_prompt_pack_context() -> None:
assert "【evidence_snapshot】" in bridge
assert "JSON.stringify(chartData.ai_prompt_pack.evidence_snapshot" in bridge
assert "JSON.stringify(chartData.ai_prompt_pack.retrieval_plan" in bridge
+ assert "external_oracle_evidence_validation: valid_packets: 0" in bridge
def test_first_use_onboarding_is_actionable() -> None:
@@ -607,6 +629,12 @@ def test_trust_center_exposes_oracle_evidence_intake_cards() -> None:
"reject_global_shadbala_scaling",
"status_not_external_verified",
"local_engine_artifact_rejected",
+ "renderOracleEvidenceProgressDashboard",
+ "Dasha/Shadbala 真实进度",
+ "0 / 5",
+ "references/oracle/artifacts/",
+ "必须打码",
+ "missing_shadbala_component",
"template_user_REDACTED_YEAR_moon_longitude_lahiri",
"template_steve_jobs_dasha_lahiri",
"template_redacted_place_shadbala_raman",
@@ -637,10 +665,94 @@ def test_trust_center_exposes_oracle_evidence_intake_cards() -> None:
".oracle-evidence-fields",
".oracle-evidence-validation-result",
".oracle-evidence-card button",
+ ".oracle-evidence-progress-dashboard",
+ ".oracle-evidence-progress-bar",
]:
assert token in style
+def test_oracle_artifact_storage_policy_is_documented() -> None:
+ artifact_readme = ROOT / "references" / "oracle" / "artifacts" / "README.md"
+ assert artifact_readme.exists()
+ text = artifact_readme.read_text(encoding="utf-8")
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
+
+ for token in [
+ "references/oracle/artifacts/",
+ "source_artifact",
+ "必须打码",
+ "不得提交私人 PDF 原件",
+ "不得提交完整出生报告",
+ "浏览器 scratch",
+ "external_oracle_artifact",
+ ]:
+ assert token in text
+
+ for token in [
+ "references/oracle/artifacts/",
+ "source_artifact",
+ "必须打码",
+ "不得提交私人 PDF 原件",
+ ]:
+ assert token in readme
+
+
+def test_runtime_smoke_html_artifacts_are_ignored() -> None:
+ gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
+ for token in [
+ "runtime-smoke-report-*.html",
+ "jyotish-app/runtime-smoke-report-*.html",
+ ]:
+ assert token in gitignore
+
+
+def test_first_jhora_capture_guide_is_actionable() -> None:
+ guide = ROOT / "docs" / "user_jhora_capture_guide.md"
+ assert guide.exists()
+ text = guide.read_text(encoding="utf-8")
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
+
+ for token in [
+ "template_user_REDACTED_YEAR_moon_longitude_lahiri",
+ "template_steve_jobs_dasha_lahiri",
+ "Lahiri",
+ "Raman",
+ "KP",
+ "mean node",
+ "true node",
+ "Moon sidereal longitude",
+ "Vimshottari start date",
+ "Shadbala 七曜六分量",
+ "Sun",
+ "Moon",
+ "Mars",
+ "Mercury",
+ "Jupiter",
+ "Venus",
+ "Saturn",
+ "sthana",
+ "dig",
+ "kala",
+ "chesta",
+ "naisargika",
+ "drik",
+ "references/oracle/artifacts/",
+ "source_artifact",
+ "external_verified",
+ "python3 scripts/oracle_collection_queue.py",
+ "python3 scripts/oracle_evidence_validator.py",
+ "valid_packets: 1",
+ "ready_for_calibration: 1",
+ "必须打码",
+ "不得提交私人 PDF 原件",
+ "不得提交完整出生报告",
+ "浏览器 scratch",
+ ]:
+ assert token in text
+
+ assert "docs/user_jhora_capture_guide.md" in readme
+
+
def test_trust_center_exposes_real_case_revalidation_to_users() -> None:
main = read("main.js")
style = read("style.css")
@@ -2079,6 +2191,7 @@ def test_dasha_reference_audit_is_documented_and_gated() -> None:
queue_script = ROOT / "scripts" / "oracle_collection_queue.py"
evidence_validator = ROOT / "scripts" / "oracle_evidence_validator.py"
oracle_fixture = ROOT / "references" / "oracle" / "dasha_shadbala_oracle_cases.json"
+ ashtakoot_oracle_fixture = ROOT / "references" / "oracle" / "ashtakoot_oracle_cases.json"
quality_gate_module = load_quality_gate_module()
quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8")
readme = (ROOT / "README.md").read_text(encoding="utf-8")
@@ -2088,6 +2201,7 @@ def test_dasha_reference_audit_is_documented_and_gated() -> None:
assert queue_script.exists()
assert evidence_validator.exists()
assert oracle_fixture.exists()
+ assert ashtakoot_oracle_fixture.exists()
assert "tests/test_oracle_collection_queue.py" in quality_gate_module.CORE_PYTEST_TARGETS
assert "tests/test_oracle_evidence_validator.py" in quality_gate_module.CORE_PYTEST_TARGETS
for token in [
@@ -2119,7 +2233,20 @@ def test_dasha_reference_audit_is_documented_and_gated() -> None:
"python3 scripts/oracle_collection_queue.py",
"python3 scripts/oracle_evidence_validator.py",
"references/oracle/dasha_shadbala_oracle_cases.json",
+ "references/oracle/ashtakoot_oracle_cases.json",
"--target-start-date 1986-05-18",
+ "Ashtakoot 外部合婚 oracle",
+ "ashtakoot_36_point",
+ "target.total_score",
+ "target.varna",
+ "target.vashya",
+ "target.tara",
+ "target.yoni",
+ "target.graha_maitri",
+ "target.gana",
+ "target.bhakoot",
+ "target.nadi",
+ "target.kuja_status",
"不要为单份 PDF 直接调生产常数",
"Moon sidereal longitude",
"production_tuning_recommended: false",
diff --git a/tests/test_oracle_collection_queue.py b/tests/test_oracle_collection_queue.py
index 46bb81f8..4b0a43e5 100644
--- a/tests/test_oracle_collection_queue.py
+++ b/tests/test_oracle_collection_queue.py
@@ -102,6 +102,67 @@ def test_oracle_collection_queue_outputs_markdown_table() -> None:
assert "capture_id" in markdown
+def test_first_jhora_evidence_packet_template_is_safe_and_fillable() -> None:
+ template = ROOT / "references" / "oracle" / "evidence_packet_templates" / "jhora_steve_jobs_lahiri_first_packet.json"
+ assert template.exists()
+ packet = json.loads(template.read_text(encoding="utf-8"))
+
+ assert packet["capture_id"] == "external_template_steve_jobs_dasha_lahiri"
+ assert packet["status"] == "draft"
+ assert packet["case_id"] == "template_steve_jobs_dasha_lahiri"
+ assert packet["metadata"]["tool_name"] == ""
+ assert packet["metadata"]["source_artifact"] == "references/oracle/artifacts/"
+ assert packet["metadata"]["ayanamsa"] == "Lahiri"
+ assert packet["metadata"]["node_mode"] == "true node"
+ assert packet["integrity_checks"]["must_not_come_from_local_engine"] is True
+ assert packet["integrity_checks"]["requires_external_artifact"] is True
+ assert packet["integrity_checks"]["reject_global_shadbala_scaling"] is True
+ assert packet["target_placeholders"]["target.vimshottari_start_date"] is None
+
+ shadbala = packet["target_placeholders"]["target.shadbala_components"]
+ assert set(shadbala) == {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
+ for row in shadbala.values():
+ assert set(row) == {"sthana", "dig", "kala", "chesta", "naisargika", "drik"}
+ assert all(value is None for value in row.values())
+
+
+def test_ashtakoot_oracle_cases_generate_collection_tasks() -> None:
+ oracle_file = ROOT / "references" / "oracle" / "ashtakoot_oracle_cases.json"
+ assert oracle_file.exists()
+
+ completed = run_queue_for_file(oracle_file, "--format", "json")
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ assert report["scope"] == "external_oracle_collection_queue"
+ assert report["summary"]["total_tasks"] == 5
+ assert report["summary"]["ready_for_calibration"] == 0
+ assert report["summary"]["production_tuning_allowed"] is False
+
+ first = report["tasks"][0]
+ assert first["case_id"] == "ashtakoot_public_couple_lahiri_01"
+ assert first["privacy"] == "public_or_synthetic_relationship_template"
+ assert "ashtakoot" in first["target_modules"]
+ assert any("JHora" in source for source in first["preferred_sources"])
+ assert any("VedAstro" in source for source in first["preferred_sources"])
+ for field in [
+ "target.total_score",
+ "target.varna",
+ "target.vashya",
+ "target.tara",
+ "target.yoni",
+ "target.graha_maitri",
+ "target.gana",
+ "target.bhakoot",
+ "target.nadi",
+ "target.kuja_status",
+ ]:
+ assert field in first["target_fields"]
+ assert field in first["evidence_packet"]["target_placeholders"]
+ assert first["evidence_packet"]["integrity_checks"]["must_not_come_from_local_engine"] is True
+ assert first["do_not_tune_production"] is True
+
+
def test_oracle_collection_queue_preserves_external_verified_evidence(tmp_path: Path) -> None:
oracle = json.loads((ROOT / "references/oracle/dasha_shadbala_oracle_cases.json").read_text(encoding="utf-8"))
case = oracle["template_cases"][0]
diff --git a/tests/test_oracle_evidence_validator.py b/tests/test_oracle_evidence_validator.py
index af460c55..5ce9a3c1 100644
--- a/tests/test_oracle_evidence_validator.py
+++ b/tests/test_oracle_evidence_validator.py
@@ -10,6 +10,18 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
+SHADBALA_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
+SHADBALA_COMPONENTS = ["sthana", "dig", "kala", "chesta", "naisargika", "drik"]
+
+
+def complete_shadbala_components() -> dict[str, dict[str, float]]:
+ return {
+ planet: {
+ component: float(40 + planet_index * 3 + component_index)
+ for component_index, component in enumerate(SHADBALA_COMPONENTS)
+ }
+ for planet_index, planet in enumerate(SHADBALA_PLANETS)
+ }
def build_queue() -> dict:
@@ -104,16 +116,7 @@ def test_validator_accepts_filled_external_packet_but_not_whole_queue(tmp_path:
packet["target_placeholders"] = {
"target.moon_sidereal_longitude_deg": 311.7897,
"target.vimshottari_start_date": "1986-05-18",
- "target.shadbala_components": {
- "Sun": {
- "sthana": 100.0,
- "dig": 50.0,
- "kala": 100.0,
- "chesta": 40.0,
- "naisargika": 60.0,
- "drik": 30.0,
- }
- },
+ "target.shadbala_components": complete_shadbala_components(),
}
packet["status"] = "external_verified"
queue_path = tmp_path / "queue.json"
@@ -131,6 +134,197 @@ def test_validator_accepts_filled_external_packet_but_not_whole_queue(tmp_path:
assert first["problems"] == []
+def test_validator_rejects_incomplete_shadbala_component_rows(tmp_path: Path) -> None:
+ queue = build_queue()
+ packet = queue["tasks"][0]["evidence_packet"]
+ packet["metadata"] = {
+ "tool_name": "JHora",
+ "tool_version_or_url": "manual-screenshot-v1",
+ "capture_date": "2026-06-25",
+ "source_artifact": "references/oracle/artifacts/manual_jhora_user_REDACTED_YEAR.png",
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "timezone": "UTC+08:00",
+ "operator_note": "Manual external screenshot; values typed from JHora screen.",
+ }
+ packet["target_placeholders"] = {
+ "target.moon_sidereal_longitude_deg": 311.7897,
+ "target.vimshottari_start_date": "1986-05-18",
+ "target.shadbala_components": {
+ "Sun": {
+ "sthana": 100.0,
+ "dig": 50.0,
+ }
+ },
+ }
+ packet["status"] = "external_verified"
+ queue_path = tmp_path / "queue.json"
+ queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
+
+ completed = run_validator(queue_path)
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ first = report["packets"][0]
+ assert first["valid"] is False
+ assert "missing_shadbala_component:Sun.kala" in first["problems"]
+ assert "missing_shadbala_component:Sun.chesta" in first["problems"]
+ assert "missing_shadbala_component:Sun.naisargika" in first["problems"]
+ assert "missing_shadbala_component:Sun.drik" in first["problems"]
+ assert "missing_shadbala_component:Moon" in first["problems"]
+
+
+def test_validator_rejects_non_numeric_or_negative_shadbala_components(tmp_path: Path) -> None:
+ queue = build_queue()
+ packet = queue["tasks"][0]["evidence_packet"]
+ shadbala = complete_shadbala_components()
+ shadbala["Sun"]["sthana"] = "100"
+ shadbala["Moon"]["dig"] = -1.0
+ packet["metadata"] = {
+ "tool_name": "JHora",
+ "tool_version_or_url": "manual-screenshot-v1",
+ "capture_date": "2026-06-25",
+ "source_artifact": "references/oracle/artifacts/manual_jhora_user_REDACTED_YEAR.png",
+ "ayanamsa": "lahiri",
+ "node_mode": "mean",
+ "timezone": "UTC+08:00",
+ "operator_note": "Manual external screenshot; values typed from JHora screen.",
+ }
+ packet["target_placeholders"] = {
+ "target.moon_sidereal_longitude_deg": 311.7897,
+ "target.vimshottari_start_date": "1986-05-18",
+ "target.shadbala_components": shadbala,
+ }
+ packet["status"] = "external_verified"
+ queue_path = tmp_path / "queue.json"
+ queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
+
+ completed = run_validator(queue_path)
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ first = report["packets"][0]
+ assert first["valid"] is False
+ assert "invalid_shadbala_component_type:Sun.sthana" in first["problems"]
+ assert "invalid_shadbala_component_negative:Moon.dig" in first["problems"]
+
+
+def test_validator_rejects_invalid_ashtakoot_score_ranges(tmp_path: Path) -> None:
+ queue = build_queue_from_file(ROOT / "references/oracle/ashtakoot_oracle_cases.json")
+ packet = queue["tasks"][0]["evidence_packet"]
+ packet["metadata"] = {
+ "tool_name": "VedAstro",
+ "tool_version_or_url": "https://vedastro.org/API",
+ "capture_date": "2026-06-25",
+ "source_artifact": "references/oracle/artifacts/manual_ashtakoot_case_01.png",
+ "ayanamsa": "lahiri",
+ "node_mode": "true",
+ "timezone": "UTC-08:00",
+ "operator_note": "External compatibility calculator screenshot.",
+ }
+ packet["target_placeholders"] = {
+ "target.total_score": 99.0,
+ "target.varna": 1.0,
+ "target.vashya": 2.0,
+ "target.tara": 3.0,
+ "target.yoni": 4.0,
+ "target.graha_maitri": 5.0,
+ "target.gana": 6.0,
+ "target.bhakoot": 7.0,
+ "target.nadi": 99.0,
+ "target.kuja_status": "",
+ }
+ packet["status"] = "external_verified"
+ queue_path = tmp_path / "queue.json"
+ queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
+
+ completed = run_validator(queue_path)
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ first = report["packets"][0]
+ assert first["valid"] is False
+ assert "invalid_ashtakoot_score_range:target.total_score" in first["problems"]
+ assert "invalid_ashtakoot_score_range:target.nadi" in first["problems"]
+ assert "placeholder_unfilled:target.kuja_status" in first["problems"]
+
+
+def test_validator_rejects_ashtakoot_score_sum_mismatch(tmp_path: Path) -> None:
+ queue = build_queue_from_file(ROOT / "references/oracle/ashtakoot_oracle_cases.json")
+ packet = queue["tasks"][0]["evidence_packet"]
+ packet["metadata"] = {
+ "tool_name": "VedAstro",
+ "tool_version_or_url": "https://vedastro.org/API",
+ "capture_date": "2026-06-25",
+ "source_artifact": "references/oracle/artifacts/manual_ashtakoot_case_01.png",
+ "ayanamsa": "lahiri",
+ "node_mode": "true",
+ "timezone": "UTC-08:00",
+ "operator_note": "External compatibility calculator screenshot.",
+ }
+ packet["target_placeholders"] = {
+ "target.total_score": 20.0,
+ "target.varna": 1.0,
+ "target.vashya": 2.0,
+ "target.tara": 3.0,
+ "target.yoni": 4.0,
+ "target.graha_maitri": 5.0,
+ "target.gana": 6.0,
+ "target.bhakoot": 7.0,
+ "target.nadi": 8.0,
+ "target.kuja_status": "no_dosha",
+ }
+ packet["status"] = "external_verified"
+ queue_path = tmp_path / "queue.json"
+ queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
+
+ completed = run_validator(queue_path)
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ first = report["packets"][0]
+ assert first["valid"] is False
+ assert "ashtakoot_score_sum_mismatch" in first["problems"]
+
+
+def test_validator_accepts_valid_ashtakoot_external_packet(tmp_path: Path) -> None:
+ queue = build_queue_from_file(ROOT / "references/oracle/ashtakoot_oracle_cases.json")
+ packet = queue["tasks"][0]["evidence_packet"]
+ packet["metadata"] = {
+ "tool_name": "VedAstro",
+ "tool_version_or_url": "https://vedastro.org/API",
+ "capture_date": "2026-06-25",
+ "source_artifact": "references/oracle/artifacts/manual_ashtakoot_case_01.png",
+ "ayanamsa": "lahiri",
+ "node_mode": "true",
+ "timezone": "UTC-08:00",
+ "operator_note": "External compatibility calculator screenshot.",
+ }
+ packet["target_placeholders"] = {
+ "target.total_score": 18.0,
+ "target.varna": 1.0,
+ "target.vashya": 1.0,
+ "target.tara": 2.0,
+ "target.yoni": 3.0,
+ "target.graha_maitri": 4.0,
+ "target.gana": 2.0,
+ "target.bhakoot": 3.0,
+ "target.nadi": 2.0,
+ "target.kuja_status": "no_dosha",
+ }
+ packet["status"] = "external_verified"
+ queue_path = tmp_path / "queue.json"
+ queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
+
+ completed = run_validator(queue_path)
+
+ assert completed.returncode == 0, completed.stderr or completed.stdout
+ report = json.loads(completed.stdout)
+ first = report["packets"][0]
+ assert first["valid"] is True
+ assert first["problems"] == []
+
+
def test_validator_rejects_local_engine_artifact(tmp_path: Path) -> None:
queue = build_queue()
packet = queue["tasks"][0]["evidence_packet"]
@@ -167,16 +361,7 @@ def test_validator_accepts_external_verified_packet_generated_from_oracle_file(t
case["target"] = {
"moon_sidereal_longitude_deg": 311.7897,
"vimshottari_start_date": "1986-05-18",
- "shadbala_components": {
- "Sun": {
- "sthana": 100.0,
- "dig": 50.0,
- "kala": 100.0,
- "chesta": 40.0,
- "naisargika": 60.0,
- "drik": 30.0,
- }
- },
+ "shadbala_components": complete_shadbala_components(),
}
case["evidence_packet"] = {
"status": "external_verified",