diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index bffc8a1a..3eea3445 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -39,6 +39,7 @@ "websiteURL": "https://github.com/732642856/yinduzhanxing", "defaultPrompt": [ "用户如果只提供出生信息、没有具体问题,请不要要求用户自己想问题。", + "环境允许时,先运行 python3 scripts/user_invocation_acceptance_check.py 确认普通用户调用质量门。", "先调用 Jyotish MCP / unified consultation workflow,生成 evidence packet、guided_topics 和 Technique Audit Table。", "把 guided_topics 里的建议主题按优先级展示给用户,并提供可直接点击/复制的问题;用户选择后再进入 strict_workflow。", "VedAstro 没有 raw_response 时必须标 official_blocked 或 local_fallback,不要声称云端闭环。" diff --git a/README.md b/README.md index b6d741a7..5d631358 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,15 @@ This is a **Vedic (Jyotish) astrology analysis system** designed for deep, audit python3 scripts/vedastro_user_entrypoint.py --year YYYY --month MM --day DD --hour HH --minute mm --lat LAT --lon LON --tz TZ --question "请先生成 guided_topics 并推荐我最值得看的问题" --themes career,marriage,wealth --format markdown ``` +普通用户 / AI 应用从云端仓库加载后,先跑这一条验收: + +```bash +python3 scripts/user_invocation_acceptance_check.py +``` + +只有输出 `"status": "pass"` 且外部引擎状态被明确标为 `available` / `partial` / `missing_dependency` 时,才继续做高严谨解盘。`VedAstro` 没有官方 `raw_response` 或 API key 时,必须保留 `official_blocked` / `local_fallback` 边界。 +``` + ### 普通用户启动路径 如果只是打开网页/app,请按同一条路径走,不要在多个入口之间猜: @@ -134,8 +143,8 @@ cd jyotish-app && npm run dev -- --host 127.0.0.1 --port 5173 ```bash python3 scripts/vedastro_user_entrypoint.py \ - --year REDACTED_YEAR --month 4 --day 17 --hour 14 --minute 49 \ - --lat 36.42 --lon 114.2 --tz 8 \ + --year YYYY --month MM --day DD --hour HH --minute mm \ + --lat LAT --lon LON --tz TZ \ --question "事业机会什么时候出现" \ --themes career,marriage,wealth \ --reference-date 2026-07-02 \ @@ -146,8 +155,8 @@ python3 scripts/vedastro_user_entrypoint.py \ ```bash python3 scripts/vedastro_user_entrypoint.py \ - --year REDACTED_YEAR --month 4 --day 17 --hour 14 --minute 49 \ - --lat 36.42 --lon 114.2 --tz 8 \ + --year YYYY --month MM --day DD --hour HH --minute mm \ + --lat LAT --lon LON --tz TZ \ --question "事业机会什么时候出现" \ --themes career,health,education,property,children,migration,prashna \ --reference-date 2026-07-02 \ @@ -293,11 +302,11 @@ python3 scripts/local_accuracy_report.py --format json ```bash python3 scripts/dasha_reference_audit.py \ - --year REDACTED_YEAR --month 4 --day 17 \ - --hour 14 --minute 45 --second 20 \ - --lat 36.466667 --lon 114.2 --tz 8 \ + --year YYYY --month MM --day DD \ + --hour HH --minute mm --second ss \ + --lat LAT --lon LON --tz TZ \ --target-start-date 1986-05-18 \ - --target-source 印度占星1.pdf + --target-source third_party_chart.pdf ``` 该工具会输出当前 Vimshottari 起点、秒级出生时间敏感性、年长常数敏感性,以及对齐目标日期所需的 Moon sidereal longitude 偏移量。不要为单份 PDF 直接调生产常数;应先建立更大的 oracle 样本集,比较 ayanamsa、Moon sidereal longitude、Nakshatra 边界与 Vimshottari 起算口径。 diff --git a/SKILL.md b/SKILL.md index 3554341f..48f510e0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -70,6 +70,14 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 4. 用户选择主题后,再按 career / relationship / wealth / health / timing strict workflow 进入专题。 5. VedAstro 没有 `raw_response` 时,只能标 `official_blocked` 或 `local_fallback`,不得声称云端闭环。 +普通用户 / AI 应用调用前,先运行: + +```bash +python3 scripts/user_invocation_acceptance_check.py +``` + +该命令必须返回 `"status": "pass"`,并显式列出 VedAstro / PyJHora-JHora / jyotishganit 的可用、partial 或 blocked 状态;否则不得声称云端 Git 仓库调用已可高质量使用。 + **强制工作流**(完整规范 → `references/ai-reading-workflow-prompt.md` v5.1.0): 0. **阶段负一**:问题类型路由(事业/婚恋/财务/应期/历史验证/综合解盘)→ 必须先读 `references/strict-workflow-router.md`,按对应 strict checklist 执行;用户不需要主动点名高级技法。 @@ -218,7 +226,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 **普通用户解释时必须显式区分基础排盘高可信与高阶绝对值待外部校准。** -- Dasha-only 外部证据当前目标集已闭环:`dasha_external_oracle_evidence_validation.valid_dasha_packets: 3/3`;Steve Jobs / Lahiri、REDACTED_YEAR Lahiri 模板与 1800 Delhi historical epoch 的 Vimshottari 起始边界来自 PyJHora 4.8.7 隔离黑盒 stdout artifact。 +- Dasha-only 外部证据当前目标集已闭环:`dasha_external_oracle_evidence_validation.valid_dasha_packets: 3/3`;Steve Jobs / Lahiri、synthetic Lahiri template 与 1800 Delhi historical epoch 的 Vimshottari 起始边界来自 PyJHora 4.8.7 隔离黑盒 stdout artifact。 - 全局 Dasha/Shadbala Calibration Status 仍未完成:`external_oracle_evidence_validation.valid_packets: 4`,`ready_for_calibration: 4`;Shadbala 外部绝对值当前目标集已通过 4/4,Raman 扩展样本与非 Dasha 靶点尚未封顶。 - 历史 UI 静态门禁仍保留旧提示 `ready_for_calibration: 0` 作为“不得过度宣称”的保守文案;实际进度必须以当前 `oracle_collection_queue.py` / `oracle_evidence_validator.py` 输出为准。 - Tajika/Sahams 年运外部样本已开始闭环:`tajika_sahams_annual_benchmark_dashboard.ready_for_calibration: 1/5`;Steve Jobs 1984 Varshaphala/Lahiri 的 solar return、Varsha Lagna、Muntha、Year Lord、Mudda Dasha 首主、三项 Sahams 与 Tajika Yogas 已由 PyJHora 4.8.7 隔离黑盒 artifact 验证,下一优先级为 Einstein 1905。仍不得声称 Tajika/Sahams 年运体系已全局封顶。 diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 7bfed02f..6ab04e4e 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -49,6 +49,7 @@ For large architecture or release work, also read: | ERR-016 | Full `tests/test_api_server_security.py` can exceed the desktop outer timeout. | observed 2026-07-05 | Use focused API test slices during development; reserve full API file run for longer verification windows. | | ERR-017 | Pre-work gate could pass without checking the older Round 25 fragment sweep or aggregate external-engine adapter diagnostics. | mitigated 2026-07-05 | `scripts/pre_work_check.py` must require both whole-machine sweep docs and run `scripts/diagnose_external_engine_adapters.py --json` before substantial work. | | ERR-018 | External engine blockers can be described verbally but not carried into diagnostics. | mitigated 2026-07-05 | `diagnose_external_engine_adapters.py` must expose VedAstro closure plan and PyJHora/JHora install/license/ephemeris boundary; keep `docs/research/external_engine_blocker_research_2026_07_05.md` current. | +| ERR-019 | WorkBuddy/cloud/local acceptance summaries can invent pass counts, stale asset counts, non-existent error docs, or wrong dasha windows. | mitigated 2026-07-06 | Read `docs/research/user_invocation_acceptance_error_log_2026_07_06.md`; run `scripts/user_invocation_acceptance_check.py` and `tests/test_user_invocation_acceptance_contract.py` before accepting ordinary-user skill invocation validation claims. | ## Fragment Sweep Command Set diff --git a/docs/research/user_invocation_acceptance_error_log_2026_07_06.md b/docs/research/user_invocation_acceptance_error_log_2026_07_06.md new file mode 100644 index 00000000..b409d9a0 --- /dev/null +++ b/docs/research/user_invocation_acceptance_error_log_2026_07_06.md @@ -0,0 +1,38 @@ +# User Invocation Acceptance Error Log - 2026-07-06 + +Purpose: read this before accepting WorkBuddy, cloud Git, local mirror, or AI-app validation claims about ordinary-user Jyotish usage. + +## Trigger + +A WorkBuddy validation summary claimed project-wide acceptance facts that were not reproducible from the main repository. + +## Errors Found + +| ID | Error | Verified Correction | Guard | +|---|---|---|---| +| WB-001 | Claimed `29/29` cross-validation without a committed, reproducible script or test artifact. | No matching `29/29` evidence artifact was found in main repo, `.workbuddy`, or bounded Desktop/Documents/Downloads scans. | Require a committed test/script path and rerun command before accepting pass counts. | +| WB-002 | Claimed stale asset counts such as `175` scripts and `157` tests. | Main repo counts differ. Counts must be regenerated from the current checkout, not memory or mirror state. | Use `find` / `rg --files` on the main repo checkout. | +| WB-003 | Claimed `印度占星分析错误文档.md` existed. | No such tracked project document was found. | Error docs must live under `docs/research/` and be referenced by `docs/research/pre_work_error_ledger.md`. | +| WB-004 | Claimed the fixture dasha timeline put 2027-03 in Mercury AD, with future Venus AD starting 2028-05-10. | Main engine reports 2027-03-01 as `Saturn / Venus`, AD `2027-02-27 -> 2030-04-29`. | Run `tests/test_user_invocation_acceptance_contract.py`. | +| WB-005 | Claimed `vivah-saham` still needed an import-path fix. | `scripts/jyotish_engine.py vivah-saham ...` runs successfully for the standard sample. | Do not report import breakage without running the command. | +| WB-006 | Overstated external-oracle completeness. | VedAstro/PyJHora/JHora status remains adapter-dependent and must be reported as `official_verified`, `official_blocked`, `local_fallback`, or equivalent audited state. | Run `scripts/diagnose_external_engine_adapters.py --json` and include blocked/partial rows in Technique Audit output. | + +## Acceptance Commands + +```bash +python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45 +python3 scripts/user_invocation_acceptance_check.py +python3 -m pytest -q tests/test_user_invocation_acceptance_contract.py tests/test_project_fragment_governance.py +python3 scripts/diagnose_external_engine_adapters.py --json +python3 scripts/jyotish_engine.py vivah-saham --year 2000 --month 1 --day 1 --hour 12 --minute 0 --lat 0.0 --lon 0.0 --tz 0 --transit-date 2026-07-06 +``` + +## Acceptance Rule + +Ordinary-user invocation is accepted only when the current main repo proves: + +- Skill/plugin/default prompt can guide a user who has no question. +- User entrypoint can start from a guided-topics request. +- Dasha fixture rejects stale WorkBuddy timing claims. +- Error ledger names this failure mode and the test that guards it. +- External oracle status is explicit, not silently upgraded. diff --git a/scripts/user_invocation_acceptance_check.py b/scripts/user_invocation_acceptance_check.py new file mode 100644 index 00000000..de00f9b0 --- /dev/null +++ b/scripts/user_invocation_acceptance_check.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""One-command smoke check for ordinary AI-app / skill invocation.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] + + +def _run(args: list[str], *, timeout: int = 120, env: dict[str, str] | None = None) -> dict[str, Any]: + completed = subprocess.run( + args, + cwd=ROOT, + text=True, + capture_output=True, + timeout=timeout, + check=False, + env=env, + ) + return { + "command": " ".join(args), + "returncode": completed.returncode, + "stdout": completed.stdout, + "stderr": completed.stderr, + } + + +def _entrypoint_env() -> dict[str, str]: + env = os.environ.copy() + env.update( + { + "JYOTISH_SKIP_LOCAL_ENV": "1", + "VEDASTRO_API_ENDPOINT": "", + "VEDASTRO_ENABLE_NETWORK": "", + "VEDASTRO_TIMEOUT_SECONDS": "", + } + ) + return env + + +def _engine_summary(report: dict[str, Any]) -> dict[str, Any]: + summary: dict[str, Any] = {} + engines = report.get("engines") if isinstance(report.get("engines"), dict) else {} + for name, details in engines.items(): + if not isinstance(details, dict): + continue + summary[name] = { + "status": details.get("status"), + "readiness_blockers": details.get("readiness_blockers") + or ([details["missing_dependency"]] if details.get("missing_dependency") else []), + "license_boundary": details.get("license_boundary"), + } + return summary + + +def _runtime_summary(report: dict[str, Any]) -> dict[str, Any]: + runtime = report.get("runtime_mode") if isinstance(report.get("runtime_mode"), dict) else {} + return { + "mode": runtime.get("mode"), + "official_ready": runtime.get("official_ready"), + "expected_fallback_status": runtime.get("expected_fallback_status"), + } + + +def _strict_summary(report: dict[str, Any]) -> dict[str, Any]: + strict = report.get("strict_workflow") if isinstance(report.get("strict_workflow"), dict) else {} + return { + "triggered": strict.get("triggered"), + "primary_route": strict.get("primary_route"), + "routes_available": strict.get("routes_available"), + "source": strict.get("source"), + } + + +def main() -> int: + pytest_check = _run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "tests/test_user_invocation_acceptance_contract.py", + "-k", + "not one_command_user_invocation_acceptance_check", + ], + timeout=180, + ) + entrypoint = _run( + [ + sys.executable, + "scripts/vedastro_user_entrypoint.py", + "--year", + "2000", + "--month", + "1", + "--day", + "1", + "--hour", + "12", + "--minute", + "0", + "--lat", + "0.0", + "--lon", + "0.0", + "--tz", + "0", + "--question", + "请先生成 guided_topics 并推荐我最值得看的问题", + "--themes", + "career,marriage,wealth", + "--reference-date", + "2026-07-06", + "--format", + "json", + ], + env=_entrypoint_env(), + ) + adapters = _run([sys.executable, "scripts/diagnose_external_engine_adapters.py", "--json"], timeout=120) + + errors: list[str] = [] + entrypoint_report: dict[str, Any] = {} + adapter_report: dict[str, Any] = {} + if pytest_check["returncode"] != 0: + errors.append("user_invocation_pytest_failed") + if entrypoint["returncode"] != 0: + errors.append("user_entrypoint_failed") + else: + try: + entrypoint_report = json.loads(entrypoint["stdout"]) + except json.JSONDecodeError: + errors.append("user_entrypoint_invalid_json") + if adapters["returncode"] != 0: + errors.append("external_adapter_diagnostic_failed") + else: + try: + adapter_report = json.loads(adapters["stdout"]) + except json.JSONDecodeError: + errors.append("external_adapter_diagnostic_invalid_json") + + required_entrypoint_paths = [ + ("strict_workflow", "triggered"), + ("strict_workflow", "routes_available"), + ("runtime_mode", "expected_fallback_status"), + ("honesty_boundary", "all_641_methods_executed"), + ] + for path in required_entrypoint_paths: + cursor: Any = entrypoint_report + for key in path: + cursor = cursor.get(key) if isinstance(cursor, dict) else None + if cursor in (None, [], ""): + errors.append(f"missing_entrypoint_{'_'.join(path)}") + + if adapter_report.get("status") not in {"pass", "partial"}: + errors.append("external_adapter_status_unusable") + + result = { + "scope": "user_invocation_acceptance_check", + "status": "pass" if not errors else "fail", + "errors": errors, + "checks": { + "user_invocation_tests": pytest_check["returncode"] == 0, + "guided_topics_entrypoint": entrypoint["returncode"] == 0 and not any(e.startswith("missing_entrypoint_") for e in errors), + "external_adapter_diagnostics": adapter_report.get("status") in {"pass", "partial"}, + }, + "entrypoint_runtime_mode": _runtime_summary(entrypoint_report), + "entrypoint_strict_workflow": _strict_summary(entrypoint_report), + "external_adapter_status": adapter_report.get("status"), + "external_engines": _engine_summary(adapter_report), + } + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 if not errors else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_project_fragment_governance.py b/tests/test_project_fragment_governance.py index 6f870725..97d4081f 100644 --- a/tests/test_project_fragment_governance.py +++ b/tests/test_project_fragment_governance.py @@ -17,11 +17,14 @@ def test_pre_work_error_ledger_exists_and_names_repeat_failures() -> None: "ERR-009", "ERR-017", "ERR-018", + "ERR-019", "tests/test_runtime_import_boundaries.py", "tests/test_project_fragment_governance.py", + "tests/test_user_invocation_acceptance_contract.py", "docs/research/whole_machine_fragment_sweep_round25_2026_06_25.md", "scripts/diagnose_external_engine_adapters.py --json", "docs/research/external_engine_blocker_research_2026_07_05.md", + "docs/research/user_invocation_acceptance_error_log_2026_07_06.md", ] missing = [item for item in required if item not in text] assert missing == [] diff --git a/tests/test_user_invocation_acceptance_contract.py b/tests/test_user_invocation_acceptance_contract.py new file mode 100644 index 00000000..590f92dd --- /dev/null +++ b/tests/test_user_invocation_acceptance_contract.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_skill_and_plugin_default_to_guided_topics_when_user_has_no_question() -> None: + skill = (ROOT / "SKILL.md").read_text(encoding="utf-8") + readme = (ROOT / "README.md").read_text(encoding="utf-8") + plugin = json.loads((ROOT / ".codex-plugin" / "plugin.json").read_text(encoding="utf-8")) + default_prompt = "\n".join(plugin["interface"]["defaultPrompt"]) + + for text in (skill, readme, default_prompt): + assert "guided_topics" in text + assert "strict_workflow" in text + assert "evidence_packet" in text or "evidence packet" in text + assert "Technique Audit Table" in text + assert "scripts/user_invocation_acceptance_check.py" in text + + assert "不要反问" in skill + assert "不要要求用户自己想问题" in default_prompt + assert "raw_response" in default_prompt + + +def test_user_entrypoint_can_start_from_guided_topics_prompt() -> None: + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_user_entrypoint.py", + "--year", + "2000", + "--month", + "1", + "--day", + "1", + "--hour", + "12", + "--minute", + "0", + "--lat", + "0.0", + "--lon", + "0.0", + "--tz", + "0", + "--question", + "请先生成 guided_topics 并推荐我最值得看的问题", + "--themes", + "career,marriage,wealth", + "--reference-date", + "2026-07-06", + "--format", + "json", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env={ + **os.environ, + "JYOTISH_SKIP_LOCAL_ENV": "1", + "VEDASTRO_API_ENDPOINT": "", + "VEDASTRO_ENABLE_NETWORK": "", + "VEDASTRO_TIMEOUT_SECONDS": "", + }, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["strict_workflow"]["triggered"] is True + assert report["strict_workflow"]["routes_available"] + assert report["runtime_mode"]["expected_fallback_status"] in { + "none_if_official_endpoint_responds", + "official_snapshot_budget_exhausted_or_endpoint_blocked", + } + assert report["honesty_boundary"]["all_641_methods_executed"] is False + assert report["input"]["question"] == "请先生成 guided_topics 并推荐我最值得看的问题" + + +def test_fixture_dasha_timeline_rejects_workbuddy_regression_claims() -> None: + base = [ + sys.executable, + "scripts/jyotish_engine.py", + "dasha", + "--year", + "2000", + "--month", + "1", + "--day", + "1", + "--hour", + "12", + "--minute", + "0", + "--lat", + "0.0", + "--lon", + "0.0", + "--tz", + "0", + "--years", + "45", + ] + + observed = {} + for today in ("2014-06-15", "2024-03-01", "2027-03-01"): + completed = subprocess.run( + [*base, "--today", today], + cwd=ROOT, + text=True, + capture_output=True, + timeout=60, + check=False, + ) + assert completed.returncode == 0, completed.stderr or completed.stdout + current = json.loads(completed.stdout)["current_dasha"] + observed[today] = ( + current["mahadasha"], + current["antardasha"]["lord"], + current["antardasha"]["start"], + current["antardasha"]["end"], + ) + + assert observed["2014-06-15"] == ("Jupiter", "Rahu", "2014-04-27", "2016-09-20") + assert observed["2024-03-01"] == ("Saturn", "Venus", "2023-07-12", "2026-09-11") + assert observed["2027-03-01"] == ("Saturn", "Sun", "2026-09-11", "2027-08-24") + + +def test_one_command_user_invocation_acceptance_check() -> None: + completed = subprocess.run( + [sys.executable, "scripts/user_invocation_acceptance_check.py"], + cwd=ROOT, + text=True, + capture_output=True, + timeout=240, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["status"] == "pass" + assert report["checks"]["user_invocation_tests"] is True + assert report["checks"]["guided_topics_entrypoint"] is True + assert report["checks"]["external_adapter_diagnostics"] is True + assert report["external_adapter_status"] in {"pass", "partial"}