From 4ee79210ef3fb2bb37f5cbb4928e6064636b287a Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 4 Sep 2026 11:03:59 +0800 Subject: [PATCH] fix: restore upstream sync consultation contracts --- CHANGELOG.md | 6 + docs/BUG_HISTORY.md | 16 +++ .../PROGRESS-upstream-sync-fix-20260903.md | 113 ++++++++++++++++++ docs/tasks/README.md | 2 +- mcp_server.py | 43 ++++++- .../interpretation_template_registry.json | 8 +- scripts/jyotish_api_server.py | 24 ++-- scripts/run_quality_gate.py | 5 + tests/test_consultation_workflow_domains.py | 24 +++- tests/test_mcp_strict_workflow_finance.py | 62 ++++------ 10 files changed, 241 insertions(+), 62 deletions(-) create mode 100644 docs/tasks/PROGRESS-upstream-sync-fix-20260903.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a39ba566..e2157c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-09-04 — 上游同步验收修复:咨询主题、finance 证据与模板门禁 + +咨询工作流重建 prompt pack 时保留既有 `evidence_snapshot`,避免 `strict_workflow_contracts` 丢失后把非原生主题从 `degraded` 误降为 `blocked`;真实 chart 未携带 engine snapshot 时复用服务端权威 `plan_route_contract()`,不伪造外部 oracle 成功。finance 采用 PyJHora Yogi Sphuta 标准式(Yogi=`Sun+Moon+93°20′`,Avayogi=`Yogi+186°40′`);D11 继续作为支持项,`wealth_promise_strength` 仍是主判据,缺 D11 不再把完整 Shadbala 的 `confidence_cap` 从 `medium-high` 压到 `low`。MCP 的公开路由保持 `finance`,明确财富问题不会因时间词先出现而漏装外部证据。 + +四个冻结模板不再引用仓内不存在的 MEVG 审计文件,分别改用现有的 transit、house-domain、prediction-boundary 与 mandatory-verification 规范。快速门 CORE 新增全域咨询、finance 严格证据、模板注册表和 VedAstro 外部证据四组回归测试。 + ## 2026-09-03 — health 三窗、专业参考导出与出生时间敏感度 health 咨询与个人报告现按压力窗、事件风险窗、恢复支持窗组织,必需证据覆盖 D6/D8/D30,Transit 缺证据时保留 blocked,并固定携带非医疗边界。报告中心新增确定性的 `professional_report_reference` JSON/Markdown 导出,不调用 writer,并净化 raw full-reading、代码路径和内部 oracle/module 标识。校正新增候选窗口自适应选题、请求级 `ayanamsa` / `node_mode`,以及只读的出生时间敏感度附录;默认仍为 Raman,既有评分与 confirmed 报告保持原字节。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 3f56c669..18c25841 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -7935,3 +7935,19 @@ - 相关记录:无 - 复发自:无 - 修复版本:待发布 + +## BUG-515 | 缺少 D11 被误判为 finance 必需证据,完整 Shadbala 的 confidence cap 降为 low + +- 状态:resolved +- 首次发现:2026-09-03 +- 最近更新:2026-09-04 +- 影响面:`mcp_server.py::_collect_strict_evidence`、finance strict workflow +- 用户现象:财富严格工作流在 Shadbala 分量完整、原本应保持 `medium-high` 置信度上限时,仅因没有 D11 Rudramsa 就返回 `confidence_cap=low`。 +- 触发条件:finance 证据包没有 `modules.varga_full.D11_Rudramsa`,其余财富主判据与完整 Shadbala 证据可用。 +- 根因:上游加入 `d11_rudramsa` 后,finance 的 `missing_evidence` 过滤表没有同步把它列为非阻断支持项;空 D11 因而被计入缺失的必需证据,触发通用 `missing -> low` 降级。 +- 修复:把 `d11_rudramsa` 加入 finance 非阻断证据集合。D11 仍可进入证据包,但只作支持项;`wealth_promise_strength` 保持主判据,完整 Shadbala 用例的 `medium-high` 断言不改。 +- 验证:聚焦回归集合 88 passed(31.71s);模板 validator `valid=true, problem_count=0, template_count=15`;本地 quick gate 为 Python CORE 585 passed / 1 skipped、前端 2628 passed、lint 0 errors、Next build 通过,退出 0(575.45s);`git diff --check` 通过。 +- 防复发:新增证据字段必须同时声明 required / supporting 角色;支持项不得因缺失进入 `missing_evidence`,也不得单独压低 confidence cap。 +- 相关记录:`TASK-upstream-sync-fix-20260903.md` +- 复发自:`45d13258` / `f2241463` 上游同步批次 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-upstream-sync-fix-20260903.md b/docs/tasks/PROGRESS-upstream-sync-fix-20260903.md new file mode 100644 index 00000000..2a724395 --- /dev/null +++ b/docs/tasks/PROGRESS-upstream-sync-fix-20260903.md @@ -0,0 +1,113 @@ +# PROGRESS · 上游同步验收修复单(2026-09-04) + +任务书:`TASK-upstream-sync-fix-20260903.md` +工作树:`/Users/jesse/Downloads/Copse/astrology/.worktrees/upstream-sync-fix-20260903` +分支:`codex/upstream-sync-fix-20260903` +实际开工基线:`origin/staging` @ `779717f48c7ce08f9c731663f2203dae19c250c3` +原任务书验收基线:`origin/staging` @ `c1e3f32a` + +当前状态:本地修复与验证完成,改动仍在独立工作树且未提交。本记录未声明提交、推送或部署完成。 + +## 1. 非原生主题恢复 `degraded` + +`scripts/jyotish_api_server.py` 重建 `ai_prompt_pack` 时,先合并既有 `evidence_snapshot`,再刷新本次请求的 birth / ayanamsa / core 等字段。这样不会丢掉上游已装配的 `strict_workflow_contracts`,报告适配器仍可识别 `upstream_contract_available=true`。 + +预期合同: + +- `general` / `timing` / `family` / `education` / `migration` / `annual`:`degraded`; +- 上述主题的 adapter:`upstream_contract_available=true`; +- `career` / `marriage` / `wealth` / `health`:继续为 `supported`。 + +真实 HTTP 十主题 smoke 全部返回 HTTP 200:`career` / `marriage` / `wealth` / `health` 为 `supported`;`education` / `migration` / `family` / `annual` / `timing` / `general` 为 `degraded`,且六个非原生主题的 `upstream_contract_available=true`。耗时 70.82s,脱敏结果保存于 `/tmp/upstream-sync-fix-http-smoke.json`;本地 API 已停止。 + +## 2. finance 严格证据口径 + +### Yogi / Avayogi 断言取舍 + +| 原值 | 新值 | 原因 | +| --- | --- | --- | +| Yogi 点公式为 `Sun + Moon` | `Sun + Moon + 93°20′`;`formula_profile=pyjhora_yogi_sphuta_v1` | 接受任务书决策:旧公式漏掉 Pushya 起点常量,改为 PyJHora Yogi Sphuta 标准式。 | +| Avayogi 由旧 Yogi 点派生 | `Yogi + 186°40′` | 与新的标准 Yogi Sphuta 点配套计算。 | +| 示例 Yogi 点 `20° / Bharani / 1宫` | `113.3333° / Ashlesha / 4宫`;Avayogi 点锁定 `300°` | 同一虚构夹具按标准公式重算后的确定性结果,不保留旧错误期望。 | +| `Yogi=Venus`、`duplicate_yogi=Mars`、`Avayogi=Saturn` | `Yogi=Mercury`、`duplicate_yogi=Moon`、`Avayogi=Mars` | 点位重算后对应星曜随之变化。 | +| `level=strong`,存在 `lagna_yogi_tight_orb`、`yogi_planet_is_2l` 和三条 strong signals | `level=weak`,距离 `93.3333°`,tight-orb / wealth-lord links / signals 为空 | 新点位不再满足旧强钩子条件,弱 Yogi 不得抬高财富主判据或写入 `yogi_active`。 | +| `primary_source=dhana_yogi_hooks`、source diversity `2`、支持源 `dhana+yogi` | `primary_source=dhana_yogas`、source diversity `1`、支持源仅 `dhana` | Yogi 仅作 modifier;该夹具的新 Yogi 为 weak,不能计作独立财富支持源。 | + +### D11 与 confidence cap + +- `d11_rudramsa` 保留在 finance 证据包中,但定位为**支持项**,不是严格证据的必需项,也不是主判据。 +- `wealth_promise_strength` 继续作为财富承诺的主判据。 +- `confidence_cap` 回归根因:finance 的 `missing_evidence` 过滤表未把新增的 `d11_rudramsa` 列入非阻断证据。结果是没有 D11 时,即使 Shadbala 分量完整,也会被误判为缺必需证据并把 cap 从 `medium-high` 压到 `low`。 +- 修复:把 `d11_rudramsa` 纳入非阻断集合;不改完整 Shadbala 用例原有的 `medium-high` 断言。 +- MCP 公开边界继续使用 `finance`:统一路由器内部 canonical `wealth` 会映射回公开名称;当 `annual` / `timing` 时间词先命中、但问题同时有明确财富关键词时,在 MCP strict 边界以 `wealth` 重新解析,避免漏装 finance 的 VedAstro / external evidence。 +- 旧 source-pack 元数据恢复到当前 Next.js 真路径:`frontend_interpretation_layer` 指向 report interpretation packs,`frontend_planet_house_details` 指向 `personal-report-generation.ts`,覆盖仍为 `9_planets_x_12_houses`。 + +对应 Bug:`BUG-515`。 + +## 3. 模板注册表引用取舍 + +没有复制缺失的 `references/mevg_chinese_full_layer_and_practice_audit_2026_08_26.md`;四个模板保留,改指向仓内已有且语义直接匹配的规范: + +| 模板 | 新引用 | 取舍理由 | +| --- | --- | --- | +| `dual_luminary_timing_context` | `references/transit-comprehensive-guide.md` | 模板要求 Moon、Lagna/Sun、Dasha 与 transit 的交叉检查,综合行运指南比缺失的通用审计文档更直接。 | +| `seventh_house_topic_split` | `references/house-domain-planet-mapping.md` | 模板核心是区分婚恋、合同、公众与对手等七宫主题,宫位—领域映射是本仓直接真源。 | +| `empty_window_not_success` | `references/prediction-boundary-protocol.md` | “没有阻碍不等于成功”属于预测边界与置信度约束,应由边界协议承载。 | +| `dasha_boundary_not_event_date` | `references/mandatory-verification-gate-protocol.md` | 换运边界不能直接写成事件日,需要多重 Dasha、transit、命盘承诺及外部验证门共同收敛。 | + +其余 `source_refs` 保留不变。 + +## 4. quick CORE 扩列 + +`scripts/run_quality_gate.py` 的 `CORE_PYTEST_TARGETS` 新增: + +- `tests/test_consultation_workflow_domains.py` +- `tests/test_mcp_strict_workflow_finance.py` +- `tests/test_interpretation_template_registry.py` +- `tests/test_vedastro_external_technique_evidence.py` + +目的:让本次发现的主题合同、finance 严格证据、模板引用与 VedAstro 外部证据回归进入自动 staging 快速门,而不是只在全量测试中暴露。 + +## 5. 本地验证实证 + +### 聚焦 pytest + +以下集合共收集并通过 88 条测试,墙钟耗时 31.71s: + +- `tests/test_consultation_workflow_domains.py`:31 条; +- `tests/test_mcp_strict_workflow_finance.py`:37 条; +- `tests/test_interpretation_template_registry.py`:2 条; +- `tests/test_vedastro_external_technique_evidence.py`:10 条; +- `tests/test_api_server_growth_contract.py`:3 条; +- `tests/test_consultation_engine_field_bridge.py`:5 条。 + +### 模板 validator + +`scripts/validate_interpretation_templates.py` 退出 0:`valid=true`、`problem_count=0`、`template_count=15`(约 0.04s)。 + +### quick gate + +首次运行因独立 worktree 没有安装前端依赖而在 `npm test` 报 `tsx: command not found`;执行 `npm ci` 后重跑通过。没有运行 `npm audit fix`,避免把依赖升级混入本修复单;`frontend/node_modules/` 受 `.gitignore` 排除,不是交付文件。 + +最终结果: + +- Python CORE:585 passed、1 skipped、183 warnings; +- 前端测试:2628 passed、0 failed、0 skipped; +- lint:0 errors、74 warnings; +- Next build:通过; +- `scripts/run_quality_gate.py --profile quick`:退出 0,墙钟 575.45s。 + +完整日志:`/tmp/upstream-sync-fix-quick-gate.log`。 + +### 文件守卫 + +- `scripts/jyotish_api_server.py`:11200 行,与 `HEAD` 同文件行数一致,满足“不增长”红线; +- `git diff --check`:通过; +- `frontend/node_modules/`:无 Git tracked 文件。 + +## 交付边界 + +- 本工作树改动目前**未提交**。 +- 未 push 到任何远端;`origin/staging` 未因本工作树前进。 +- 未触发 CI、迁移或部署。 +- 未检查或声称 staging `/api/health` 已包含本修复。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 35459e00..019fa384 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -99,7 +99,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | | `TASK-upstream-sync-20260903.md` | `PROGRESS-upstream-sync-20260903.md`、`PROGRESS-upstream-capabilities-20260903.md` | 上游快照推进到 `a6f47abd`、引擎三方合并、health 三窗 / VedAstro REST 桥 / 专业报告导出 / 新字段接聊天与报告 / 校正解冻三项 | 已验收(带修复单) | 0/1/2/4/6:`45d13258`、`f2241463`、`734d2059`;3/5/7:`c2f23131`~`6063c1f7`(BUG-514)。staging 已部署 `6063c1f7`。未过项全在 `TASK-upstream-sync-fix-20260903.md` | -| `TASK-upstream-sync-fix-20260903.md` | — | 非原生主题合同回 `degraded`(**P1,已随 6063c1f7 上 staging:timing/general 主题回执 blocked**)、finance 严格证据口径与 confidence_cap(7 红)、模板注册表缺引用(4 problems)、vedastro 外部证据 4 红、门禁 CORE 扩列 | 待领取 | 分支 `codex/upstream-sync-fix-20260903`;2026-09-04 复核:任务 3/5/7 合入后本单各项仍原样 | +| `TASK-upstream-sync-fix-20260903.md` | `PROGRESS-upstream-sync-fix-20260903.md` | 非原生主题恢复 `degraded`、finance Yogi/D11/confidence cap、模板注册表引用、VedAstro/MCP finance 路由与 quick CORE 扩列 | 本地验证完成 | 独立 worktree 基线 `779717f4`;BUG-515。88 条聚焦回归与 quick gate 通过,十主题 HTTP smoke 符合合同;未提交、未推送、未部署 | ## 命名与归档 diff --git a/mcp_server.py b/mcp_server.py index 56a640f9..a5593661 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -500,6 +500,11 @@ def _existing_interpretation_source_pack() -> Dict[str, Any]: promote_batch2_paths = PROMOTE_BATCH2_TOPIC_SOURCE_REFS reference_only_paths = REFERENCE_ONLY_CONFLICT_SOURCE_REFS blocked_non_runtime_paths = BLOCKED_NON_RUNTIME_SOURCE_REFS + frontend_interpretation_paths = [ + "frontend/src/lib/report-interpretation-packs/general.ts", + "frontend/src/lib/report-interpretation-packs/themes.ts", + ] + planet_house_paths = ["frontend/src/lib/personal-report-generation.ts"] yoga_rule_paths = [ core_yogas_path, "references/yoga_rules.json", @@ -533,6 +538,8 @@ def _existing_interpretation_source_pack() -> Dict[str, Any]: *REAL_CASE_STUDIES_BATCH1_SOURCE_REFS, *RISHI_AI_MCP_BATCH1_SOURCE_REFS, *VEDIC_ASTRO_SKILLS_BATCH1_SOURCE_REFS, + *frontend_interpretation_paths, + *planet_house_paths, qa_rules_path, reader_chart_rules_path, reader_validation_rules_path, @@ -564,6 +571,7 @@ def _existing_interpretation_source_pack() -> Dict[str, Any]: "reference_only_conflict_sources", "qa_governance_rules", "reader_validation_rules", + "frontend_interpretation_layer", "yoga_rule_layer", "saham_rule_layer", ], @@ -655,6 +663,18 @@ def _existing_interpretation_source_pack() -> Dict[str, Any]: "next_batches": REMAINING_PRIORITY1_BATCH_QUEUE, "boundary": "Future batches remain audit-only until classified and tested.", }, + "frontend_interpretation_layer": { + "status": "available" if all(_repo_relative_exists(path) for path in frontend_interpretation_paths) else "partial", + "source_refs": frontend_interpretation_paths, + "promotion_status": "reference_layer", + }, + "frontend_planet_house_details": { + "status": "available" if all(_repo_relative_exists(path) for path in planet_house_paths) else "partial", + "coverage": "9_planets_x_12_houses", + "planet_count": 9, + "house_count": 12, + "source_refs": planet_house_paths, + }, "qa_governance_layer": { "status": "available" if _repo_relative_exists(qa_rules_path) else "partial", "source_refs": [qa_rules_path], @@ -3930,7 +3950,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An present["functional_benefic_malefic"] = _derive_functional_benefic_malefic(modules) present["interpretation_source_pack"] = _existing_interpretation_source_pack() missing = [key for key, value in present.items() if key not in { - "chart", "external_activation", "external_technique_evidence", "vedastro_official_snapshot", "source_priority", "dignity_guardrail", "gains_convergence", "career_convergence", "avayogi_risk", "ashtakavarga_finance_support", "shadbala_component_audit", "asc_sign", "pav_finance_support", "sodhita_finance_support", "kakshya_finance_support", "functional_benefic_malefic", "interpretation_source_pack", "d4_turyamsa", "d16_shodasamsa", "d24_siddhamsa", "bhava_bala", "hora_lagna", "indu_lagna", "sree_lagna", "dhan_saham", "artha_saham", "labha_saham" + "chart", "external_activation", "external_technique_evidence", "vedastro_official_snapshot", "source_priority", "dignity_guardrail", "gains_convergence", "career_convergence", "avayogi_risk", "ashtakavarga_finance_support", "shadbala_component_audit", "asc_sign", "pav_finance_support", "sodhita_finance_support", "kakshya_finance_support", "functional_benefic_malefic", "interpretation_source_pack", "d4_turyamsa", "d11_rudramsa", "d16_shodasamsa", "d24_siddhamsa", "bhava_bala", "hora_lagna", "indu_lagna", "sree_lagna", "dhan_saham", "artha_saham", "labha_saham" } and value in (None, {}, [], "")] convergence_hits: List[Dict[str, Any]] = [ item for item in [ @@ -4632,6 +4652,17 @@ def strict_workflow( JSON with routed analysis and confidence level """ route_packet = _UNIFIED_CONSULTATION_ORCHESTRATOR.resolve_route(question, None) + mcp_declared_route = None + question_text = (question or "").lower() + if route_packet["question_type"] in {"annual", "timing"} and any( + alias in question_text + for alias, domain in _UNIFIED_CONSULTATION_ORCHESTRATOR._THEME_ALIASES.items() + if domain == "wealth" + ): + mcp_declared_route = "wealth" + route_packet = _UNIFIED_CONSULTATION_ORCHESTRATOR.resolve_route( + question, None, declared_route=mcp_declared_route + ) normalized_themes = _UNIFIED_CONSULTATION_ORCHESTRATOR.normalize_themes(route_packet["primary_theme"]) result = _execute_mcp_consultation_workflow( @@ -4653,12 +4684,18 @@ def strict_workflow( western_oracle_payload=western_oracle_payload, ) chart = result.get("chart") if isinstance(result, dict) else {} - route = _safe_get(result, "routing", "question_type") or route_packet["question_type"] or "general" + route = mcp_declared_route or _safe_get(result, "routing", "question_type") or route_packet["question_type"] or "general" route_surface = _UNIFIED_CONSULTATION_ORCHESTRATOR.route_surface_contract(route) + route = "finance" if route_surface["runtime_route"] == "wealth" else route if isinstance(result, dict): routing = result.get("routing") if isinstance(result.get("routing"), dict) else {} - result["routing"] = {**route_packet, **routing, **route_surface} + result["routing"] = {**route_packet, **routing, **route_surface, "question_type": route} result["route_normalization"] = route_surface + for key in ("unified_orchestrator", "runtime_planner"): + metadata = result.get(key) + if isinstance(metadata, dict): + metadata_route = metadata.get("route") if isinstance(metadata.get("route"), dict) else {} + result[key] = {**metadata, "route": {**metadata_route, "question_type": route}} planner = result.get("runtime_planner") if isinstance(result.get("runtime_planner"), dict) else {} result["execution_receipt"] = _build_execution_receipt(chart, planner) if isinstance(chart, dict) and "error" not in chart: diff --git a/references/interpretation_template_registry.json b/references/interpretation_template_registry.json index 8bed94d8..46ea78b6 100644 --- a/references/interpretation_template_registry.json +++ b/references/interpretation_template_registry.json @@ -425,7 +425,7 @@ "authority_level": "B/C guarded", "domain": ["timing", "transit", "dasha", "moon", "lagna"], "source_refs": [ - "references/mevg_chinese_full_layer_and_practice_audit_2026_08_26.md", + "references/transit-comprehensive-guide.md", "references/mandatory-verification-gate-protocol.md", "references/strict-workflow-router.md" ], @@ -461,7 +461,7 @@ "authority_level": "B/C guarded", "domain": ["relationship", "contract", "public", "opponent", "house_judgment"], "source_refs": [ - "references/mevg_chinese_full_layer_and_practice_audit_2026_08_26.md", + "references/house-domain-planet-mapping.md", "references/event_judgment_marriage.md", "references/strict-workflow-router.md" ], @@ -498,7 +498,7 @@ "authority_level": "B/C guarded", "domain": ["prediction_boundary", "timing", "transit", "dasha"], "source_refs": [ - "references/mevg_chinese_full_layer_and_practice_audit_2026_08_26.md", + "references/prediction-boundary-protocol.md", "references/prediction-output-protocol.md", "references/mandatory-verification-gate-protocol.md" ], @@ -534,7 +534,7 @@ "authority_level": "B/C guarded", "domain": ["dasha", "timing", "event_prediction", "sandhi"], "source_refs": [ - "references/mevg_chinese_full_layer_and_practice_audit_2026_08_26.md", + "references/mandatory-verification-gate-protocol.md", "references/dasa-convergence-methodology.md", "assets/timing-prediction-template.md" ], diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index c9dfdbff..0cffecca 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -2177,14 +2177,14 @@ def execute_consultation_workflow( from scripts.consultation_plan_contract import ( ConsultationPlanContractError, apply_plan_precision_boundary, - declared_workflow_route, + declared_workflow_route, plan_route_contract, validate_consultation_plan_contract, ) except ModuleNotFoundError: # pragma: no cover - direct script execution from consultation_plan_contract import ( ConsultationPlanContractError, apply_plan_precision_boundary, - declared_workflow_route, + declared_workflow_route, plan_route_contract, validate_consultation_plan_contract, ) try: @@ -2338,9 +2338,9 @@ def execute_consultation_workflow( modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {} prompt_snapshot = (((chart.get('ai_prompt_pack') or {}).get('evidence_snapshot')) or {}) if isinstance(chart, dict) else {} strict_workflow_contracts = prompt_snapshot.get('strict_workflow_contracts') if isinstance(prompt_snapshot.get('strict_workflow_contracts'), dict) else {} + route_contracts = {theme: plan_route_contract(theme) for theme in themes} chart_guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else [] audited_remedies = handler._build_audited_remedies_from_guided_topics(chart_guided_topics) - thematic_report = {} if 'run_thematic_report' in runtime_planner.get('sync_steps', []): thematic_report = handler._compute_thematic_report({ @@ -2354,12 +2354,11 @@ def execute_consultation_workflow( 'skip_full_reading_for_thematic': True, 'upstream_contract': { 'chart': chart_for_theme, - 'strict_workflow_contracts': strict_workflow_contracts, + 'strict_workflow_contracts': strict_workflow_contracts, 'route_contracts': route_contracts, 'guided_topics': chart_guided_topics, }, }) executed_steps.append('run_thematic_report') - vedastro_gateway = rectification.get('vedastro_gateway') if isinstance(rectification, dict) else None if defer_optional_external_evidence: vedastro_gateway = _join_foreground_vedastro( @@ -4282,6 +4281,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): adapter_evidence = {} derived_context = None strict_workflow_contracts = upstream_contract.get('strict_workflow_contracts') if isinstance(upstream_contract.get('strict_workflow_contracts'), dict) else {} + route_contracts = upstream_contract.get('route_contracts') if isinstance(upstream_contract.get('route_contracts'), dict) else {} upstream_guided_topics = upstream_contract.get('guided_topics') if isinstance(upstream_contract.get('guided_topics'), list) else [] if not has_custom_evidence and not strict_workflow_contracts and self._can_derive_thematic_evidence(chart_data): derived_context = self._derive_thematic_evidence(chart_data, report_orchestrator) @@ -4349,9 +4349,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): theme_name, evidence=adapter_evidence.get(theme_name, []), upstream_contract_available=self._thematic_upstream_contract_available( - strict_workflow_contracts, - theme_name, - report_orchestrator, + strict_workflow_contracts, route_contracts, + theme_name, report_orchestrator, ), ) @@ -5179,10 +5178,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): def _thematic_upstream_contract_available( self, - strict_workflow_contracts, - domain, - report_orchestrator, + strict_workflow_contracts, route_contracts, + domain, report_orchestrator, ): + if isinstance(route_contracts, dict) and bool(route_contracts.get(domain)): + return True if not isinstance(strict_workflow_contracts, dict): return False for raw_domain, contract in strict_workflow_contracts.items(): @@ -7182,7 +7182,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'mode': 'jyotish_structured_prompt_pack', 'prompt_zh': '\n'.join(prompt_lines), 'evidence_snapshot': { - 'birth': birth, + **((chart.get('ai_prompt_pack') or {}).get('evidence_snapshot') or {}), 'birth': birth, 'ayanamsa': { 'name': birth.get('ayanamsa_name', DEFAULT_AYANAMSA_NAME), 'display': ayanamsa_display, diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index bd4daaa5..5f9e3c13 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -77,6 +77,11 @@ CORE_PYTEST_TARGETS = [ "tests/test_api_server_growth_contract.py", # Fail-fast heavy-compute concurrency gate (429 + Retry-After, health ungated). "tests/test_api_heavy_compute_gate.py", + # Upstream-sync acceptance regressions must fail the automatic staging gate. + "tests/test_consultation_workflow_domains.py", + "tests/test_mcp_strict_workflow_finance.py", + "tests/test_interpretation_template_registry.py", + "tests/test_vedastro_external_technique_evidence.py", ] RUNTIME_TRUTH_PYTEST_TARGETS = [ diff --git a/tests/test_consultation_workflow_domains.py b/tests/test_consultation_workflow_domains.py index 5e6562d2..1e62ee26 100644 --- a/tests/test_consultation_workflow_domains.py +++ b/tests/test_consultation_workflow_domains.py @@ -22,7 +22,7 @@ def _handler() -> JyotishAPIHandler: return JyotishAPIHandler.__new__(JyotishAPIHandler) -def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler) -> None: +def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler, *, include_strict_contracts: bool = True) -> None: chart = { "success": True, "birth_info": {"date": "1997-08-08", "time": "05:00", "tz": 8}, @@ -34,7 +34,7 @@ def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler) -> None: "strict_workflow_contracts": { domain: {"status": "available", "domain": domain} for domain in CANONICAL_DOMAINS - } + } if include_strict_contracts else {} } }, } @@ -109,6 +109,26 @@ def test_complete_consultation_workflow_handles_every_canonical_domain( assert "No general-theme fallback" in domain_report["boundary"] +@pytest.mark.parametrize("domain", ["education", "migration", "family", "annual", "timing", "general"]) +def test_non_native_domains_reuse_server_route_contract_without_engine_snapshot( + monkeypatch, + domain: str, +) -> None: + handler = _handler() + _stub_consultation_runtime(monkeypatch, handler, include_strict_contracts=False) + + result = handler._compute_consultation_workflow({ + "entry_mode": "direct_chart", "question": domain, "theme": [domain], + "year": 2000, "month": 1, "day": 1, "hour": 12, "minute": 0, + "lat": 0.0, "lon": 0.0, "tz": 0.0, "western_mode": False, + "defer_optional_external_evidence": True, + }) + + report = result["thematic_report"]["themes"][domain] + assert report["status"] == "degraded" + assert report["thematic_adapter"]["upstream_contract_available"] is True + + @pytest.mark.parametrize("domain", CANONICAL_DOMAINS) def test_versioned_plan_domains_execute_their_declared_route_not_the_question_text_route( monkeypatch, diff --git a/tests/test_mcp_strict_workflow_finance.py b/tests/test_mcp_strict_workflow_finance.py index 8f8ec3b0..af7f0996 100644 --- a/tests/test_mcp_strict_workflow_finance.py +++ b/tests/test_mcp_strict_workflow_finance.py @@ -1105,20 +1105,21 @@ def test_derive_yogi_wealth_support_detects_strong_native_hook() -> None: } support = _derive_yogi_wealth_support(modules) assert support is not None - assert support["level"] == "strong" + assert support["level"] == "weak" assert support["source"] == "yogi_asc_tight_orb_wealth" - assert support["yogi_planet"] == "Venus" - assert support["duplicate_yogi"] == "Mars" - assert support["avayogi"] == "Saturn" - assert support["yogi_point_house"] == 1 - assert support["tight_orb_hits"] == ["lagna_yogi_tight_orb"] - assert support["wealth_lord_links"] == ["yogi_planet_is_2l"] + assert support["formula_profile"] == "pyjhora_yogi_sphuta_v1" + assert support["yogi_planet"] == "Mercury" + assert support["duplicate_yogi"] == "Moon" + assert support["avayogi"] == "Mars" + assert support["yogi_point_longitude"] == 113.3333 + assert support["avayogi_point_longitude"] == 300.0 + assert support["yogi_point_nakshatra"] == "Ashlesha" + assert support["yogi_point_house"] == 4 + assert support["lagna_yogi_distance_deg"] == 93.3333 + assert support["tight_orb_hits"] == [] + assert support["wealth_lord_links"] == [] assert support["risk_flags"] == [] - assert support["signals"] == [ - "yogi_planet_in_wealth_house", - "yogi_planet_is_2l", - "lagna_yogi_tight_orb", - ] + assert support["signals"] == [] def test_wealth_folding_dhana_keeps_yogi_quiet_when_native_support_is_weak() -> None: @@ -1165,11 +1166,10 @@ def test_wealth_folding_adds_native_yogi_support_only_when_base_promise_exists() } res = _derive_wealth_promise_strength(modules) assert res["level"] == "moderate" - assert res["primary_source"] == "dhana_yogi_hooks" - assert res["source_diversity"] == 2 - assert res["supporting_sources"] == ["dhana", "yogi"] - assert res["yogi_support"]["level"] == "strong" - assert res["yogi_support"]["wealth_lord_links"] == ["yogi_planet_is_2l"] + assert res["primary_source"] == "dhana_yogas" + assert res["source_diversity"] == 1 + assert res["supporting_sources"] == ["dhana"] + assert res["yogi_support"] is None def test_wealth_folding_yogi_only_is_blocked_without_base_promise() -> None: @@ -1227,31 +1227,13 @@ def test_collect_strict_evidence_finance_adds_native_yogi_hook_without_external_ strict = _collect_strict_evidence("finance", result) assert strict["present_evidence"]["wealth_promise_strength"] == { "level": "moderate", - "primary_source": "dhana_yogi_hooks", + "primary_source": "dhana_yogas", "count": 1, - "source_diversity": 2, - "supporting_sources": ["dhana", "yogi"], - "yogi_support": { - "avayogi": "Saturn", - "duplicate_yogi": "Mars", - "lagna_yogi_distance_deg": 0.0, - "level": "strong", - "risk_flags": [], - "signals": [ - "yogi_planet_in_wealth_house", - "yogi_planet_is_2l", - "lagna_yogi_tight_orb", - ], - "source": "yogi_asc_tight_orb_wealth", - "tight_orb_hits": ["lagna_yogi_tight_orb"], - "wealth_lord_links": ["yogi_planet_is_2l"], - "yogi_planet": "Venus", - "yogi_point_house": 1, - "yogi_point_longitude": 20.0, - "yogi_point_nakshatra": "Bharani", - }, + "source_diversity": 1, + "supporting_sources": ["dhana"], + "yogi_support": None, } - assert "yogi_active" in strict["event_judgement"]["secondary_context"] + assert "yogi_active" not in strict["event_judgement"]["secondary_context"] def test_collect_strict_evidence_finance_keeps_native_yogi_quiet_when_support_is_weak() -> None: