Compare commits

..
@@ -0,0 +1,131 @@
# TASK:拦截检查要查分盘宫位的起算点(#183,BUG-1826,2026-11-11)
- Gitea:#183
- 分支:`codex/d9-reference-guard-20261111`(基于 staging `1505645287c59d53bc0c608c2c10b2da32d61f6a`,已含 #182)
- 前置:#182(格局状态词)已于 2026-11-11 合入 staging(产品负责人选 A:采用 Claude 原型 `855cc71c`,见 #182 评论)。本单**在同一个拦截检查上扩展**:`frontend/src/lib/consultation-answer-fact-guard.ts` 里 #182 加的格局状态检查保持原样,本单只在旁边新增一个检查。
- Bug 编号:BUG-1826(1809–1830 已为这一批预留;开工时核对 `docs/BUG_HISTORY.md` 末尾)
- 负责验收:Claude(jyotisha-2a)
- 依据:占星师 2026-11-09 第七轮回复 §5 P0.1
不得自行写“产品决定/裁定”;任务书没写的取舍一律选需拍板。
不得修改或删除既有断言来让测试通过;确需修改必须写原值/新值/原因并在 PROGRESS 列出。
## 现象(她已确认的一例)
婚恋题深度回答第 38 行:「D9 第 7 宫为巨蟹,宫内无星,宫主月亮在 D9 金牛入旺(从 D9 上升数第 11 宫)。」
- 卡上:`base.natal.d9.lagna = Capricorn`,`base.natal.d9.planets.Moon.d9_sign = Taurus`。
- 从 D9 上升(摩羯)数,金牛是第 **5** 宫;第 11 宫是从巨蟹(D9 第 7 宫)数的。句子写明了“从 D9 上升数”,所以是错的。
- 现有检查没拦住:句中的「从 … 数」命中 `BREAKER`,整句被跳过;而且现在没有任何“分盘宫位”检查。
她的要求原文:修复必须绑定 `chart_layer + entity + reference + relation + value`,不是只检查星座文本是否出现。
## 现状(staging `15056452` 实测,已含 #182)
- `frontend/src/lib/consultation-answer-fact-guard.ts`
- `factGuardCardView`:`view.vargas[layer] = { lagna, planets: {行星: 星座}, path }`;D9 来自 `base.natal.d9`,其他分盘来自 `domain_sections.<领域>.vargas`;各领域给出的同一分盘不一致时,这个分盘整个不进视图。
- `layerSpans` / `spanAt`:句子里显式写出的层(D9 / 婚姻分盘 / D10 …)管到哪里。
- `BREAKER`(约 :80):含 `起算|起第|起数|从…起/数|月亮起|月亮盘…`,命中后各检查跳过该句。
- 已有检查:分盘行星星座(`varga_sign`)、分盘上升(`varga_lagna`)、分盘宫主(`varga_house_lord`)、D1 的星座/宫位/上升/宫主、Karakamsha、Arudha;**没有分盘宫位**。
- `push` 会按“同实体、文本重叠”合并,不符优先。
- #182 已加(**只读,不改**):`FactGuardKind` 里的 `yoga_status`、`CardView.yogas` 与 `yogaCardEntries`、`normalizeYogaName`、`yogaNamePattern`、`splitProse`(约 :357)、`yogaSentences`(约 :375)、`yogaWordClass`、`checkYogaStatus`(约 :430,在 `checkKarakamsha` 开头调用)、重写提示里的「格局状态」一条。
- `checkLine`(约 :469)末尾依次调用 `checkKarakamsha`、`checkArudha`。
## 要做的
只**新增**一个检查,不改已有检查的任何正则、`BREAKER`、`layerSpans` 和判定(包括 #182 的格局状态检查)。新检查的入口单独写(例如 `checkVargaHouse(line, offset, view, push)`),在 `checkLine` 末尾 `checkArudha(...)` 之后调用。
### 1. 新的事实种类 `varga_house`
加进 `FactGuardKind`。一条记录绑定五样:
| 字段 | 含义 | 本例 |
|---|---|---|
| `layer` | 分盘(D2…D60,不含 D1) | `D9` |
| `entity` | 行星 | `Moon` |
| `reference` | 起算点,写成 `from D9 lagna (Capricorn)`、`from sign Cancer` 或 `from D9 Venus (Aquarius)` | `from D9 lagna (Capricorn)` |
| 关系 | 整宫制,含起点计数:`((实体星座序号 − 起点星座序号 + 12) % 12) + 1` | — |
| `claimed` / `card` | 句中的宫数 / 按卡算出的宫数 | `11 宫` / `5 宫(从摩羯数;第 11 宫是从巨蟹数)` |
`fact_id` 写 `<layer>:<entity>@<星座>/from:<起点星座>`,`source_path` 写实体星座的卡上路径(D9 是 `base.natal.d9.planets.<行星>.d9_sign`,其他分盘是 `<varga.path>.planets.<行星>`)。
### 2. 只查“写全了”的句子(高精度子集)
一句(正文按 `。!?;` 与换行断句,括号内不断;表格行先按 `|` 分格,格内同样只按 `。!?;` 断句,不跨格、不跨句沿用行星名或起算点——与 #182 验收采用的分句口径一致;可以调用 `splitProse(line, base, /[。!?;\n]/u)`,但不得改它的默认参数或实现)只有同时满足下面四条才检查,否则不检查(也不记 `not_on_card`):
1. **层**:句子在某个显式分盘的范围里(`spanAt` 给出的层不是 `D1`),且视图里有这一层。
2. **实体**:同一句里,宫数之前最近的一个行星名,且这颗行星在这一层的星座在句中写出、并与卡一致(不一致的已由 `varga_sign` 报告,这里不重复检查);或句中没写星座、但卡上这一层有这颗行星。
3. **起算点**:句中明确写出,且属于下面三种之一:
- 这一层的上升:`从 D9 上升数`、`从 D9 上升起`、`D9 上升起算`、`从 D9 Lagna 数` 以及去掉“D9”、但在 D9 范围内的同样写法(`从上升数`);视图里这一层要有 `lagna`;
- 一个星座:`从巨蟹数`、`从巨蟹座起`、`以巨蟹为第 1 宫`;
- 这一层里的一颗行星:`从金星数`、`从 D9 金星起`;视图里这一层要有这颗行星。
- 其他起算点(Karakamsha、AL / UL、月亮盘、D1 的宫、行运、年盘等)一律不检查。
4. **值**:`第 N 宫`(`N` 为 1–12),紧跟在起算点写法之后,或与起算点在同一对括号里。
`BREAKER` 对本检查不起作用(本检查自己识别起算点);但句中若还有 `行运|过境|Transit|流年|年盘|Varsha|Tajika|KP|宫头|Chalit` 这类词,仍不检查。
### 3. 判定与输出
- 按第 1 节的关系算出宫数,等于句中值 → `push(…, "match")`;不等 → 不符。
- 不符时 `card` 文字:先写按句中起算点算出的宫数;若句中的数恰好等于从“这一层上升”或“这一层某个宫的星座”数出来的值,补一句是从哪里数的(本例:`5 宫(从摩羯数;第 11 宫是从巨蟹数)`)。只补一处,找不到就不补。
- `consultationFactRepairPrompt` 的“分清层与实体”那一条后面(即 #182 加的“格局状态”那一条之前)加一条(逐字照写):`- 分盘宫位:从哪里起算就按哪里数,写明起算点;从上升数和从某一宫数是两件事,不能互相照抄。` 其余提示不改。
## 测试
新建 `frontend/tests/consult-varga-house-reference-1826.test.ts`,用**本文件内新建的合成卡**(不写真人资料、不改 `tests/fixtures/fact-guard-minimal-card.ts`):
1. **她的最小反例**:D9 `lagna: "Capricorn"`、`Moon.d9_sign: "Taurus"`;答案用原句「D9 第 7 宫为巨蟹,宫内无星,宫主月亮在 D9 金牛入旺(从 D9 上升数第 11 宫)。」→ 恰好 1 条 `varga_house` 不符:`layer: "D9"`、`entity: "Moon"`、`reference` 含 `lagna`、`claimed: "11 宫"`、`card` 以 `5 宫` 开头并含 `巨蟹`。
2. **同值不同体系(负例)**:同一张卡,
- 「宫主月亮在 D9 金牛(从巨蟹数第 11 宫)。」→ 0 条不符(起算点是巨蟹,11 是对的),1 条 `match`;
- 「宫主月亮在 D9 金牛(从 D9 上升数第 5 宫)。」→ 0 条不符;
- D1 句「本命月亮在第 11 宫」,卡上 D1 月亮在 11 宫 → 本检查不产生任何 `varga_house` 记录;
- 没写起算点的「月亮在 D9 第 11 宫」→ 本检查不检查(0 条 `varga_house`)。
3. **其他起算点**:从 D9 金星数(卡上 D9 金星在水瓶,月亮金牛 → 第 4 宫)写成第 4 宫相符、写成第 5 宫不符;从 Karakamsha / 月亮盘 / AL 起算 → 不检查。
4. **其他分盘**:D10 一例(上升、某行星、写错宫数)不符;该分盘在两个领域段里不一致(视图里没有)→ 不检查。
5. **排除词**:句中有「行运」「年盘」「KP」→ 不检查。
6. **已有检查不变**(不改其中任何断言):`frontend/tests/consult-answer-fact-guard-1789.test.ts` 与 #182 的 `consult-yoga-status-wording-1825.test.ts` 全部照样通过;在新测试里再加一条:她的原句中「D9 金牛」这一处的 `varga_sign` 记录(相符)仍然存在。
7. **重写**:第 1 条的单元,替换句仍写「从 D9 上升数第 11 宫」→ `factRepairRejection` 返回 `still_mismatched`;改成「从 D9 上升数第 5 宫」→ `null`。
## 允许改的文件(只有这些)
- 新建:`frontend/tests/consult-varga-house-reference-1826.test.ts`、`docs/tasks/PROGRESS-d9-reference-guard-20261111.md`
- 修改:`frontend/src/lib/consultation-answer-fact-guard.ts`(只加第 1–3 节的内容和 repair 提示一行)、`docs/BUG_HISTORY.md`(只在末尾追加 BUG-1826,不得用 sed 改整个文件)
## 不要碰
- `references/**`(占星师的方法文件),包括 `references/product_runtime_contract.md`:本单不改合同文字。
- `frontend/src/lib/consult-preset-prompts.ts`、`frontend/src/lib/consultation-methodology.ts`、`frontend/src/mastra/index.ts`、`frontend/src/mastra/product-voice.ts`。
- `frontend/src/lib/yoga-status-words.ts`,以及 #182 加进 fact guard 的格局状态检查:`yoga_status`、`CardView.yogas`、`yogaCardEntries`、`normalizeYogaName`、`yogaNamePattern`、`splitProse`、`yogaSentences`、`yogaWordClass`、`checkYogaStatus`、`checkKarakamsha` 开头那次调用、重写提示里的「格局状态」一条。
- 既有测试文件:`frontend/tests/consult-answer-fact-guard-1789.test.ts`、`consult-fact-guard-stream-1789.test.ts`、`consult-yoga-status-wording-1825.test.ts` 一行不改。
- `frontend/src/lib/stream-agent-response.ts`、`frontend/src/lib/consultation-evidence-card.ts`、`frontend/tests/fixtures/fact-guard-minimal-card.ts`。
- 冻结文件:`scripts/jyotish_api_server.py`、`scripts/jyotish_engine.py`、`scripts/domain_calculation_service.py`、`scripts/dasha_analyzer.py`、`scripts/narayana_dasha.py`、`scripts/shadbala.py`、`scripts/ashtakavarga.py`、`scripts/jaimini.py`、`scripts/varga.py`、`scripts/functional_benefics.py`、`scripts/divisional_charts_extended.py`;本单不改任何 `scripts/**`。
- 进行中单子的地盘:#177 / #178(`scripts/ondemand_calc/**` 与注册表)、#172(`frontend/src/mastra/consultation-tools.ts`、`frontend/src/app/api/consult/route.ts`);已验收的 #184 / #185 / #186 的文件本单同样不碰。
## 验收标准
1. 她的原句被拦下,记录五样齐全(层、实体、起算点、关系对应的宫数、句中值);`card` 指出第 11 宫是从巨蟹数的。
2. 同值不同体系的负例全部不报(第 2 条),没写起算点的不检查。
3. 已有检查的结果不变:`consult-answer-fact-guard-1789` 与 `consult-yoga-status-wording-1825` 全过,不改任何既有断言。
4. 只出现允许清单里的改动。
5. **精度**:Claude 验收时用第七轮验收包 10 份回答与第八轮盲测回答跑一遍,人工核对每一条 `varga_house`(不符与相符都看),报告精度;PROGRESS 里请列出测试覆盖的句式和“已知未覆盖”的句式。
## 自测命令(Windows / PowerShell)
- `frontend\node_modules\.bin\tsc --noEmit -p frontend`
- 在 `frontend` 下运行 `npm run lint`
- 在 `frontend` 下运行 `node node_modules/tsx/dist/cli.mjs --test tests/consult-varga-house-reference-1826.test.ts tests/consult-answer-fact-guard-1789.test.ts tests/consult-fact-guard-stream-1789.test.ts tests/consult-yoga-status-wording-1825.test.ts`
- 再在 `frontend` 下跑一次 `npm test`,把失败的测试名写进 PROGRESS。
前端全量测试、Python 快速门和 eslint 由 Claude 在验收时再跑。
## 进度记录
`docs/tasks/PROGRESS-d9-reference-guard-20261111.md`
## 何时停下来选“需拍板”
- 要改 `BREAKER`、`layerSpans`、#182 的格局状态检查或任何已有检查才能做到时。
- 要支持第 2 节第 3 条以外的起算点时。
- 句中宫数的写法与第 2 节第 4 条不同、又想纳入检查时。
- 任务书没写到的任何文案或取舍。