Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e3fda184 | ||
|
|
e5e087daf7 | ||
|
|
1a24e4f184 | ||
|
|
016d267711 | ||
|
|
0b6e9d7c57 | ||
|
|
bafe6b6b70 | ||
|
|
4c9e25ce58 | ||
|
|
fafabf8e48 | ||
|
|
3fe65bfc23 |
+14
-1
@@ -24776,4 +24776,17 @@
|
||||
- 修复:注册 `dasha_date`,直接读取原生日期。Narayana 未决时保留具名分支及其独立日期,主轨只包含已确定的段。旧版条件分支不参与主轨状态。
|
||||
- 验证与防复发:`tests/test_ondemand_dasha_date_20261111.py` 覆盖原生日期逐字段对照、出生余额、日期边界、第二轮、0 年段、分支与未穷尽、输入口径及错误信封。实测见 `docs/tasks/PROGRESS-ondemand-dasha-date-20261111.md`。
|
||||
- 关联记录:BUG-1556、BUG-1811、BUG-1819、BUG-1829、BUG-1830、#177。
|
||||
- 修复版本:`codex/ondemand-dasha-date-20261111`。
|
||||
- 验收(2026-11-11,Claude):审查 0b701a53,范围只有新工具文件、注册表两行、新测试和文档,没有碰禁改文件。Linux 上 Python 快速门全部通过(Windows 报告的 3 条 `test_upstream_technique_layers` 失败在 staging 和本单都没有复现)。tsc 0,eslint 0 error;前端全量 3631 条,失败 27 条,逐条与 staging 15056452 的基线相同。
|
||||
- 修复版本:`codex/ondemand-dasha-date-20261111` @ 0b701a53,经验收分支 `codex/accept-batch6-1111` 合入 staging。
|
||||
|
||||
## BUG-1826 | 回答拦截漏查分盘宫位的起算点
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)
|
||||
- 首次发现 / 最近更新:2026-11-09 / 2026-11-11
|
||||
- 现象与触发条件:回答把从分盘另一星座数出的宫数写为从该分盘上升数,证据卡虽有行星与上升的星座,拦截未拒收该句。
|
||||
- 根因:原规则只检查分盘星座、上升和宫主。显式起算措辞也会阻止通用位置规则将其当作本命宫位处理。
|
||||
- 修复:新增 `varga_house`,在同句同格绑定层、实体、起算点、整宫关系与句中值,给出现有重写流程可用的卡上值。
|
||||
- 验证与防复发:`frontend/tests/consult-varga-house-reference-1826.test.ts` 覆盖原反例、不同起算点、分盘冲突、表格和重写。实测及边界见 `docs/tasks/PROGRESS-d9-reference-guard-20261111.md`。
|
||||
- 关联记录:BUG-1789、BUG-1825、#183。
|
||||
- 修复版本:`codex/d9-reference-guard-20261111`。
|
||||
- 验收退回第 1 次(2026-11-11,评论 40947):回放发现宫数后出现行星和「第 N 宫的宫主」两类误拦。返工优先绑定紧跟宫数的行星,排除宫主描述,向前找实体时在分号、破折号和冒号处停止。新增合成回归覆盖这两类问题,待独立回放复验。
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# FIX:分盘宫位检查的实体绑定误拦(#183,BUG-1826,第 1 次退回,2026-11-11)
|
||||
|
||||
- 验收提交:`4c9e25ce585510fac579fe5845afff65f2ed4892`(分支 `codex/d9-reference-guard-20261111`)
|
||||
- 原任务书:`docs/tasks/TASK-d9-reference-guard-20261111.md`(其余要求不变)
|
||||
|
||||
不得自行写“产品决定/裁定”。
|
||||
不得修改或删除既有断言来让测试通过。
|
||||
|
||||
## 验收结果
|
||||
|
||||
不调用模型,确定性回放既有回答:第七轮验收包 10 份 + phase2-chat 30 组生成目录,共 1844 份回答。
|
||||
`varga_house` 共 105 条:4 条不符、101 条相符。4 条不符里 2 条是真错(她的原句,两次出现),另外 2 条把**正确的句子**判成不符。
|
||||
标准是正确的 D9 句子 0 误拦,所以本次退回。
|
||||
|
||||
### 误拦 1:实体写在宫数后面
|
||||
|
||||
(公开名人盘,wealth)D9 上升天蝎,水星 D9 射手,金星 D9 双鱼。
|
||||
|
||||
> (……11 宫主金星同时是功能凶星;D9 从上升数第 2 宫的水星入敌;……)
|
||||
|
||||
这句说的是水星在第 2 宫,从天蝎数到射手正是第 2 宫,所以句子是对的。
|
||||
现在的实现取“宫数之前最近的行星”,于是取到了前一分句里的金星,判成不符(金星应为第 5 宫)。
|
||||
|
||||
### 误拦 2:「第 N 宫的宫主」是宫主,不是落宫
|
||||
|
||||
(公开名人盘,marriage)D9 上升巨蟹,土星 D9 白羊。
|
||||
|
||||
> D9 里第 7 宫为摩羯,主星土星落在白羊……只说明……:从 D9 上升数第 7 宫的宫主本身需要「取消落陷」才站得住。
|
||||
|
||||
这句说的是第 7 宫的宫主(土星),不是说土星落在第 7 宫,所以句子是对的。
|
||||
现在的实现把土星绑成“落第 7 宫”,判成不符(土星应为第 10 宫)。
|
||||
|
||||
## 要改的
|
||||
|
||||
只改 `checkVargaHouse`。不改任务书禁止的部分(`BREAKER`、`layerSpans`、#182 的检查、已有检查)。
|
||||
|
||||
1. **宫数后面紧跟宫主写法时不检查。** 宫数后面紧跟(中间只允许空白和「的」)`宫主`、`主星`、`之主`、`主` 时,这一句里的这个宫数不产生 `varga_house` 记录,也不记 `not_on_card`。
|
||||
2. **宫数后面紧跟行星时,实体取后面这颗。** 宫数后面紧跟(中间只允许空白、`的`、`里的`、`内的`、`中的`)一个行星名时,实体就是这颗行星,不再往前找。
|
||||
- 这颗行星在这一层的星座照第 2 节第 2 条核对:句中写了星座就必须与卡一致,没写就用卡上的值。
|
||||
- 前后两边都没有可用行星时,照旧不检查。
|
||||
3. **实体不跨分号、破折号、冒号。** 从宫数往前找实体时,不得越过 `;`、`;`、`——`、`:`、`:`。找不到就不检查。
|
||||
- 括号不算分隔:「太阳水瓶(从 D6 上升数第 12 宫)」照旧检查。
|
||||
- 若这条与已有测试冲突,就停下来写进 PROGRESS,选需拍板,不要改旧断言。
|
||||
|
||||
## 必须加的测试(写进 `frontend/tests/consult-varga-house-reference-1826.test.ts`,只新增)
|
||||
|
||||
用文件里已有的合成卡思路,另建一张卡:D9 上升天蝎、水星射手、金星双鱼、土星白羊(上升巨蟹的卡另建)。
|
||||
|
||||
1. 「11 宫主金星同时是功能凶星;D9 从上升数第 2 宫的水星入敌。」→ 0 条不符,1 条 `match`,`entity: "Mercury"`。
|
||||
2. 同句写成「D9 从上升数第 5 宫的水星入敌。」→ 1 条不符,`entity: "Mercury"`。
|
||||
3. D9 上升巨蟹、土星白羊:「D9 里第 7 宫为摩羯,主星土星落在白羊……从 D9 上升数第 7 宫的宫主本身需要取消落陷。」→ 0 条 `varga_house`。
|
||||
另加「从 D9 上升数第 7 宫之主」「从 D9 上升数第 7 宫主星」两种写法,同样为 0 条。
|
||||
4. 「金星在 D9 双鱼;从上升数第 2 宫。」→ 0 条(分号隔开)。
|
||||
5. 原有 9 个测试原样通过。她的原句仍然恰好 1 条不符。
|
||||
|
||||
## 验收时会再做的
|
||||
|
||||
用同一套回放(`/workspace/eval-blind-1107/phase2-chat/d9guard/varga_house_replay.mts`,1844 份回答)复跑:
|
||||
- 正确句子 0 误拦;
|
||||
- 她的原句仍被拦下;
|
||||
- 相符条数不得因为改了绑定而变成误拦。
|
||||
|
||||
## 进度记录
|
||||
|
||||
追加到 `docs/tasks/PROGRESS-d9-reference-guard-20261111.md`:
|
||||
- 改了什么;
|
||||
- 新测试名;
|
||||
- 命令和通过数。
|
||||
|
||||
`docs/BUG_HISTORY.md` 只在 BUG-1826 条目内追加一行“验收退回第 1 次”,不得用 sed 改整个文件。
|
||||
@@ -0,0 +1,210 @@
|
||||
# PROGRESS · 分盘宫位起算点检查(#183,BUG-1826,2026-11-11)
|
||||
|
||||
- 基线为 staging `1505645287c59d53bc0c608c2c10b2da32d61f6a`。
|
||||
- 工单分支为 `codex/d9-reference-guard-20261111`。
|
||||
- 实施依据见 `docs/tasks/TASK-d9-reference-guard-20261111.md`。
|
||||
|
||||
## 实现
|
||||
|
||||
在既有回答拦截末尾新增 `varga_house` 检查。它先在一句或一格内绑定显式分盘、宫数、
|
||||
起算点和该宫数前最近的实体,再用卡片的星座从指定起算点按整宫计数。修正提示给出正确
|
||||
宫数,卡上有该盘上升时也给出原宫数对应的起算星座。
|
||||
|
||||
基线原句里的「月亮在 D9 金牛」因分盘名插在行星和星座之间,旧星座检查没有识别。
|
||||
新检查在核实这种内联星座与卡一致后,同时记录已确认的 `varga_sign`,保留原句要求的
|
||||
星座证据。原有星座规则、格局状态规则、`BREAKER`、`layerSpans`、去重和重写判定保留。
|
||||
当宫数在同一括号的起算点前面,新宫位事实的定位落在起算点,使原星座事实与宫位事实
|
||||
各有自己的文本范围。
|
||||
|
||||
## 覆盖范围
|
||||
|
||||
| 句式 | 验证 |
|
||||
| --- | --- |
|
||||
| D9 原反例 | 恰好一条宫位不符,绑定 D9、Moon、D9 上升、11 宫与卡上 5 宫。提示巨蟹为原值对应的起算星座 |
|
||||
| 起算点变更 | D9 上升、上升起算、Lagna、巨蟹数/起/为第 1 宫、D9 金星数/起 |
|
||||
| 值的位置 | 紧接起算点,或位于同一括号的起算点前面 |
|
||||
| 其他分盘 | D10 例检查实体星座来源。卡片跨领域给出冲突 D10 时跳过 |
|
||||
| 分句 | 正文按 。!?; 和换行断句,表格按格处理,格内同样保留逗号 |
|
||||
| 修复 | 原值再次出现返回 `still_mismatched`,改为正确宫数后通过 |
|
||||
|
||||
本次覆盖范围限于阿拉伯数字 1–12、任务书列明的三类起算点及已有显式分盘范围。中文
|
||||
数字、未写起算点、分开的括号、跨句跨格的实体或起算点,以及 Karakamsha、AL/UL、
|
||||
月亮盘、D1 参照、行运、年盘和 KP 等其他体系不进入该检查。星座有歧义或与卡不符、卡
|
||||
缺层或缺值,以及无法唯一配对的宫数与参照也跳过。
|
||||
|
||||
真实回答抽查与精度统计由独立验收侧完成。
|
||||
|
||||
## 验证
|
||||
|
||||
- 四份定向套件共 39 项通过,0 失败、0 取消。原 `consult-answer-fact-guard-1789`、
|
||||
流式重写与 `consult-yoga-status-wording-1825` 的断言均原样保留。
|
||||
- TypeScript `--noEmit` 通过。ESLint 0 error、62 warnings。隐私检查 62 项通过。
|
||||
- 两份生产构建退出码均为 0,首页 `/` 均为 Static。以首页 HTML 直接引用的 36 份
|
||||
JS 资源按路径去重后分别 gzip,staging 和本单均为 515086 字节,变动 0%。基线构建
|
||||
另有 Windows 文件跟踪复制警告。
|
||||
- staging 基线 `150564528` 的 Gitea 全量前端门禁 3631 通过、0 失败、0 取消。
|
||||
PR 代码提交 `fafabf8e4` 的 Gitea 全量前端测试 3640 通过、0 失败、0 取消。
|
||||
- Windows 本机 `npm test -- --test-concurrency=4` 退出码 1,3231 通过、76 失败、0
|
||||
取消。失败包含 Windows 路径和 Shell 兼容、Docker 夹具、启动子进程失败及断言。
|
||||
将 32 个文件级失败单独以并发 1 重跑,365 项全部通过。管理员兑换码的单条失败
|
||||
定向重跑也通过。
|
||||
- 同机 staging 基线全量退出码 1,3599 通过、32 失败、0 取消。以两边 TAP 的顶层
|
||||
测试名逐项对照,将本单 32 个文件级失败的占位行替换为重跑名录后,staging 的
|
||||
3611 个测试名全部保留,新增 9 个。本机失败名有 30 个在两边重合,含两条回答
|
||||
时钟断言。其余 13 个本单首轮失败未在基线复现,下文标出。同机基线另有
|
||||
`synastry reports reuse a pair, drop with the person, and settle the synastry.full reservation`
|
||||
和 `database roles have no cluster privileges` 两项本单通过的失败。两组 Gitea
|
||||
全量前端测试均通过,Windows 首轮失败保留在此供验收核查。
|
||||
|
||||
### Windows 本机首次全量失败名
|
||||
|
||||
以下为 TAP 失败行去重后的测试名或测试文件。
|
||||
|
||||
- `admin code functions reject immutable codes, revoked redemption, and roll back on audit failure`
|
||||
- `operation-level admin email reauthentication is removed from routes and UI`
|
||||
- `a lookup does not reset the answer clock: the same 70 s signal bounds the whole answer phase`
|
||||
- `the answer clock cutting the answer step mid-sentence is truncated, recorded and not charged`
|
||||
- `dated adoption 23:59 persists date/spec atomically and rejects malformed candidates`(基线通过)
|
||||
- `staging backups are encrypted, atomic, private, and retain the newest three`
|
||||
- `rejects destructive backup directory aliases and symlink components before mutation`
|
||||
- `rejects unsafe writable backup parents before creating the target`
|
||||
- `rejects a direct canonical sticky shared backup directory before chmod`
|
||||
- `creates every absent backup path component privately despite a permissive caller umask`
|
||||
- `same-second backups publish once without overwriting the completed archive`
|
||||
- `find enumeration failures preserve existing backups and do not report completion`
|
||||
- `refuses full disks and removes a failed-pipeline partial file`
|
||||
- `actual subject deletion releases queued and running reservations once but never refunds delivered reports`(基线通过)
|
||||
- `database env validator accepts punctuated literal required secrets`
|
||||
- `database env validator enforces an explicit staging owner uid without printing values`
|
||||
- `database env validator accepts a private valid file without printing values`
|
||||
- `migration runner is serialized, atomic, drift-safe, and read-only in check mode`(基线通过)
|
||||
- `foundation grants no direct runtime table DML and exposes only reviewed functions`(基线通过)
|
||||
- `personal report job migration is atomic, lease-bound, recoverable, and owner read-only`
|
||||
- `personal report sections enforce owner-read RLS and service-owned durable transitions`
|
||||
- `rectification session titles follow the result and do not bump updated_at`(基线通过)
|
||||
- `a delivered report found by recovery is never refunded`(基线通过)
|
||||
- `self-hosted identity migration creates Better Auth tables with least privilege`(基线通过)
|
||||
- `session list query drops retired rectification rows and empty consultations`(基线通过)
|
||||
- `retire archive migration clears archived_at without bumping updated_at`(基线通过)
|
||||
- `createSynastryFullReport reserves synastry.full and enqueues a job on the real client`
|
||||
- `synastry chat binds an owned section, rejects another report, and stays readable after delete`(基线通过)
|
||||
- `staging model provider env preparation removes legacy settings and keeps one stable key`
|
||||
- `staging env validator rejects selector drift, duplicates, and unsafe permissions`
|
||||
- `production env validators accept only self-hosted production selectors and role URLs`
|
||||
- `Better Auth supports shared user OTP/password sessions for admins`(基线通过)
|
||||
- `BUG-1526: outside self-hosted auth the profile read does not ask for avatar columns`(基线通过)
|
||||
- `tests/notify-toasts-20260930.test.ts`
|
||||
- `tests/people-archive-order.test.ts`
|
||||
- `tests/people-archive-regressions.test.ts`
|
||||
- `tests/people-archive-view.test.tsx`
|
||||
- `tests/people-pagination-lifecycle.test.tsx`
|
||||
- `tests/personal-report-agent-telemetry.test.ts`
|
||||
- `tests/personal-report-api.test.ts`
|
||||
- `tests/personal-report-contract.test.ts`
|
||||
- `tests/personal-report-density-retry.test.ts`
|
||||
- `tests/personal-report-entitlement.test.ts`
|
||||
- `tests/personal-report-entry.test.ts`
|
||||
- `tests/personal-report-export.test.ts`
|
||||
- `tests/personal-report-generation-v2.test.ts`
|
||||
- `tests/personal-report-generation.test.ts`
|
||||
- `tests/personal-report-job-service.test.ts`
|
||||
- `tests/personal-report-job-state.test.ts`
|
||||
- `tests/personal-report-longform-appendix.test.ts`
|
||||
- `tests/personal-report-longform-md.test.ts`
|
||||
- `tests/personal-report-markdown-view.test.ts`
|
||||
- `tests/personal-report-markdown-view.test.tsx`
|
||||
- `tests/personal-report-migration.test.ts`
|
||||
- `tests/personal-report-plan.test.ts`
|
||||
- `tests/personal-report-progress.test.ts`
|
||||
- `tests/personal-report-theme-contract.test.ts`
|
||||
- `tests/personal-report-view.test.ts`
|
||||
- `tests/personal-report-worker-sleep-listener.test.ts`
|
||||
- `tests/postgres-fixture-contract.test.ts`
|
||||
- `tests/pricing-simulation.test.ts`
|
||||
- `tests/product-domain-registry.test.ts`
|
||||
- `tests/profile-gender-20260927.test.ts`
|
||||
- `tests/profile-gender-ui-20260927.test.tsx`
|
||||
- `immutable Skill migration enforces RPC-only identity, legacy adoption, and cascade-safe receipts`
|
||||
- `importing the registry creates no runtime directory`
|
||||
- `a used runtime root is removed when the process exits`
|
||||
- `changed staging workflows are syntactically valid YAML`
|
||||
- `live staging sync preserves env, state, incoming files, and encrypted backups`
|
||||
- `live staging sync repairs nested deploy-tree drift without preserving foreign ownership`
|
||||
- `first immutable deployment rolls back to validated local image IDs`
|
||||
- `deployment scripts pass shell syntax validation`
|
||||
- `is-docs-only-range.sh decides from local history and refuses non-ancestor ranges`
|
||||
- `bounded npm install distinguishes forced timeout, OOM, and unknown failure and cleans its container`
|
||||
- `session and report lists keep legacy self rows and hide another person`(基线通过)
|
||||
- `tests/synastry-report-chat.test.ts`
|
||||
|
||||
## 第 1 次验收退回
|
||||
|
||||
验收针对 `4c9e25ce585510fac579fe5845afff65f2ed4892`。独立验收对 1844 份既有回答
|
||||
回放,得到 105 条宫位记录。4 条不符中有 2 条误拦。返工按评论 40947 与
|
||||
`docs/tasks/FIX-d9-reference-guard-1.md` 执行,保留同一 PR,并合入最新 staging
|
||||
`016d2677111170bdf6b75fe1211752ab42752f3b` 至工单分支。合并冲突仅在 Bug 历史,
|
||||
保留 staging 历史及本单 BUG-1826 条目。
|
||||
|
||||
- 紧跟宫数的行星优先成为实体。它若明确写了星座,仍须与证据卡一致。
|
||||
- 「第 N 宫的宫主」「第 N 宫之主」「第 N 宫主星」等措辞跳过宫位检查。
|
||||
- 向前绑定实体时以分号、破折号和冒号为界,括号内也生效。括号仍可将实体与其宫数连接。
|
||||
|
||||
新增用例名
|
||||
|
||||
- `a planet immediately after the house number is the entity for correct and incorrect counts`
|
||||
- `a sign written after the house number and planet must agree with the evidence`
|
||||
- `house-lord wording does not claim a planet placement`
|
||||
- `backward entity binding stops at semicolons, dashes and colons, including inside parentheses`
|
||||
|
||||
在未修复的代码上四条新用例全部失败,原 9 条仍通过。修复后通过
|
||||
`node node_modules/tsx/dist/cli.mjs --test --test-concurrency=2 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`
|
||||
共 43 项,0 失败、0 取消。包括原误写仍恰好产生一条不符的断言,既有断言未改。
|
||||
|
||||
- Gitea [门禁 7152](https://git.copse.top/root/Jyotisha/actions/runs/7152) 对返工代码
|
||||
`e5e087daf` 通过。前端全量 3644 通过、0 失败、0 取消,Python 验证任务通过。
|
||||
- 本机 `npm test -- --test-concurrency=4` 退出码 1,3614 通过、30 失败、0 取消。当前
|
||||
staging 与初次执行时的前端 Git tree 完全相同。对照同机 staging TAP 名录,3611 个
|
||||
既有顶层测试名全部保留,本分支新增 13 个,其中本次返工新增 4 个。30 个失败里 29 个
|
||||
与基线重合。数据库身份隔离用例在共享夹具目录报 `EPERM`,改用独立临时目录单独重跑
|
||||
通过。Windows 全量启动后补了一个冒号歧义断言,最终代码已由四个定向套件和 Gitea
|
||||
全量覆盖。
|
||||
- TypeScript `--noEmit` 和生产构建通过,首页 `/` 保持 Static。首页直接引用的 36 份 JS
|
||||
合计 515086 字节 gzip,与 staging 基线相同。ESLint 0 error、62 warnings,隐私检查
|
||||
62 项通过。
|
||||
- 1844 份回答的原回放材料位于独立验收环境,同一套回放由验收侧复跑。
|
||||
|
||||
<details>
|
||||
<summary>本机首次全量的 30 个失败名</summary>
|
||||
|
||||
- `operation-level admin email reauthentication is removed from routes and UI`
|
||||
- `a lookup does not reset the answer clock: the same 70 s signal bounds the whole answer phase`
|
||||
- `the answer clock cutting the answer step mid-sentence is truncated, recorded and not charged`
|
||||
- `service and restricted admin database identities stay separated`
|
||||
- `staging backups are encrypted, atomic, private, and retain the newest three`
|
||||
- `rejects destructive backup directory aliases and symlink components before mutation`
|
||||
- `rejects unsafe writable backup parents before creating the target`
|
||||
- `rejects a direct canonical sticky shared backup directory before chmod`
|
||||
- `creates every absent backup path component privately despite a permissive caller umask`
|
||||
- `same-second backups publish once without overwriting the completed archive`
|
||||
- `find enumeration failures preserve existing backups and do not report completion`
|
||||
- `refuses full disks and removes a failed-pipeline partial file`
|
||||
- `database env validator accepts punctuated literal required secrets`
|
||||
- `database env validator enforces an explicit staging owner uid without printing values`
|
||||
- `database env validator accepts a private valid file without printing values`
|
||||
- `personal report job migration is atomic, lease-bound, recoverable, and owner read-only`
|
||||
- `personal report sections enforce owner-read RLS and service-owned durable transitions`
|
||||
- `staging model provider env preparation removes legacy settings and keeps one stable key`
|
||||
- `staging env validator rejects selector drift, duplicates, and unsafe permissions`
|
||||
- `production env validators accept only self-hosted production selectors and role URLs`
|
||||
- `immutable Skill migration enforces RPC-only identity, legacy adoption, and cascade-safe receipts`
|
||||
- `importing the registry creates no runtime directory`
|
||||
- `a used runtime root is removed when the process exits`
|
||||
- `changed staging workflows are syntactically valid YAML`
|
||||
- `live staging sync preserves env, state, incoming files, and encrypted backups`
|
||||
- `live staging sync repairs nested deploy-tree drift without preserving foreign ownership`
|
||||
- `first immutable deployment rolls back to validated local image IDs`
|
||||
- `deployment scripts pass shell syntax validation`
|
||||
- `is-docs-only-range.sh decides from local history and refuses non-ancestor ranges`
|
||||
- `bounded npm install distinguishes forced timeout, OOM, and unknown failure and cleans its container`
|
||||
|
||||
</details>
|
||||
@@ -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 条不同、又想纳入检查时。
|
||||
- 任务书没写到的任何文案或取舍。
|
||||
@@ -80,7 +80,7 @@ const TIME_CONTEXT = /\d{4}\s*年|\d{4}-\d{1,2}|\d{1,2}\s*月|现在|目前|眼
|
||||
const BREAKER = /行运|过境|Gochara|[Tt]ransit|流年|年盘|Varsha|Tajika|Muntha|年主|KP|Placidus|宫头|起算|起第|起数|为第\s*1\s*宫|作为第?一宫|从\s*[^\s,。;]{0,10}\s*(起|数)|月亮起|月亮盘|Chandra|Sudar[sś]han|Chalit|D1\s*投射|投到/gu;
|
||||
|
||||
export type FactGuardKind = "varga_sign" | "varga_lagna" | "varga_house_lord" | "d1_sign" | "d1_house" | "d1_lagna" | "d1_house_lord"
|
||||
| "karakamsha_sign" | "karakamsha_house" | "karakamsha_lord" | "karakamsha_lord_house" | "arudha_sign" | "arudha_lord" | "arudha_house" | "arudha_lord_house" | "yoga_status";
|
||||
| "karakamsha_sign" | "karakamsha_house" | "karakamsha_lord" | "karakamsha_lord_house" | "arudha_sign" | "arudha_lord" | "arudha_house" | "arudha_lord_house" | "yoga_status" | "varga_house";
|
||||
|
||||
export type AnswerFactClaim = AnswerFactMismatch & { status: "match" | "mismatch" | "not_on_card" };
|
||||
type Emit = (item: AnswerFactMismatch, status?: AnswerFactClaim["status"]) => void;
|
||||
@@ -588,6 +588,149 @@ function checkLine(line: string, offset: number, view: CardView, push: Emit) {
|
||||
|
||||
checkKarakamsha(line, offset, view, push, brokenBetween);
|
||||
checkArudha(line, offset, view, push, brokenBetween);
|
||||
checkVargaHouse(line, offset, spans, view, push);
|
||||
}
|
||||
|
||||
const VARGA_REFERENCE = new RegExp([
|
||||
`从\\s*(?:(?<fromLayer>D\\d{1,2})\\s*)?(?:(?<lagna>上升|Lagna)|(?<sign>${S})座?|(?<planet>${P}))\\s*(?:起算|起数|数|起)`,
|
||||
"(?<![\\p{L}\\p{N}])(?:(?<lagnaLayer>D\\d{1,2})\\s*)?(?<lagnaOnly>上升|Lagna)\\s*起算",
|
||||
`以\\s*(?<firstSign>${S})座?\\s*为\\s*第\\s*1\\s*宫`,
|
||||
].join("|"), "gu");
|
||||
const VARGA_HOUSE_NUMBER = /第\s*(1[0-2]|[1-9])\s*宫/gu;
|
||||
const VARGA_HOUSE_EXCLUDED = /行运|过境|Gochara|Transit|流年|年盘|Varsha|Tajika|Muntha|年主|KP|Placidus|宫头|Chalit|月亮盘|Chandra|Sudar[sś]han/iu;
|
||||
const VARGA_HOUSE_BREAKS = /[。!?;\n]/u;
|
||||
const VARGA_LAYER = /^D(?:[2-9]|[1-5]\d|60)$/u;
|
||||
const VARGA_PLACEMENT_PREFIX = new RegExp(`^\\s*[((]?\\s*${PLACED}`, "u");
|
||||
const VARGA_PLACEMENT_SIGN = new RegExp(`^\\s*(${S})座?`, "u");
|
||||
const VARGA_SIGN_MENTION = new RegExp(S, "u");
|
||||
const VARGA_PLANET_MENTION = new RegExp(P, "gu");
|
||||
const VARGA_AMBIGUOUS_SIGN = /^\s*(?:或|或者|\/|~|~|到|至|宫?\s*的|对应|所对应|对宫|方向)/u;
|
||||
|
||||
function enclosingParenthesis(text: string, at: number): number | undefined {
|
||||
const open: number[] = [];
|
||||
for (let i = 0; i < at; i += 1) {
|
||||
if (text[i] === "(" || text[i] === "(") open.push(i);
|
||||
else if (text[i] === ")" || text[i] === ")") open.pop();
|
||||
}
|
||||
return open.at(-1);
|
||||
}
|
||||
|
||||
function wholeSignHouse(sign: Sign, reference: Sign): number {
|
||||
return (FACT_GUARD_SIGNS.indexOf(sign) - FACT_GUARD_SIGNS.indexOf(reference) + 12) % 12 + 1;
|
||||
}
|
||||
|
||||
/** BUG-1826: only an explicit varga, entity, counting point and house form a claim. */
|
||||
function checkVargaHouse(line: string, offset: number, spans: readonly Span[], view: CardView, push: Emit) {
|
||||
const entityBoundary = /[;;::]|——/u;
|
||||
const cells = line.trimStart().startsWith("|") ? line.split("|") : [line];
|
||||
let cellOffset = 0;
|
||||
for (const cell of cells) {
|
||||
const sentences = splitProse(cell, cellOffset, VARGA_HOUSE_BREAKS);
|
||||
cellOffset += cell.length + 1;
|
||||
for (const scope of sentences) {
|
||||
const sentence = line.slice(scope.start, scope.end);
|
||||
if (VARGA_HOUSE_EXCLUDED.test(sentence)) continue;
|
||||
const references = [...sentence.matchAll(VARGA_REFERENCE)].filter((hit) =>
|
||||
!hit.groups?.lagnaOnly || !/(?:从|以)[^,,。!?;(())]*$/u.test(sentence.slice(0, hit.index)),
|
||||
);
|
||||
if (!references.length) continue;
|
||||
const absolute = offset + scope.start;
|
||||
const planets = [...sentence.matchAll(VARGA_PLANET_MENTION)].filter((planet) =>
|
||||
!references.some((hit) => hit.index <= planet.index && planet.index < hit.index + hit[0].length),
|
||||
);
|
||||
const houses = [...sentence.matchAll(VARGA_HOUSE_NUMBER)].filter((house) =>
|
||||
!references.some((hit) => hit.index <= house.index && house.index < hit.index + hit[0].length),
|
||||
);
|
||||
const bindings = references.map((reference) => {
|
||||
const end = reference.index + reference[0].length;
|
||||
const adjacent = houses.find((house) => end <= house.index && /^[\s,,::]*$/u.test(sentence.slice(end, house.index)));
|
||||
if (adjacent) return { reference, house: adjacent };
|
||||
const bracket = enclosingParenthesis(sentence, reference.index);
|
||||
const nearby = bracket === undefined ? [] : houses.filter((house) => enclosingParenthesis(sentence, house.index) === bracket);
|
||||
return { reference, house: nearby.length === 1 ? nearby[0] : undefined };
|
||||
});
|
||||
for (const house of houses) {
|
||||
const houseEnd = house.index + house[0].length;
|
||||
if (/^\s*(?:的\s*)?(?:宫主|主星|之主|主)/u.test(sentence.slice(houseEnd))) continue;
|
||||
const compatible = bindings.filter((binding) => binding.house === house);
|
||||
if (compatible.length !== 1) continue;
|
||||
const reference = compatible[0]!.reference;
|
||||
const groups = reference.groups!;
|
||||
const namedLayer = groups.fromLayer ?? groups.lagnaLayer;
|
||||
const referenceLayerAt = namedLayer ? reference.index + reference[0].indexOf(namedLayer) : undefined;
|
||||
const start = absolute + house.index;
|
||||
const end = absolute + houseEnd;
|
||||
const span = spanAt(spans, start, end) ?? (referenceLayerAt === undefined ? undefined
|
||||
: spanAt(spans, absolute + referenceLayerAt, absolute + referenceLayerAt + namedLayer!.length));
|
||||
const layer = span?.layer;
|
||||
if (!layer || !VARGA_LAYER.test(layer) || (namedLayer && namedLayer !== layer)) continue;
|
||||
const varga = view.vargas[layer];
|
||||
const following = planets.find((planet) => planet.index >= houseEnd
|
||||
&& /^\s*(?:(?:里的|内的|中的|的)\s*)?$/u.test(sentence.slice(houseEnd, planet.index)));
|
||||
const entityHit = following ?? planets.findLast((planet) => planet.index < house.index
|
||||
&& !entityBoundary.test(sentence.slice(planet.index + planet[0].length, house.index)));
|
||||
if (!varga || !entityHit) continue;
|
||||
const entity = PLANET_NAMES[entityHit[0]]!;
|
||||
const sign = varga.planets[entity];
|
||||
if (!sign) continue;
|
||||
|
||||
const placementStart = entityHit.index + entityHit[0].length;
|
||||
const placementEnd = reference.index >= placementStart ? Math.min(reference.index, house.index) : house.index;
|
||||
const tail = following
|
||||
? sentence.slice(placementStart, references.find((hit) => hit.index >= placementStart)?.index)
|
||||
.split(/[;;]|——/u, 1)[0]!
|
||||
: sentence.slice(placementStart, placementEnd);
|
||||
const prefix = VARGA_PLACEMENT_PREFIX.exec(tail)?.[0].length ?? 0;
|
||||
const placement = tail.slice(prefix);
|
||||
const layerToken = [...placement.matchAll(LAYER_TOKEN)].find((hit) => hit.index === 0);
|
||||
const explicitPlacementLayer = layerToken ? LAYER_ALIAS(layerToken) : undefined;
|
||||
const entityLayer = explicitPlacementLayer
|
||||
?? spanAt(spans, absolute + entityHit.index, absolute + placementStart)?.layer;
|
||||
if (entityLayer && entityLayer !== layer) continue;
|
||||
const signText = placement.slice(layerToken?.[0].length ?? 0);
|
||||
const written = VARGA_PLACEMENT_SIGN.exec(signText);
|
||||
if (written) {
|
||||
const remainder = signText.slice(written[0].length);
|
||||
if (SIGN_NAMES[written[1]!] !== sign || SIGN_THEN_LORD.test(remainder) || VARGA_AMBIGUOUS_SIGN.test(remainder)) continue;
|
||||
}
|
||||
if (!written && VARGA_SIGN_MENTION.test(signText)) continue;
|
||||
|
||||
const planetReference = groups.planet ? PLANET_NAMES[groups.planet] : undefined;
|
||||
const signReference = groups.sign ?? groups.firstSign;
|
||||
const referenceSign = signReference ? SIGN_NAMES[signReference]
|
||||
: planetReference ? varga.planets[planetReference] : varga.lagna;
|
||||
if (!referenceSign) continue;
|
||||
const point = signReference ? `from sign ${referenceSign}`
|
||||
: `from ${layer} ${planetReference ?? "lagna"} (${referenceSign})`;
|
||||
const claimed = Number(house[1]);
|
||||
const actual = wholeSignHouse(sign, referenceSign);
|
||||
const alternative = FACT_GUARD_SIGNS[(FACT_GUARD_SIGNS.indexOf(sign) - claimed + 13) % 12]!;
|
||||
const card = claimed !== actual && varga.lagna
|
||||
? `${actual} 宫(从${SIGN_ZH[referenceSign]}数;第 ${claimed} 宫是从${SIGN_ZH[alternative]}数)`
|
||||
: `${actual} 宫`;
|
||||
const source_path = `${varga.path}.planets.${entity}${varga.path === "base.natal.d9" ? ".d9_sign" : ""}`;
|
||||
// The legacy placement matcher cannot read 「月亮在 D9 金牛」. When the new
|
||||
// rule verifies that inline sign, record it separately from the house claim.
|
||||
if (explicitPlacementLayer && written) {
|
||||
const signEnd = placementStart + prefix + (layerToken?.[0].length ?? 0) + written[0].length;
|
||||
push({
|
||||
kind: "varga_sign", layer, entity, reference: "sign", claimed: SIGN_ZH[sign], card: SIGN_ZH[sign],
|
||||
fact_id: `${layer}:${entity}@${sign}`, source_path,
|
||||
quote: sentence.slice(entityHit.index, signEnd), start: absolute + entityHit.index, end: absolute + signEnd,
|
||||
}, "match");
|
||||
}
|
||||
// When the reference follows the house, the legacy sign quote may
|
||||
// already include the number. Anchor the new claim at the reference.
|
||||
const anchor = house.index < reference.index ? reference : house;
|
||||
const anchorStart = absolute + anchor.index;
|
||||
push({
|
||||
kind: "varga_house", layer, entity, reference: point, claimed: `${claimed} 宫`, card,
|
||||
fact_id: `${layer}:${entity}@${sign}/from:${referenceSign}`, source_path,
|
||||
quote: anchor[0], start: anchorStart, end: anchorStart + anchor[0].length,
|
||||
}, claimed === actual ? "match" : "mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkKarakamsha(line: string, offset: number, view: CardView, push: Emit, brokenBetween: (from: number, to: number) => boolean) {
|
||||
@@ -754,6 +897,7 @@ export function consultationFactRepairPrompt(units: readonly FactGuardUnit[]): s
|
||||
"下面这些句子引用的盘面事实与本轮证据卡的层、实体或参照不符,已被拒收。逐条重写这一句(表格里的只重写这一格):",
|
||||
"- 只改与卡不符的部分,换成下面给出的卡上值;其余内容、语气和判断保持不变;如果改了值以后原来的推论不成立,就删掉那半句推论,不另找理由。",
|
||||
"- 分清层与实体:婚姻分盘(D9)里的木星、Karakamsha 星座所在的宫、Karakamsha 主星的本命落宫,是三件不同的事,不能互相照抄。",
|
||||
"- 分盘宫位:从哪里起算就按哪里数,写明起算点;从上升数和从某一宫数是两件事,不能互相照抄。",
|
||||
"- 格局状态:照卡上给出的状态文字改写,不写「成立」「不成立」「非成立」「落空」,除非卡上的状态文字本身就是这个意思。",
|
||||
"- 不新增计算、技法或执行状态:不写 executed / blocked / 已执行 / 已核验一类的词,不加审计表行,不换行,不写「|」。",
|
||||
"- 只输出 JSON 数组:[{\"id\": 1, \"text\": \"重写后的整句\"}],不要别的文字。",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
consultationFactRepairPrompt,
|
||||
factGuardUnits,
|
||||
factRepairRejection,
|
||||
findAnswerFactClaims,
|
||||
findAnswerFactMismatches,
|
||||
} from "../src/lib/consultation-answer-fact-guard.ts";
|
||||
import type { EvidenceCard } from "../src/lib/consultation-evidence-card.ts";
|
||||
|
||||
function syntheticCard(conflictingD10 = false): EvidenceCard {
|
||||
return {
|
||||
base: {
|
||||
natal: {
|
||||
planets: { Moon: { sign: "Aquarius", house: 11 } },
|
||||
ascendant: { sign: "Aries" },
|
||||
d9: {
|
||||
lagna: "Capricorn",
|
||||
planets: { Moon: { d9_sign: "Taurus" }, Venus: { d9_sign: "Aquarius" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
domain_sections: {
|
||||
career: { vargas: { D10: { lagna: "Aries", planets: { Mercury: "Gemini" } } } },
|
||||
...(conflictingD10 ? { general: { vargas: { D10: { lagna: "Aries", planets: { Mercury: "Cancer" } } } } } : {}),
|
||||
},
|
||||
} as unknown as EvidenceCard;
|
||||
}
|
||||
|
||||
const card = syntheticCard();
|
||||
const original = "D9 第 7 宫为巨蟹,宫内无星,宫主月亮在 D9 金牛入旺(从 D9 上升数第 11 宫)。";
|
||||
const claims = (text: string, evidence = card) =>
|
||||
findAnswerFactClaims(text, evidence).filter((claim) => claim.kind === "varga_house");
|
||||
|
||||
test("the reported D9 sentence identifies the five house-claim parts and the alternative counting point", () => {
|
||||
const matches = claims(original);
|
||||
assert.equal(matches.length, 1);
|
||||
const [claim] = matches;
|
||||
assert.equal(claim?.status, "mismatch");
|
||||
assert.equal(claim?.layer, "D9");
|
||||
assert.equal(claim?.entity, "Moon");
|
||||
assert.equal(claim?.reference, "from D9 lagna (Capricorn)");
|
||||
assert.equal(claim?.claimed, "11 宫");
|
||||
assert.equal(claim?.card, "5 宫(从摩羯数;第 11 宫是从巨蟹数)");
|
||||
assert.equal(claim?.fact_id, "D9:Moon@Taurus/from:Capricorn");
|
||||
assert.equal(claim?.source_path, "base.natal.d9.planets.Moon.d9_sign");
|
||||
assert.equal(original.slice(claim?.start, claim?.end), "第 11 宫");
|
||||
|
||||
const sign = findAnswerFactClaims(original, card).find((claim) => claim.kind === "varga_sign");
|
||||
assert.equal(sign?.status, "match");
|
||||
assert.equal(sign?.card, "金牛");
|
||||
assert.ok(findAnswerFactClaims("D9:月亮在金牛。", card).some((claim) =>
|
||||
claim.kind === "varga_sign" && claim.status === "match"));
|
||||
});
|
||||
|
||||
test("the same placement gives different correct houses under explicit lagna and sign references", () => {
|
||||
for (const [text, reference, value] of [
|
||||
["宫主月亮在 D9 金牛(从巨蟹数第 11 宫)。", "from sign Cancer", "11 宫"],
|
||||
["宫主月亮在 D9 金牛(从巨蟹座起第 11 宫)。", "from sign Cancer", "11 宫"],
|
||||
["宫主月亮在 D9 金牛(以巨蟹为第 1 宫,第 11 宫)。", "from sign Cancer", "11 宫"],
|
||||
["宫主月亮在 D9 金牛(从 D9 上升数第 5 宫)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
["宫主月亮在 D9 金牛(从 D9 上升起第 5 宫)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
["宫主月亮在 D9 金牛(从 D9 Lagna 数第 5 宫)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
["宫主月亮在 D9 金牛(从上升数第 5 宫)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
["D9:月亮在金牛(第 5 宫,D9 上升起算)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
["D9:月亮(从上升数第 5 宫)。", "from D9 lagna (Capricorn)", "5 宫"],
|
||||
]) {
|
||||
assert.deepEqual(claims(text!).map((claim) => [claim.status, claim.reference, claim.claimed]),
|
||||
[["match", reference, value]], text);
|
||||
}
|
||||
});
|
||||
|
||||
test("a reference planet is excluded from the entity search", () => {
|
||||
for (const [point, number, status] of [
|
||||
["从 D9 金星起", 4, "match"], ["从金星数", 5, "mismatch"],
|
||||
] as const) {
|
||||
const text = `宫主月亮在 D9 金牛(${point}第 ${number} 宫)。`;
|
||||
const out = claims(text);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0]?.entity, "Moon");
|
||||
assert.equal(out[0]?.reference, "from D9 Venus (Aquarius)");
|
||||
assert.equal(out[0]?.status, status);
|
||||
assert.equal(out[0]?.fact_id, "D9:Moon@Taurus/from:Aquarius");
|
||||
}
|
||||
});
|
||||
|
||||
test("unsupported, unbound and incomplete house claims are left alone", () => {
|
||||
for (const text of [
|
||||
"本命月亮在第 11 宫。",
|
||||
"月亮在 D9 第 11 宫。",
|
||||
"D9 月亮在金牛(从 Karakamsha 数第 11 宫)。",
|
||||
"D9 月亮在金牛(从月亮盘数第 11 宫)。",
|
||||
"D9 月亮在金牛(从月亮盘 上升起算第 11 宫)。",
|
||||
"D9 月亮在金牛(从 AL 数第 11 宫)。",
|
||||
"D9 月亮在金牛(从 D1 上升数第 11 宫)。",
|
||||
"本命月亮在金牛(从 D9 上升数第 11 宫)。",
|
||||
"D9 月亮在金牛(从上升数第 13 宫)。",
|
||||
"D9 月亮在金牛(从火星数第 11 宫)。",
|
||||
"D9 火星(从上升数第 11 宫)。",
|
||||
"D7 月亮(从上升数第 11 宫)。",
|
||||
"月亮在金牛(从上升数第 11 宫)。",
|
||||
"D9 月亮在金牛第 11 宫(从上升数)。",
|
||||
"D9:月亮在金牛或双子(从上升数第 11 宫)。",
|
||||
"D9:月亮在金牛(第 5 宫或第 11 宫,从上升数)。",
|
||||
]) assert.deepEqual(claims(text), [], text);
|
||||
assert.equal(claims("D9:月亮在金牛(从上升数第 5 宫,第 11 宫是另一处)。").length, 1);
|
||||
});
|
||||
|
||||
test("an explicitly wrong sign still belongs to the existing sign check", () => {
|
||||
const text = "D9:月亮在双子(从上升数第 11 宫)。";
|
||||
assert.deepEqual(claims(text), []);
|
||||
assert.ok(findAnswerFactMismatches(text, card).some((claim) =>
|
||||
claim.kind === "varga_sign" && claim.claimed === "双子"));
|
||||
});
|
||||
|
||||
test("other available vargas are checked, and conflicting evidence for a layer is ignored", () => {
|
||||
const text = "D10:水星在双子(从上升数第 4 宫)。";
|
||||
const [claim] = claims(text);
|
||||
assert.equal(claim?.status, "mismatch");
|
||||
assert.equal(claim?.layer, "D10");
|
||||
assert.equal(claim?.source_path, "domain_sections.career.vargas.D10.planets.Mercury");
|
||||
assert.equal(claim?.card.startsWith("3 宫"), true);
|
||||
assert.deepEqual(claims(text, syntheticCard(true)), []);
|
||||
});
|
||||
|
||||
test("transit, annual and KP wording prevents a natal varga check in that sentence", () => {
|
||||
for (const word of ["行运", "过境", "Transit", "流年", "年盘", "Varsha", "Tajika", "KP", "宫头", "Chalit"]) {
|
||||
assert.deepEqual(claims(`D9:月亮在金牛(从上升数第 11 宫),这是${word}。`), [], word);
|
||||
}
|
||||
});
|
||||
|
||||
test("the new check keeps commas inside a sentence and never carries an entity or reference across cells", () => {
|
||||
const checked = "| D9 | 月亮在金牛,位置写作(从上升数第 5 宫) |";
|
||||
assert.equal(claims(checked)[0]?.status, "match");
|
||||
for (const text of [
|
||||
"| D9 | 月亮在金牛 | 从上升数第 5 宫 |",
|
||||
"| D9 | 月亮在金牛,从上升数 | 第 5 宫 |",
|
||||
"| D9 | 月亮在金牛;从上升数第 5 宫 |",
|
||||
"D9:月亮在金牛。从上升数第 5 宫。",
|
||||
"D9:月亮在金牛\n从上升数第 5 宫。",
|
||||
]) assert.deepEqual(claims(text), [], text);
|
||||
});
|
||||
|
||||
test("the repair path rejects the same wrong count and accepts the corrected sentence", () => {
|
||||
const mismatches = findAnswerFactMismatches(original, card);
|
||||
const [unit] = factGuardUnits(original, mismatches);
|
||||
assert.ok(unit);
|
||||
assert.equal(factRepairRejection(unit, original, card), "still_mismatched");
|
||||
assert.equal(factRepairRejection(unit, original.replace("第 11 宫", "第 5 宫"), card), null);
|
||||
assert.match(consultationFactRepairPrompt([unit]), /分盘宫位:从哪里起算就按哪里数/);
|
||||
});
|
||||
|
||||
function houseBindingCard(lagna = "Scorpio"): EvidenceCard {
|
||||
return {
|
||||
base: {
|
||||
natal: {
|
||||
d9: {
|
||||
lagna,
|
||||
planets: {
|
||||
Mercury: { d9_sign: "Sagittarius" },
|
||||
Venus: { d9_sign: "Pisces" },
|
||||
Saturn: { d9_sign: "Aries" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
domain_sections: { general: { vargas: { D6: { lagna: "Pisces", planets: { Sun: "Aquarius" } } } } },
|
||||
} as unknown as EvidenceCard;
|
||||
}
|
||||
|
||||
test("a planet immediately after the house number is the entity for correct and incorrect counts", () => {
|
||||
const evidence = houseBindingCard();
|
||||
const correct = "11 宫主金星同时是功能凶星;D9 从上升数第 2 宫的水星入敌。";
|
||||
const wrong = "D9 从上升数第 5 宫的水星入敌。";
|
||||
assert.deepEqual(claims(correct, evidence).map(({ status, entity }) => [status, entity]), [["match", "Mercury"]]);
|
||||
assert.deepEqual(claims(wrong, evidence).map(({ status, entity }) => [status, entity]), [["mismatch", "Mercury"]]);
|
||||
for (const connector of ["", " 的 ", "里的", "内的", "中的"]) {
|
||||
const text = `D9 金星在双鱼,从上升数第 2 宫${connector}水星入敌。`;
|
||||
assert.deepEqual(claims(text, evidence).map(({ status, entity }) => [status, entity]), [["match", "Mercury"]], text);
|
||||
}
|
||||
});
|
||||
|
||||
test("a sign written after the house number and planet must agree with the evidence", () => {
|
||||
const evidence = houseBindingCard();
|
||||
assert.equal(claims("D9 从上升数第 2 宫里的水星在射手。", evidence)[0]?.status, "match");
|
||||
assert.deepEqual(claims("D9 金星在双鱼,从上升数第 2 宫的水星在白羊。", evidence), []);
|
||||
assert.deepEqual(claims("D9 从上升数第 5 宫的水星:白羊。", evidence), []);
|
||||
});
|
||||
|
||||
test("house-lord wording does not claim a planet placement", () => {
|
||||
const evidence = houseBindingCard("Cancer");
|
||||
for (const ending of ["的宫主", "之主", "主星", " 的 主", "宫主"]) {
|
||||
const text = `D9 里第 7 宫为摩羯,主星土星落在白羊……从 D9 上升数第 7 宫${ending}本身需要取消落陷。`;
|
||||
assert.deepEqual(claims(text, evidence), [], text);
|
||||
}
|
||||
});
|
||||
|
||||
test("backward entity binding stops at semicolons, dashes and colons, including inside parentheses", () => {
|
||||
const evidence = houseBindingCard();
|
||||
assert.deepEqual(claims("金星在 D9 双鱼;从上升数第 2 宫。", evidence), []);
|
||||
for (const separator of [";", ";", "——", ":", ":"]) {
|
||||
const text = `D9(11 宫主金星${separator}从上升数第 2 宫)。`;
|
||||
assert.deepEqual(claims(text, evidence), [], text);
|
||||
}
|
||||
assert.equal(claims("D6 太阳水瓶(从 D6 上升数第 12 宫)。", evidence)[0]?.status, "match");
|
||||
});
|
||||
Reference in New Issue
Block a user