Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a4536333 | ||
|
|
2415e751fe | ||
|
|
b6a70aa7df | ||
|
|
58cae371bb | ||
|
|
487fed40de | ||
|
|
2dba9e2597 | ||
|
|
4ac9883de6 | ||
|
|
2974ca5b85 | ||
|
|
591ffd21a1 | ||
|
|
74aaa1ba99 | ||
|
|
62e1592001 | ||
|
|
d159f08eb2 |
@@ -4,10 +4,10 @@
|
||||
"description": "Jyotish local Codex plugin wrapper for Vedic astrology skills and MCP tools.",
|
||||
"author": {
|
||||
"name": "jyotish-skill",
|
||||
"url": "https://github.com/732642856/yinduzhanxing"
|
||||
"url": "https://git.copse.top/root/Jyotisha"
|
||||
},
|
||||
"homepage": "https://github.com/732642856/yinduzhanxing",
|
||||
"repository": "https://github.com/732642856/yinduzhanxing",
|
||||
"homepage": "https://git.copse.top/root/Jyotisha",
|
||||
"repository": "https://git.copse.top/root/Jyotisha",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"jyotish",
|
||||
@@ -36,7 +36,7 @@
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://github.com/732642856/yinduzhanxing",
|
||||
"websiteURL": "https://git.copse.top/root/Jyotisha",
|
||||
"defaultPrompt": [
|
||||
"用户如果只提供出生信息、没有具体问题,请不要要求用户自己想问题。",
|
||||
"环境允许时,先运行 python3 scripts/user_invocation_acceptance_check.py 确认普通用户调用质量门。",
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
| 真人验收清单 | `docs/testing/` | 自动化做不了的浏览器级验收写成可照做的条目 |
|
||||
| 领域术语 | `CONTEXT.md` | 新术语先加到 glossary 再用 |
|
||||
|
||||
`progress.md`、`findings.md`、`task_plan.md` 是早期实现日志,不是运行或部署说明,不再追加。
|
||||
`docs/history/progress.md`、`docs/history/findings.md`、`docs/history/task_plan.md` 是早期实现日志,不是运行或部署说明,不再追加。
|
||||
|
||||
## 5. Bug History Workflow Hard Constraint
|
||||
|
||||
|
||||
+18
@@ -1,5 +1,23 @@
|
||||
# BLOCKED
|
||||
|
||||
## 仓库整备任务 1:发布门仍有三条非环境红(2026-09-03,分支 `codex/repo-hygiene-20260903`)
|
||||
|
||||
任务 0 之后,附录里 staging 独有的 6 条里,夹具大运 / 用户调用验收 / 技能包验收已绿。剩下 3 条**不改期望值、不改校正打分**:
|
||||
|
||||
| 测试 | 现象 | 分类 | 处置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `test_local_accuracy_report_outputs_machine_readable_baseline` | `valid_packets` 3 ≥ 4;`dasha_shadbala_oracle_cases.json` 4 包里 1 个仍是 `template_only` | 参考值/oracle 未齐 | 不改断言、不升级模板包。公开案例复验已 `66/66`。 |
|
||||
| `test_historical_event_priority_preserves_vimshottari_actor_difference` | `selection_priority == 0.0`(1879-03-14 相邻分钟、1922 career) | 校正引擎 / Vimshottari 边界,本轮禁止改阈值 | 交产品 |
|
||||
| `test_v3_development_result_stays_shadow_only` | `v3_improved_case_count` 1 ≠ 0 | 同上 | 交产品 |
|
||||
|
||||
因此 `run_quality_gate.py --profile release` **不能**在本轮声称退出 0。全量 pytest 里附录那 83 条生产同样红的环境/过期项不在本轮修。
|
||||
|
||||
复现:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest -q tests/test_local_accuracy_report.py tests/test_dynamic_rectification_fact_priority.py::test_historical_event_priority_preserves_vimshottari_actor_difference tests/test_minute_rectification_development.py::test_v3_development_result_stays_shadow_only
|
||||
```
|
||||
|
||||
## 生时校正会话面:消除空白假死与交互摩擦(2026-09-03,分支 `codex/rectification-ux-20260903`,重做)
|
||||
|
||||
- **浏览器级手工验收未做:执行环境无登录态、无 Chrome。** 任务书任务 0 的手工项与任务 1 的整轮录屏(开场 → 3 道选择题 → 候选 → 采用)都做不了;已按任务书把它们写进 `docs/testing/staging-manual-walkthrough-20260901.md` 第 8 节(含深链/刷新与缺口重试两条),交给有真实会话的人。本轮的替代证据:`tests/rectification-surface-state.test.ts`(纯函数会话态/缺口态、hydration 超时/失败、turn 解析)与 `tests/rectification-surface-contract.test.ts`(源码锁:一次揭幕、prepare 阶段 hydration、无重挂、续接同一行、空态、板首态)。
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-03 — 岁差成为用户设置(默认仍是 Raman)
|
||||
|
||||
自 `80102459`(2026-08-20)起,产品计算默认岁差已是 Raman,但参考集对照仍按 Lahiri 口径写死期望值,API 回执里的岁差标签也和实际计算对不上。本轮**不改**默认值(仍是 Raman),把岁差做成账户设置:`raman` / `lahiri` / `kp` / `true_pushya`。公开案例复验与夹具大运显式传 `--ayanamsa lahiri`。升级后老用户可能看到 Moon nakshatra 或大运主星与 8 月 20 日前的 Lahiri 盘不同;可在「星盘资料 → 编辑本人资料」里切回 Lahiri。切换只影响之后的新计算。
|
||||
|
||||
## 2026-08-30 — 非收敛区间提议不再是无出口死胡同(Skill 10.0.13 不变)
|
||||
|
||||
训练门已开、覆盖层仍挡住确认、可渲染区分题又问不出来时,助手会说「可以先按当前区间看盘,也可以再补一件记得时间的经历」,但时间卡、下一问、区间数值都不给。本轮把这句话改成与真实承载一致。
|
||||
|
||||
@@ -156,7 +156,7 @@ The Python engine routes the technique registry (`references/technique_registry.
|
||||
| Skill 方法与真相覆盖层 | [`SKILL.md`](SKILL.md)、`skills/*/SKILL.md`、`references/` |
|
||||
| 引擎/基础设施历史错误台账 | [`docs/research/pre_work_error_ledger.md`](docs/research/pre_work_error_ledger.md) |
|
||||
| 架构决策记录 | [`docs/adr/`](docs/adr/) |
|
||||
| 早期实现日志(不是运行说明) | `progress.md`、`findings.md`、`task_plan.md`、`CHANGELOG_v6.2_to_v6.9.md` |
|
||||
| 早期实现日志(不是运行说明) | [`docs/history/`](docs/history/README.md) |
|
||||
|
||||
## 隐私与安全红线
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: jyotish-vedic-astrology
|
||||
version: 6.9.14
|
||||
description: "印度占星(Jyotish)商业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→受合同约束的推运方向输出。Dasha、Yoga、分盘、合盘与补救建议按计算回执和能力状态交付;精确应期及受限技法不会作为确定性商业结论。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga。GitHub: https://github.com/732642856/yinduzhanxing"
|
||||
description: "印度占星(Jyotish)商业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→受合同约束的推运方向输出。Dasha、Yoga、分盘、合盘与补救建议按计算回执和能力状态交付;精确应期及受限技法不会作为确定性商业结论。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga。Repo: https://git.copse.top/root/Jyotisha"
|
||||
---
|
||||
|
||||
# 印度占星专业解盘与推运系统
|
||||
|
||||
@@ -7856,3 +7856,35 @@
|
||||
- 相关记录:BUG-505
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-510 | 采集阶段盘外核对选择题画出 A–D 却点不了
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-03
|
||||
- 最近更新:2026-09-03
|
||||
- 影响面:`projectRectificationChoiceCard`、GET `/api/rectification/cases/[caseId]` `choice_card`、`rectification-agentic-chat` 内嵌卡
|
||||
- 用户现象:助手记下带月份的职场事后,下面出现「某年前后,有没有开始一段认真关系、分手或结婚?」和 A–D,点任何一项都没有反应。`current_question` 已是选择题,`choice_card` 为 null。
|
||||
- 触发条件:采集阶段已有带年份事件,下一问是 `intent=out_of_sample_check` 的四点选存在性题;schema 有合法 A–D,但没有 `probe_year`。该回合未调用 `rectification-set-focus`,题目由 idle persist 挂到最后一条助手消息。`selection_allowed` 仍为 false(采用门,不是点选门)。
|
||||
- 根因:keep 路径 `forceChoice` 会尝试重铸 `choice_frame`。缺 `probe_year` 时 `hypothesisFor` 没有具体时期,frame 为空,GET 按设计把 `choice_card` 置空。界面仍从 turn 上的 `question.options` 画出 A–D,但 `liveQuestion` 要求 GET 卡与 `focus_id` 一致,`disabled={!liveQuestion}`,`submitChoice` 在没有 `choiceCard` 时直接 return。
|
||||
- 修复:`projectRectificationChoiceCard` 在重建 followup 没有 `choice_frame` 时,若当前焦点已是 `reverse_verify` / `out_of_sample_check` 且 schema 能解析出四点选,按已持久化文案投影 `choice_card`。口述采集 schema 仍不发卡。未放宽区分题身份校验,未打开 `selection_allowed`。
|
||||
- 验证:`rectification-choice-card` 锁定无 `probe_year` 的盘外存在性卡仍可投影、口述采集盘外题仍不发卡;`rectification-agentic-entry` 锁内嵌卡 `disabled={!liveQuestion}` 且点选要求 GET 卡 `focus_id` 一致。
|
||||
- 防复发:GET 已有合法持久化 reverse_verify / out_of_sample_check 四点选 schema 时,不得因重建缺 `choice_frame` 把 `choice_card` 置空。界面画出 A–D 时 GET 必须给出可点的卡。不得把 `selection_allowed` 当成 A–D 点选门。不得从题干正则反推年份。
|
||||
- 相关记录:BUG-416、BUG-431、BUG-498
|
||||
- 复发自:BUG-431
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-511 | 公开案例复验因默认岁差改 Raman 而失败,标签与计算不一致
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-03
|
||||
- 最近更新:2026-09-03
|
||||
- 影响面:`tests/run_real_case_revalidation.py`、`scripts/jyotish_api_server.py` 岁差回执、账户 `profiles.ayanamsa`、咨询/报告/校正/每日星语请求口径
|
||||
- 用户现象:发布门真实案例复验 `valid=false`,gated 通过率掉到门槛以下。同一张虚构盘在旧生产与当前默认下 Moon nakshatra / 首段大运不同。回执上的岁差名有时写成 Lahiri 或 Raman,与该次实际计算不是同一套。
|
||||
- 触发条件:`80102459` 把引擎默认改成 Raman 后,参考集调用没有显式锁定 Lahiri;VedAstro `ayanamsa_policy` 与部分回执字段仍写死字面量。
|
||||
- 根因:参考集对照依赖「默认值恰好是 Lahiri」;产品默认改为 Raman 后口径漂移。三处标签用字面量兜底,不读该次请求实际岁差。账户没有用户可选岁差。
|
||||
- 修复:参考集与夹具大运显式 `--ayanamsa lahiri`。API 回执与 VedAstro 对照改用该次请求实际岁差。`profiles.ayanamsa` 默认 `raman`,设置里可选四个值;咨询、报告、校正、星盘库、每日星语走 `resolveAyanamsa`。`DEFAULT_AYANAMSA_NAME` 本轮不改。
|
||||
- 验证:夹具大运日期不变;前端 `resolveAyanamsa`、账户 PATCH 四值校验、咨询 toolInput 带 profile 岁差、校正快照带岁差但不进 fingerprint;`npm run test:db` 跑迁移。公开案例复验命令见本轮 PROGRESS。
|
||||
- 防复发:参考集与外部对照必须显式传岁差,不得依赖默认值。回执只显示实际名。岁差不进校正 `baselineFingerprint`,不重算历史报告。
|
||||
- 相关记录:无
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 早期实现日志
|
||||
|
||||
这里是 2026-06~08 的实现日志,不是运行说明,也不再追加。
|
||||
|
||||
- `progress.md`
|
||||
- `findings.md`
|
||||
- `task_plan.md`
|
||||
- `CHANGELOG_v6.2_to_v6.9.md`
|
||||
@@ -0,0 +1,103 @@
|
||||
# PROGRESS · 仓库整备(2026-09-03)
|
||||
|
||||
工作树:`/Users/jesse/Downloads/Copse/astrology/.worktrees/repo-hygiene-20260903`
|
||||
分支:`codex/repo-hygiene-20260903`
|
||||
基线:开工后快进到 `origin/staging` @ `58cae371`(含 BUG-510 与上游同步任务书)。任务书:`docs/tasks/TASK-repo-hygiene-20260903.md`。
|
||||
未碰 `main`。未改 `DEFAULT_AYANAMSA_NAME`(保持 `raman`)。未改校正打分/阈值/golden。未改 `page.tsx`(仍 2041 行)。`scripts/jyotish_api_server.py` 仍 11120 行。
|
||||
|
||||
BUG 编号:开工时最大号 **BUG-510**。岁差记 **BUG-511**。
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 0 岁差设置 + 参考集锁口径 | 待验收 | `b6a70aa7` 已推功能分支。产品 staging 实测仍欠 |
|
||||
| 1 发布门本地可复现 | 部分完成 | 选项 (b) 已做;release profile 因 3 条 BLOCKED 不能声称退出 0 |
|
||||
| 2 注册表白名单 | 完成 | `audit_capabilities.py --mode validate` → `valid=true, problem_count=0` |
|
||||
| 3 仓库地址 | 完成 | Gitea 产品仓;slug 解析用例保留 |
|
||||
| 4 早期日志搬家 | 完成 | 根目录 `ls *.md` 只剩规定 8 个(外加 gitignore 的 `COVERAGE_AUDIT_REPORT.md`) |
|
||||
| 5 远端分支清理 | 完成 | 名单 67 条已从 origin 删除;`ls-remote --heads` **74 → 7** |
|
||||
|
||||
## 环境备忘
|
||||
|
||||
- 本 worktree **没有** `.venv`。预检与 Python 测试用主仓 `/Users/jesse/Downloads/Copse/astrology/yinduzhanxing/.venv`(`mcp` 1.28.1,`hypothesis` 已装)。
|
||||
- 本地错装 `mcp 2.x` / 缺 `hypothesis` 是环境问题,不改 pin。`requirements.txt` 仍是 `mcp>=1.0,<2`;`requirements-dev.txt` 已有 `hypothesis>=6.0`。
|
||||
- 任务书写把迁移放进 `frontend/db/migrations/`。实际 migrator 跑 supabase + db 两目录。本轮唯一迁移:`frontend/supabase/migrations/20260903010000_profile_ayanamsa.sql`。
|
||||
- 「账户与出生资料」在现界面是 **星盘资料** 本人编辑表。岁差单选在那里,未改 `page.tsx`,未进 `个人资料`。
|
||||
- `npm run test:db` 不可与 `rectification-*.test.ts` 并行(fixture 槽默认 2)。
|
||||
- `python3 scripts/pre_work_check.py` 在本 worktree 用 Anaconda 3.12:`remote_visibility_status=verified`(不再因旧 slug blocked)。失败项是 `fragment_scan` 90s timeout 与 focused tests 45s timeout,属环境预算,不是 slug。
|
||||
|
||||
## 任务 0
|
||||
|
||||
见提交 `b6a70aa7`。公开案例复验 `valid: true`、gated **66/66**、`pass_rate: 1.0`。`npm run test:db` **34/34**。产品虚构盘实测仍欠。部署 SHA 待合入 staging 后补。
|
||||
|
||||
### 0.a 三栏
|
||||
|
||||
夹具大运 `base`:原值=依赖默认岁差;新值=显式 `--ayanamsa lahiri`;原因=夹具日期是 Lahiri 口径。期望日期未改。
|
||||
|
||||
### 偏离
|
||||
|
||||
- 迁移目录:supabase。
|
||||
- UI:星盘资料本人编辑,不是不存在的「账户与出生资料」面板。
|
||||
|
||||
## 任务 1
|
||||
|
||||
选 **(b)**:PyJHora `missing_dependency` 时 `user_invocation_acceptance_check` 的 `status` 为 `partial`,退出码 0。硬错误仍 `fail`。
|
||||
|
||||
三栏(`test_one_command_user_invocation_acceptance_check` / 技能包验收):原值=`status == "pass"`;新值=`pass` 或 `partial`;原因=外部参照引擎缺席应降级不应失败。`external_adapter_status` 仍必须在 `{pass, partial, complete}`。
|
||||
|
||||
测试不改写仓库:`character_level_inventory_manifest.py` 增加 `--output-dir`;写报告的测试改写 `tmp_path`。`numeric_oracle_gap_queue_v3.py` / `hard_gap_source_hunt_2026_07_23.py` 的测试加 `--no-write`。跑完后 `git status` 无 `docs/research/character_level_*_latest.*`、无那两份 oracle JSON 被改写。
|
||||
|
||||
### 全量红名单处理表
|
||||
|
||||
附录 6 条 staging 独有:
|
||||
|
||||
| 测试 | 分类 | 本轮 |
|
||||
| --- | --- | --- |
|
||||
| `test_fixture_dasha_timeline_rejects_workbuddy_regression_claims` | 任务 0.a 口径 | 绿 |
|
||||
| `test_one_command_user_invocation_acceptance_check` | 连带 + 选项 b | 绿(本机 PyJHora 可用时 `pass`) |
|
||||
| `test_premium_skill_zip_runs_from_clean_directory` | 连带 + 选项 b | 绿 |
|
||||
| `test_local_accuracy_report.py`(`valid_packets >= 4`) | 参考值/oracle:4 包 1 个 `template_only`,校验 3 valid | BLOCKED,不改值 |
|
||||
| `test_historical_event_priority_preserves_vimshottari_actor_difference` | 校正引擎 | BLOCKED |
|
||||
| `test_v3_development_result_stays_shadow_only` | 校正引擎 | BLOCKED |
|
||||
|
||||
附录 83 条生产同样红、13 个收集 ERROR(缺 hypothesis / mcp 2.x):环境类。本机 CI 同款 venv 已有 hypothesis 与 mcp 1.x,收集 ERROR 应消失。未在本轮把 `run_quality_gate.py --profile release` 跑到退出 0。
|
||||
|
||||
## 任务 2
|
||||
|
||||
- 白名单加 `experimental_variant`、`comparison_only`;`calculation` 允许 `experimental`(注册表现况);`rule`/`prediction` 允许 `blocked`。
|
||||
- `comparison_only` 计入 `comparison_only_entries`,政策位与 `audit_only` 同等:不能影响结论。
|
||||
- 断言:`experimental_variant` 的 prediction/rule 必须 `blocked`,`user_visibility` 必须 `expert_audit`。
|
||||
- `scripts/audit_capabilities.py --mode validate`:`valid=true, problem_count=0`(既有 warning:Rangacharya knowledge_ref 缺一篇 spec)。
|
||||
|
||||
## 任务 3
|
||||
|
||||
- `pyproject.toml`、`.codex-plugin/plugin.json`、根 `SKILL.md`、`skill_release_package.py`、`skill_release_manifest.py`、`skills/jyotish-engine-modules/SKILL.md` 改为 `https://git.copse.top/root/Jyotisha`(Bug Reports `/issues`,Documentation 指向 Gitea 上的 `README.md`)。
|
||||
- 发行测试三栏:原值=上游研究仓 GitHub;新值=产品 Gitea;原因=安装说明必须指向产品仓。
|
||||
- `remote_repo_visibility_check.py`:非 GitHub remote 不再退回 `732642856/yinduzhanxing`;报告 `provider: gitea`,用 `git ls-remote`。slug 解析用例未改,新增 Gitea 用例。
|
||||
- 任务书 grep 剩余:`tests/test_remote_repo_visibility_check.py` 的 GitHub slug 解析行(任务书要求保留)。`COVERAGE_AUDIT_REPORT.md` 在 `.gitignore`。hashed `skills/*/versions`、`references/`、`docs/`、import 测试按任务书排除。
|
||||
|
||||
## 任务 4
|
||||
|
||||
`git mv` 四份早期日志到 `docs/history/`,新增 `docs/history/README.md`。`RELEASE_CRITICAL_UNTRACKED_PATHS` 三个路径改为 `docs/history/...`。`README.md` 文档地图与 `AGENTS.md` §4 最后一行已同步。`frontend/tests/staging-backend-workflows.test.ts` 的 docs-only 名单跟着改,避免根路径幽灵。
|
||||
|
||||
## 任务 5
|
||||
|
||||
`git fetch origin --prune` 后,`git branch -r --merged origin/staging` 相对任务书名单:
|
||||
|
||||
- 名单 67 条全部仍在,不少。
|
||||
- 多出已合入、**不删**(不在名单):`codex/rectification-ux-20260903`。
|
||||
- 未合入、**不删**:`codex/rectification-ux-20260902`、`codex/diagnose-rectification-request-20260827`、`fix/staging-gate-checkout-retry`,外加本分支 `codex/repo-hygiene-20260903`。
|
||||
- `git ls-remote --heads origin` 当前 **74**。删名单后预期约 7(`main`、`staging`、本分支、3 条未合入保留、外加已合入但未列入名单的 `rectification-ux-20260903`)。
|
||||
|
||||
删除结果:名单 67 条全部从 origin 删除。`git ls-remote --heads origin` **74 → 7**:
|
||||
|
||||
```text
|
||||
main
|
||||
staging
|
||||
codex/repo-hygiene-20260903
|
||||
codex/rectification-ux-20260903
|
||||
codex/rectification-ux-20260902
|
||||
codex/diagnose-rectification-request-20260827
|
||||
fix/staging-gate-checkout-retry
|
||||
```
|
||||
|
||||
任务书预期 5 条;多出的 2 条是本执行分支,以及已合入但未列入删除名单的 `codex/rectification-ux-20260903`。
|
||||
+13
-2
@@ -44,7 +44,7 @@
|
||||
| `TASK-rectification-adopt-flow-20260902.md` | — | 采用流程整治 | 已验收 | `35e5781e`(BUG-497~500) |
|
||||
| `TASK-rectification-adopt-flow-fix-20260903.md` | — | 采用流程修复单(含删「用这个时间看盘」) | 已验收 | `e8c98c37`(BUG-501/502) |
|
||||
| `TASK-rectification-uncertainty-stop-20260903.md` | — | 不确定度停止规则加样本下限 | 已验收 | `0c0df426`(BUG-503) |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 待合并 | 分支 `codex/rectification-ux-20260903`(09-03 在新基线重做,BUG-505–509,已推送待验收) |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -85,9 +85,20 @@
|
||||
|
||||
| 主题 | 内容 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| 仓库小活 | `pyproject.toml` / `SKILL.md` 仓库地址更新;registry `experimental_variant` 让 evidence-pool 测试变绿;三份早期日志移出根目录并改 `run_quality_gate.py`;清理已合入的远端分支 | 待写任务书 |
|
||||
| staging 测试账号 | 给验收环境一个虚构资料的受控账号,让 API 级端到端验收可做 | 待产品负责人提供 |
|
||||
|
||||
### 仓库整备与发布前置
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-repo-hygiene-20260903.md` | `PROGRESS-repo-hygiene-20260903.md` | 岁差默认 Raman + 用户设置可选、参考集锁口径(P0,单独先推)、发布门本地可复现、注册表白名单、仓库地址、早期日志搬家、远端分支清理 | 待验收 | 分支 `codex/repo-hygiene-20260903` `2415e751` |
|
||||
|
||||
### 上游同步与能力接入
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-upstream-sync-20260903.md` | — | 上游快照推进到 `a6f47abd`、引擎三方合并、health 三窗 / VedAstro REST 桥 / 专业报告导出 / 新字段接聊天与报告 / 校正解冻三项(自适应选题、岁差按请求、报告敏感度节) | 待领取 | — |
|
||||
|
||||
## 命名与归档
|
||||
|
||||
- 文件名:`TASK-<kebab-主题>-<YYYYMMDD>.md`;同主题的修复单加 `-fix`;进度记录同名换前缀。
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# 任务书 · 仓库整备与 main 提升前置:岁差默认 Raman 且用户可选、发布门可跑、地址/日志/分支清理(2026-09-03)
|
||||
|
||||
基线:`origin/staging` @ `d159f08e`(校正会话面重做 `codex/rectification-ux-20260903` 已合入)。分支 `codex/repo-hygiene-20260903`。BUG 编号从 **510** 起(staging 当前最大 BUG-509;开工再 `grep -oE '^## BUG-[0-9]+' docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对)。
|
||||
|
||||
本任务书是 2026-09-03 产品审查决定的"仓库小活",但调查时发现任务 0 不是小活:它决定 `main` 能不能提升。**任务 0 单独先做、先推、先验收**,任务 1–5 随后。
|
||||
|
||||
---
|
||||
|
||||
## 为什么要做(事故实证)
|
||||
|
||||
### A. 默认岁差(ayanamsa)在 staging 上从 Lahiri 变成了 Raman,公开真实案例复验因此失效(P0)
|
||||
|
||||
- 来源:`80102459`(2026-08-20,`feat(product): present consult and rectification in local skill form`,Cursor 执行)把 `scripts/ayanamsa_utils.py` 的 `ACTIVE_AYANAMSA_NAME = 'lahiri'` 改为 `DEFAULT_AYANAMSA_NAME = 'raman'`,并把 `scripts/jyotish_engine.py` 全部 `default='lahiri'` 改成 `DEFAULT_AYANAMSA_NAME`;前端 `birth-time-journey-assessment.ts:28,40`、`global-birth-payloads.ts:49` 同步硬编码 `"raman"`。提交信息只有一句 "Default ayanamsa to Raman with true_pushya support",`CHANGELOG.md` 与 `docs/BUG_HISTORY.md` 均无记录。
|
||||
- 影响量化(同一台机器、同一 `.venv`,只换 checkout):
|
||||
|
||||
| 检查 | 生产 `7b620c7a`(Lahiri) | staging `8e214e29`(Raman) |
|
||||
| --- | --- | --- |
|
||||
| `tests/run_real_case_revalidation.py` `valid` | true | **false** |
|
||||
| `pass_rate`(门槛 0.98) | 1.0 | **0.9394** |
|
||||
| `gated_passed / gated_total` | 66 / 66 | **62 / 66** |
|
||||
| 虚构盘 2000-01-01 12:00 UTC (0,0) Moon nakshatra | Swati | Vishakha |
|
||||
| 同盘 Vimshottari 首段 | Rahu 余运 → Jupiter 2000-09-20~2016-09-20 | Jupiter 余运 1998-11-27~2014-11-27 |
|
||||
| `tests/test_user_invocation_acceptance_contract.py::test_fixture_dasha_timeline_rejects_workbuddy_regression_claims` | 通过 | **失败** |
|
||||
|
||||
两套岁差相差约 1°26′,Moon 落在 nakshatra 边界附近的盘会整段大运换主;公开案例复验的参考值来自 Lahiri 口径的公开来源,而 `tests/run_real_case_revalidation.py:64` 调用 `jyotish_engine.py` 时**没有显式传 `--ayanamsa`**,跟着默认走,所以门槛从满分掉到 0.94。`run_quality_gate.py --profile release` 包含真实案例复验(`skip_real_cases: False`),也就是说**现在的 staging 过不了发布门**,`main` 无法提升。根因不是 Raman 本身,而是:参考集对照没有锁定自己的口径;产品没有把岁差作为用户可见、可选的设置;三处标签与实际计算不一致。
|
||||
|
||||
- 一致性问题(无论默认取哪个都要修):`scripts/jyotish_api_server.py:2853` 与 `:4017` 把 `ayanamsa_policy` 兜底成字面量 `'lahiri'` 交给 VedAstro 适配器,`:7030` 把返回体里的 `'ayanamsa'` 写死 `'lahiri'`,而本地引擎按 `DEFAULT_AYANAMSA_NAME` 算——本地与 VedAstro 对照用的不是同一套岁差,且证据包标签可能与真实计算不一致。`:7086` 的 `ayanamsa_display` 又兜底 `'Raman'`。
|
||||
|
||||
### B. 技法注册表新 entry_type 没进白名单
|
||||
|
||||
`references/technique_registry.json` 的 `rangacharya_jaimini_variant` 用了 `entry_type: "experimental_variant"`、`evidence_role: "comparison_only"`,`tests/test_capability_evidence_pool.py::test_registry_is_backend_evidence_pool_not_flat_user_skill_list` 的白名单没有这两个值 → 常红。`scripts/capability_evidence_pool.py` 对未知 role 既不计入 `audit_only_entries` 也不报错。
|
||||
|
||||
### C. 仓库地址还是旧研究仓
|
||||
|
||||
`pyproject.toml:54-58`、`.codex-plugin/plugin.json:7,9,10,39`、根 `SKILL.md:4` 的 description、`scripts/skill_release_package.py:27`、`scripts/skill_release_manifest.py:15`、`skills/jyotish-engine-modules/SKILL.md:67` 都指向 `https://github.com/732642856/yinduzhanxing`。这是**上游研究仓**,不是本产品仓。`tests/test_skill_release_package.py:59`、`tests/test_skill_release_manifest.py:16` 锁着旧地址。`scripts/remote_repo_visibility_check.py:22` 在 remote 不是 GitHub 时退回这个旧 slug 做可见性检查,而本仓 origin 是 Gitea。
|
||||
|
||||
### D. 根目录还剩四份早期实现日志
|
||||
|
||||
`progress.md`(209 KB)、`findings.md`、`task_plan.md`、`CHANGELOG_v6.2_to_v6.9.md`。`scripts/run_quality_gate.py:125,126,137` 的 `RELEASE_CRITICAL_UNTRACKED_PATHS` 锁着前三个路径,所以 09-03 的文档轮没能搬。
|
||||
|
||||
### E. 远端 70 条分支,66 条已完全合入 staging
|
||||
|
||||
`git branch -r --merged origin/staging` 的结果见任务 5。未合入的只有 4 条。
|
||||
|
||||
### F. 本地环境与 CI 依赖不一致,全量 pytest 在本地无法收集
|
||||
|
||||
本地 `.venv` 装的是 `mcp 2.1.1`(`requirements.txt` 钉 `mcp>=1.0,<2`)且没装 `hypothesis` → 13 个测试模块收集失败(`test_mcp_strict_workflow_*`、`test_varga_bphs`、`test_historical_event_backtest` 等)。`scripts/user_invocation_acceptance_check.py` 因 PyJHora `missing_dependency` 输出 `status: fail`,对应测试红;`requirements-reference-engines.txt` 不在任何 workflow 的安装步骤里。这些不是代码 bug,但发布门要的是"一条命令本地可复现 CI 结论"。
|
||||
|
||||
---
|
||||
|
||||
## 决策记录(产品授权,2026-09-03)
|
||||
|
||||
1. 产品已同意:每周提升一次 `main`;仓库小活交 coding agent;删除已合入的远端分支。
|
||||
2. **产品负责人 2026-09-03 拍板:默认岁差保持 Raman,并在用户设置里可选。** 可选值 `raman`(默认)/ `lahiri` / `kp` / `true_pushya`,与 Python `AYANAMSA_MODES` 一致。PM 曾建议回 Lahiri(理由:公开来源与既有 benchmark 都按 Lahiri 校准;生产用户此前看到的是 Lahiri 盘),产品明确否决;因此本轮的原则改为:**产品计算用用户设置(默认 Raman),参考集与外部对照各自显式锁定口径,任何地方不再依赖"默认值恰好是什么"**。提升 `main` 后生产用户的 Moon nakshatra / 大运可能相对旧版变化,这是产品已知并接受的后果,CHANGELOG 必须写明并告诉用户可在设置里切回 Lahiri。
|
||||
3. 本任务书**不改**任何被哈希锁定的 `skills/*/versions/**`,也不改指向上游研究仓的 `references/cross_project_contract/**`、`references/upstream/**`、`tests/test_import_yinduzhanxing.py`、`tests/test_upstream_import_plan.py`——那些地址是导入来源的真实记录。
|
||||
4. `progress.md` / `findings.md` / `task_plan.md` 的"发布关键文件必须已跟踪"检查保留,只改路径。
|
||||
|
||||
---
|
||||
|
||||
## 硬红线
|
||||
|
||||
1. 任务 0 不得通过改期望值让测试变绿:`test_fixture_dasha_timeline_rejects_workbuddy_regression_claims` 的日期、`tests/run_real_case_revalidation.py` 的参考值、`references/**` 的 oracle 目标值一律不动;让它们变绿的唯一合法手段是**显式传参口径**(`--ayanamsa lahiri`),并在三栏说明里写清"原来依赖默认值"。
|
||||
2. 不得在同一轮里改校正引擎的打分/阈值;`DEFAULT_AYANAMSA_NAME` 本轮**不改**(保持 `raman`)。
|
||||
3. `scripts/jyotish_api_server.py` 行数不得增长(`tests/test_api_server_growth_contract.py`);一致性修复是改字面量为常量,不是加逻辑。
|
||||
4. 分支删除只删任务 5 列出的已合入名单;4 条未合入分支不删;本地 worktree 正在使用的分支(`codex/unified-loading-20260902`、`codex/frontend-optimization-20260828`、`codex/streaming-ux-20260901`)远端可删、本地不动。
|
||||
5. 不改 `.gitea/workflows/**` 的触发条件;给发布门补依赖只允许改 `requirements-dev.txt` / `requirements.txt` 与 workflow 的 `pip install` 行。
|
||||
6. `npm run lint` 0 error、`tsc --noEmit` 通过、测试总数不低于开工基线;无 Docker 的失败清单与基线逐条比对。
|
||||
7. 不在脏工作树切分支;不自行提升 `main`。
|
||||
|
||||
让步顺序:功能与测试不回归 > 发布门本地可复现 > 岁差一致性 > 地址/日志/分支整洁。
|
||||
|
||||
---
|
||||
|
||||
## 开工前置
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/repo-hygiene-20260903 ../.worktrees/repo-hygiene-20260903 origin/staging
|
||||
# CI 同款依赖(本地复现发布门)
|
||||
.venv/bin/python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
.venv/bin/python -m pip show mcp | grep Version # 必须 1.x
|
||||
```
|
||||
|
||||
先读:`AGENTS.md` §2/§3/§5/§9、`docs/research/pre_work_error_ledger.md`(本轮碰引擎默认值与远端分支,§9 预检必跑)、`scripts/ayanamsa_utils.py` 全文、`git show 80102459 --stat`、`scripts/run_quality_gate.py` 的 profile 表。
|
||||
|
||||
---
|
||||
|
||||
## 任务分解
|
||||
|
||||
### 任务 0(P0,单独先推)· 岁差成为用户设置(默认 Raman),参考集显式锁口径,"算的"和"写的"一致
|
||||
|
||||
**0.a 参考集与外部对照显式锁定口径(先做,这一步单独就能让发布门变绿)**
|
||||
|
||||
1. `tests/run_real_case_revalidation.py:64` 的 `jyotish_engine.py` 调用显式加 `--ayanamsa lahiri`(参考值来自 Lahiri 口径的公开来源),报告 JSON 增加 `reference_ayanamsa: "lahiri"` 字段;`scripts/local_accuracy_report.py` 同理。
|
||||
2. `tests/test_user_invocation_acceptance_contract.py::test_fixture_dasha_timeline_rejects_workbuddy_regression_claims` 的 `base` 命令显式加 `--ayanamsa lahiri`(三栏说明:原值=依赖默认、新值=显式 lahiri、原因=夹具日期是 Lahiri 口径);期望日期一字不改。
|
||||
3. VedAstro / oracle 对照:`scripts/jyotish_api_server.py:2853`、`:4017` 的 `ayanamsa_policy` 不再兜底字面量 `'lahiri'`,改为**该次请求实际使用的岁差名**,让本地与 VedAstro 对照同口径;`references/oracle/**` 的目标值不动(它们各自的 `ayanamsa` 元数据字段就是口径)。
|
||||
4. 验收:`tests/run_real_case_revalidation.py` → `valid: true`、`gated 66/66`、`pass_rate ≥ 0.98`;上述夹具测试通过;`scripts/user_invocation_acceptance_check.py` 的 `checks.user_invocation_tests: true`。
|
||||
|
||||
**0.b 标签与实际计算一致**
|
||||
|
||||
5. `scripts/jyotish_api_server.py:7030` 的 `'ayanamsa': 'lahiri'` 改为实际值;`:7086` 的 `ayanamsa_display` 从实际名派生(`AYANAMSA_LABELS` 或等价映射),不再兜底 `'Raman'`。所有回执(`/api/consultation_workflow`、`/api/chart`、校正、报告证据包)必须带 `ayanamsa` 实际名,前端只显示回执,不得自己猜。
|
||||
6. 验收:`grep -rn "'lahiri'\|\"lahiri\"\|'raman'\|\"raman\"" scripts/jyotish_api_server.py frontend/src --include=*.py --include=*.ts --include=*.tsx | grep -v test` 只剩常量定义、标签映射与设置选项,没有散落的兜底字面量。
|
||||
|
||||
**0.c 用户设置:账户与出生资料面板里可选岁差**
|
||||
|
||||
7. 数据:`profiles` 加 `ayanamsa` 列(Drizzle 迁移,`frontend/db/migrations/`;`text not null default 'raman'`,check 约束四个值)。这是本轮唯一的动表,`npm run test:db` 必须真跑(AGENTS §7.6)。
|
||||
8. 接口:`/api/account` 读写该字段;服务端校验只接受四个值;未登录/旧客户端不带字段时按 `raman`。
|
||||
9. 界面:「账户与出生资料」面板加一个单选(Raman【默认】/ Lahiri / KP / True Pushya),每项一句话说明(文案先对 `frontend/docs/VOICE.md`),下方一行提示"切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径"。`frontend/DESIGN.md` 同提交更新。
|
||||
10. 贯通:`frontend/src/lib/global-birth-payloads.ts:49`、`birth-time-journey-assessment.ts:28,40` 及 `80102459` 触及的其余前端文件(`consult/route.ts`、`birth-time-journey-service.ts`、`personal-report-generation.ts`、`consultation-technique-audit.ts`)不再硬编码,统一从一个导出 `resolveAyanamsa(profile)`(新建 `frontend/src/lib/ayanamsa.ts`,类型 `"raman" | "lahiri" | "kp" | "true_pushya"`,默认常量 `DEFAULT_AYANAMSA = "raman"`)取值;咨询、校正、报告、星盘库、每日星语全部把它传给 Python。
|
||||
11. 已有数据:校正案例与已生成报告记录创建时的岁差(若表里没有字段,本轮只在新写入的 JSON 载荷里带 `ayanamsa`,不追加第二个迁移);切换设置不重算历史。校正引擎(`frontend/src/lib/rectification-agentic/v9/**`、`scripts/active_rectification_*.py`)里若有按 Raman 校准的阈值或 golden,本轮**不动**,只确认它们在 `lahiri` 下不会崩(跑一遍 `tests/rectification-*.test.ts`),异常写 BLOCKED。
|
||||
12. 验收:
|
||||
- 设置切到 Lahiri 后,用附录那张虚构盘(2000-01-01 12:00 UTC,0/0)发一次咨询:回执 `ayanamsa = lahiri`、Moon nakshatra = Swati;切回 Raman:`ayanamsa = raman`、Vishakha。技法审计表与星盘头显示的岁差与回执一致。
|
||||
- `tsc` 0 错、`npm run lint` 0 error、`npm run test:db` 通过、相关前端套件 fail=0;`page.tsx` 行数不增长。
|
||||
- 产品负责人在 staging 部署后按上面两步实测一次。
|
||||
|
||||
**0.d 记录**
|
||||
|
||||
13. `CHANGELOG.md` 新条目:默认岁差自 `80102459`(2026-08-20)起为 Raman,本轮补齐用户设置与四个可选值,说明升级后老用户可能看到 Moon nakshatra / 大运变化并可在设置切回 Lahiri。`docs/BUG_HISTORY.md` 新增 BUG(现象=公开案例复验 `valid=false` 与三处标签不一致、根因=参考集未锁口径 + 字面量兜底、修复=0.a/0.b、关联记录=无、复发自=无)。
|
||||
|
||||
### 任务 1(P0)· 发布门一条命令本地可复现
|
||||
|
||||
1. 依赖:`requirements-dev.txt` 已有 `hypothesis`;确认 `mcp>=1.0,<2` 在 `requirements.txt` 生效(本地 `.venv` 装错版本是环境问题,写进 PROGRESS 的"环境备忘"即可,不改 pin)。
|
||||
2. `scripts/user_invocation_acceptance_check.py`:PyJHora 缺失时 `status` 现为 `fail`。改法二选一并写明:(a) 在 workflow 安装 `requirements-reference-engines.txt`(注意 AGPL 隔离说明,只装在门禁 runner,不进镜像);(b) 让 `missing_dependency` 归入 `partial` 且 `test_one_command_user_invocation_acceptance_check` 的断言 `report["status"] == "pass"` 按三栏说明改为接受 `partial`,同时 `external_adapter_status` 仍必须在 `{"pass","partial","complete"}`。建议 (b):PyJHora 是外部参照引擎,缺席应降级不应失败。
|
||||
3. 用 CI 同款依赖跑 `python -m pytest -q --maxfail=1`(发布门原命令)与 `.venv/bin/python scripts/run_quality_gate.py --profile release`,把每一条红按"代码 bug / 环境 / 参考值过期"分类写进 PROGRESS,代码 bug 的修在本轮内,环境类给出复现命令,参考值过期的**不改值、写 BLOCKED**。起点数据见附录。
|
||||
4. 测试不得改写仓库文件:本地全量跑一遍后 `git status` 出现 12 个 `docs/research/character_level_*_latest.*` 与 2 个 `references/oracle/*_2026_07_2x.json` 被改写(`scripts/character_level_inventory_manifest.py:27-30` 等把"latest"报告直接写进仓库路径)。让这些脚本在测试里写 `tmp_path`,或测试用只读模式;验收标准是全量 pytest 后 `git status --short` 为空。
|
||||
5. 验收:release profile 本地退出 0;PROGRESS 里有全量红名单处理表;`docs/tasks/README.md` 状态板"已合入 / 待核对"各行不受影响。
|
||||
|
||||
### 任务 2(P1)· 注册表 `experimental_variant`
|
||||
|
||||
1. `tests/test_capability_evidence_pool.py` 白名单加 `experimental_variant`(entry_type)与 `comparison_only`(evidence_role);`scripts/capability_evidence_pool.py` 把 `comparison_only` 计入 `audit_only_entries` 同等处理(不能影响结论),summary 增加 `comparison_only_entries`。
|
||||
2. 加一条断言:所有 `entry_type == "experimental_variant"` 的条目 `verification_level.prediction` 与 `.rule` 必须是 `blocked`,`user_visibility` 必须是 `expert_audit`。
|
||||
3. 验收:该测试文件全绿;`scripts/audit_capabilities.py --mode validate` 仍 `valid=true, problem_count=0`。
|
||||
|
||||
### 任务 3(P1)· 仓库地址
|
||||
|
||||
1. 改:`pyproject.toml` 五个 URL(Homepage/Repository/Changelog → `https://git.copse.top/root/Jyotisha`,Bug Reports → `https://git.copse.top/root/Jyotisha/issues`,Documentation → 仓内 `README.md` 的 Gitea 链接);`.codex-plugin/plugin.json` 四处;根 `SKILL.md:4` description 里的 `GitHub:` 改为 `Repo: https://git.copse.top/root/Jyotisha`;`scripts/skill_release_package.py:27`、`scripts/skill_release_manifest.py:15`、`skills/jyotish-engine-modules/SKILL.md:67`。
|
||||
2. `tests/test_skill_release_package.py:59`、`tests/test_skill_release_manifest.py:16` 同步(三栏说明)。
|
||||
3. `scripts/remote_repo_visibility_check.py`:remote 不是 GitHub 时不再退回旧 slug,改为报告 `provider: gitea` 并用 `git ls-remote` 验证可达;`tests/test_remote_repo_visibility_check.py` 的 slug 解析用例不动,新增一条 Gitea remote 用例。
|
||||
4. 验收:`grep -rn '732642856/yinduzhanxing' --include=*.py --include=*.toml --include=*.json --include=*.md . | grep -v 'references/\|skills/.*/versions/\|docs/\|progress.md\|findings.md\|tests/test_import_yinduzhanxing\|tests/test_upstream_import_plan\|tests/test_project_fragment_governance'` 为空;`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 不因 slug 误报 blocked。
|
||||
|
||||
### 任务 4(P2)· 早期日志搬家
|
||||
|
||||
1. `git mv progress.md findings.md task_plan.md CHANGELOG_v6.2_to_v6.9.md docs/history/`,新建 `docs/history/README.md` 一段说明("2026-06~08 的实现日志,不是运行说明")。
|
||||
2. `scripts/run_quality_gate.py` `RELEASE_CRITICAL_UNTRACKED_PATHS` 三个路径改为 `docs/history/...`;根 `README.md` 文档地图与 `AGENTS.md` §4 最后一行同步。
|
||||
3. 验收:`run_quality_gate.py --profile release` 的"关键文件已跟踪"检查通过;根目录 `ls *.md` 只剩 `AGENTS.md BLOCKED.md CHANGELOG.md CLAUDE.md CONTEXT.md README.md SKILL.md`(`COVERAGE_AUDIT_REPORT.md` 在 .gitignore,不算)。
|
||||
|
||||
### 任务 5(P2)· 远端分支清理
|
||||
|
||||
已完全合入 `origin/staging` 的 66 条,一次删除(先 `git fetch origin --prune` 再核对 `git branch -r --merged origin/staging` 与下表一致,多出或少了的写 PROGRESS):
|
||||
|
||||
```text
|
||||
codex/admin-account-reset codex/agent-voice-20260901 codex/agentic-consultation-runtime
|
||||
codex/consultation-birth-accuracy-20260901 codex/fix-daily-starlanguage-guard-20260818
|
||||
codex/fix-feature-flag-admin-runtime-policy-20260811 codex/fix-gitea-artifact-upload-20260805
|
||||
codex/fix-gitea-manifest-auth-20260805 codex/fix-local-artifact-inputs-20260805
|
||||
codex/fix-rectification-case-context-20260811 codex/fix-rectification-current-turn-binding-20260812
|
||||
codex/fix-rectification-duplicate-collect-20260902 codex/fix-rectification-focus-baseline
|
||||
codex/fix-rectification-free-settlement-20260811 codex/fix-rectification-history-hydration-20260812
|
||||
codex/fix-rectification-home-new-session-20260812 codex/fix-rectification-session-model-20260811
|
||||
codex/fix-rectification-turn-projection-20260812 codex/fix-report-scroll-product-admin-20260818
|
||||
codex/fix-sidebar-contract-ci-20260820 codex/fix-staging-artifact-resolution-20260805
|
||||
codex/fix-vendored-artifact-inputs-20260805 codex/frontend-optimization-20260828
|
||||
codex/integrate-github-main-gitea-20260805 codex/internal-gitea-artifact-action-20260805
|
||||
codex/pr1-skill-registry codex/pr2-final-response codex/pr3-rectification-v10
|
||||
codex/pr4-rectification-event-contract-v2 codex/pr5-report-evidence-bundle-v2
|
||||
codex/pr6-report-document-v2 codex/pr7-product-domain-registry codex/pr8-evals-observability
|
||||
codex/pr8-local-gate-closure codex/production-selfhosted-migration
|
||||
codex/rectification-activity-birth-context-20260812 codex/rectification-p0-p1-20260820
|
||||
codex/rectification-provisional-adopt-20260901 codex/rectification-question-in-message-20260902
|
||||
codex/rectification-same-turn-confirm-20260812 codex/rectification-tails-20260901
|
||||
codex/remove-admin-step-up-20260816 codex/report-blocked-repairs-20260902
|
||||
codex/report-writer-failure-20260902 codex/run-vendored-artifact-uploader-20260805
|
||||
codex/staging-gate-repair-20260811 codex/staging-skill-style-present-20260820
|
||||
codex/streaming-ux-20260901 codex/ui-optimize-staging-20260817 codex/unified-loading-20260902
|
||||
codex/use-gitea-artifact-action-20260805 delivery/payment-package-epay
|
||||
docs/close-staging-controller-incident docs/close-staging-release-acceptance
|
||||
docs/correct-report-smoke docs/personal-report-staging-acceptance
|
||||
docs/record-staging-env-owner-drift docs/record-staging-host-key-rotation
|
||||
feat/one-way-skill-personal-report fix/staging-admin-verification-host
|
||||
fix/staging-controller-fetch-timeout fix/staging-gate-attested-controller
|
||||
fix/staging-gate-build-timeout fix/staging-gate-retry-and-admin-audit
|
||||
fix/staging-public-verification-race fix/staging-ssh-secret-and-env-owner
|
||||
revert/staging-from-main
|
||||
```
|
||||
|
||||
```bash
|
||||
git push origin --delete <上表>
|
||||
```
|
||||
|
||||
未合入的 4 条不删,只登记:
|
||||
|
||||
| 分支 | 最后提交 | 领先 staging | 处置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `codex/rectification-ux-20260903` | 2026-09-03 | 5 | 进行中(已验收待合入),不动 |
|
||||
| `codex/rectification-ux-20260902` | 2026-09-02 | 3 | 被 0903 版替代,等 0903 合入后由产品决定删除 |
|
||||
| `codex/diagnose-rectification-request-20260827` | 2026-08-27 | 5 | 8 月诊断分支,产品决定 |
|
||||
| `fix/staging-gate-checkout-retry` | 2026-08-06 | 1 | 已被后续门禁修复覆盖的可能性大,产品决定 |
|
||||
|
||||
验收:`git ls-remote --heads origin | wc -l` 从 71 降到 5(`main`、`staging` 加 3 条保留分支,若 0902 已删则 4)。
|
||||
|
||||
---
|
||||
|
||||
## 附录 · 2026-09-03 本地全量 pytest 起点数据
|
||||
|
||||
环境:同一台机器、同一 `.venv`(缺 `hypothesis`、`mcp` 为 2.1.1、无 PyJHora / Docker)。`python -m pytest -q --continue-on-collection-errors tests`:
|
||||
|
||||
| 检出 | FAILED | 收集 ERROR |
|
||||
| --- | --- | --- |
|
||||
| staging `8e214e29` | 89 | 13 |
|
||||
| 生产 `7b620c7a`(只跑 staging 红的 54 个文件) | 83 | — |
|
||||
|
||||
也就是说 89 条红里 **83 条在生产提交上同样红**,属于环境缺依赖或参考值/产物过期,不是这两周的回归;执行方分类时可直接从这 83 条的"环境 / 过期"两类起手,重点核对以下 **6 条 staging 独有的红**:
|
||||
|
||||
| 测试 | 本地现象 | 初判 |
|
||||
| --- | --- | --- |
|
||||
| `test_user_invocation_acceptance_contract.py::test_fixture_dasha_timeline_rejects_workbuddy_regression_claims` | Jupiter 段 2014-11-27 ≠ 期望 2016-09-20 | 夹具是 Lahiri 口径却跟着默认 Raman 跑(任务 0.a) |
|
||||
| `test_user_invocation_acceptance_contract.py::test_one_command_user_invocation_acceptance_check` | `checks.user_invocation_tests: false` → `status: fail`(`external_adapter_status` 已是 `partial`) | 同上,上一条的连带 |
|
||||
| `test_skill_release_clean_trial.py::test_premium_skill_zip_runs_from_clean_directory` | 打包后的 acceptance check 同样 `fail` | 同上,连带 |
|
||||
| `test_local_accuracy_report.py`(2 条) | `scripts/local_accuracy_report.py` 抛 SystemExit | 大概率是公开案例复验 `valid=false` 传导,任务 0 完成后复跑;若仍红再单独查 |
|
||||
| `test_dynamic_rectification_fact_priority.py::test_historical_event_priority_preserves_vimshottari_actor_difference` | `assert 0.0 > 0`(信息增益为 0) | 依赖 Vimshottari 边界,先在任务 0 之后复跑;仍红则归校正引擎,写 BLOCKED 交产品 |
|
||||
| `test_minute_rectification_development.py::test_v3_development_result_stays_shadow_only` | `assert 1 == 0` | 同上 |
|
||||
|
||||
13 个收集 ERROR 全部是 `No module named 'hypothesis'` 或 `mcp.server.fastmcp`(mcp 2.x),装对依赖即消失。
|
||||
|
||||
## 交付物
|
||||
|
||||
- 任务 0 单独一次推送(含 CHANGELOG + BUG_HISTORY),推送后在 PROGRESS 写明 staging 部署 SHA。
|
||||
- `docs/tasks/PROGRESS-repo-hygiene-20260903.md`:每个任务的命令输出摘要、全量 pytest 红名单处理表、三栏说明、环境备忘。
|
||||
- `docs/tasks/README.md` 状态板:本任务书一行,并把"待开的任务书 · 仓库小活"那行改为指向本文件。
|
||||
- 做不了的写 `BLOCKED.md`,不写"通过"。
|
||||
@@ -0,0 +1,297 @@
|
||||
# 任务书 · 上游研究仓同步与三项新能力接入(2026-09-03)
|
||||
|
||||
基线:`origin/staging` @ `591ffd21`。上游研究仓 `732642856/yinduzhanxing` 目标提交 `a6f47abd2c9b7c6baa911ea00c971e1ec902380c`(2026-09-03,`main`)。本地干净检出在 `/workspace/yinduzhanxing`(origin 与策略文件一致,`git status` 干净)。
|
||||
|
||||
## 现状实证
|
||||
|
||||
1. **快照落后 20 天、629 个提交。** `references/upstream/yinduzhanxing/source-manifest.json` 锁在 `5db72537`(2026-08-14)。上游到 `a6f47abd` 之间:references 1040 文件、docs 539、scripts 707(约 640 个是新研究脚本)、tests 750。上游自己的 9 月总结(`docs/research/september_2026_execution_log.md`)写明"本地代码工作已关闭,剩余全部是外部证据准入阻塞,不允许任何真相/运行时/发布/预测层升级"。**这批不是能力大升级,是研究台账为主。**
|
||||
2. **导入器只覆盖 SKILL.md。** `references/cross_project_contract/sync_policy.v2.json`:唯一字节镜像是上游 `SKILL.md` → `references/upstream/yinduzhanxing/SKILL.md`;六个"语义合并"文件(`SKILL.md`、`AGENTS.md`、`references/strict-workflow-router.md`、`scripts/unified_consultation_orchestrator.py`、`scripts/report_orchestrator.py`、`scripts/jyotish_api_server.py`)导入器只列不写;其余引擎代码不在策略范围内。
|
||||
3. **引擎是分叉,不是镜像。** 本仓 `scripts/` + `jyotish_vedic/` + `mcp_server.py` 共 389 个 Python 文件:242 个与快照 `5db72537` 逐字节一致,92 个有商业改动,55 个本仓独有。上游 HEAD 另有 736 个本仓没有的 Python 文件。因此引擎同步只能做**三方合并**(base = 上游 `5db72537`,ours = 本仓,theirs = 上游 `a6f47abd`)。
|
||||
4. **三方合并干跑结果**(`git merge-file -p ours base theirs`,冲突块数):
|
||||
|
||||
| 文件 | ours 改动行 | theirs 改动行 | 冲突块 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `scripts/jyotish_api_server.py` | 8283 | 3383 | 36 |
|
||||
| `scripts/jyotish_engine.py` | 3649 | 10924 | 25 |
|
||||
| `scripts/unified_consultation_orchestrator.py` | 553 | 753 | 18 |
|
||||
| `mcp_server.py` | 655 | 625 | 11 |
|
||||
| `scripts/active_rectification_questions.py` | 197 | 160 | 7 |
|
||||
| `scripts/ashtottari_dasha.py` | 264 | 134 | 5 |
|
||||
| `scripts/active_rectification_events.py` | 204 | 101 | 5 |
|
||||
| `scripts/domain_calculation_service.py` | 410 | 20 | 5 |
|
||||
| `references/technique_registry.json` | 3084(仅格式,内容与快照一致) | 966 | 5 |
|
||||
| `scripts/narayana_dasha.py` | 486 | 269 | 4 |
|
||||
| `scripts/run_quality_gate.py` | 132 | 54 | 4 |
|
||||
| `scripts/kp_system.py` | 205 | 145 | 3 |
|
||||
| `scripts/varga.py` / `tajika.py` / `report_orchestrator.py` | 35 / 324 / 108 | 43 / 88 / 28 | 各 1 |
|
||||
| `scripts/jaimini.py` / `muhurta.py` / `western_timing_engine.py` / `solar_return.py` | 1 / 2 / 0 / 54 | 137 / 1618 / 230 / 84 | **0** |
|
||||
|
||||
`jyotish_engine.py` 的 25 处冲突集中在本仓商业改动过的同一批函数:`_build_ai_prompt_pack`、`compute_chart_data`、`cmd_chart`、`cmd_dasha`、`cmd_shadbala`、`cmd_tajika`、`_calc_actionable_context`、`_add_chart_args`、`_ayanamsa_display_name`、`main`。
|
||||
5. **上游真正涉及业务能力的变化**(按本仓产品是否用到排序):
|
||||
- `references/strict-workflow-router.md`:新增 `health-timing-strict` 路由(D1/D6/D8/D30,Narayana 交叉核对,输出必须分**压力窗 / 事件风险窗 / 恢复支持窗**三段,非医疗边界),`wealth-timing-strict` 改名 `finance-timing-strict` 并保留旧别名。上游编排器 `route_surface_contract` 已把 `wealth-timing-strict` 作为 `finance` 的别名。
|
||||
- `references/technique_registry.json`:91 → 116 项。新增 25 项全是 `western_*`(`western_profection` 为 `covered`,其余 `guarded`)、`classical_*`(全 `guarded`)、`ziwei_*`(`comparison-only` 或 `covered`)。
|
||||
- Ashtottari 解冻:`blocked_rule_source_conflict` → `selected_rule_family_parameter_sensitive`,可执行但未验证,只对规则族 `rahu_from_lagna_lord_kendra_trikona_plus_paksha_daynight` 适用;`scripts/ashtottari_dasha.py` 与 `SKILL.md` 同步改动。
|
||||
- VedAstro:官方 MCP 端点 `tools/call` 持续返回 `Invalid or Outdated Call`,上游把默认外部对标通路切到 REST 桥 `scripts/vedastro_rest_bridge.py`(`https://api.vedastro.org/api/Calculate`,horoscope / dasa 两个子命令实测 `Status: Pass`,免费层 5 次/分钟)。本仓 `scripts/vedastro_gateway.py` 的 `OFFICIAL_ENDPOINT` 本来就是 REST 根地址,但本仓 `scripts/vedastro_official_mcp_bridge.py` 仍被 `vedastro_parity_matrix.py` 使用。
|
||||
- `scripts/jyotish_engine.py`:`pl9-export` 子命令(`build_pl9_style_export_packet(full_reading)` + `render_pl9_markdown`,格式 json / markdown / pdf / authority,`--pack` 选包),术语规范改为 `professional_report_reference`(`docs/architecture/professional_report_reference_contract_v1.md`),markdown 导出带引擎边界声明(上游 `7a2793cb`)。与咨询链路直接相关的改动落在 `_build_ai_prompt_pack`(7 块)、`cmd_full_reading`(10 块)、`cmd_varga_full`(4 块)、`cmd_dasha`(4 块)、`_build_career/finance/relationship_narrative_payload`,以及新增的 `_build_pratyantar_timeline`、`_build_birth_time_sensitivity`、`_build_planetary_friendship_snapshot`、`_build_three_year_predictive_ephemeris_pack`、`_build_module_execution_audit`。
|
||||
- `scripts/muhurta.py` +1600 行择日能力(Panchaka、Tarabala/Chandrabala、Siddha Yoga、Durmuhurtha 窗口、择日盘 dosha 与取消权重);`jaimini.py`、`narayana_dasha.py`、`kp_system.py`、`western_timing_engine.py`、`tajika.py`、`solar_return.py`、`varga.py` 各有修改。
|
||||
- `mcp_server.py` 新增 `get_pl9_expert_reader_report`、`get_pl9_full_report_authority`、`route_jyotish_question`;编排器新增专家判断影子层(`build_expert_judgment_shadow_input` 等)与 `route_surface_contract`。
|
||||
- 新增两个子 skill:`skills/financial-astrology`(市场占星)、上游版 `skills/jyotish-birth-time-rectification`。
|
||||
6. **本仓相关现状**:
|
||||
- 咨询链路的 `health` 路由**已经存在**(`scripts/unified_consultation_orchestrator.py` `_ROUTE_DEFINITIONS["health"]`:D1/D6/D8/D30,输出段 `non_medical_pattern / pressure_factors / protective_factors / verification_questions`),前端 `frontend/src/lib/consultation-methodology.ts` 已把 `health` 映射到 `health-timing-strict`。差距在于:输出合同不是上游的三窗结构;`focus_techniques` 缺 Narayana / Transit / 功能吉凶;证据包无 health 专属必需段;**个人报告主题枚举没有 health**(`frontend/src/lib/personal-report-route-core.ts:37` 只有 career / marriage / wealth / timing / general);引擎没有 health 叙事种子生成器。
|
||||
- 前端 `wealth` 主题硬编码 `wealth-timing-strict`,本仓编排器没有 finance 别名。
|
||||
- 导入器 dry-run 在主检出直接被拒:`{"status": "rejected", "reason": "target_must_be_clean"}`(根目录有未跟踪的 TASK 文件)。必须在干净 worktree 里跑。
|
||||
|
||||
## 根因
|
||||
|
||||
上游是研究仓,日更数十个提交;本仓是分叉后带商业改动的产品仓,两者没有共同 git 历史。导入策略 v2 设计时只解决了"真相层"(SKILL.md)的镜像与审计,没有给引擎代码定同步办法,于是 20 天没有同步,再往后只会更难合。
|
||||
|
||||
## 决策记录(产品负责人 2026-09-03 授权)
|
||||
|
||||
- **(a)** 推进上游快照到 `a6f47abd`,按策略 v2 镜像 SKILL.md、语义合并六个文件、整体接收 116 项注册表并由商业覆盖层定状态。
|
||||
- **(b)** 引擎代码做三方合并(基线 `5db72537`),商业改动优先,只取对产品路径有意义或纯新增的上游改动。
|
||||
- **(c)** 三项新能力**全部要做**:health 路由对齐上游三窗合同并进入个人报告主题;VedAstro 外部对标切到 REST 桥;`pl9-export` 专业报告导出接进个人报告产品。
|
||||
- **(d) 校正解冻(2026-09-03 用户决定)**:撤销"校正冻结新行为直到走查清零"对本任务书的限制,但只解冻到任务 7 列出的三项(自适应选题、岁差/交点模式按请求传递、不确定出生时间的报告敏感度节)。校正打分算法(V5 score service、POLICY v3、matrix-scoring-7)、采用/停止规则(BUG-463 / BUG-503 口径)、会话面交互本轮**仍不动**。解冻后欠着的浏览器走查(`docs/testing/`)必须在任务 7 合入后重新跑一遍。
|
||||
- **授权不包括**:接收上游约 640 个研究脚本、150 万行证据台账、750 个研究测试、两份 PDF、`skills/financial-astrology`、上游版校正 skill;修改岁差默认值(保持 Raman,见 `TASK-repo-hygiene-20260903.md`);放松写作 agent 输入隔离;改数据库结构;改 `.gitea/workflows/**`。
|
||||
- **注册表新增 25 项的商业状态默认规则**:凡网页路径(`/api/consultation_workflow`)没有实际执行的,覆盖层一律 `research_only_blocked`;只有 `scripts/full_technique_invocation_matrix.py` 证明会执行的才可写 `partial` 或 `reference_only`。不得因上游写 `covered` 就进确定性结论。
|
||||
|
||||
## 三个产品面的覆盖对照
|
||||
|
||||
| 产品面 | 上游相关变化 | 本任务书落点 |
|
||||
| --- | --- | --- |
|
||||
| 普通聊天(`/api/consultation_workflow` → 折叠技法审计表) | finance 别名、health 三窗、Ashtottari 状态、`_build_technique_audit_table` / `_build_module_execution_audit`、叙事种子 `timing_narrative`、`planetary_friendship` 模块 | 任务 1(覆盖层与路由文档)、任务 2(引擎合并)、任务 3(health)、**任务 6(把新字段接到响应与前端)** |
|
||||
| 生时校正(`/api/rectification/*`、`/api/active_rectification_events*`、前端 birth-time-journey) | `3bb90620` 自适应选题器(含答案泄漏)、`dbb9804a` 亲属事件域 + 岁差/交点按请求传递、`d5b83c71` `compare_candidate_minutes` + 事件域注册表、`_build_birth_time_sensitivity` + 四个 `flexible_birth_time_*` 模块、便携版 skill v2.0.0 | **任务 7**(只取选题器、岁差/交点传递、报告敏感度节三项);其余不取,理由见硬红线 3 |
|
||||
| 报告生成(个人报告管线、报告中心) | health 主题、`pl9-export`、解读模板注册表 +145 行、`timing_narrative` / `pratyantar_dasha_timeline` / `planetary_friendship` 可作解读性事实 | 任务 1(模板注册表)、任务 3(health 主题)、任务 5(专业导出)、任务 6(新字段进 bundle) |
|
||||
|
||||
## 硬红线
|
||||
|
||||
1. **方向单向。** 只从 `/workspace/yinduzhanxing` 读,不得向其写入、提交或推送;不得改 `sync_policy.v2.json` 的方向、受保护路径与最小保护集。
|
||||
2. **受保护路径不接收上游内容**:`skills/jyotish-birth-time-rectification/**`、`skills/jyotish-vedic-astrology/**`(只能新增版本目录,不能改既有版本)、根 `SKILL.md`(只允许语义合并本任务书列出的三段)、`frontend/**`、`deploy/**`、`.gitea/**`、`references/oracle/commercial_skill_truth_overlay.v1.json`(只允许按决策记录新增条目)。
|
||||
3. **校正只按任务 7 的范围动。** 上游自快照以来对校正的改动共四个提交,逐个核过:
|
||||
- `3bb90620`(08-31)新增 `_known_case_regression_hint`:对 `1993-04-17` 出生时间读作者本人的已知案例标定文件,把 `14:49` 当回归目标并在分钟排名里 +5 分。**这就是已定性的答案泄漏,一行都不得进入本仓**;同一提交里按 `sensitivity_count` 排分钟的 `minute_rankings` 也不取(比本仓 V5 打分弱)。该提交里的 `active_rectification_selector.py`、`factual_reliability`、把 `scan_candidate_times` 挂进 `_candidate_scan` 是可取部分,见任务 7a。
|
||||
- `dbb9804a`(08-18)亲属事件域(parent / sibling / spouse_family)与分盘扩到 D3/D7/D12:本仓 `family` 域已含 D12/D7/D3,分盘列表 `[2,3,4,5,7,9,10,12,24,30]` 是超集,不取;该提交里 `ayanamsa` / `node_mode` 从请求读取的模式取,见任务 7b。
|
||||
- `d5b83c71`(08-27)`compare_candidate_minutes` 固定分钟微比较:本仓 V5 score service、`minute_rectification_pairwise_v3.py`、`minute_candidate_discriminability.py`、`build_stability_diagnostics` 已覆盖且更强,不取。`references/rectification_evidence_contract_v1.json` 事件域注册表是治理数据,本仓 `rectification_technique_contract.py` 已是代码化合同且不含 D60/D40/D45,规则等价,不取,PROGRESS 记一句。
|
||||
- `a9058663`(08-31)便携版校正 skill v2.0.0:与本仓 v10.0.14 服务器权威 Case 模型不同源,受保护路径,不取。
|
||||
- `_build_birth_time_sensitivity` + `flexible_birth_time_profile / report_support / full_report_projection / report_section`(四模块 761 行,测试 440 行,除被守护的 1993 分段表外无数据文件依赖):取,见任务 7c,但窗口来源必须改为本仓校正案例的可信区间,不用上游固定 ±1 分钟。
|
||||
校正的打分算法、阈值、采用/停止规则、`frontend/src/lib/rectification-agentic/v9/**` 的会话状态机本轮不动;`minute_rectification_*` / `dynamic_rectification_*` 不合并上游(上游没有对应文件)。
|
||||
4. **咨询响应契约不得变形。** `/api/consultation_workflow` 响应里前端已读取的键路径与类型(任务 0 的 golden 快照)改后必须逐一存在且类型一致;上游改动只允许**新增**键,不得改名、删键、改形状。`frontend/tests` 里的契约测试与 `tests/test_api_server_growth_contract.py` 等 Python 契约测试必须绿,不得改断言来迁就(除非断言锁的就是本轮要改的注册表计数或 README 徽章,改动处注明原值原因并在 PROGRESS 单列)。
|
||||
5. **合并冲突的裁决规则**:商业侧函数体优先;上游改动仅在(i)纯新增函数/子命令且其 import 在本仓可解析,或(ii)修的是计算正确性且本仓同函数无商业改动时才取。任何 import 了本仓不存在模块的上游代码块一律不取,不得为了让它跑而顺手拖入研究脚本。每处冲突的取舍写进 PROGRESS 的冲突表。
|
||||
6. **隐私与体积**:不得引入 `.pdf`、私有出生资料、PL9 参考报告原文、`review-only` 全量 payload;`scripts/public_release_privacy_scan.py` 与 `scripts/commercial_privacy_artifact_scan.py` 必须过。fixture 一律用虚构 smoke 出生数据。
|
||||
7. **不得新增模型调用。** 任务 5 的专业报告导出是确定性引擎产物,不经过写作 agent,不计费为报告生成。
|
||||
8. 推 staging 前 `./node_modules/.bin/tsc --noEmit` 必须通过(不要用 `npx tsc`);`scripts/**` 与 `references/**` 都在 `deploy/gated-paths.txt` 内,每次推送都会跑完整门禁并部署 staging,推之前本地先跑 `run_quality_gate.py --profile quick`。
|
||||
9. 不得自行提升 `main`,不得改 workflow。
|
||||
|
||||
让步顺序:数据与隐私不泄漏 > 方向与受保护路径不越界 > 咨询契约与校正不回归 > 真相层同步完整 > 引擎代码合并覆盖面 > 三项新能力 > 代码整洁。
|
||||
|
||||
## 开工前置
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/upstream-sync-20260903 \
|
||||
../.worktrees/upstream-sync-20260903 origin/staging
|
||||
cd ../.worktrees/upstream-sync-20260903
|
||||
.venv/bin/python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
|
||||
git -C /workspace/yinduzhanxing status --short --branch # 必须干净且在 a6f47abd
|
||||
git -C /workspace/yinduzhanxing rev-parse HEAD
|
||||
```
|
||||
|
||||
读 `docs/research/yinduzhanxing_one_way_import_2026_08_06.md`、`docs/research/pr0_upstream_governance_baseline_2026_08_14.md`、`docs/research/pre_work_error_ledger.md`、`frontend/AGENTS.md`;在 `docs/BUG_HISTORY.md` 检索 "上游 / import / merge / 契约 / varga 形状"。
|
||||
|
||||
**先读这几个文件再动手**:
|
||||
|
||||
- `scripts/import_yinduzhanxing.py` —— git 模式要求、manifest 生成、`--test-run` 记录
|
||||
- `references/cross_project_contract/sync_policy.v2.json`、`references/cross_project_contract/imports/commit-5db72537….json` —— 上次 manifest 的字段
|
||||
- `scripts/unified_consultation_orchestrator.py` —— `_ROUTE_DEFINITIONS`、`_DOMAIN_PROFILE_SECTIONS`、health 路由
|
||||
- `scripts/jyotish_api_server.py` `_compute_consultation_workflow`(约 4319 行起)、`_compute_full_reading_for_thematic`、`skip_full_reading_for_thematic`
|
||||
- `scripts/jyotish_engine.py` `_build_ai_prompt_pack`、`cmd_full_reading`、`compute_chart_data`、`main`
|
||||
- `frontend/src/lib/personal-report-generation.ts`、`report-theme-evidence-plan.ts`、`personal-report-route-core.ts`、`consultation-methodology.ts`
|
||||
- 上游:`docs/architecture/professional_report_reference_contract_v1.md`、`docs/research/vedastro_rest_bridge_default_decision_2026_09_01.md`、`docs/research/ashtottari_primary_text_verification_2026_09_01.md`、`references/strict-workflow-router.md` §7
|
||||
|
||||
**与 `TASK-repo-hygiene-20260903.md` 的并行规则(2026-09-03 补)**:整备任务书正在另一执行方手上,两份任务书有共同文件——`scripts/jyotish_api_server.py`(整备改岁差字面量 `:2853` `:4017` `:7030` `:7086`;本任务书任务 2 对全文件三方合并)、`scripts/run_quality_gate.py`(整备任务 1 改发布门;本任务书任务 2 合并上游改动)、`references/technique_registry.json` 与 `tests/test_capability_evidence_pool.py`(整备任务 2 加 `experimental_variant` 白名单断言;本任务书任务 1 换成 116 项注册表,必须满足该断言)、根 `SKILL.md` / `pyproject.toml` / `README.md`(整备任务 3 改仓库地址;本任务书任务 1 加三段与徽章计数)、`frontend/src/lib/personal-report-generation.ts` 与 `birth-time-journey-*.ts`(整备 0.c 贯通岁差;本任务书任务 3 / 6 / 7 改同文件)。因此:
|
||||
|
||||
- **现在就可以并行开工的**:任务 0(导入器、golden、冲突表,只写 `references/cross_project_contract/imports/`、`references/upstream/`、`tests/golden/`、新测试文件)和任务 4(VedAstro,文件集与整备无交集)。两者在 `codex/upstream-sync-20260903` 上做,基线取当时的 `origin/staging`。
|
||||
- **必须等整备任务 0–3 合入 staging 后再开的**:任务 1、2,以及其后的 3 / 5 / 6 / 7。开工前 `git rebase origin/staging`,任务 2 的三方合并以 rebase 后的文件为 ours,不得在旧基线上合完再解冲突。任务 0 的 golden 只记键路径与类型,整备改的是岁差取值与标签,golden 不需要重采;但任务 2 完成后按任务 0 的要求重采一次。
|
||||
- 整备任务书里"校正引擎按 Raman 校准的阈值与 golden 本轮不动"的约束继续有效,本任务书 7b 在它之后。
|
||||
|
||||
**串行顺序**:任务 0 → 1 → 2 在分支 `codex/upstream-sync-20260903` 上完成并验收后合入 staging;任务 3 / 4 / 5 / 6 / 7 在其上另开 `codex/upstream-capabilities-20260903`,任务 6 先于任务 3 与 7c 做(两者都沿用任务 6 打通的透传路径),7b 排在 `TASK-repo-hygiene-20260903.md` 之后,其余可并行但推送串行。任务 4 不依赖任务 2,如需提前可单独分支。
|
||||
|
||||
---
|
||||
|
||||
## 任务 0(P0,门控)· 快照推进与咨询契约 golden
|
||||
|
||||
### 做法
|
||||
|
||||
1. 在干净 worktree 里跑导入器 git 模式:
|
||||
|
||||
```bash
|
||||
.venv/bin/python scripts/import_yinduzhanxing.py \
|
||||
--source /workspace/yinduzhanxing --source-mode git \
|
||||
--policy references/cross_project_contract/sync_policy.v2.json \
|
||||
--dry-run --output artifacts/yinduzhanxing-import-plan.json
|
||||
```
|
||||
|
||||
审阅 semantic_merge 行后再 `--apply`,manifest 只允许进入 `references/cross_project_contract/imports/`,同步更新 `references/upstream/yinduzhanxing/source-manifest.json` 与 `import-plan.json`。
|
||||
2. **咨询契约 golden**:用虚构 smoke 出生数据,起本地 Python API,按前端实际请求(career / marriage / wealth / timing / health 各一次,`skip_full_reading_for_thematic` 两种取值各跑一次)调用 `/api/consultation_workflow`、`/api/chart`、`/api/varga_full`、`/api/daily_guidance`、`/api/synastry`,把响应**键路径 + 类型**(不记值)写成 fixture 存 `tests/golden/`(沿用 `tests/run_golden_cases.py` 的形式),加一条测试 `tests/test_consultation_contract_golden.py`:合并后重新采集,键路径集合只允许是超集。
|
||||
3. 把三方合并干跑的冲突表(本任务书"现状实证 4")复核一遍写进 PROGRESS,作为任务 2 的工作清单。
|
||||
|
||||
### 验收
|
||||
|
||||
- `references/cross_project_contract/imports/commit-a6f47abd….json` 存在,`source_mode=git`,`source_commit` 为 40 位全哈希,`source_tree_sha256` 与 manifest 一致;`references/upstream/yinduzhanxing/SKILL.md` 的 sha256 等于上游 HEAD 的 SKILL.md。
|
||||
- golden fixture 无隐私(出生数据为虚构 smoke),测试在改前基线上绿。
|
||||
- `tests/test_import_yinduzhanxing.py` 绿。
|
||||
|
||||
---
|
||||
|
||||
## 任务 1(P0)· 真相层同步
|
||||
|
||||
### 做法
|
||||
|
||||
1. `references/strict-workflow-router.md`:语义合并上游 §5 更名(`finance-timing-strict`,保留 `wealth-timing-strict` 别名)、§7 `health-timing-strict` 全文、Level 2 分盘清单加 D6/D8/D30;章节号顺延。`references/quick-reference-guide.md` 同步 finance 别名。**不要**逐字覆盖,本仓两文件有商业改动。
|
||||
2. `references/technique_registry.json`:接收上游 116 项内容(保持本仓的 JSON 格式化方式,不要把 3084 行格式 diff 反向带回去)。`references/oracle/commercial_skill_truth_overlay.v1.json` 为 25 个新项按决策记录逐条加 `technique_id / status / claim_boundary`;Ashtottari 加一条:`status=partial`,`claim_boundary` 注明规则族限制与 "unverified",且 `selected_rule_family_parameter_sensitive` 不得进确定性结论。
|
||||
3. 根 `SKILL.md`(商业入口)只加三段:Ashtottari 状态与适用族;`professional_report_reference` 术语规则(对外首选该名,`PL9` 只作旧代号);VedAstro 官方 MCP 故障与 REST 桥为默认对标通路。其余上游 SKILL.md 改动只通过镜像快照体现。
|
||||
4. 技法计数变化会打红 `tests/test_readme_badges.py` 与 README 徽章:徽章改为注册表实际新值,测试断言同步,改动处注明原值。
|
||||
5. skill 发布包:`skills/jyotish-vedic-astrology/versions/` 下**新增**一个版本目录(版本号由 `scripts/skill_release_manifest.py` 与 `pyproject.toml` 现有规则决定,例如 `6.9.15`),不改 `6.9.14`;`scripts/skill_release_package.py` dry-run 通过。
|
||||
6. 检查 `scripts/full_technique_invocation_matrix.py` 与 `scripts/audit_capabilities.py` 对 116 项注册表的输出:新项在网页路径上应显示为未执行/阻塞,不得出现"executed"。
|
||||
|
||||
### 验收
|
||||
|
||||
- `.venv/bin/python -m pytest tests/test_readme_badges.py tests/test_skill_hard_constraints.py tests/test_capability_evidence_pool.py tests/test_import_yinduzhanxing.py`:除 `test_capability_evidence_pool` 中基线已红的 `experimental_variant` 一条外全绿;若上游新项让它多红,必须修注册表数据而不是改测试。
|
||||
- `run_quality_gate.py --profile quick` 绿。
|
||||
- PROGRESS 给出 25 项新技法的覆盖层状态表。
|
||||
|
||||
---
|
||||
|
||||
## 任务 2(P0)· 引擎代码三方合并
|
||||
|
||||
### 做法
|
||||
|
||||
按下列顺序逐文件 `git merge-file`,每个文件合并后立刻跑该文件对应的本仓测试:
|
||||
|
||||
1. **零冲突文件**:`scripts/jaimini.py`、`scripts/muhurta.py`、`scripts/western_timing_engine.py`、`scripts/solar_return.py`。先检查它们新增的 import 是否都在本仓;缺的模块若是纯计算模块(无外部证据文件依赖)可一并带入并注明来源提交,否则删掉该 import 所属功能。上游对应测试(`tests/test_muhurta*.py`、`test_jaimini*.py` 等)本仓已有的版本一并更新。
|
||||
2. **低冲突计算模块**:`varga.py`、`tajika.py`、`tajika_kernel.py`、`kp_system.py`、`narayana_dasha.py`、`ashtottari_dasha.py`、`gulika.py`、`ayanamsa_utils.py`、`domain_calculation_service.py`、`run_quality_gate.py`、`report_orchestrator.py`、`report_pack_contract.py`。按硬红线 5 裁决。`run_quality_gate.py` 的 `RELEASE_CRITICAL_UNTRACKED_PATHS` 与门禁范围不得被上游改窄。
|
||||
3. **高冲突入口文件**:`unified_consultation_orchestrator.py`(取 `route_surface_contract` 与 finance 别名、health 路由的 `focus_techniques` 与 `evidence_packet_required_sections`;专家判断影子层只在其依赖模块可解析时取)、`mcp_server.py`(取三个新工具,前提是 `pl9-export` 已可运行)、`jyotish_api_server.py`(保留本仓全部商业路由;上游 nadi leaf assistant 路由不取)、`jyotish_engine.py`(`_build_ai_prompt_pack` / `cmd_full_reading` / `cmd_varga_full` / `cmd_dasha` / 叙事种子生成器 / `pl9-export` 相关函数按硬红线 5 取;`cmd_classical_*` / `cmd_nadi_*` 全部不取;`main` 中对应子命令注册不取)。
|
||||
4. 校正相关文件按硬红线 3 跳过。
|
||||
5. **引擎运行时读取的数据文件,按下面规则处理**(2026-09-03 核实:批 B 计算模块 muhurta / jaimini / narayana_dasha / ashtottari_dasha / varga / western_timing_engine / solar_return 不读任何数据文件;`yoga_rules.json`、`saham_rules.json`、`assets/**` 本仓与上游逐字节一致;算法不依赖上游证据台账):
|
||||
- **必须带入**(引擎运行时无守护地读取、内容是状态/合同元数据、无隐私):`scripts/kp_system.py` `kp_maturity_profile()` 读的四个文件 `references/oracle/kp_exact_cusp_closure_dashboard_2026_08_22.json`、`kp_exact_cusp_mainline_status_2026_08_22.json`、`kp_12_cusp_numeric_oracle_readiness_2026_07_23.json`、`kp_real_event_replay_gate_2026_07_30.json`。不带入则合并后 KP 分析路径直接抛异常。带入前逐个打开确认无出生资料。
|
||||
- **应同步的产品面内容**(非研究台账):`references/interpretation_template_registry.json`(上游 +145 行,报告生产者索引它)、`references/ai-reading-workflow-prompt.md`、`references/mandatory-verification-gate-protocol.md`,走语义合并。
|
||||
- **不带入、靠守护降级**:`_event_replay_contract` 读的 `references/oracle/1993_birth_time_candidate_segment_table_2026_08_23.json`(上游作者本人出生案例,隐私,代码已 try/except)、`_build_timing_boundary_attribution` / `_profile_aware_benchmark_appendix` / `_load_rectification_evidence_contract` 读的状态文件(均已守护,缺失时该段显示 blocked / unavailable,这是保守方向,可接受)。PROGRESS 记录哪些段因此为 blocked。
|
||||
- **随研究子命令一起不取**:`references/nadi_low_risk_rule_pack_draft.json`、`references/real_case_calibration/classical_doctrine_loci_manifest_2026_08_28.json`、`references/chat_safety_audit_manifest.json`。
|
||||
- 通用规则:合并后 `grep` 引擎入口文件里所有 `references/` / `docs/` 路径字面量与 `Path(...) / 'references'` 拼接,逐一核对本地存在性;不存在且无守护的,要么按上面带入,要么加守护返回 `blocked`,不得让缺文件变成运行时异常。
|
||||
5. 合并完成后重新采集任务 0 的 golden,跑契约测试;用虚构数据实跑一次 `python scripts/jyotish_engine.py full-reading` 与 `/api/consultation_workflow`(career / wealth / health 三题),确认 `modules` 里没有新增 `status=failed` 的模块。
|
||||
|
||||
### 验收
|
||||
|
||||
- PROGRESS 冲突表:每个冲突块一行(文件、函数、取 ours / theirs / 手工合成、理由)。
|
||||
- 任务 0 的 golden 测试绿(键路径超集);`frontend` `tsc` 0 错、`npm test` fail 数不高于基线;`run_quality_gate.py` 完整 profile 绿;`tests/test_api_server_growth_contract.py`、`tests/test_commercial_domain_calculation_contract.py` 绿。
|
||||
- 本地全量 `pytest` 红测清单与 `TASK-repo-hygiene-20260903.md` 记录的基线逐条比对,新增红项为 0,或每一项都有归因。
|
||||
- staging 部署后 `GET /api/health` 的 `deployment.gitCommit` 等于合并提交;`/api/consultation_workflow` 用 smoke 数据实测三题正常。
|
||||
|
||||
---
|
||||
|
||||
## 任务 3(P1)· health 路由对齐上游三窗合同并进入个人报告
|
||||
|
||||
### 做法
|
||||
|
||||
1. **编排器**:`_ROUTE_DEFINITIONS["health"]` 的 `focus_techniques` 对齐上游(D6/D8/D30/Dasha/Narayana Dasha/Shadbala/Transit/Functional Benefic-Malefic);`_DOMAIN_PROFILE_SECTIONS["health"]` 改为上游三窗结构:`pressure_windows`、`event_risk_windows`、`recovery_support_windows`、`non_medical_boundary`、`verification_questions`(旧四段的内容映射进新段,不丢);新增 `evidence_packet_required_sections(route)`(本仓没有,取上游实现),health 路由追加 D6 / D8 / D30 / narayana_dasha / functional_benefic_malefic / transit / non_medical_boundary。
|
||||
2. **引擎**:`_build_ai_prompt_pack` 为 health 主题产出叙事种子(对齐 `_build_career_narrative_payload` 的结构:headline / strengths / risks / boundaries),来源限于 D6/D8/D30 宫位与主星、6/8/12 宫主 Dasha 激活、Shadbala 相关行星。`boundaries` 固定包含非医疗声明。
|
||||
3. **前端咨询链**:`consultation-methodology.ts` health 条目保留 `health-timing-strict`;wealth 条目加 `finance-timing-strict` 为主名、`wealth-timing-strict` 为别名,与编排器 `route_surface_contract` 对齐;Technique Audit Table 折叠展示对 health 新段生效。
|
||||
4. **个人报告**:`personal-report-route-core.ts` 主题枚举加 `health`;`report-theme-evidence-plan.ts` 为 health 定最低证据组(D1 + D6 + D8 + D30 + Vimshottari,Transit 为可选加分项,参照 `TASK-report-blocked-repairs-20260902.md` 的 fail-closed 口径);`personal-report-generation.ts` 的 `interpretiveFacts` / `themeNarrativeSeeds` allowlist 加 health 种子;writer 的 per-theme 静态知识包(`TASK-report-skill-parity-20260901.md` 任务 3 的机制)新增 health 包,≤3000 字符,禁路径与内部名词,且开头写明非医疗边界;报告中心的主题选择器加 health;计费口径与其他主题一致(不新增计费项)。
|
||||
5. `docs/BUG_HISTORY.md` 不需要新条目,除非实施中发现既有 health 路由缺陷。
|
||||
|
||||
### 验收
|
||||
|
||||
- smoke 数据实跑 `/api/consultation_workflow` health 题:响应 `strict_workflow_contracts` 显示 health 路由,三窗段齐全,D6/D8/D30 在 `available_layers` 里为 executed;`Transit` 无则为 blocked 而非缺席。
|
||||
- 个人报告含 health 主题时:确定性管线(假 writer)到 `PIPELINE-READY`;health claim card 的 conclusion 为真实内容且含非医疗声明;`assertionLevel` 防升级规则未改。
|
||||
- 前端 `tsc` 0 错;`frontend/tests/consultation-methodology.test.ts`、`consultation-agentic-runtime.test.ts`、报告相关测试绿。
|
||||
|
||||
---
|
||||
|
||||
## 任务 4(P1)· VedAstro 外部对标切到 REST 桥
|
||||
|
||||
### 做法
|
||||
|
||||
1. 引入 `scripts/vedastro_rest_bridge.py`(上游 `a6f47abd` 版本),去掉其内置的 yn-1993 默认出生参数(本仓不得携带真实出生资料,默认改为虚构 smoke 数据或必填)。
|
||||
2. `scripts/vedastro_gateway.py`:官方通路的健康判定改为对 `/api/Calculate` 的实际 `Status: Pass`;`official_verified` / `official_blocked` / `local_fallback` 三态语义不变;免费层 5 次/分钟写进 gateway 的限流参数与 `/api/vedastro_gateway/status` 输出。
|
||||
3. `scripts/vedastro_parity_matrix.py` 与其他调用 `vedastro_official_mcp_bridge.py` 的路径改为走 REST 桥;MCP 桥保留文件与测试,标注"仅协议探测",不再是默认通路。`match` 合盘子命令上游尚未实现,本仓合盘对标继续标 `official_blocked`,不得伪造。
|
||||
4. 更新 `docs/engine/README.md`(或本仓 VedAstro 文档所在处)与 `scripts/diagnose_vedastro_mode.py` 的说明。
|
||||
|
||||
### 验收
|
||||
|
||||
- 无网络时 `tests/test_vedastro_*.py` 全绿(REST 桥测试用录制响应,不打真网)。
|
||||
- 有网络时手工跑一次 REST 桥 horoscope 子命令(虚构数据),PROGRESS 记录 `Status` 与耗时;命中限流时 gateway 降级 `official_blocked` 而不是报错。
|
||||
- `/api/vedastro_gateway/status` 显示当前通路为 REST 与限流配置。
|
||||
|
||||
---
|
||||
|
||||
## 任务 5(P1)· 专业报告导出接进个人报告产品
|
||||
|
||||
### 做法
|
||||
|
||||
1. **引擎**:确保任务 2 已带入 `build_pl9_style_export_packet`、`render_pl9_markdown`、`cmd_pl9_export` 及其依赖;上游 `--format authority` 与 `--archive-dir` 不需要,可不接;markdown 输出保留上游的引擎边界声明段。所有对外文案与新代码标识用 `professional_report_reference`,旧 `pl9_*` 标识只在兼容处保留。
|
||||
2. **API**:`scripts/jyotish_api_server.py` 新增 `POST /api/professional_report_reference`(入参与 `/api/consultation_workflow` 相同的出生参数 + `format ∈ {json, markdown}` + `packs`),走 `api_heavy_compute_gate`,复用 `_compute_full_reading_for_thematic` 的缓存/复用逻辑,不重复算全盘;响应不含 raw full-reading(上游 `--include-raw` 语义在这里关闭)。
|
||||
3. **前端**:报告中心(`personal-report-center.tsx`)在已 ready 的报告上增加"专业参考版(导出)"入口,调用上述 API 生成 markdown 并按既有导出方式交付(下载或页内查看,沿用报告中心现有导出交互,不新做组件);导出不经写作 agent、不计费为报告生成、不写 `personal_report_sections`。入口对未 ready 报告不显示。
|
||||
4. **不做**:把导出内容喂给写作 agent、把导出存进数据库、PDF 格式(上游 pdf 依赖另行评估)。
|
||||
|
||||
### 验收
|
||||
|
||||
- smoke 数据 `python scripts/jyotish_engine.py pl9-export --format markdown` 与 `/api/professional_report_reference` 都能产出;markdown 含引擎边界声明;输出中不出现文件路径、内部模块名。
|
||||
- 隐私扫描通过;仓库内没有任何 PL9 参考报告原文。
|
||||
- 前端入口只对 ready 报告出现,`tsc` 0 错,报告中心测试绿;导出一次不产生任何模型调用(telemetry 计数为 0)。
|
||||
|
||||
---
|
||||
|
||||
## 任务 6(P1)· 把合并进来的引擎新字段接到聊天与报告
|
||||
|
||||
任务 2 只是把上游代码合进引擎;`/api/consultation_workflow` 的 `ai_prompt_pack` 是 `scripts/jyotish_api_server.py` 自己的精简版 `_build_chart_prompt_pack`,前端的报告提取层又是 allowlist 制,**不做这一步,聊天和报告什么都不会变**。
|
||||
|
||||
### 做法
|
||||
|
||||
1. **响应侧(只新增键)**:在 `_compute_consultation_workflow` / `_build_chart_prompt_pack` 里新增并透传:`timing_narrative`(上游 `_build_timing_narrative_payload(modules)`,结构对齐既有 career / relationship / finance 种子)、`modules.planetary_friendship`(上游 `_build_planetary_friendship_snapshot`)、`module_execution_audit`(上游 `_build_module_execution_audit`)、`pratyantar_dasha_timeline`(上游在 `cmd_dasha` 里挂的,需要在 workflow 的 dasha 子周期里同样挂上)。上游 `response_envelope` 键是纯附加,透传或忽略都可以,不得据此改既有键。任务 0 的 golden 重新采集后必须是超集。
|
||||
2. **聊天侧**:折叠技法审计表新增 `planetary_friendship` 与 `module_execution_audit` 派生的行(executed / blocked / not_applicable 三态照旧);Ashtottari 行按任务 1 覆盖层的 `partial` 显示为"参数敏感、未验证",不得显示 executed 为确定性;finance / wealth 别名在审计表与路由展示一致。`_build_chart_prompt_pack` 里给聊天模型的证据快照加 `timing_narrative`,长度上限与既有种子一致。
|
||||
3. **报告侧**:`personal-report-generation.ts` 的 `interpretiveFacts` allowlist 加 `planetary_friendship`(只取友敌等级表,不取自由文本)与 `pratyantar_dasha_timeline`(只取当前与下一段的边界日期),`themeNarrativeSeeds` 加 `timing_narrative`(timing 主题);沿用 `filterReportEvidenceBundleForSection` 按主题裁剪;净化、长度上限、hash 覆盖与 `TASK-report-skill-parity-20260901.md` 的规则一致。timing 主题 claim card 的 conclusion 可以引用 pratyantar 边界。
|
||||
4. 成本:报告每章 `inputTokens` 改前改后对照写进 PROGRESS,p50 超过改前 1.3 倍必须停下摆数据(这轮只是增补字段,不应接近报告对齐那轮的 2 倍线)。
|
||||
|
||||
### 验收
|
||||
|
||||
- golden 超集测试绿;前端 `tsc` 0 错;聊天与报告相关测试绿。
|
||||
- smoke 数据实跑一次聊天(timing 题)与一份 personal_full 报告:审计表出现新行且三态正确;timing 章节的 claim card 含 pratyantar 边界日期;bundle hash 稳定(同输入两次一致)。
|
||||
- 每章 `inputTokens` 对照表在 PROGRESS 里。
|
||||
|
||||
---
|
||||
|
||||
## 任务 7(P1)· 校正:自适应选题、岁差按请求传递、报告出生时间敏感度节
|
||||
|
||||
### 7a · 自适应选题器(P1)
|
||||
|
||||
- 引入上游 `scripts/active_rectification_selector.py`(`select_next_questions`:按可区分答案跨度、事实可靠度、作答成本、域多样性、剩余候选数、**分钟相关度**打分选题)与其测试 `tests/test_active_rectification_selector.py`。分钟相关度是核心价值:只优先问"其敏感层在剩余候选分钟窗口内真的会变"的题,直接针对本仓已知的"题库分不开相邻 top-2"问题。
|
||||
- `scripts/active_rectification_questions.py`:按上游给题库加 `factual_reliability`、`positive_cluster` / `negative_cluster`,`_candidate_scan` 挂 `scan_candidate_times` 的 `minute_scan`,`build_questionnaire` / `score_answers` 返回 `selection` / `next_round_selection`(**新增键**,`next_round_questions` 等旧键保留)。**不引入** `_known_case_regression_hint`、`regression_hint`、`minute_rankings`、`minute_focus`。
|
||||
- 本仓 `scan_candidate_times` 只返回各分盘上升星座;选题器的 `_minute_feature_value` 需要更细特征。按上游改法给扫描行**新增** `d1_longitude` 与完整上升对象(旧字段不动),并把分盘列表里的 D1 去掉。
|
||||
- 前端 `birth-time-journey-engine-model.ts` / `birth-time-journey-turn.ts`:自适应轮的 `nextQuestion` 改为优先取服务端 `next_round_selection.selected_questions[0]`,服务端没给时回退现有逻辑。基线轮不变。
|
||||
- 验收:`tests/test_active_rectification_questions.py`(按上游 +64 行的断言更新,但删掉与 regression_hint 相关的用例)与选题器测试绿;`npx tsx --test tests/rectification-*.test.ts` fail=0 且用例数不少于基线 62 个文件;用本仓虚构 smoke 案例跑一次完整问答,PROGRESS 附"每轮所选题 + 其分钟相关度"表,证明选题确实随候选窗口变化;V5 打分输出对同一答案集逐字节不变(选题只改顺序,不改分数)。
|
||||
|
||||
### 7b · 岁差与交点模式按请求传递(P1,与 `TASK-repo-hygiene-20260903.md` 串行)
|
||||
|
||||
- `scripts/active_rectification_event_engine.py`(`AYANAMSA` / `NODE_MODE` 全局常量三处)、`active_rectification_questions.py`、`candidate_time_sensitivity_scan.py`(本仓签名无 `ayanamsa`):改为 `request.get("ayanamsa", DEFAULT_AYANAMSA_NAME)` / `request.get("node_mode", ...)`,默认值不变(Raman)。`/api/rectification/*` 与 `/api/active_rectification_*` 的请求体接受这两个键并写进回执。
|
||||
- 顺序:仓库整备任务书的任务 0 已规定"校正引擎按 Raman 校准的阈值与 golden 本轮不动,只确认 lahiri 下不崩"。本项在其之后做:整备任务书交付 `resolveAyanamsa(profile)` 后,校正前端把它传给 Python。若整备任务书尚未合入,本项只做 Python 侧并保持默认值,前端传参留到整备任务书合入后补一个小提交。
|
||||
- 验收:默认请求(不带 ayanamsa)的 V5 输出逐字节不变;带 `ayanamsa=lahiri` 的请求回执里 `ayanamsa` 为 `lahiri` 且计算确实按 Lahiri(用 smoke 案例对比上升度数);`tests/test_active_rectification_api.py` 及本仓 38 个 Python 校正测试绿。
|
||||
|
||||
### 7c · 不确定出生时间的报告敏感度节(P2,依赖任务 2 与任务 6)
|
||||
|
||||
- 引入上游四个 `flexible_birth_time_*` 模块与测试;`_build_birth_time_sensitivity` 里读 1993 分段表的 `_load_1993_candidate_segment_table` 与 `candidate_segment_table` 一并删掉,`candidate_micro_compare` 段不要。窗口来源改为:报告请求带的出生时间精度真值(`BIRTH_TIME_ACCURACY`,`TASK-consultation-birth-accuracy-20260901.md` 已贯通)为 `confirmed` 时**不生成**本节;为 `provisional` / `approximate` 时窗口取校正案例已采用的可信区间(案例不存在时用精度等级对应的固定宽度,宽度表写进代码常量并在 PROGRESS 说明),上限 15 个候选分钟,超出则只取区间两端与代表分钟。
|
||||
- 产物:`consultation_workflow` 响应新增 `birth_time_sensitivity`(schema 沿用上游 `jyotish.report_birth_time_sensitivity.v1`,`status ∈ {not_applicable, candidate_window_only}`),个人报告 bundle 新增对应的解读性事实(哪些主题结论在窗口内稳定、哪些敏感),报告附录多一节"出生时间敏感度",正文各章在该主题被标为敏感时加一句边界提示。writer 输入隔离与 schema 规则沿用。
|
||||
- 验收:`confirmed` 案例的报告与改前逐字节一致(bundle hash 不变);`provisional` 案例的报告附录出现敏感度节,且每个"敏感"判断能追溯到窗口内至少一个分钟的层变化;引擎侧 `python scripts/jyotish_engine.py full-reading` 带窗口参数不依赖任何 references 数据文件;前端 `tsc` 0 错,报告测试绿。
|
||||
|
||||
### 任务 7 通用红线
|
||||
|
||||
- 仓库内不得出现任何真实出生案例;任务 7 全部 fixture 用虚构 smoke 数据。合并前 `grep -rn "1993-04-17\|14:49" scripts tests frontend/src` 必须为空(或只命中与该日期无关的既有测试数据,逐条说明)。
|
||||
- `ALGORITHM_VERSION` / `POLICY` 常量不变;若任何 golden 变化,说明是新增键还是数值变化,数值变化即视为越界。
|
||||
- 合入后重跑 `docs/testing/` 里校正的浏览器走查清单,结果写 PROGRESS;走不了的写环境缺口。
|
||||
|
||||
---
|
||||
|
||||
## BUG 编号起点
|
||||
|
||||
开工时核对 `docs/BUG_HISTORY.md` 最大号;截至本任务书为 **BUG-510**,新条目从 **BUG-511** 起。
|
||||
|
||||
## 回报格式
|
||||
|
||||
`docs/tasks/PROGRESS-upstream-sync-20260903.md`(任务 0–2)与 `docs/tasks/PROGRESS-upstream-capabilities-20260903.md`(任务 3–7):每个任务写做了什么、测试数字、冲突表、偏离与原因、环境缺口;并在 `docs/tasks/README.md` 状态板改状态。
|
||||
@@ -200,6 +200,16 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3:
|
||||
- **Surface:** canvas fill, hairline border, `--shadow-elevated`; hover uses the warm card surface. No utility-class shadows.
|
||||
- **Accessibility:** a real button in document order with a visible label matching its accessible name, 44px target, and the focus ring; the icon is decorative.
|
||||
|
||||
### Ayanamsa preference
|
||||
|
||||
The self-profile editor in 星盘资料 (`ChartLibraryPanel`) owns the ayanamsa radios. Personal 个人资料 stays account basics only.
|
||||
|
||||
- **Structure:** four 44px stacked radios reused from `.theme-preference-option`, plus a caption under the group. Raman is marked 默认. Each option has a one-line description; selected rows show the same `Check` as the theme panel.
|
||||
- **Values:** `raman` (default) / `lahiri` / `kp` / `true_pushya`. Copy lives in `frontend/src/lib/ayanamsa.ts`.
|
||||
- **Hint:** “切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。”
|
||||
- **Placement:** after `ProfileFields` on the self-profile form only. Not on other-chart save, not on `page.tsx`.
|
||||
- **States:** default, hover, selected (`aria-checked` only), keyboard focus. Switching does not clear a confirmed birth minute.
|
||||
|
||||
### Rectification surface
|
||||
|
||||
The birth-time rectification session is the consultation transcript plus a house board; questions live inside the assistant message that asked them. It shares every waiting vocabulary with the consultation surface; nothing here spins or says "loading" after the reveal.
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function GET() {
|
||||
const admin = createAdminSupabaseClient();
|
||||
let { data: profile, error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,name,birth_time,rectification_case_id")
|
||||
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa,name,birth_time,rectification_case_id")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function GET() {
|
||||
...withoutDeclaredWindow.data,
|
||||
declared_window_start: null,
|
||||
declared_window_end: null,
|
||||
ayanamsa: undefined,
|
||||
};
|
||||
profileError = withoutDeclaredWindow.error;
|
||||
} else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) {
|
||||
@@ -82,6 +83,7 @@ export async function GET() {
|
||||
birth_place_provider_id: undefined,
|
||||
timezone_id: undefined,
|
||||
timezone_source: undefined,
|
||||
ayanamsa: undefined,
|
||||
} : null;
|
||||
profileError = fallback.error;
|
||||
} else {
|
||||
@@ -281,6 +283,7 @@ export async function PATCH(request: Request) {
|
||||
...(payload.birth_place_provider_id !== undefined ? { birth_place_provider_id: payload.birth_place_provider_id } : {}),
|
||||
...(payload.timezone_id !== undefined ? { timezone_id: payload.timezone_id } : {}),
|
||||
...(payload.timezone_source !== undefined ? { timezone_source: payload.timezone_source } : {}),
|
||||
...(payload.ayanamsa !== undefined ? { ayanamsa: payload.ayanamsa } : {}),
|
||||
};
|
||||
const withoutCoordinates = baseProfile;
|
||||
const invalidatesUnconfirmedApplication = Object.keys(applicationPatch).length > 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { parseBirthTimeProfile } from "@/lib/birth-time-journey-adapters";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { BirthProfileTimezoneError, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import {
|
||||
createJyotishBirthTimeJourneyEngine,
|
||||
@@ -96,7 +97,7 @@ export async function POST(request: Request) {
|
||||
case "assess": {
|
||||
const { data: profile, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset")
|
||||
.select("birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset,ayanamsa")
|
||||
.eq("id", user.id)
|
||||
.maybeSingle();
|
||||
if (error) throw new BirthTimeJourneyStoreError("load_case");
|
||||
@@ -107,7 +108,10 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
const assessment = parseBirthTimeProfile(await resolveMissingBirthTimezoneOffset(profile));
|
||||
return responseWithJourneyMetric(service.assess(user.id, assessment), "turn_advanced");
|
||||
return responseWithJourneyMetric(
|
||||
service.assess(user.id, { ...assessment, ayanamsa: resolveAyanamsa(profile) }),
|
||||
"turn_advanced",
|
||||
);
|
||||
}
|
||||
case "answer_question":
|
||||
return responseWithJourneyMetric(service.answerQuestion(
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function POST(request: Request) {
|
||||
async loadProfile(profileUserId) {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa")
|
||||
.eq("id", profileUserId)
|
||||
.single();
|
||||
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
|
||||
|
||||
@@ -20,7 +20,7 @@ function createProfileRepository(
|
||||
async loadProfile(userId) {
|
||||
return admin
|
||||
.from("profiles")
|
||||
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,onboarding_payload,onboarding_version,onboarding_generated_at")
|
||||
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa,onboarding_payload,onboarding_version,onboarding_generated_at")
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function POST(request: Request) {
|
||||
if (userId) {
|
||||
const result = await supabase
|
||||
.from("profiles")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_status,latitude,longitude,timezone_offset,birth_place_label")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_status,latitude,longitude,timezone_offset,birth_place_label,ayanamsa")
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
profile = result.data ?? null;
|
||||
|
||||
@@ -1623,6 +1623,10 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.theme-preference-option:hover, .theme-preference-option[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.theme-preference-option > svg { width: 18px; height: 18px; color: var(--color-ink-tertiary); }
|
||||
.theme-preference-option > svg:last-child { color: var(--color-action); }
|
||||
.ayanamsa-preference-option { grid-template-columns: minmax(0, 1fr) 18px; }
|
||||
.ayanamsa-preference-option[aria-checked="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.ayanamsa-preference-option small { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); font-weight: 400; }
|
||||
.ayanamsa-preference-hint { margin: var(--space-3) 0 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.account-menu-danger, .account-menu-danger > svg { color: var(--color-danger); }
|
||||
.account-menu-danger[data-highlighted] { background: var(--color-danger-muted); }
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import {
|
||||
AYANAMSA_OPTIONS,
|
||||
AYANAMSA_SWITCH_HINT,
|
||||
type AyanamsaName,
|
||||
} from "@/lib/ayanamsa";
|
||||
|
||||
export function AyanamsaPreferenceField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
readonly value: AyanamsaName;
|
||||
readonly onChange: (next: AyanamsaName) => void;
|
||||
}) {
|
||||
return (
|
||||
<section className="sheet-section ayanamsa-preference" aria-labelledby="ayanamsa-preference-title">
|
||||
<div className="section-heading">
|
||||
<b id="ayanamsa-preference-title">岁差</b>
|
||||
<small>用来把黄道转成印度占星用的恒星黄道</small>
|
||||
</div>
|
||||
<div className="theme-preference-options" role="radiogroup" aria-labelledby="ayanamsa-preference-title">
|
||||
{AYANAMSA_OPTIONS.map((option) => {
|
||||
const selected = value === option.value;
|
||||
return (
|
||||
<button
|
||||
className="theme-preference-option ayanamsa-preference-option"
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onChange(option.value)}
|
||||
>
|
||||
<span>
|
||||
{option.label}
|
||||
<small>{option.description}</small>
|
||||
</span>
|
||||
{selected ? <Check aria-hidden="true" /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="ayanamsa-preference-hint">{AYANAMSA_SWITCH_HINT}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,18 @@ import {
|
||||
readProfile,
|
||||
upsertSelfChart,
|
||||
} from "@/lib/home-profile";
|
||||
import { emptyProfile, timestamp, type Account, type ChartLibraryRecord, type ChartRelationship, type Profile, type SynastryRelationshipType, type SynastryReportCard } from "@/lib/home-types";
|
||||
import {
|
||||
emptyProfile,
|
||||
timestamp,
|
||||
type Account,
|
||||
type ChartLibraryRecord,
|
||||
type ChartRelationship,
|
||||
type Profile,
|
||||
type SynastryRelationshipType,
|
||||
type SynastryReportCard,
|
||||
} from "@/lib/home-types";
|
||||
import { AyanamsaPreferenceField } from "@/components/ayanamsa-preference-field";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
export type ChartLibraryPanelProps = {
|
||||
readonly account: Account | null;
|
||||
@@ -179,6 +190,10 @@ export function ChartLibraryPanel({
|
||||
<form className="profile-form chart-library-form" onSubmit={saveProfile}>
|
||||
<div className="section-heading"><b>编辑本人星盘</b><small>这份资料会用于默认解盘与新对话。</small></div>
|
||||
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="self-profile-name" />
|
||||
<AyanamsaPreferenceField
|
||||
value={resolveAyanamsa(profileDraft)}
|
||||
onChange={(ayanamsa) => setProfileDraft({ ...profileDraft, ayanamsa })}
|
||||
/>
|
||||
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
|
||||
<div className="dialog-actions">
|
||||
<button className="button-secondary" type="button" onClick={() => { setEditingSelfChart(false); setProfileDraft(profile); }}>取消编辑</button>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
readProfile,
|
||||
selectedBirthPlace,
|
||||
} from "@/lib/home-profile";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { createStartGreeting } from "@/lib/onboarding-client";
|
||||
import {
|
||||
timestamp,
|
||||
@@ -226,6 +227,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
timezone_id: nextProfile.timezoneId || null,
|
||||
timezone_offset: birthPlace?.tz ?? null,
|
||||
timezone_source: nextProfile.timezoneSource || null,
|
||||
ayanamsa: resolveAyanamsa(nextProfile),
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { isBirthClockTime, parseBirthDate } from "./birth-time-intake-model.ts";
|
||||
import { AYANAMSA_VALUES } from "./ayanamsa.ts";
|
||||
|
||||
const nullableTrimmedString = (maximum: number) => z.string().trim().min(1).max(maximum).nullable();
|
||||
const nullableBirthDate = z.string().refine((value) => parseBirthDate(value) !== undefined, {
|
||||
@@ -52,6 +53,7 @@ export const accountProfilePatchSchema = z.object({
|
||||
birth_place_provider_id: nullableTrimmedString(160).optional(),
|
||||
timezone_id: nullableTrimmedString(80).optional(),
|
||||
timezone_source: z.literal("iana_historical").nullable().optional(),
|
||||
ayanamsa: z.enum(AYANAMSA_VALUES).optional(),
|
||||
}).strict().superRefine((value, context) => {
|
||||
const source = value.birth_time_source;
|
||||
const time = value.reported_birth_time;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export const AYANAMSA_VALUES = ["raman", "lahiri", "kp", "true_pushya"] as const;
|
||||
export type AyanamsaName = (typeof AYANAMSA_VALUES)[number];
|
||||
export const DEFAULT_AYANAMSA: AyanamsaName = "raman";
|
||||
|
||||
export const AYANAMSA_OPTIONS = [
|
||||
{
|
||||
value: "raman",
|
||||
label: "Raman(默认)",
|
||||
description: "本站现在默认用的岁差。",
|
||||
},
|
||||
{
|
||||
value: "lahiri",
|
||||
label: "Lahiri",
|
||||
description: "公开星历和教材里最常见的岁差。",
|
||||
},
|
||||
{
|
||||
value: "kp",
|
||||
label: "KP",
|
||||
description: "克里希那穆提体系用的岁差。",
|
||||
},
|
||||
{
|
||||
value: "true_pushya",
|
||||
label: "True Pushya",
|
||||
description: "True Pushya 岁差。",
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
value: AyanamsaName;
|
||||
label: string;
|
||||
description: string;
|
||||
}>;
|
||||
|
||||
export const AYANAMSA_SWITCH_HINT =
|
||||
"切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。";
|
||||
|
||||
export function isAyanamsaName(value: unknown): value is AyanamsaName {
|
||||
return typeof value === "string" && (AYANAMSA_VALUES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function resolveAyanamsa(
|
||||
profile?: { ayanamsa?: unknown } | null,
|
||||
): AyanamsaName {
|
||||
const raw = profile?.ayanamsa;
|
||||
if (typeof raw === "string") {
|
||||
const key = raw.trim().toLowerCase().replace(/-/g, "_");
|
||||
if (key === "krishnamurti" || key === "krishnamurti_paddhati") return "kp";
|
||||
if (isAyanamsaName(key)) return key;
|
||||
}
|
||||
return DEFAULT_AYANAMSA;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { chinaLocations } from "@/data/china-locations";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
import { finiteBirthNumber, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
|
||||
export type BirthRectificationProfile = {
|
||||
@@ -11,6 +12,7 @@ export type BirthRectificationProfile = {
|
||||
longitude?: number | null;
|
||||
timezoneId?: string;
|
||||
timezoneOffset?: number | null;
|
||||
ayanamsa?: AyanamsaName | string | null;
|
||||
};
|
||||
|
||||
export async function payloadFromProfile(profile: BirthRectificationProfile) {
|
||||
@@ -33,5 +35,6 @@ export async function payloadFromProfile(profile: BirthRectificationProfile) {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(resolved),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import type {
|
||||
BirthTimeAssessment,
|
||||
ScanStability,
|
||||
} from "./birth-time-journey.ts";
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
|
||||
function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
function scanInput(assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown }): JourneyScanInput | null {
|
||||
if (assessment.source === "unknown") return null;
|
||||
if (assessment.source === "period_only") {
|
||||
const periodScan = {
|
||||
@@ -25,7 +26,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
lat: assessment.location.lat,
|
||||
lon: assessment.location.lon,
|
||||
tz: assessment.location.tz,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(assessment),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -37,7 +38,7 @@ function scanInput(assessment: BirthTimeAssessment): JourneyScanInput | null {
|
||||
lat: assessment.location.lat,
|
||||
lon: assessment.location.lon,
|
||||
tz: assessment.location.tz,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(assessment),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ function questionnaireStability(
|
||||
|
||||
export async function scanAssessment(
|
||||
engine: Pick<LegacyBirthTimeJourneyEngine, "scan">,
|
||||
assessment: BirthTimeAssessment,
|
||||
assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown },
|
||||
): Promise<{
|
||||
readonly stability: ScanStability;
|
||||
readonly questionnaire: RectificationQuestionnaire | null;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { DynamicStoredFields, LegacyStoredFields } from "./birth-time-journ
|
||||
import { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError } from "./birth-time-journey-errors.ts";
|
||||
import { createDynamicJourneyMethods } from "./birth-time-dynamic-service-methods.ts";
|
||||
import { createDynamicCandidateConfirmation } from "./birth-time-dynamic-candidate-confirmation.ts";
|
||||
import type { AyanamsaName } from "./ayanamsa.ts";
|
||||
|
||||
export { RectificationCaseNotFoundError, RectificationQuestionsUnavailableError };
|
||||
|
||||
@@ -53,7 +54,7 @@ export type JourneyScanInput = {
|
||||
readonly lat: number;
|
||||
readonly lon: number;
|
||||
readonly tz: number;
|
||||
readonly ayanamsa: "raman" | "lahiri";
|
||||
readonly ayanamsa: AyanamsaName;
|
||||
};
|
||||
|
||||
export type JourneyScoreInput = {
|
||||
@@ -207,7 +208,7 @@ export function createBirthTimeJourneyService(ports: BirthTimeJourneyPorts) {
|
||||
const dynamicMethods = createDynamicJourneyMethods(ports);
|
||||
const dynamicCandidates = createDynamicCandidateConfirmation(ports);
|
||||
return {
|
||||
async assess(userId: string, assessment: BirthTimeAssessment): Promise<VersionedJourneyResponse | DynamicVersionedJourneyResponse> {
|
||||
async assess(userId: string, assessment: BirthTimeAssessment & { readonly ayanamsa?: unknown }): Promise<VersionedJourneyResponse | DynamicVersionedJourneyResponse> {
|
||||
const scan = await scanAssessment(ports.engine, assessment);
|
||||
const snapshot = assessBirthTime(assessment, scan.stability);
|
||||
const persisted = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ConsultationBirthTimeMode,
|
||||
} from "./consultation-birth-time-mode.ts";
|
||||
import type { GeneralDailyReference } from "./general-daily-panchanga.ts";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "./ayanamsa.ts";
|
||||
|
||||
export type ConsultationProfileTruthErrorCode =
|
||||
| "profile_unavailable"
|
||||
@@ -42,6 +43,7 @@ type ServerChartToolInput = Readonly<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
tz: number;
|
||||
ayanamsa: AyanamsaName;
|
||||
declared_accuracy: DeclaredBirthAccuracy["declaredAccuracy"];
|
||||
time_source: string;
|
||||
}>;
|
||||
@@ -84,6 +86,7 @@ export type DeclaredBirthWindowConsultation = Readonly<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
tz: number;
|
||||
ayanamsa: AyanamsaName;
|
||||
rangeStart: string;
|
||||
rangeEnd: string;
|
||||
}>;
|
||||
@@ -352,6 +355,7 @@ function serverChartFromProfile(
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
declared_accuracy: accuracy.declaredAccuracy,
|
||||
time_source: accuracy.timeSource,
|
||||
}),
|
||||
@@ -435,6 +439,7 @@ function declaredWindowFromProfile(value: unknown): DeclaredBirthWindowConsultat
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
rangeStart: range.startTime,
|
||||
rangeEnd: range.endTime,
|
||||
}),
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function fetchDeclaredWindowChart(
|
||||
lat: toolInput.lat,
|
||||
lon: toolInput.lon,
|
||||
tz: toolInput.tz,
|
||||
ayanamsa: toolInput.ayanamsa,
|
||||
range_start: toolInput.rangeStart,
|
||||
range_end: toolInput.rangeEnd,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { chinaLocations } from "@/data/china-locations";
|
||||
import { finiteBirthNumber, resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
|
||||
export type GlobalBirthProfile = {
|
||||
name?: string;
|
||||
@@ -13,6 +14,7 @@ export type GlobalBirthProfile = {
|
||||
longitude?: number | null;
|
||||
timezoneId?: string;
|
||||
timezoneOffset?: number | null;
|
||||
ayanamsa?: AyanamsaName | string;
|
||||
};
|
||||
|
||||
function resolvedLocation(profile: GlobalBirthProfile) {
|
||||
@@ -46,7 +48,7 @@ export async function dailyProfilePayload(profile: GlobalBirthProfile, today: st
|
||||
tz,
|
||||
transit_date: today,
|
||||
today,
|
||||
ayanamsa: "raman",
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
node_mode: "mean",
|
||||
};
|
||||
}
|
||||
@@ -76,5 +78,6 @@ export async function synastryBirthPayload(profile: GlobalBirthProfile) {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type Profile,
|
||||
type SynastryRelationshipType,
|
||||
} from "@/lib/home-types";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
export function findProvince(code: string) {
|
||||
return china.provinces.find((province) => province.code === code);
|
||||
@@ -303,6 +304,7 @@ export function readProfile(value: unknown): Profile {
|
||||
latitude,
|
||||
longitude,
|
||||
timezoneOffset,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
...(chartRelationship ? { chartRelationship } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BeamAvatar } from "@/lib/beam-avatar";
|
||||
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
|
||||
import type { BirthTimeDraft } from "@/lib/birth-time-intake-model";
|
||||
import { DEFAULT_AYANAMSA, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
import type { AgentActivityView, ChatMessage } from "@/lib/chat-message-view";
|
||||
import type { ConsultationTimelineRow } from "@/lib/consultation-run-timeline";
|
||||
import type { PublicThinkingSection } from "@/lib/consultation-thinking-plan";
|
||||
@@ -28,6 +29,7 @@ export type Profile = BirthTimeDraft & {
|
||||
longitude: number | null;
|
||||
timezoneOffset: number | null;
|
||||
rectificationCaseId: string;
|
||||
ayanamsa: AyanamsaName;
|
||||
chartRelationship?: ChartRelationship;
|
||||
};
|
||||
export type ChartRelationship = "self" | "partner" | "family" | "friend" | "client" | "other";
|
||||
@@ -230,6 +232,7 @@ export const emptyProfile: Profile = {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
timezoneOffset: null,
|
||||
ayanamsa: DEFAULT_AYANAMSA,
|
||||
};
|
||||
export type StoredDailyStarlanguage = {
|
||||
readonly day: string;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import type { ConsultationInput } from "@/mastra";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import type {
|
||||
ReportAgentPort,
|
||||
ReportEvidenceBundleV2,
|
||||
@@ -415,6 +416,7 @@ export async function resolveReportCreate(deps: ReportCreateCoreDeps): Promise<R
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
city: birthPlaceLabel,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
question: `请为个人报告计算 ${theme} 主题证据`,
|
||||
theme,
|
||||
entryMode: "direct_chart",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { resolveReportBirthClock } from "@/lib/personal-report-route-core";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { completeUsage, releaseUsage } from "@/lib/consultation-billing";
|
||||
import type { ConsultationInput } from "@/mastra/consultation-workflow";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
|
||||
const PROFILE_COLUMNS = [
|
||||
"name",
|
||||
@@ -33,6 +34,7 @@ const PROFILE_COLUMNS = [
|
||||
"longitude",
|
||||
"timezone_offset",
|
||||
"birth_place_label",
|
||||
"ayanamsa",
|
||||
].join(",");
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
@@ -119,6 +121,7 @@ async function generateProductionReport(context: PersonalReportWorkerGenerationC
|
||||
lon: longitude,
|
||||
tz: timezoneOffset,
|
||||
city: birthPlaceLabel,
|
||||
ayanamsa: resolveAyanamsa(profile),
|
||||
question: `请为个人报告计算 ${rawTheme} 主题证据`,
|
||||
theme: rawTheme as ConsultationInput["theme"],
|
||||
entryMode: "direct_chart",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createHash } from "node:crypto";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { resolveMissingBirthTimezoneOffset } from "../../birth-profile-timezone.ts";
|
||||
import { declaredClockRange } from "../../declared-birth-window.ts";
|
||||
import { resolveAyanamsa, type AyanamsaName } from "../../ayanamsa.ts";
|
||||
import { normalizePersistedBirthDate } from "../../birth-time-intake-model.ts";
|
||||
import {
|
||||
resolveActiveSkillPackage,
|
||||
@@ -46,6 +47,7 @@ export type V9BaselineSnapshot = Readonly<{
|
||||
active_birth_time: string | null;
|
||||
uncertainty_before_minutes: number | null;
|
||||
uncertainty_after_minutes: number | null;
|
||||
ayanamsa: AyanamsaName;
|
||||
}>;
|
||||
|
||||
export type V9RectificationProfile = Readonly<{
|
||||
@@ -167,7 +169,7 @@ export async function loadV9RectificationProfile(
|
||||
const { data, error } = await accounting
|
||||
.from("profiles")
|
||||
.select(
|
||||
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset",
|
||||
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset,ayanamsa",
|
||||
)
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
@@ -213,6 +215,7 @@ export async function loadV9RectificationProfile(
|
||||
active_birth_time: null,
|
||||
uncertainty_before_minutes: uncertaintyBefore,
|
||||
uncertainty_after_minutes: uncertaintyAfter,
|
||||
ayanamsa: resolveAyanamsa(row),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -524,6 +524,38 @@ export function mergeChoiceCard(
|
||||
};
|
||||
}
|
||||
|
||||
export function choiceCardFromPersistedVerifyCopy(input: {
|
||||
copy: AgentChoiceCopy;
|
||||
questionId: string;
|
||||
methodId: string;
|
||||
scoring: boolean;
|
||||
probeId: string | null;
|
||||
caseRevision: number | null;
|
||||
focusId: string;
|
||||
}): RectificationChoiceCard | null {
|
||||
if (!input.copy.prompt.trim() || !input.questionId.trim()) return null;
|
||||
if (!isPersistedFocusId(input.focusId)) return null;
|
||||
return {
|
||||
question_id: input.questionId,
|
||||
method_id: input.methodId,
|
||||
prompt: input.copy.prompt,
|
||||
why: "",
|
||||
varga: null,
|
||||
choice_mode: CHOICE_MODE,
|
||||
options: input.copy.options.map((option) => ({
|
||||
...option,
|
||||
role: "primary" as const,
|
||||
})),
|
||||
stop_label: CHOICE_SKIP_QUESTION_LABEL,
|
||||
stop_message: CHOICE_SKIP_QUESTION_MESSAGE,
|
||||
scoring: input.scoring,
|
||||
probe_id: input.probeId,
|
||||
case_revision: input.caseRevision,
|
||||
focus_id: input.focusId,
|
||||
choice_kind: "existence",
|
||||
};
|
||||
}
|
||||
|
||||
export function isHoldoutVerificationQuote(quote: string): boolean {
|
||||
return quote.includes(HOLDOUT_MESSAGE_PREFIX);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type WindowScan,
|
||||
} from "./varga-observations";
|
||||
import { questionContractVersionIsCompatible } from "./probe-question-contract";
|
||||
import { resolveAyanamsa } from "../../ayanamsa.ts";
|
||||
|
||||
export class RectificationEngineError extends Error {
|
||||
readonly code: string;
|
||||
@@ -475,6 +476,7 @@ function engineRequestBody(input: {
|
||||
lat,
|
||||
lon,
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(snapshot),
|
||||
events: input.events,
|
||||
birth_time_source: snapshot.birth_time_source,
|
||||
timezone_id: snapshot.timezone_id,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Public interview projection for the Case GET payload.
|
||||
*
|
||||
* The tap card is shown only when the follow-up is a discriminator
|
||||
* (candidates already diverge or holdout) and the Agent wrote choice copy.
|
||||
* The tap card is shown when the rebuilt follow-up has a matching
|
||||
* discriminator frame, or when the open focus is already a reverse-verify /
|
||||
* out-of-sample A–D schema (even if the recast has no choice_frame).
|
||||
* Card identity is the persisted focus UUID plus the inference revision.
|
||||
*/
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
parseAgentChoiceCopy,
|
||||
preferConcreteChoicePrompt,
|
||||
mergeChoiceCard,
|
||||
choiceCardFromPersistedVerifyCopy,
|
||||
serverOwnedChoiceCopy,
|
||||
type RectificationChoiceCard,
|
||||
type RectificationChoiceFrame,
|
||||
@@ -2194,11 +2195,37 @@ export function projectRectificationChoiceCard(
|
||||
? input.activeFocus.id.trim()
|
||||
: "";
|
||||
if (!isPersistedFocusId(focusId)) return null;
|
||||
const followup = plan.next_followup ?? plan.deferred_followup ?? null;
|
||||
if (!followup) return null;
|
||||
const frame = followup.choice_frame;
|
||||
if (!frame) return null;
|
||||
const schema = input.activeFocus?.expectedAnswerSchema ?? null;
|
||||
const schemaCopy = parseAgentChoiceCopy(schema);
|
||||
const intent = input.activeFocus?.intent ?? "";
|
||||
const verifyOnly = intent === "reverse_verify" || intent === "out_of_sample_check";
|
||||
// Recast can drop choice_frame when the schema has no probe_year. The open
|
||||
// A–D schema is still the live question; GET must not silence it.
|
||||
const persistedVerifyCard = (): RectificationChoiceCard | null => {
|
||||
if (!verifyOnly || !schemaCopy) return null;
|
||||
const parsed = parsePersistedFollowupQuestionId(input.activeFocus?.questionId);
|
||||
const questionId = input.activeFocus?.questionId?.trim() ?? "";
|
||||
const probeId = schema && typeof schema === "object" && typeof (schema as { probe_id?: unknown }).probe_id === "string"
|
||||
? (schema as { probe_id: string }).probe_id
|
||||
: null;
|
||||
return choiceCardFromPersistedVerifyCopy({
|
||||
copy: {
|
||||
...schemaCopy,
|
||||
prompt: overlayChoicePromptFromSpoken(schemaCopy.prompt, input.latestAssistantText),
|
||||
},
|
||||
questionId,
|
||||
methodId: parsed?.method_id
|
||||
?? (intent === "out_of_sample_check" ? "oos_blind" : "reverse_verify"),
|
||||
scoring: parsed?.scoring !== false,
|
||||
probeId,
|
||||
caseRevision: input.caseRevision ?? null,
|
||||
focusId,
|
||||
});
|
||||
};
|
||||
const followup = plan.next_followup ?? plan.deferred_followup ?? null;
|
||||
if (!followup) return persistedVerifyCard();
|
||||
const frame = followup.choice_frame;
|
||||
if (!frame) return persistedVerifyCard();
|
||||
const schemaRow = schema && typeof schema === "object" && !Array.isArray(schema)
|
||||
? schema as Record<string, unknown>
|
||||
: null;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { GlobalBirthProfile } from "./global-birth-payloads.ts";
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
|
||||
const usableActiveStatuses = new Set(["accepted", "confirmed"]);
|
||||
|
||||
export const ACCOUNT_BIRTH_SELECT =
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id" as const;
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa" as const;
|
||||
|
||||
export type AccountBirthRow = Readonly<{
|
||||
name?: unknown;
|
||||
@@ -20,6 +21,7 @@ export type AccountBirthRow = Readonly<{
|
||||
longitude?: unknown;
|
||||
timezone_offset?: unknown;
|
||||
timezone_id?: unknown;
|
||||
ayanamsa?: unknown;
|
||||
}>;
|
||||
|
||||
function asAccountBirthRow(row: unknown): AccountBirthRow {
|
||||
@@ -84,6 +86,7 @@ export function globalBirthProfileFromAccountRow(row: unknown): GlobalBirthProfi
|
||||
longitude: finiteNumber(accountRow.longitude) ?? null,
|
||||
timezoneOffset: finiteNumber(accountRow.timezone_offset) ?? null,
|
||||
timezoneId: text(accountRow.timezone_id),
|
||||
ayanamsa: resolveAyanamsa(accountRow),
|
||||
birthTimeStatus: text(accountRow.birth_time_status),
|
||||
};
|
||||
}
|
||||
@@ -106,6 +109,7 @@ export function globalBirthProfileFromStoredChart(value: unknown): GlobalBirthPr
|
||||
longitude: record.longitude,
|
||||
timezone_offset: record.timezoneOffset ?? record.timezone_offset,
|
||||
timezone_id: record.timezoneId ?? record.timezone_id,
|
||||
ayanamsa: record.ayanamsa,
|
||||
});
|
||||
if (!profile.date || !profile.time) return null;
|
||||
return profile;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { consultationEvidenceCategoryValues, createConsultationPlan, type ConsultationPlan } from "../lib/consultation-plan.ts";
|
||||
import { consultationThemeValues, projectConsultationWorkflowRequest } from "../lib/consultation-workflow-request.ts";
|
||||
import { AYANAMSA_VALUES, DEFAULT_AYANAMSA } from "../lib/ayanamsa.ts";
|
||||
|
||||
export const consultationInputSchema = z.object({
|
||||
year: z.number().int().min(1900).max(2100),
|
||||
@@ -11,6 +12,7 @@ export const consultationInputSchema = z.object({
|
||||
lat: z.number().min(-90).max(90),
|
||||
lon: z.number().min(-180).max(180),
|
||||
tz: z.number().min(-12).max(14),
|
||||
ayanamsa: z.enum(AYANAMSA_VALUES).default(DEFAULT_AYANAMSA),
|
||||
city: z.string().trim().min(1).max(120),
|
||||
question: z.string().trim().min(1).max(500),
|
||||
theme: z.enum(consultationThemeValues),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
begin;
|
||||
|
||||
alter table public.profiles
|
||||
add column if not exists ayanamsa text not null default 'raman';
|
||||
|
||||
alter table public.profiles
|
||||
drop constraint if exists profiles_ayanamsa_check;
|
||||
|
||||
alter table public.profiles
|
||||
add constraint profiles_ayanamsa_check
|
||||
check (ayanamsa in ('raman', 'lahiri', 'kp', 'true_pushya'));
|
||||
|
||||
grant update (ayanamsa) on table public.profiles to authenticated;
|
||||
|
||||
commit;
|
||||
@@ -47,6 +47,7 @@ test("account GET falls back when global birthplace columns are not migrated yet
|
||||
assert.match(getSource, /select\("credits,active_birth_time[^"]*timezone_offset(?:,[^"]*)?"\)/);
|
||||
assert.match(getSource, /birth_place_label: undefined/);
|
||||
assert.match(getSource, /timezone_id: undefined/);
|
||||
assert.match(getSource, /timezone_source,ayanamsa,name,birth_time,rectification_case_id/);
|
||||
});
|
||||
|
||||
test("account API no longer reads or returns retired rectification cases", () => {
|
||||
@@ -92,6 +93,11 @@ test("profile patch schema validates calendar, clock, source requirements, and l
|
||||
timezone_source: "iana_historical",
|
||||
}).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ name: "只改称呼" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "lahiri" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "kp" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "true_pushya" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "raman" }).success, true);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "fagan" }).success, false);
|
||||
assert.equal(accountProfilePatchSchema.safeParse({
|
||||
...valid,
|
||||
reported_birth_time: null,
|
||||
@@ -200,6 +206,10 @@ test("ordinary declaration edits clear stale candidate application but never ove
|
||||
...candidate,
|
||||
birth_time_status: "confirmed",
|
||||
}, edited), {});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
birth_time_status: "confirmed",
|
||||
}, { ayanamsa: "lahiri" }), {});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
active_birth_time: null,
|
||||
@@ -457,6 +467,19 @@ test("reported birth-time status repair is forward-only and applied by the self-
|
||||
assert.match(selfHostedMigrator, /supabaseCompatibilityDirectory,/);
|
||||
});
|
||||
|
||||
test("profile ayanamsa is added by the self-hosted migrator with Raman as the default", () => {
|
||||
const ayanamsaMigration = readFileSync(
|
||||
new URL("../supabase/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(ayanamsaMigration, /add column if not exists ayanamsa text not null default 'raman'/);
|
||||
assert.match(ayanamsaMigration, /'raman', 'lahiri', 'kp', 'true_pushya'/);
|
||||
assert.equal(
|
||||
existsSync(new URL("../db/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url)),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("existing exact family declarations are forward-repaired without claiming confirmation", () => {
|
||||
assert.match(acceptedExactFamilyMigration, /update public\.profiles/);
|
||||
assert.match(acceptedExactFamilyMigration, /active_birth_time = reported_birth_time/);
|
||||
@@ -484,6 +507,13 @@ test("account PATCH uses the shared validator and never writes client birth_time
|
||||
assert.match(patchSource, /"active_birth_time"/);
|
||||
assert.match(source, /latitude,longitude,timezone_offset/);
|
||||
assert.match(source, /birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source/);
|
||||
assert.match(source, /payload\.ayanamsa !== undefined \? \{ ayanamsa: payload\.ayanamsa \}/);
|
||||
assert.match(patchSource, /ayanamsa: z\.enum\(AYANAMSA_VALUES\)\.optional\(\)/);
|
||||
const declarationBlock = patchSource.slice(
|
||||
patchSource.indexOf("const declarationFields"),
|
||||
patchSource.indexOf("const concurrencyFields"),
|
||||
);
|
||||
assert.doesNotMatch(declarationBlock, /ayanamsa/);
|
||||
assert.match(source, /applyAccountProfileConcurrencyGuards/);
|
||||
assert.match(source, /最新确认结果已保留/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
AYANAMSA_VALUES,
|
||||
DEFAULT_AYANAMSA,
|
||||
isAyanamsaName,
|
||||
resolveAyanamsa,
|
||||
} from "../src/lib/ayanamsa.ts";
|
||||
|
||||
test("resolveAyanamsa defaults to Raman and accepts the four product values", () => {
|
||||
assert.equal(DEFAULT_AYANAMSA, "raman");
|
||||
assert.deepEqual([...AYANAMSA_VALUES], ["raman", "lahiri", "kp", "true_pushya"]);
|
||||
assert.equal(resolveAyanamsa(), "raman");
|
||||
assert.equal(resolveAyanamsa(null), "raman");
|
||||
assert.equal(resolveAyanamsa({}), "raman");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "lahiri" }), "lahiri");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "KP" }), "kp");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "krishnamurti" }), "kp");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "true-pushya" }), "true_pushya");
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "fagan" }), "raman");
|
||||
assert.equal(isAyanamsaName("raman"), true);
|
||||
assert.equal(isAyanamsaName("sidereal"), false);
|
||||
});
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
|
||||
const serverChart = {
|
||||
name: "测试",
|
||||
toolInput: { year: 1990, month: 1, day: 2, hour: 3, minute: 4, city: "台北", lat: 25.03, lon: 121.56, tz: 8, declared_accuracy: "15min" as const, time_source: "family_vague" },
|
||||
toolInput: { year: 1990, month: 1, day: 2, hour: 3, minute: 4, city: "台北", lat: 25.03, lon: 121.56, tz: 8, ayanamsa: "raman" as const, declared_accuracy: "15min" as const, time_source: "family_vague" },
|
||||
truth: {
|
||||
birthDate: "1990-01-02", reportedBirthTime: "03:04", activeBirthTime: null,
|
||||
selectedTimeKind: "reported" as const, birthTimeSource: "reported", birthTimeStatus: "reported",
|
||||
|
||||
@@ -58,6 +58,7 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
lat: 36.420487,
|
||||
lon: 114.209936,
|
||||
tz: 8,
|
||||
ayanamsa: "raman",
|
||||
declared_accuracy: "15min",
|
||||
time_source: "family_vague",
|
||||
});
|
||||
@@ -72,6 +73,32 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
});
|
||||
});
|
||||
|
||||
test("route service passes the profile ayanamsa into both natal and declared-window tool input", async () => {
|
||||
const natal = await prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
mode: "unverified_birth_time",
|
||||
loadProfile: async () => ({ ...profile, ayanamsa: "lahiri" }),
|
||||
reserve: async () => "reserved",
|
||||
});
|
||||
assert.equal(natal.serverChart?.toolInput.ayanamsa, "lahiri");
|
||||
|
||||
const windowed = await prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
mode: "general_no_birth_time",
|
||||
loadProfile: async () => ({
|
||||
...profile,
|
||||
ayanamsa: "kp",
|
||||
reported_birth_time: null,
|
||||
active_birth_time: null,
|
||||
birth_time_source: "period_only",
|
||||
birth_time_period: "evening",
|
||||
birth_time_status: "reported",
|
||||
}),
|
||||
reserve: async () => "reserved",
|
||||
});
|
||||
assert.equal(windowed.declaredWindow?.toolInput.ayanamsa, "kp");
|
||||
});
|
||||
|
||||
|
||||
test("pre-reserve plan guard sees resolved server mode and blocks billing on failure", async () => {
|
||||
const order: string[] = [];
|
||||
@@ -158,6 +185,7 @@ test("global normalized places use their exact coordinates and historical offset
|
||||
lat: 25.033,
|
||||
lon: 121.5654,
|
||||
tz: 9,
|
||||
ayanamsa: "raman",
|
||||
declared_accuracy: "15min",
|
||||
time_source: "family_vague",
|
||||
});
|
||||
@@ -316,6 +344,7 @@ test("stale general mode upgrades period-only profiles to a declared birth windo
|
||||
assert.equal(prepared.serverChart, null);
|
||||
assert.equal(prepared.declaredWindow?.toolInput.rangeStart, "18:00");
|
||||
assert.equal(prepared.declaredWindow?.toolInput.rangeEnd, "22:59");
|
||||
assert.equal(prepared.declaredWindow?.toolInput.ayanamsa, "raman");
|
||||
assert.equal("hour" in (prepared.declaredWindow?.toolInput ?? {}), false);
|
||||
assert.equal("minute" in (prepared.declaredWindow?.toolInput ?? {}), false);
|
||||
assert.deepEqual(prepared.generalDailyReference, {
|
||||
@@ -355,6 +384,7 @@ test("consult route constructs workflow input from the route service rather than
|
||||
|
||||
assert.match(route, new RegExp(select));
|
||||
assert.match(route, /prepareConsultationRoute/);
|
||||
assert.match(route, /timezone_id,timezone_source,ayanamsa/);
|
||||
assert.match(route, /\.\.\.prepared\.serverChart\.toolInput/);
|
||||
const toolInput = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
|
||||
assert.doesNotMatch(toolInput.slice(0, toolInput.indexOf("const workflowContext")), /\.\.\.parsed\.data/);
|
||||
|
||||
@@ -48,6 +48,7 @@ test("timing input projects only legal private workflow fields", async () => {
|
||||
lat: 25,
|
||||
lon: 121,
|
||||
tz: 8,
|
||||
ayanamsa: "raman",
|
||||
city: "台北",
|
||||
question: "未来哪些阶段值得把握?",
|
||||
theme: "timing",
|
||||
|
||||
@@ -33,18 +33,23 @@ test("daily, synastry, and legacy rectification use global coordinates instead o
|
||||
});
|
||||
|
||||
const synastry = await synastryBirthPayload(sanFrancisco);
|
||||
assert.deepEqual({ lat: synastry.lat, lon: synastry.lon, tz: synastry.tz }, {
|
||||
assert.deepEqual({ lat: synastry.lat, lon: synastry.lon, tz: synastry.tz, ayanamsa: synastry.ayanamsa }, {
|
||||
lat: 37.7879363,
|
||||
lon: -122.4075201,
|
||||
tz: -8,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
|
||||
const legacy = await payloadFromProfile(sanFrancisco);
|
||||
assert.deepEqual(legacy && { lat: legacy.lat, lon: legacy.lon, tz: legacy.tz }, {
|
||||
assert.deepEqual(legacy && { lat: legacy.lat, lon: legacy.lon, tz: legacy.tz, ayanamsa: legacy.ayanamsa }, {
|
||||
lat: 37.7879363,
|
||||
lon: -122.4075201,
|
||||
tz: -8,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
|
||||
const lahiriDaily = await dailyProfilePayload({ ...sanFrancisco, ayanamsa: "lahiri" }, "2026-07-24");
|
||||
assert.equal(lahiriDaily?.ayanamsa, "lahiri");
|
||||
});
|
||||
|
||||
test("shared timezone resolver supports camelCase browser profiles and preserves the historical reference time", async () => {
|
||||
|
||||
@@ -593,6 +593,8 @@ test("time-selection cards use server adoption state and stay mutually exclusive
|
||||
assert.doesNotMatch(chat, /send\("message", choiceCard\?\.stop_message/);
|
||||
assert.doesNotMatch(chat, /choiceCardUserMessage/);
|
||||
assert.match(caseRoute, /choice_card: choiceCardFromCaseDossier/);
|
||||
assert.match(chat, /choiceCard\s+&& choiceCard\.focus_id === question\.focus_id/);
|
||||
assert.match(chat, /disabled=\{!liveQuestion\}/);
|
||||
assert.match(caseRoute, /current_question: projectCurrentQuestion/);
|
||||
assert.match(caseRoute, /overlayPublicDecision/);
|
||||
assert.match(caseRoute, /const fields = publicDecisionFields\(decision\)/);
|
||||
|
||||
@@ -1524,3 +1524,142 @@ test("GET keeps hiding the card when the persisted split belongs to another prob
|
||||
);
|
||||
assert.equal(card, null);
|
||||
});
|
||||
|
||||
test("GET still renders an open out-of-sample existence card without probe_year", () => {
|
||||
const oosCopy = {
|
||||
prompt: "2020 年前后,有没有开始一段认真关系、分手或结婚?",
|
||||
option_a: DYNAMIC_STYLE_OPTIONS[0].label,
|
||||
option_b: DYNAMIC_STYLE_OPTIONS[1].label,
|
||||
option_c: DYNAMIC_STYLE_OPTIONS[2].label,
|
||||
option_d: DYNAMIC_STYLE_OPTIONS[3].label,
|
||||
options: DYNAMIC_STYLE_OPTIONS.map((option, index) => ({
|
||||
key: (["A", "B", "C", "D"] as const)[index]!,
|
||||
label: option.label,
|
||||
answer_class: option.answer_class,
|
||||
})),
|
||||
};
|
||||
const card = choiceCardFromCaseDossier({
|
||||
evidence: [{
|
||||
id: "ev-education",
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: "2016-01-01",
|
||||
eventKind: "education_start",
|
||||
summary: "2016 年开始上大学",
|
||||
}, {
|
||||
id: "ev-career-entry",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-04-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
summary: "2020年4月开始实习",
|
||||
}, {
|
||||
id: "ev-career-exit",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-10-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_exit",
|
||||
summary: "2020年10月实习离职",
|
||||
}],
|
||||
conversationSummary: {
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
questionId: "oos_blind:relationship_style:score",
|
||||
intent: "out_of_sample_check",
|
||||
targetDomain: "relationship",
|
||||
targetKind: null,
|
||||
expectedAnswerSchema: {
|
||||
choice: oosCopy,
|
||||
},
|
||||
},
|
||||
declinedSkippedTopics: [],
|
||||
},
|
||||
latestResult: {
|
||||
resultId: "result-oos-open",
|
||||
selectionAllowed: false,
|
||||
confirmationAllowed: false,
|
||||
candidates: [
|
||||
{ time: "05:12", rank: 1, relativeSupport: 15, tiedMinuteCount: 1 },
|
||||
{ time: "05:14", rank: 2, relativeSupport: 15, tiedMinuteCount: 1 },
|
||||
],
|
||||
decisionReceipt: {
|
||||
selection_allowed: false,
|
||||
propose_allowed: false,
|
||||
confirmation_allowed: false,
|
||||
precision_stage: { current: "d9_refine", can_stop: true },
|
||||
discriminating_event_probes: [],
|
||||
oos_blind_prompts: [{
|
||||
domain: "relationship",
|
||||
user_meaning: "校时还没用过感情这条线。有没有一件没提过、但记得大概时间的关系变化?",
|
||||
used_for_scoring: false,
|
||||
}],
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "04:45-05:15:05:12,05:14",
|
||||
revision: 1,
|
||||
phase: "event_collection",
|
||||
result_status: "insufficient_evidence",
|
||||
range_start: "04:45",
|
||||
range_end: "05:15",
|
||||
candidates: [
|
||||
{ id: "05:12", time: "05:12", rank: 1, status: "equivalent", prior_score: 15, posterior_score: 15, probability: 0.5, cluster_range: ["05:12", "05:14"], strong_conflict_count: 0 },
|
||||
{ id: "05:14", time: "05:14", rank: 2, status: "equivalent", prior_score: 15, posterior_score: 15, probability: 0.5, cluster_range: ["05:12", "05:14"], strong_conflict_count: 0 },
|
||||
],
|
||||
events: [
|
||||
{ id: "ev-education", year: 2016, usage: "training", domain: "education", precision: "year" },
|
||||
{ id: "ev-career-entry", year: 2020, usage: "training", domain: "career", precision: "month" },
|
||||
{ id: "ev-career-exit", year: 2020, usage: "holdout", domain: "career", precision: "month" },
|
||||
],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 1,
|
||||
representative_time: "05:12",
|
||||
credible_range: ["05:12", "05:14"],
|
||||
},
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null, status: "collecting_evidence" },
|
||||
turns: [{
|
||||
role: "assistant",
|
||||
text: "2020 年 4 月开始实习、同年 10 月离职,两件都记下了。接下来我们继续。",
|
||||
}],
|
||||
});
|
||||
assert.ok(card, "an already-open A–D out-of-sample card must stay tappable");
|
||||
assert.equal(card.focus_id, FOCUS_ID);
|
||||
assert.equal(card.question_id, "oos_blind:relationship_style:score");
|
||||
assert.equal(card.prompt, oosCopy.prompt);
|
||||
assert.equal(card.options.length, 4);
|
||||
assert.equal(card.options[0]?.label, "明确发生且时间吻合");
|
||||
assert.equal(parseRectificationChoiceCard(card)?.focus_id, FOCUS_ID);
|
||||
});
|
||||
|
||||
test("GET does not mint a tap card from an out-of-sample spoken collect focus", () => {
|
||||
const card = projectRectificationChoiceCard({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
questionId: "oos_blind:holdout",
|
||||
intent: "out_of_sample_check",
|
||||
targetDomain: "career",
|
||||
targetKind: null,
|
||||
expectedAnswerSchema: {
|
||||
collect: true,
|
||||
prompt: "上大学之外,你还记得第一份正式工作大概是从哪一年开始干的吗?",
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(card, null);
|
||||
});
|
||||
|
||||
@@ -79,6 +79,7 @@ test("loadV9RectificationProfile uses a server-owned radius around reported time
|
||||
assert.equal(profile.baselineFingerprint.length, 64);
|
||||
assert.equal(profile.baseline.uncertainty_before_minutes, 10);
|
||||
assert.equal(profile.baseline.uncertainty_after_minutes, 10);
|
||||
assert.equal(profile.baseline.ayanamsa, "raman");
|
||||
assert.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
|
||||
assert.ok(!("password" in profile.baseline));
|
||||
});
|
||||
@@ -98,6 +99,18 @@ test("profile fingerprint ignores display labels and prior accepted minutes", as
|
||||
assert.equal(first.baselineFingerprint, presentationOnly.baselineFingerprint);
|
||||
});
|
||||
|
||||
test("ayanamsa is stored on new snapshots but does not change the baseline fingerprint", async () => {
|
||||
const raman = await loadV9RectificationProfile(fakeAccounting({
|
||||
profile: completeProfile,
|
||||
}), "user-1");
|
||||
const lahiri = await loadV9RectificationProfile(fakeAccounting({
|
||||
profile: { ...completeProfile, ayanamsa: "lahiri" },
|
||||
}), "user-1");
|
||||
assert.equal(raman.baseline.ayanamsa, "raman");
|
||||
assert.equal(lahiri.baseline.ayanamsa, "lahiri");
|
||||
assert.equal(raman.baselineFingerprint, lahiri.baselineFingerprint);
|
||||
});
|
||||
|
||||
test("loadV9RectificationProfile normalizes PostgreSQL Date birth dates", async () => {
|
||||
const dateProfile = {
|
||||
...completeProfile,
|
||||
@@ -425,6 +438,7 @@ test("homepage and new opens ignore active time and legacy uncertainty for their
|
||||
active_birth_time: null,
|
||||
uncertainty_before_minutes: 10,
|
||||
uncertainty_after_minutes: 10,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ test("stored other-chart JSON keeps the library camelCase shape", () => {
|
||||
longitude: 116.4,
|
||||
timezoneOffset: 8,
|
||||
timezoneId: "Asia/Shanghai",
|
||||
ayanamsa: "raman",
|
||||
birthTimeStatus: undefined,
|
||||
});
|
||||
});
|
||||
@@ -82,7 +83,7 @@ test("daily and synastry routes share a literal account-birth select string", ()
|
||||
const synastry = readFileSync(new URL("../src/app/api/synastry/route.ts", import.meta.url), "utf8");
|
||||
assert.equal(
|
||||
ACCOUNT_BIRTH_SELECT,
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id",
|
||||
"name,birth_date,reported_birth_time,active_birth_time,birth_time,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,timezone_id,ayanamsa",
|
||||
);
|
||||
assert.match(daily, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
assert.match(synastry, /select\(ACCOUNT_BIRTH_SELECT\)/);
|
||||
|
||||
@@ -33,10 +33,13 @@ test("personal profile is limited to account basics and links to chart settings"
|
||||
|
||||
test("the chart library owns self-profile editing and keeps other-chart save separate", () => {
|
||||
const charts = readFileSync(new URL("../src/components/chart-library-panel.tsx", import.meta.url), "utf8");
|
||||
const personalProfile = between(page, " renderProfile() {", " renderChartLibrary() {");
|
||||
assert.match(charts, /编辑本人资料/);
|
||||
assert.match(charts, /onSubmit=\{saveProfile\}/);
|
||||
assert.match(charts, /onSubmit=\{saveOtherChart\}/);
|
||||
assert.match(charts, /setEditingSelfChart\(true\)/);
|
||||
assert.match(charts, /AyanamsaPreferenceField/);
|
||||
assert.doesNotMatch(personalProfile, /AyanamsaPreferenceField/);
|
||||
});
|
||||
|
||||
test("the general dialog renders the shared theme preference panel", () => {
|
||||
|
||||
@@ -1368,9 +1368,9 @@ test("gated paths cover every image input, package input, and gate-read reposito
|
||||
"TASK-example-20260901.md",
|
||||
"PROGRESS-example-20260901.md",
|
||||
"CHANGELOG.md",
|
||||
"progress.md",
|
||||
"task_plan.md",
|
||||
"findings.md",
|
||||
"docs/history/progress.md",
|
||||
"docs/history/task_plan.md",
|
||||
"docs/history/findings.md",
|
||||
"BLOCKED.md",
|
||||
"CONTEXT.md",
|
||||
"AGENTS.md",
|
||||
|
||||
+5
-5
@@ -51,11 +51,11 @@ jyotish-engine = "jyotish_vedic.engine:cli_main"
|
||||
jyotish-mcp = "jyotish_vedic.mcp_server:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/732642856/yinduzhanxing"
|
||||
Documentation = "https://github.com/732642856/yinduzhanxing#readme"
|
||||
Repository = "https://github.com/732642856/yinduzhanxing"
|
||||
"Bug Reports" = "https://github.com/732642856/yinduzhanxing/issues"
|
||||
Changelog = "https://github.com/732642856/yinduzhanxing/blob/main/CHANGELOG.md"
|
||||
Homepage = "https://git.copse.top/root/Jyotisha"
|
||||
Documentation = "https://git.copse.top/root/Jyotisha/src/branch/main/README.md"
|
||||
Repository = "https://git.copse.top/root/Jyotisha"
|
||||
"Bug Reports" = "https://git.copse.top/root/Jyotisha/issues"
|
||||
Changelog = "https://git.copse.top/root/Jyotisha"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
|
||||
@@ -30,6 +30,7 @@ def build_capability_evidence_pool_summary(registry: dict[str, Any] | None = Non
|
||||
prediction_counts = Counter()
|
||||
primary_entries: list[str] = []
|
||||
audit_only_entries: list[str] = []
|
||||
comparison_only_entries: list[str] = []
|
||||
alias_entries: list[str] = []
|
||||
|
||||
for tech_id, tech in techniques.items():
|
||||
@@ -50,6 +51,8 @@ def build_capability_evidence_pool_summary(registry: dict[str, Any] | None = Non
|
||||
primary_entries.append(tech_id)
|
||||
elif evidence_role == "audit_only":
|
||||
audit_only_entries.append(tech_id)
|
||||
elif evidence_role == "comparison_only":
|
||||
comparison_only_entries.append(tech_id)
|
||||
elif evidence_role == "alias":
|
||||
alias_entries.append(tech_id)
|
||||
|
||||
@@ -67,12 +70,14 @@ def build_capability_evidence_pool_summary(registry: dict[str, Any] | None = Non
|
||||
"prediction_verification_counts": dict(sorted(prediction_counts.items())),
|
||||
"primary_entries": sorted(primary_entries),
|
||||
"audit_only_entries": sorted(audit_only_entries),
|
||||
"comparison_only_entries": sorted(comparison_only_entries),
|
||||
"alias_entries": sorted(alias_entries),
|
||||
"conclusion_policy": {
|
||||
"primary_chain_required": True,
|
||||
"all_89_entries_must_not_be_flattened_into_conclusions": True,
|
||||
"supporting_entries_can_only_raise_or_lower_confidence": True,
|
||||
"audit_only_entries_cannot_affect_astrological_conclusions": True,
|
||||
"comparison_only_entries_cannot_affect_astrological_conclusions": True,
|
||||
"conflicts_must_downgrade_confidence": True,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -161,6 +161,19 @@ def _relative(path: Path) -> str:
|
||||
return str(path.relative_to(ROOT))
|
||||
|
||||
|
||||
def _artifact_ref(path: Path) -> str:
|
||||
try:
|
||||
return path.resolve().relative_to(ROOT.resolve()).as_posix()
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def _relocate_report(path: Path, output_dir: Path | None) -> Path:
|
||||
if output_dir is None:
|
||||
return path
|
||||
return Path(output_dir) / path.name
|
||||
|
||||
|
||||
def _should_skip(path: Path) -> bool:
|
||||
try:
|
||||
rel_parts = path.relative_to(ROOT).parts
|
||||
@@ -1313,9 +1326,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
parser.add_argument("--no-write", action="store_true", help="Print the manifest without writing report artifacts.")
|
||||
parser.add_argument("--summary-only", action="store_true", help="Print only summary fields; still writes full artifacts unless --no-write is set.")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default="",
|
||||
help="Write report artifacts here instead of docs/research. Tests must pass a tmp directory.",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
report = build_manifest(scope=args.scope, write=not args.no_write)
|
||||
report = build_manifest(scope=args.scope, write=False)
|
||||
output_dir = Path(args.output_dir) if args.output_dir else None
|
||||
json_path = _relocate_report(ROOT / report["artifacts"]["json_report"], output_dir)
|
||||
markdown_path = _relocate_report(ROOT / report["artifacts"]["markdown_report"], output_dir)
|
||||
report["artifacts"]["json_report"] = _artifact_ref(json_path)
|
||||
report["artifacts"]["markdown_report"] = _artifact_ref(markdown_path)
|
||||
if not args.no_write:
|
||||
_write_reports(report, json_report=json_path, markdown_report=markdown_path)
|
||||
printable = _summary_view(report) if args.summary_only else report
|
||||
print(json.dumps(printable, ensure_ascii=False, indent=2))
|
||||
return 0 if report["status"] == "pass" else 1
|
||||
|
||||
@@ -17,5 +17,9 @@ ROWS=[
|
||||
def build():
|
||||
return {'scope':'hard_gap_source_hunt','created_at':'2026-07-23','claim_status':'source_hunt_only','truth_matrix_allowed':False,'production_tuning_allowed':False,'rows':ROWS,'summary':{'source_count':len(ROWS),'ready_numeric_oracle_count':0,'partial_numeric_candidate_count':1,'timing_holdout_status':'blocked_until_independent_human_labels'},'next_actions':['If RoxyAPI key/terms are available, capture pinned KP /cusps raw for a public fixed chart and compare against VedicAstro observation only.','For Gulika fragment, locate place/timezone or keep candidate blocked.','For Tajika/Saham, require solar return input plus expected Saham longitude before replay.','For timing/rectification, freeze independent human labels before blind ranking.'],'boundary':'No source found in this pass satisfies complete input + expected numeric values + replay raw/hash. All hard gaps remain blocked or candidate-only.'}
|
||||
def main():
|
||||
data=build(); OUT.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n'); print(json.dumps(data,ensure_ascii=False,indent=2)); return 0
|
||||
import argparse
|
||||
ap=argparse.ArgumentParser(); ap.add_argument('--no-write', action='store_true'); args=ap.parse_args(); data=build()
|
||||
if not args.no_write:
|
||||
OUT.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
|
||||
print(json.dumps(data,ensure_ascii=False,indent=2)); return 0
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
|
||||
@@ -106,7 +106,7 @@ except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
release_heavy_compute_slot,
|
||||
)
|
||||
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, normalize_ayanamsa_name
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, ayanamsa_display_name, normalize_ayanamsa_name
|
||||
from raman_support_observations import build_raman_support_observations
|
||||
|
||||
load_local_env(REPO_ROOT)
|
||||
@@ -807,7 +807,7 @@ _CHARA_DASHA_ROUTES = frozenset({'career', 'timing', 'annual', 'marriage'})
|
||||
def _request_ayanamsa(*sources) -> str:
|
||||
for source in sources:
|
||||
if isinstance(source, dict):
|
||||
raw = source.get('ayanamsa') or source.get('ayanamsa_name')
|
||||
raw = source.get('ayanamsa') or source.get('ayanamsa_name') or source.get('ayanamsa_policy')
|
||||
else:
|
||||
raw = source
|
||||
if raw not in (None, ''):
|
||||
@@ -2850,7 +2850,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
|
||||
'lat': birth_payload.get('lat'),
|
||||
'lon': birth_payload.get('lon'),
|
||||
'tz': birth_payload.get('tz'),
|
||||
'ayanamsa_policy': birth_payload.get('ayanamsa') or 'lahiri',
|
||||
'ayanamsa_policy': _request_ayanamsa(birth_payload),
|
||||
'node_policy': birth_payload.get('node_mode') or birth_payload.get('nodeMode') or 'mean',
|
||||
}, route='overview', reference_date=reference_date, case_id='api_chart')
|
||||
if isinstance(vedastro_evidence, dict):
|
||||
@@ -4014,7 +4014,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
'ayanamsa_policy': body.get('ayanamsa_policy') or body.get('ayanamsa') or 'lahiri',
|
||||
'ayanamsa_policy': _request_ayanamsa(body),
|
||||
'node_policy': body.get('node_policy') or body.get('node_mode') or 'mean',
|
||||
}
|
||||
result = _load_local_module('vedastro_service_adapter').run_range_scan_for_case(
|
||||
@@ -6944,13 +6944,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
except ImportError:
|
||||
fallback = self._fallback_chart(
|
||||
year, month, day, hour, minute, second, lat, lon, tz,
|
||||
skip_vedastro_main_entry_overview=bool(body.get('skip_vedastro_main_entry_overview')),
|
||||
skip_vedastro_main_entry_overview=bool(body.get('skip_vedastro_main_entry_overview')), ayanamsa_name=_request_ayanamsa(body),
|
||||
)
|
||||
return _store_api_chart_response_cache(cache_payload, fallback)
|
||||
|
||||
def _fallback_chart(
|
||||
self, year, month, day, hour, minute, second, lat, lon, tz,
|
||||
*, skip_vedastro_main_entry_overview=False,
|
||||
*, skip_vedastro_main_entry_overview=False, ayanamsa_name=DEFAULT_AYANAMSA_NAME,
|
||||
):
|
||||
"""无Swiss Ephemeris时的简化计算"""
|
||||
import hashlib
|
||||
@@ -6996,7 +6996,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'second': int(second),
|
||||
'tz': f"UTC{'+' if tz >= 0 else ''}{tz}",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'lon': lon, 'ayanamsa_name': ayanamsa_name, 'ayanamsa_display': ayanamsa_display_name(ayanamsa_name),
|
||||
},
|
||||
'ascendant': {'sign': asc_sign, 'sign_idx': asc_sign_idx},
|
||||
'planets': planets, 'houses': houses,
|
||||
@@ -7027,7 +7027,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
'ayanamsa': 'lahiri',
|
||||
'ayanamsa': ayanamsa_name,
|
||||
'node_mode': 'mean',
|
||||
})
|
||||
_attach_guided_topics(result)
|
||||
@@ -7083,7 +7083,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
if planet in {'Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'}
|
||||
and isinstance(pdata, dict)
|
||||
}
|
||||
ayanamsa_display = birth.get('ayanamsa_display') or 'Raman'
|
||||
ayanamsa_display = birth.get('ayanamsa_display') or ayanamsa_display_name(birth.get('ayanamsa_name') or birth.get('ayanamsa'))
|
||||
node_mode = birth.get('node_mode') or 'mean'
|
||||
prompt_lines = [
|
||||
'你是一个审慎的 AI Native 印度/吠陀占星分析助手。',
|
||||
|
||||
@@ -227,6 +227,7 @@ def build_report() -> dict[str, Any]:
|
||||
}
|
||||
return {
|
||||
"scope": "local_jyotish_accuracy_report",
|
||||
"reference_ayanamsa": "lahiri",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"summary": summary,
|
||||
"checks": checks,
|
||||
|
||||
@@ -17,5 +17,9 @@ ROWS=[
|
||||
def build():
|
||||
return {'scope':'numeric_oracle_gap_queue_v3','created_at':'2026-07-23','claim_status':'candidate_queue','truth_matrix_allowed':False,'production_tuning_allowed':False,'rows':ROWS,'summary':{'candidate_count':len(ROWS),'numeric_packet_ready_count':0,'partial_numeric_candidate_count':sum(r['status']=='partial_numeric_candidate' for r in ROWS),'blocked_domains':sorted(set(r['domain'] for r in ROWS))},'promotion_requirements':['complete input','expected numeric values','source URL/page/line or screenshot hash','license/copyright-safe boundary','local replay raw/hash','claim gate remains non-truth until cross-source consistency'],'boundary':'This queue records public candidates only. It does not upgrade KP, Prashna, Tajika, Saham, Gulika or Sphuta truth status.'}
|
||||
def main():
|
||||
data=build(); OUT.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n'); print(json.dumps(data,ensure_ascii=False,indent=2)); return 0
|
||||
import argparse
|
||||
ap=argparse.ArgumentParser(); ap.add_argument('--no-write', action='store_true'); args=ap.parse_args(); data=build()
|
||||
if not args.no_write:
|
||||
OUT.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
|
||||
print(json.dumps(data,ensure_ascii=False,indent=2)); return 0
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
|
||||
@@ -19,7 +19,14 @@ from urllib import request
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_GITHUB_SLUG = "732642856/yinduzhanxing"
|
||||
|
||||
|
||||
def classify_remote_provider(url: str) -> str:
|
||||
if github_slug_from_remote_url(url):
|
||||
return "github"
|
||||
if re.search(r"git\.copse\.top[:/]", url.strip()):
|
||||
return "gitea"
|
||||
return "other"
|
||||
|
||||
|
||||
def run_command(args: list[str], timeout: int) -> dict[str, Any]:
|
||||
@@ -103,21 +110,39 @@ def build_report(timeout: int) -> dict[str, Any]:
|
||||
local_branch = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], timeout)
|
||||
local_head = run_command(["git", "rev-parse", "HEAD"], timeout)
|
||||
urls = git_remote_urls(timeout)
|
||||
slug = next((github_slug_from_remote_url(url) for url in urls if github_slug_from_remote_url(url)), DEFAULT_GITHUB_SLUG)
|
||||
git_checks = [git_ls_remote(url, timeout) for url in urls] or [git_ls_remote(f"https://github.com/{slug}.git", timeout)]
|
||||
api_check = github_api_branches(slug, timeout)
|
||||
github_slug = next((github_slug_from_remote_url(url) for url in urls if github_slug_from_remote_url(url)), None)
|
||||
providers = [classify_remote_provider(url) for url in urls]
|
||||
if "github" in providers:
|
||||
provider = "github"
|
||||
elif "gitea" in providers:
|
||||
provider = "gitea"
|
||||
else:
|
||||
provider = providers[0] if providers else "unknown"
|
||||
git_checks = [git_ls_remote(url, timeout) for url in urls]
|
||||
git_verified = any(check["ok"] and check["ref_count"] > 0 for check in git_checks)
|
||||
api_visible = api_check["ok"]
|
||||
status = "verified" if git_verified else ("web_visible_git_blocked" if api_visible else "blocked")
|
||||
if github_slug:
|
||||
api_check = github_api_branches(github_slug, timeout)
|
||||
api_visible = api_check["ok"]
|
||||
status = "verified" if git_verified else ("web_visible_git_blocked" if api_visible else "blocked")
|
||||
else:
|
||||
api_check = {
|
||||
"method": "github_api_branches",
|
||||
"url": None,
|
||||
"ok": False,
|
||||
"branches": [],
|
||||
"error": "not_github_remote",
|
||||
}
|
||||
status = "verified" if git_verified else "blocked"
|
||||
return {
|
||||
"scope": "remote_repo_visibility_check",
|
||||
"status": status,
|
||||
"provider": provider,
|
||||
"must_not_claim_synced": status != "verified",
|
||||
"local": {
|
||||
"branch": local_branch.get("stdout", "").strip(),
|
||||
"head": local_head.get("stdout", "").strip(),
|
||||
},
|
||||
"github_slug": slug,
|
||||
"github_slug": github_slug,
|
||||
"git_checks": git_checks,
|
||||
"github_api": api_check,
|
||||
}
|
||||
|
||||
@@ -122,8 +122,8 @@ RELEASE_CRITICAL_UNTRACKED_PATHS = [
|
||||
"docs/research/open_source_scan_2026_06_22.md",
|
||||
"docs/research/product_gap_matrix_2026_06_22.md",
|
||||
"docs/research/whole_machine_git_audit_2026_06_23.md",
|
||||
"findings.md",
|
||||
"progress.md",
|
||||
"docs/history/findings.md",
|
||||
"docs/history/progress.md",
|
||||
"references/oracle/dasha_shadbala_oracle_cases.json",
|
||||
"scripts/audit_fragments.py",
|
||||
"scripts/deep_varga_avastha.py",
|
||||
@@ -134,7 +134,7 @@ RELEASE_CRITICAL_UNTRACKED_PATHS = [
|
||||
"scripts/oracle_boundary_audit.py",
|
||||
"scripts/oracle_collection_queue.py",
|
||||
"scripts/oracle_evidence_validator.py",
|
||||
"task_plan.md",
|
||||
"docs/history/task_plan.md",
|
||||
"tests/test_api_server_security.py",
|
||||
"tests/test_dasha_reference_audit.py",
|
||||
"tests/test_deep_varga_avastha.py",
|
||||
|
||||
@@ -12,7 +12,7 @@ except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from diagnose_external_engine_adapters import build_report as external_engine_report
|
||||
|
||||
|
||||
REPO_URL = "https://github.com/732642856/yinduzhanxing"
|
||||
REPO_URL = "https://git.copse.top/root/Jyotisha"
|
||||
|
||||
|
||||
def _external_boundary() -> dict[str, Any]:
|
||||
|
||||
@@ -24,7 +24,7 @@ SKIP_NAMES = {".env", ".env.local"}
|
||||
GENERATED_DOCS = {
|
||||
"INSTALL.md": """# Jyotish Skill Install
|
||||
|
||||
Basic Git version: https://github.com/732642856/yinduzhanxing
|
||||
Basic Git version: https://git.copse.top/root/Jyotisha
|
||||
|
||||
1. Unzip this package into a local folder.
|
||||
2. Run `python3 scripts/public_release_privacy_scan.py`.
|
||||
|
||||
@@ -79,6 +79,20 @@ def _strict_summary(report: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def resolve_acceptance_status(errors: list[str], adapter_report: dict[str, Any]) -> str:
|
||||
"""Hard failures stay fail; a missing reference engine is a partial, not a fail."""
|
||||
if errors:
|
||||
return "fail"
|
||||
engines = adapter_report.get("engines") if isinstance(adapter_report.get("engines"), dict) else {}
|
||||
for details in engines.values():
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
blockers = details.get("readiness_blockers") or []
|
||||
if details.get("missing_dependency") or "missing_dependency" in blockers:
|
||||
return "partial"
|
||||
return "pass"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
pytest_check = _run(
|
||||
[
|
||||
@@ -162,9 +176,10 @@ def main() -> int:
|
||||
if adapter_report.get("status") not in usable_adapter_statuses:
|
||||
errors.append("external_adapter_status_unusable")
|
||||
|
||||
status = resolve_acceptance_status(errors, adapter_report)
|
||||
result = {
|
||||
"scope": "user_invocation_acceptance_check",
|
||||
"status": "pass" if not errors else "fail",
|
||||
"status": status,
|
||||
"errors": errors,
|
||||
"checks": {
|
||||
"user_invocation_tests": pytest_check["returncode"] == 0,
|
||||
@@ -177,7 +192,7 @@ def main() -> int:
|
||||
"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
|
||||
return 0 if status != "fail" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -64,7 +64,7 @@ cd ~/Projects/yinduzhanxing-local
|
||||
```bash
|
||||
# 克隆GitHub仓库
|
||||
cd ~/Projects
|
||||
git clone https://github.com/732642856/yinduzhanxing.git
|
||||
git clone https://git.copse.top/root/Jyotisha.git
|
||||
cd yinduzhanxing
|
||||
|
||||
# 复制新模块
|
||||
|
||||
@@ -26,6 +26,7 @@ INDASTRO_CASES = ROOT / INDASTRO_CASES_RELATIVE
|
||||
|
||||
DEFAULT_MIN_PASS_RATE = 0.98
|
||||
DEFAULT_DEGREE_TOLERANCE = 1.0
|
||||
REFERENCE_AYANAMSA = "lahiri"
|
||||
CONTROVERSIAL_HINTS = [
|
||||
"内部矛盾",
|
||||
"需进一步验证",
|
||||
@@ -60,6 +61,8 @@ def run_engine(case: dict[str, Any], python: str) -> dict[str, Any]:
|
||||
str(case["lon"]),
|
||||
"--tz",
|
||||
str(case["tz"]),
|
||||
"--ayanamsa",
|
||||
REFERENCE_AYANAMSA,
|
||||
]
|
||||
completed = subprocess.run(
|
||||
cmd,
|
||||
@@ -178,6 +181,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
|
||||
|
||||
return {
|
||||
"valid": pass_rate >= args.min_pass_rate and not failures,
|
||||
"reference_ayanamsa": REFERENCE_AYANAMSA,
|
||||
"scope": "public real-person chart revalidation; not event_prediction_accuracy",
|
||||
"min_pass_rate": args.min_pass_rate,
|
||||
"pass_rate": round(pass_rate, 4),
|
||||
|
||||
@@ -25,24 +25,32 @@ def test_registry_is_backend_evidence_pool_not_flat_user_skill_list() -> None:
|
||||
"composite_adjudicator",
|
||||
"workflow_or_engineering",
|
||||
"alias_entry",
|
||||
"experimental_variant",
|
||||
}
|
||||
allowed_roles = {"primary", "secondary", "context", "audit_only", "alias"}
|
||||
allowed_roles = {"primary", "secondary", "context", "audit_only", "alias", "comparison_only"}
|
||||
allowed_visibility = {"ordinary_topic_router", "expert_audit", "hidden"}
|
||||
allowed_prediction = {
|
||||
"case_validated_partial",
|
||||
"support_only",
|
||||
"not_claimed",
|
||||
"not_applicable",
|
||||
"blocked",
|
||||
}
|
||||
allowed_rule = {"verified", "partial", "not_applicable", "blocked"}
|
||||
allowed_calculation = {"verified", "partial", "not_applicable", "experimental"}
|
||||
|
||||
for tech_id, tech in techniques.items():
|
||||
assert tech["entry_type"] in allowed_entry_types, tech_id
|
||||
assert tech["evidence_role"] in allowed_roles, tech_id
|
||||
assert tech["user_visibility"] in allowed_visibility, tech_id
|
||||
assert tech["verification_level"]["calculation"] in {"verified", "partial", "not_applicable"}, tech_id
|
||||
assert tech["verification_level"]["rule"] in {"verified", "partial", "not_applicable"}, tech_id
|
||||
assert tech["verification_level"]["calculation"] in allowed_calculation, tech_id
|
||||
assert tech["verification_level"]["rule"] in allowed_rule, tech_id
|
||||
assert tech["verification_level"]["prediction"] in allowed_prediction, tech_id
|
||||
assert tech["conclusion_policy"], tech_id
|
||||
if tech["entry_type"] == "experimental_variant":
|
||||
assert tech["verification_level"]["prediction"] == "blocked", tech_id
|
||||
assert tech["verification_level"]["rule"] == "blocked", tech_id
|
||||
assert tech["user_visibility"] == "expert_audit", tech_id
|
||||
|
||||
assert techniques["case_validator"]["evidence_role"] == "audit_only"
|
||||
assert techniques["thematic_report_orchestrator"]["entry_type"] == "workflow_or_engineering"
|
||||
@@ -59,6 +67,8 @@ def test_evidence_pool_summary_routes_few_primary_items_and_many_support_items()
|
||||
assert summary["evidence_role_counts"]["primary"] >= 8
|
||||
assert summary["evidence_role_counts"]["secondary"] > summary["evidence_role_counts"]["primary"]
|
||||
assert summary["evidence_role_counts"]["audit_only"] >= 3
|
||||
assert summary["comparison_only_entries"] == ["rangacharya_jaimini_variant"]
|
||||
assert summary["conclusion_policy"]["comparison_only_entries_cannot_affect_astrological_conclusions"] is True
|
||||
assert summary["prediction_verification_counts"]["not_claimed"] > 0
|
||||
assert summary["conclusion_policy"]["primary_chain_required"] is True
|
||||
assert summary["conclusion_policy"]["all_89_entries_must_not_be_flattened_into_conclusions"] is True
|
||||
|
||||
@@ -25,6 +25,11 @@ def _run_manifest(*args: str) -> dict:
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def _saved(report: dict, key: str) -> Path:
|
||||
raw = Path(report["artifacts"][key])
|
||||
return raw if raw.is_absolute() else ROOT / raw
|
||||
|
||||
|
||||
def test_manifest_indexes_project_source_layers_without_heavy_ocr() -> None:
|
||||
report = _run_manifest("--scope", "project", "--no-write")
|
||||
|
||||
@@ -64,13 +69,14 @@ def test_manifest_indexes_project_source_layers_without_heavy_ocr() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_manifest_writes_json_and_markdown_reports() -> None:
|
||||
report = _run_manifest("--scope", "project")
|
||||
def test_manifest_writes_json_and_markdown_reports(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "project", "--output-dir", str(tmp_path))
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
|
||||
saved = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
assert saved["summary"] == report["summary"]
|
||||
@@ -136,22 +142,23 @@ def test_external_manifest_indexes_high_relevance_machine_fragments_without_copy
|
||||
assert "text_preview" not in item
|
||||
|
||||
|
||||
def test_external_manifest_writes_separate_reports() -> None:
|
||||
report = _run_manifest("--scope", "external", "--summary-only")
|
||||
def test_external_manifest_writes_separate_reports(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "external", "--summary-only", "--output-dir", str(tmp_path))
|
||||
|
||||
assert "by_path" not in report
|
||||
assert report["artifacts"]["json_report"] == "docs/research/character_level_external_manifest_latest.json"
|
||||
assert report["artifacts"]["markdown_report"] == "docs/research/character_level_external_manifest_latest.md"
|
||||
assert Path(report["artifacts"]["json_report"]).name == "character_level_external_manifest_latest.json"
|
||||
assert Path(report["artifacts"]["markdown_report"]).name == "character_level_external_manifest_latest.md"
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
assert "External High-Relevance Inventory Manifest" in md_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_extraction_queue_report_combines_project_and_external_binary_work() -> None:
|
||||
report = _run_manifest("--scope", "extraction-queue")
|
||||
def test_extraction_queue_report_combines_project_and_external_binary_work(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "extraction-queue", "--output-dir", str(tmp_path))
|
||||
|
||||
assert report["scope"] == "extraction-queue"
|
||||
assert report["status"] == "pass"
|
||||
@@ -171,15 +178,16 @@ def test_extraction_queue_report_combines_project_and_external_binary_work() ->
|
||||
}
|
||||
assert "text_preview" not in item
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
assert "Extraction Queue Manifest" in md_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_extraction_results_extract_pdf_and_docx_without_storing_text() -> None:
|
||||
report = _run_manifest("--scope", "extraction-results")
|
||||
def test_extraction_results_extract_pdf_and_docx_without_storing_text(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "extraction-results", "--output-dir", str(tmp_path))
|
||||
|
||||
assert report["scope"] == "extraction-results"
|
||||
assert report["status"] == "pass"
|
||||
@@ -218,15 +226,16 @@ def test_extraction_results_extract_pdf_and_docx_without_storing_text() -> None:
|
||||
assert item["extraction_result"] == "ocr_blocked_missing_engine"
|
||||
assert item["ocr_engine_available"] is False
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
assert "Extraction Results Manifest" in md_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_extraction_review_grades_extracted_sources_without_promoting_runtime_truth() -> None:
|
||||
report = _run_manifest("--scope", "extraction-review")
|
||||
def test_extraction_review_grades_extracted_sources_without_promoting_runtime_truth(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "extraction-review", "--output-dir", str(tmp_path))
|
||||
|
||||
assert report["scope"] == "extraction-review"
|
||||
assert report["status"] == "pass"
|
||||
@@ -250,15 +259,16 @@ def test_extraction_review_grades_extracted_sources_without_promoting_runtime_tr
|
||||
assert "text" not in item
|
||||
assert "text_preview" not in item
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
assert "Extraction Review Manifest" in md_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_source_grading_batch1_classifies_promote_review_candidates_without_runtime_promotion() -> None:
|
||||
report = _run_manifest("--scope", "source-grading-batch1")
|
||||
def test_source_grading_batch1_classifies_promote_review_candidates_without_runtime_promotion(tmp_path: Path) -> None:
|
||||
report = _run_manifest("--scope", "source-grading-batch1", "--output-dir", str(tmp_path))
|
||||
|
||||
assert report["scope"] == "source-grading-batch1"
|
||||
assert report["status"] == "pass"
|
||||
@@ -299,8 +309,9 @@ def test_source_grading_batch1_classifies_promote_review_candidates_without_runt
|
||||
assert "text" not in item
|
||||
assert "text_preview" not in item
|
||||
|
||||
json_path = ROOT / report["artifacts"]["json_report"]
|
||||
md_path = ROOT / report["artifacts"]["markdown_report"]
|
||||
json_path = _saved(report, "json_report")
|
||||
md_path = _saved(report, "markdown_report")
|
||||
assert json_path.exists()
|
||||
assert md_path.exists()
|
||||
assert json_path.parent == tmp_path
|
||||
assert "Source Grading Batch 1 Manifest" in md_path.read_text(encoding="utf-8")
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
PACKET=ROOT/'references/oracle/hard_gap_source_hunt_2026_07_23.json'
|
||||
def test_hard_gap_source_hunt_does_not_upgrade_oracles():
|
||||
data=json.loads(subprocess.check_output(['python3','scripts/hard_gap_source_hunt_2026_07_23.py'],cwd=ROOT,text=True))
|
||||
data=json.loads(subprocess.check_output(['python3','scripts/hard_gap_source_hunt_2026_07_23.py','--no-write'],cwd=ROOT,text=True))
|
||||
assert data['claim_status']=='source_hunt_only'
|
||||
assert data['truth_matrix_allowed'] is False
|
||||
assert data['summary']['ready_numeric_oracle_count']==0
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
PACKET=ROOT/'references/oracle/numeric_oracle_gap_queue_v3_2026_07_23.json'
|
||||
def test_numeric_oracle_gap_queue_v3_blocks_truth_upgrade():
|
||||
data=json.loads(subprocess.check_output(['python3','scripts/numeric_oracle_gap_queue_v3.py'],cwd=ROOT,text=True))
|
||||
data=json.loads(subprocess.check_output(['python3','scripts/numeric_oracle_gap_queue_v3.py','--no-write'],cwd=ROOT,text=True))
|
||||
assert data['claim_status']=='candidate_queue'
|
||||
assert data['truth_matrix_allowed'] is False
|
||||
assert data['summary']['numeric_packet_ready_count']==0
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.remote_repo_visibility_check import github_slug_from_remote_url, parse_ls_remote
|
||||
import json
|
||||
|
||||
from scripts.remote_repo_visibility_check import (
|
||||
build_report,
|
||||
github_slug_from_remote_url,
|
||||
parse_ls_remote,
|
||||
)
|
||||
|
||||
|
||||
def test_github_slug_from_remote_url_accepts_ssh_and_https() -> None:
|
||||
@@ -23,3 +29,40 @@ def test_parse_ls_remote_separates_heads_and_tags() -> None:
|
||||
assert parsed["heads"]["codex/release-hygiene-ci"] == "def456"
|
||||
assert parsed["tags"]["v1.0.0"] == "aaa111"
|
||||
assert parsed["ref_count"] == 3
|
||||
|
||||
|
||||
def test_gitea_remote_reports_provider_and_does_not_fall_back_to_legacy_slug(monkeypatch) -> None:
|
||||
from scripts import remote_repo_visibility_check as mod
|
||||
|
||||
def fake_run_command(args: list[str], timeout: int) -> dict:
|
||||
joined = " ".join(args)
|
||||
if args[:3] == ["git", "rev-parse", "--abbrev-ref"]:
|
||||
return {"ok": True, "returncode": 0, "stdout": "staging\n", "stderr": "", "error": ""}
|
||||
if args[:2] == ["git", "rev-parse"]:
|
||||
return {"ok": True, "returncode": 0, "stdout": "abc123\n", "stderr": "", "error": ""}
|
||||
if args[:3] == ["git", "remote", "get-url"]:
|
||||
return {
|
||||
"ok": True,
|
||||
"returncode": 0,
|
||||
"stdout": "https://git.copse.top/root/Jyotisha.git\n",
|
||||
"stderr": "",
|
||||
"error": "",
|
||||
}
|
||||
if args[:2] == ["git", "ls-remote"]:
|
||||
return {
|
||||
"ok": True,
|
||||
"returncode": 0,
|
||||
"stdout": "abc123 refs/heads/main\ndef456 refs/heads/staging\n",
|
||||
"stderr": "",
|
||||
"error": "",
|
||||
}
|
||||
raise AssertionError(f"unexpected command: {joined}")
|
||||
|
||||
monkeypatch.setattr(mod, "run_command", fake_run_command)
|
||||
report = build_report(8)
|
||||
dumped = json.dumps(report)
|
||||
assert report["provider"] == "gitea"
|
||||
assert report["github_slug"] is None
|
||||
assert report["status"] == "verified"
|
||||
assert "732642856/yinduzhanxing" not in dumped
|
||||
assert report["github_api"]["error"] == "not_github_remote"
|
||||
|
||||
@@ -68,5 +68,6 @@ def test_premium_skill_zip_runs_from_clean_directory(tmp_path: Path) -> None:
|
||||
)
|
||||
assert user_acceptance.returncode == 0, user_acceptance.stderr or user_acceptance.stdout
|
||||
report = json.loads(user_acceptance.stdout)
|
||||
assert report["status"] == "pass"
|
||||
# 原值=status 必须 pass;新值=pass 或 partial;原因=打包目录里缺 PyJHora 时应降级而不是 fail
|
||||
assert report["status"] in {"pass", "partial"}
|
||||
assert report["checks"]["guided_topics_entrypoint"] is True
|
||||
|
||||
@@ -13,7 +13,8 @@ def test_skill_release_manifest_defines_basic_and_premium_boundaries() -> None:
|
||||
assert set(editions) == {"basic_git", "premium_cloud_drive"}
|
||||
assert editions["basic_git"]["distribution"] == "public_git_repository"
|
||||
assert editions["premium_cloud_drive"]["distribution"] == "cloud_drive_zip"
|
||||
assert "https://github.com/732642856/yinduzhanxing" in editions["basic_git"]["source"]
|
||||
# 原值=上游研究仓 GitHub 地址;新值=本产品 Gitea 仓;原因=发行清单 source 必须指向产品仓
|
||||
assert "https://git.copse.top/root/Jyotisha" in editions["basic_git"]["source"]
|
||||
assert any("scripts/user_invocation_acceptance_check.py" in command for command in report["acceptance_commands"])
|
||||
assert any("scripts/public_release_privacy_scan.py" in command for command in report["acceptance_commands"])
|
||||
assert report["privacy_boundary"]["private_birth_data_allowed"] is False
|
||||
|
||||
@@ -56,7 +56,8 @@ def test_skill_release_package_can_write_zip(tmp_path) -> None:
|
||||
guided = archive.read("GUIDED_ENTRYPOINT.md").decode("utf-8")
|
||||
acceptance = json.loads(archive.read("PACKAGE_ACCEPTANCE.json").decode("utf-8"))
|
||||
assert "python3 scripts/user_invocation_acceptance_check.py" in install
|
||||
assert "https://github.com/732642856/yinduzhanxing" in install
|
||||
# 原值=上游研究仓 GitHub 地址;新值=本产品 Gitea 仓;原因=安装说明必须指向可克隆的产品仓
|
||||
assert "https://git.copse.top/root/Jyotisha" in install
|
||||
assert "guided_topics" in prompts
|
||||
assert "official_blocked" in prompts
|
||||
assert "western_oracle_payload" in prompts
|
||||
|
||||
@@ -85,6 +85,7 @@ def test_user_entrypoint_can_start_from_guided_topics_prompt() -> None:
|
||||
|
||||
|
||||
def test_fixture_dasha_timeline_rejects_workbuddy_regression_claims() -> None:
|
||||
# 原值=依赖引擎默认岁差;新值=显式 lahiri;原因=夹具日期是 Lahiri 口径。
|
||||
base = [
|
||||
sys.executable,
|
||||
"scripts/jyotish_engine.py",
|
||||
@@ -107,6 +108,8 @@ def test_fixture_dasha_timeline_rejects_workbuddy_regression_claims() -> None:
|
||||
"0",
|
||||
"--years",
|
||||
"45",
|
||||
"--ayanamsa",
|
||||
"lahiri",
|
||||
]
|
||||
|
||||
observed = {}
|
||||
@@ -145,8 +148,23 @@ def test_one_command_user_invocation_acceptance_check() -> None:
|
||||
|
||||
assert completed.returncode == 0, completed.stderr or completed.stdout
|
||||
report = json.loads(completed.stdout)
|
||||
assert report["status"] == "pass"
|
||||
# 原值=status 必须 pass;新值=pass 或 partial;原因=PyJHora 缺席是外部参照降级,不应把验收检查打成 fail
|
||||
assert report["status"] in {"pass", "partial"}
|
||||
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", "complete"}
|
||||
|
||||
|
||||
def test_missing_reference_engine_is_partial_not_fail() -> None:
|
||||
from scripts.user_invocation_acceptance_check import resolve_acceptance_status
|
||||
|
||||
assert resolve_acceptance_status([], {"engines": {"PyJHora/JHora": {"status": "available"}}}) == "pass"
|
||||
assert (
|
||||
resolve_acceptance_status(
|
||||
[],
|
||||
{"engines": {"PyJHora/JHora": {"missing_dependency": "pyjhora"}}},
|
||||
)
|
||||
== "partial"
|
||||
)
|
||||
assert resolve_acceptance_status(["user_invocation_pytest_failed"], {"engines": {}}) == "fail"
|
||||
|
||||
Reference in New Issue
Block a user