From fd1bd758895ba98908c193e1c3bb2ca2884425de Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 21:28:55 +0800 Subject: [PATCH 1/6] docs(tasks): brief for #218 reading-postcheck-facts (rebased on #207) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbBXozsiu43ob7B8UZCmDM --- .../TASK-reading-postcheck-facts-20261112.md | 112 ++++++++++++ ...ef-astrologer-acceptance-reply-20261112.md | 169 ++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 docs/tasks/TASK-reading-postcheck-facts-20261112.md create mode 100644 docs/tasks/ref-astrologer-acceptance-reply-20261112.md diff --git a/docs/tasks/TASK-reading-postcheck-facts-20261112.md b/docs/tasks/TASK-reading-postcheck-facts-20261112.md new file mode 100644 index 00000000..ddfca3c0 --- /dev/null +++ b/docs/tasks/TASK-reading-postcheck-facts-20261112.md @@ -0,0 +1,112 @@ +# TASK:报告解读复查补事实核对规则,修正误拦,依据节被拦时给确定性审计(2026-11-12) + +- Gitea:#218 +- 分支:`codex/reading-postcheck-facts-20261112`,基于 staging `2f374fff281622f342151a97fa6f6d758de74cc8`。#207 已合入(2f374fff)。 +- Bug 编号:BUG-1860 +- 验收:Claude(jyotisha-e6) + +不得自行写「产品决定/裁定」。任务书没写到的取舍,一律选「需拍板」。 + +不得修改或删除既有断言来让测试通过。确需修改的,在 PROGRESS 里逐条写明原值、新值和原因。 + +占星师的引擎和资料(`scripts/jyotish_engine.py`、`scripts/yoga_engine.py`、`references/` 下她仓库已有的文件、`SKILL.md`)一律不改。产品差异只能放进单独的产品文件,并在 `references/product_runtime_contract.md` 公告(项目规则 2026-11-07)。 + +占星师回复原文:`docs/tasks/ref-astrologer-acceptance-reply-20261112.md`,由本分支第一个提交带入。下文的「她 §N」都指这份文件。 + +## 背景 + +报告解读写完后,由 `frontend/src/lib/personal-report-reading.ts` 的 `checkSection` 逐节复查。没通过的那一节会被隐藏,原处显示 `READING_SECTION_WITHHELD_TEXT`。#207 已经补了三条复查:历史回放不给当前建议、双行运范围、形成与未验证。 + +她 §3 指出成稿里还有这些错误,要求修正,而不是降到 C 了事: + +1. 「水星掌管第五宫与第十宫」:事实包 `planets.Mercury.owns_houses = [1,10]`,第五宫只是落宫(`d1_house = 5`)。这是宫主和落宫混用。 +2. 「SAV 总分为337,显示不缺维持力」:337 是固定的标准总和,只能用来校验,不能说明个人强弱。 +3. 「Badhaka(阻碍星)落在第七宫」:第七宫是阻碍宫,木星是阻碍宫主,两者混成了一个说法。 +4. Obama 样稿的健康节写了「运动或炎症类风险」:这是没有依据的具体现实风险。 + +她 §4 还指出两处问题: + +5. `BANNED_PHRASES` 一刀切。「检测器检出,待核实」是合法的状态说明;「不得预测死亡」这类边界说明也不该被拦。 +6. 依据节被拦时整节隐藏,行动性结论还在页面上,证据限制却看不到了。 + +## 要改的文件 + +- `frontend/src/lib/personal-report-reading.ts`:只改复查部分 +- `frontend/src/lib/personal-report-reading-contract.ts`:只在第 6 条需要时新增类型或文案常量 +- `frontend/src/components/personal-report/report-reading-section.tsx`、`frontend/src/lib/report-export-blocks.ts`:只为第 6 条 +- 新建 `frontend/tests/report-reading-postcheck-facts-20261112.test.ts` +- `docs/BUG_HISTORY.md`:末尾追加一条 +- `docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md` + +## 要做的 + +每条新检查不通过时只退回那一节,原因加进 `ReadingRejection`。 + +1. **`house_lord_mismatch`** + - 句子里出现「X 掌管 / 主管 / 管 / 是 第 N 宫(主)」「第 N 宫主 X」「第 N 宫的宫主 X」「X 为第 N、M 宫主」等写法时,按事实包 `planets..owns_houses` 核对。N 不在其中就退回。 + - 「第 N 宫与第 M 宫」这类并列写法要逐个核对。 + - 「X 落 / 落在 / 位于 / 坐 第 N 宫」不是宫主写法,不查。 + - 行星名用 `PLANETS_ZH`。数字同时支持阿拉伯数字和中文数字(一到十二)。 + - 分句规则:以「。;!?」分句。宫主词与行星之间如果隔着另一颗行星名,就不算命中。 +2. **`sav_total_as_strength`** + - 句子里出现「337」或「三百三十七」,同时又出现「不缺」「充足」「强」「弱」「维持力」「底气」「力量」中任何一个,退回。 + - 「校验」「合计」「总和为标准值」这类纯校验写法不退回。 +3. **`badhaka_role_mix`** + - 健康领域的 Badhaka 值里有宫位和宫主。 + - 退回的情况:句子里出现「阻碍星」,同一分句又出现「第 N 宫」,而 N 是 Badhaka 宫的宫位。例如「阻碍星落在第七宫」。 + - 不退回的情况:「阻碍宫在第七宫」「阻碍宫主木星在第十宫」。 + - 字段名先按当前 `badhaka_facts.py` 的 `house` / `lord` 读取。如果 BUG-1859 已经合入,改为读 `badhaka_sthana` / `badhakesh`,两种都要兼容。 +4. **`health_outcome`** + - 健康节和总评节里,命中具体病症、伤害或部位风险词表就退回。 + - 起始词表:炎症、感染、外伤、受伤、损伤、骨折、手术、出血、发炎、结石、肝、肾、心、肺、胃、血压、血糖、过敏、失眠、运动风险、运动损伤。 + - 「心」只在「心脏」「心血管」「心律」中算命中;「心理」「心情」「关心」等不算。 + - 写进 PROGRESS 的内容:完整词表,以及用现有三份公开样稿(下文路径)跑出的误伤列表。 +5. **`banned_phrase` 收窄**(只改这两个词,其他禁用词不动) + - 「检测器」:只有出现在「检测器检出」「检测器识别」之后,并且同一分句含「待核实」「未确认」「候选」,才放行。其余照旧拦。 + - 「死亡」:同一分句里有「不得 / 不能 / 不会 / 不作 / 不做 / 不预测 / 禁止」中任何一个,并且出现在「死亡」之前,就放行。其余照旧拦。 +6. **依据节被拦时的确定性审计** + - 当 `basis` 节被拦时,原处不再只显示「暂不显示」,而是显示固定生成的最小审计。内容只取自事实包的 `evidence_grades.claims`,不经过模型: + - 每类结论一行:中文名、上限等级、必需依赖里缺失或在边界上的项,以及 `must_disclose` 列表; + - 一句固定说明:「这一节的模型文字没有通过核对,下面是由计算直接生成的依据清单。」 + - 中文名和依赖项的中文写法放在 `personal-report-reading-contract.ts` 的常量里。不认识的 id 原样显示,不能编造。 + - 页面和 MD 导出(`report-export-blocks.ts`)都要换成这份审计。 + - 其他节被拦时,行为不变。 +7. 拦截正文里的准确率说法:句子里出现「成功率」「准确率」「命中率」,并且带百分数或「几成」,就退回,原因记为 `accuracy_claim`(她 §4 末条)。 + +## 不要碰 + +- 现有检查的逻辑,第 5 条除外 +- `writeReportReading` +- `references/**`、`scripts/**` +- 冻结文件 + +## 验收标准(写进新测试,全部用合成文字) + +1. 第 1 到 5 条和第 7 条,每条至少测两种情况:一个触发、一个合法写法不触发。第 1 条还要测:「水星为第一、十宫主,落第五宫」不触发。 +2. 第 5 条:「检测器检出,待核实」不拦,「检测器显示」照旧拦;「本报告不得预测死亡」不拦,「有死亡风险」照旧拦。 +3. 第 6 条:`basis` 被拦时,页面渲染和 MD 导出都出现审计行,其他节不受影响。 +4. 用 `/workspace/eval-upstream-1107/report-192-acceptance-1112/报告解读层-验收材料-1112.zip` 里三份 `draft-*.raw.json` 与对应的 `facts-*.json` 跑复查: + - A-2 的财富节(第 1、2 条)和健康节(第 3 条)必须被拦; + - B-1 的健康节(第 4 条)必须被拦; + - A-1 的依据节不能再因为「检测器」或「死亡」被拦(如果还因为别的原因被拦,把原因写进 PROGRESS)。 + - 这一条可以写成一次性脚本,结果写进 PROGRESS,不进测试。zip 里的 facts 不得提交进仓库。 +5. 已有的复查测试全部通过:`report-reading-20261027`、#207 的测试和 MD 导出测试。 + +## 自测命令(在 frontend 目录) + +``` +npx tsx --test tests/report-reading-postcheck-facts-20261112.test.ts tests/report-reading-postcheck-gaps-20261112.test.ts tests/report-reading-20261027.test.tsx tests/report-md-export-reading-20261111.test.ts +npx tsc --noEmit -p . +npx eslint . +``` + +另外在仓库根目录运行 Python 快速质量门: + +``` +python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime +``` + +## 何时停下来选「需拍板」 + +- 第 4 条的词表在三份样稿里误伤了合法的结构描述(例如「第六宫主」),而且不放宽就无法避免。 +- 第 6 条需要改模型提示或事实包。 diff --git a/docs/tasks/ref-astrologer-acceptance-reply-20261112.md b/docs/tasks/ref-astrologer-acceptance-reply-20261112.md new file mode 100644 index 00000000..0adfeede --- /dev/null +++ b/docs/tasks/ref-astrologer-acceptance-reply-20261112.md @@ -0,0 +1,169 @@ +# 报告解读层验收回复与年运七项裁定 + +日期:2026-11-12。对象:网站开发人员。 + +**结论:本轮可以确认部分交付结构已经改进,但报告解读层尚不能整体验收通过。年运七项问题已在下文给出实施口径,不必等待模板补齐才开工。** + +本次是提交材料与资料源的工程、语义合同审核,不是重新为样例人物解盘。没有访问网站服务器,没有运行网站代码,也没有验证部署。资料源基线为 `8aaf60991c32a84ac30d6d7b6a62c4f166507bce`;网站构建 `1ba0364a7a8c0f964c3e21c21cb304aaa1899375` 是来件声明,不能视为本仓已复现。 + +## 1. 审查对象与可复查入口 + +- `报告解读层-验收材料-请占星师审核-1112.md` +- `报告解读层-验收材料-1112.zip`,SHA256:`8e9352fc59623778b51b6fd923670e82e9075530730d000b0650d8e5e588ae94` +- `年运报告-请占星师裁定-1112.md` +- 上轮合同:`handoff/2026-11-12/preset_q3/README.md` +- 区间合同:`handoff/2026-11-09/interval_rectification/README.md` +- 年盘来源边界:`skills/tajika-annual/SKILL.md` 及其 usage/research 引用;`docs/research/tajika_source_adjudication_2026_11_06.md`。 + +同目录 `review_packet.py` 只读取 ZIP,不解压、不执行来件程序;使用 `python3 review_packet.py <验收ZIP绝对路径>` 可重新取得成员哈希、状态矛盾、段落标签与字面命中。它不是网站回归测试,也不自动判断整段占星语义正确。 + +### 已确认与尚不能确认 + +| 项目 | 本轮结论 | +|---|---| +| 提交 facts/caps/草稿/落库/成稿/提示词 | 已提供,可追踪部分处理阶段 | +| caps 与 facts 内 evidence_grades | A、B 均一致 | +| 提示词长度 | 92,392 个 Unicode 字符;长度不代表有效调用 | +| 无适配案例不升到 A/B | 样稿采取 C 上限,方向正确;不等于 C 内容都正确 | +| 技法未进路由与仓内不存在分开 | 方向正确;仍需说明相关技法为何未执行 | +| 历史回放、分段 withheld | 已有结构;放行规则仍有漏洞 | +| 后端拦截器、产品 Badhaka 实现、自动测试 | ZIP 未提供相关代码,不能签署代码验收 | +| 真实案例、用途区间稳定性、完整端到端复现 | 尚未完成 | + +## 2. 必须先修复的发布阻断项 + +### P0-1:不能把一分钟阈值冒充用途区间稳定性 + +`facts-A/B.json → birth_precision.purpose_stability` 都是 `not_evaluated`。但 `caps-A.json → claims.*.ledger.purpose_stability` 八项都是 `stable`;B 有七项 `stable`,仅 health_pressure 为 `boundary_sensitive`。 + +实际依据是 `birth_record_minute_v1` 下,分盘最近边界与固定 `tolerance_minutes=1.0` 比较。它只能描述该规则下的局部边界检查,不能证明用户真实出生区间内的解盘与推运结论稳定。timing_window 更不能无精度依赖便标为稳定。 + +改法:保留原边界数值与旧算法标识;用途稳定性必须来自真实输入区间、相关分盘/派生点/应期边界、区间内结论差异的独立评估。未做则仍是 `not_evaluated`,不能用 minute_confirmed 替代。未评估不意味着整份报告一律 D:保留不依赖这些边界的事实与条件性解读;会跨界的判断须分支或暂缓确定结论。 + +### P0-2:分级必须覆盖实际判断,不能只覆盖六个栏目 + +overview/advice/basis 的段落允许缺少 claimType,实际却出现职业方向、能力、结果强弱等实质判断。A2 的事业第三段含时间窗口,仍标为 `career_promise`,没有绑定 timing_window 的依赖。 + +改法:每条实质判断绑定 claim ID、所用 fact ID、规则 ID 与所需依赖;混合段落拆开,或采用依赖并集及最严格适用上限。摘要和建议引用已审核 claim,继承等级,不能另开豁免通道。纯事实描述、一般性操作说明不强迫变成预测 claim,但不得夹带新推断。 + +高严谨事业 D10+A10、财富 D2/D11、婚恋 D9+UL 等最低要求应落实到适用判断与执行审计。仅放入 checked 列表,不等于已经建立该结论的完整依赖;也不能将无关技法变为所有句子的硬门槛。 + +### P0-3:生成时快照与验收快照没有闭环 + +来件说明草稿生成后又重建 facts,新增/修改 Badhaka 状态,未重新生成模型内容。即使位置数字相同,`pending_astrologer_review` 等状态也会改变解释权限。不能据此宣称同一 facts→caps→生成→校验→展示全链已通过。 + +每次尝试须绑定 input、facts、caps、system prompt、规则版本、原始回复、修复记录和最终成稿哈希;补交生成当时的 facts。对当前快照重新校验,必要时重生成受影响段落。A2 draft 与 stored 不相等,请附字段级转换记录;这本身不证明错误,但需解释转换。公开样例输入还应补齐完整坐标/时区解析参数,当前省略坐标无法独立重算。 + +## 3. 成稿已经存在的错误,不接受仅靠降为 C 处理 + +| 定位 | 问题 | 必须如何修正 | +|---|---|---| +| final-A-2 财富 | “水星掌管第五宫与第十宫” | facts 中 owns_houses 为 `[1,10]`;第五宫是所在宫。宫主与落宫分别绑定字段 | +| final-A-2 财富 | “SAV 总分为337”推出“不缺维持力” | 该标准总和用作校验,不能充当个体强弱。解释须引用相关宫位分布、方法与条件 | +| final-A-2 健康 | “Badhaka(阻碍星)落在第七宫双鱼座,主星木星在第十宫” | 分开 Badhaka sthana(宫位)与 Badhakesh(宫主星)。A 是第七宫为阻碍宫,木星在第十宫 | +| final-A-2 依据 | “没有满足定义的功能吉凶组合” | 空 Yogakaraka 结果不等于没有功能吉凶星;按实际字段限定陈述 | +| final-A-2 历史回放 | 仍有“若有实际问题应依现实就医评估” | 面向已故样例的当前建议应去除,改成历史解释边界 | +| final-B-1 健康 | “运动或炎症类风险”等具体现实风险 | 现有结构与待审 Badhaka 不能直接支撑该结论;删除无依据的结果断言,保留结构与待核验主题 | + +B 的 D8 边界约 0.25 分钟,但 health_obstruction 文本引用 D8,所需依赖却仅含 D1/Badhaka。必须按实际引用补依赖,不能让段落借用未纳入精度检查的证据。 + +“C 级”“仅供参考”不是事实错误、医学化推断或无来源结论的免责补丁。结构成立、传统解释、事件兑现三层必须分开。 + +## 4. 两项专项答复及审计修正 + +### 功能吉凶 profile:不要二选一混用 + +当前 facts 明确选的是 `pvr_table30_v1`。仓内该表将双子、处女上升的土星列为 neutral。产品在 BPHS 文档中新增的 mixed,和源文档的 BPHS 回退规则,都不能覆盖 PVR 结果。 + +按已选 profile 返回计算结论;BPHS 修订作为另一个具名、版本化 profile,附来源定位、完整差异与十二上升测试后另审。产品文档变化不止两条土星标签,不接受“只有两个词差异”的笼统说明。自然吉凶与功能吉凶仍保持独立。 + +### Badhaka:可以纳入,不能冒充已审定健康结论 + +健康路由最低清单未显式列 Badhaka,不表示不能用。它可作为相关补充层;一旦段落确实引用它,就成为该条判断的依赖,而非所有健康结论的统一发布门槛。 + +按所选规则,A 的 dual 上升取第七宫、宫主木星;B 的 movable 上升取第十一宫、宫主火星,提交的位置关系可与 facts 对照。但产品计算代码未提交,`product_computation_pending_astrologer_review` 不能改称引擎已验证,也不能由宫位直接推身体疾病。下一包提供实现、profile、十二上升单测、星体与宫位区分测试。 + +### 状态与可见审计 + +- **已经计算但未读用:execution_status=executed,interpretation_usage=not_used。** 不可将其改成计算未执行。annual 来件“未使用,不写已执行”的表述须纠正。 +- `blocked` 是缺输入/调用/验证等具体阻碍;未运行的相关可用技法不能靠 `not_in_domain_route` 自动免责;`not_applicable` 需要范围或适用条件理由。 +- 真实案例与 MEVG 状态必须在用户可读的证据附录保留,不能只留后台;没有完成就如实显示,不伪造通过。 +- “检测器检出,待核实”是合法状态说明;不能因含“检测器”就过滤。死亡预测禁令也不应误杀“不得预测死亡”等边界陈述。用语境与结论类型检查。 +- A1 的依据章被 withheld 时,必须有确定性生成的最小证据审计替代;不能保留行动性结论却把其证据限制整体隐藏。 +- 原提示资料含旧 ABC 口径与“平均85%的预测成功率”等表述。保留历史原文可追溯,但需显式废止其运行效力,不得靠末尾新合同隐式覆盖。没有验证设计、分母与适用范围,不得向用户宣称准确率。 + +## 5. 年运 Q1–Q7 正式回复 + +### Q1:选 (a),同一逐结论证据合同 + +年运采用 `report_evidence_grade_v1`,替换“全篇最高 B”。后端定上限、前端保留标签;没有适配案例最高 C,关键依赖缺失的结果性判断不能由 C 放行。独立一致性必须实际执行且适用,Narayana 未决候选不能充当第二套确定证据。年运不是另一套放宽标准。 + +### Q2:选带条件的 (a),正文比较表保留研究方法 + +Yoga Pravesha、Nakshatra Pravesha 纳入具名比较表,显示回归时刻、位置/年份参数、方法与条件状态;全部原始结构留附录。不要混成一个“最佳年度”。当前属于有来源边界的研究链路,不能仅因计算可运行升级为外部事件验证。 + +Satabdika 按所选方法的 Moon/Lagna/D9 等实际适用条件检查;满足才运行,不满足写 not_applicable 并给条件依据;缺输入则 blocked,不能强行补齐。固定天数年钟与 `pvr_jhora_360_tithi` 角度年钟必须分开,360 tithi 不能写成360个民用日,也不能偷偷用365.25替代。多候选保留独立结构,不多数投票。 + +### Q3:按主题映射实际执行;不是“未做也可以随意阻塞” + +以下为综合年运覆盖相关主题时的要求。专题年运仅执行问题相关项;适用条件失败可 not_applicable,缺真实输入/工具可 blocked。单纯尚未开发/运行,要明确未完成,不能签署完整验收。 + +| 技法 | 实施裁定 | 可阻塞/不适用的真实条件 | +|---|---|---| +| Double Transit | 做对应事件主题的窗口交叉检查 | 未冻结事件类或缺行运数据;不得心动、成对、成婚混用 | +| Chara Dasha | 综合年运相关主题执行,profile 具名 | 必需输入缺失;流派差异保留,不直接等于不适用 | +| Yogini | 相关应期执行 | 实际输入/计算受阻须注明 | +| Ashtottari | 先检条件,满足时执行 | 条件不满足为 not_applicable,条件未知为 blocked | +| Kalachakra | 相关应期执行,保留分支与精度敏感性 | 缺所需月宿/参数或实现无法运行 | +| Bhrigu Pada | 相关主题执行;补回提问表遗漏项 | 真正无该主题才不适用,来源未闭环仅限制解释强度 | +| Sudarshana | 相关年度主题执行 | 明确说明缺失输入或不相关范围 | +| KP | 涉及事件/细时窗时执行并保留独立参数 | 出生精度不支持时分支/阻塞精确结论;不伪造精度 | +| 西方核心 | 全谱跨体系年运中运行相关项并单列 | 明确约定“仅印度占星”的产品路由可不适用;不得为省事省略或混入印度术语 | + +这张表不是年运完整清单。仍需运行既有路线中的本命基础、功能吉凶、相关分盘、力量、Yoga、Dasha、Gochara,以及适用的 Varshaphala、年主、Muntha、Tajika、Saham、Panchavargiya/Harsha、Patyayini/Mudda、Varsha Narayana、月盘/Tithi 等层。先返回仓内当前能力审计清单,再逐项记录执行,不凭旧文档数量声称全量。 + +Tithi 出生地与现居地是不同位置假设:现居地未提供时若用出生地回退,必须披露,不能当成“两地独立印证”。Varsha Narayana 应使用年度入口与年度时间轴,不以本命 Narayana 改标题顶替。 + +### Q4:接受 (a) 的前台去总评,修正“只保留后台” + +取消“年度能量 A–D”和笼统每月吉凶栏。用有证据的生活主题、条件窗口、冲突和核验问题呈现,不强行编造具体事件。证据 A–D 与运势好坏不是同一个量纲。 + +原始计算、方法差异和必要限制仍应在用户可展开/导出的证据附录可见,不只后台可见。旧模板的主观总评分无须继续计算来满足版式。 + +### Q5:结果文案选 (c) 不直接移植;结构可复用 + +核查源代码:四个年主/Muntha/Munthesh相关主题字典各只有一条,但年度 Dasha 行星—宫位主题表有九对,不能笼统说全部各一条。它们是有限覆盖模板,不是通用判词库;内部 partial_verified 字样也不能当成事件外部验证。 + +`_pl9_annual_dasha_results` 默认 `without_dasha_balance`,当前调用没有显式覆盖。不能因它在源仓就压过已批准的余额口径。 + +允许复用行结构、事实映射、时间边界和缺项表示;网站生产调用须显式传入已裁定的 balance profile、标识实际使用口径,禁止静默回退到另一套。无余额可作为具名对照,不得伪装主结果。不要盲目替换源引擎所有默认值,本轮没有授权改变冻结个人导出链。 + +没有对应文案模板时,照常保留真实周期和原始星体/宫位数据,再提供有来源的条件性解释或标为暂无判词;不得把“无模板”写成“没有计算”。不必等全部模板齐全才提供可用年运数据。 + +### Q6:两张指南表不作直接、已验证的结果规则 + +可以作为待逐条溯源的传统参考假设,不直接输出“事业突破”“财务好年”。规则需补来源定位、所用流派、必要条件、冲突处理与解释权限。年主/宫位/尊贵/相位结构成立,不等于生活事件兑现。 + +分别记录:①年盘内对 Muntha 的结构关系;②年盘 Munthesh 的状态;③实际行运对该年固定 Muntha 点/星座的经过;④Muntha 自身逐年的推进。四者不是同一事件,也不能重复计证。 + +不设只有土星、罗睺才算的全局白名单。按来源规则与主题选相关星体,但每条必须说明是同宫、同星座、精确度数接触还是某种相位;采用哪套角距/容许度,不能自行发明统一阈值。只有星座位置时,不得输出精确到日的度数交会。 + +行运要扫描覆盖区间,保留逆行重复命中、停滞、进入/离开边界与实际日期;单一观察日不代表全年。双方统一 ayanamsa、node mode、年盘位置方案;不能拿另一种回归盘中的点无说明混算。Moon/快速星体如使用,只是相应时间尺度的触发候选,不自动获得年度主导权。 + +### Q7:具名 Yoga 沿用五态;相位基础结构另列 + +具名 Tajika Yoga 沿用第七轮形成状态合同,不新建一套同名不同义的状态。相位基础层另外描述几何命中、入相/出相、精确/停滞及判断参数,不能把某种相位直接命名成成立的 Ithasala/Easarapha。 + +按具名方法检查有效容许度、速度关系、未来能否成相、第三星与干扰等所需条件。缺条件不是“已证明不成立”。2026-11-06 来源审查记录仍标记多项待裁定和不允许提升预测因子;必须保留该边界。 + +尤其核对 Ithasala 的容许度与未来成相、Nakta/Yamaya 的第三星速度、Manahoo 的作用方式、Khallsara/Rudda/Iqabala 的定义差异。可显示“按某 profile 检出的候选结构”,不得因一个 angle hit 提升事件等级。 + +## 6. 下一次验收包与执行顺序 + +1. 先修 P0:真实用途区间状态、逐 claim 依赖、生成快照一致性。旧样稿全部重新通过事实校验,修复上述实际错误。 +2. 提供 grade gate、事实校验、Badhaka 与状态映射的实现或可审查补丁;公开样例补完整可重算输入。不要提交私人案例与密钥。 +3. 回归至少覆盖:落宫/宫主错置;337误作个体强度;摘要绕过分级;事业段夹带未绑定时间窗;D8跨界;已故对象当前建议;合法状态词误杀;证据章 withheld;facts状态变更后旧草稿;PVR/BPHS profile 混用。 +4. 年运补:适用与不适用 Satabdika/Ashtottari、缺现居地、不同年钟、Mudda四边界、余额口径显式传递、相位命中但具名 Yoga 条件不全、多候选 Narayana。不以单一成功样例替代分支覆盖。 +5. 每份样例提供完整快照哈希、测试命令/结果、分段修复记录与可读审计附录。区分“本地测试通过”“暂存部署成功”“占星语义已审”三个结论。 + +本轮资料源预检通过,包含外部适配器诊断、远端可见性与既有治理测试。该结果只说明本仓预检,不是网站算法或新拦截器测试通过。本轮不修改冻结导出器、不改网站服务器、不重新输出个人命盘。 From 6a1417e0fb57deda1366fbbff8c39c6e32f1cf13 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 12 Nov 2026 23:34:22 +0800 Subject: [PATCH 2/6] fix(report): check factual claims and retain withheld basis audit (#218) --- BLOCKED.md | 5 + CHANGELOG.md | 5 + docs/BUG_HISTORY.md | 15 +++ ...OGRESS-reading-postcheck-facts-20261112.md | 14 ++ frontend/DESIGN.md | 1 + .../report-reading-section.tsx | 11 +- .../lib/personal-report-reading-contract.ts | 51 +++++++- frontend/src/lib/personal-report-reading.ts | 109 +++++++++++++++- frontend/src/lib/report-export-blocks.ts | 8 +- ...t-reading-postcheck-facts-20261112.test.ts | 120 ++++++++++++++++++ 10 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md create mode 100644 frontend/tests/report-reading-postcheck-facts-20261112.test.ts diff --git a/BLOCKED.md b/BLOCKED.md index 12e85986..5aa0a54f 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,10 @@ # BLOCKED +## TASK-reading-postcheck-facts-20261112:指定样稿待回放(#218) + +- 本机缺少任务书指定的三组草稿与事实包。合成用例已覆盖题例及页面、Markdown 依据清单。详见 `docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md`。 +- 解除:在持有验收包的受控环境对 PR head 回放 A-1、A-2、B-1,记录逐节结果及健康词表误伤。只记录脱敏结果。 + ## TASK-voice-reading-20261027:真实模型输出没看(issue #113,2026-10-27) - **报告解读**:本机没有 `DS_KEY`、`BACKTEST_MODEL_KEY`、`OPENAI_API_KEY`、`ANTHROPIC_API_KEY`。解读的形状、校验、失败兜底、记账都用假模型测过;真实模型写出来的解读是什么样、几成能一次过校验、耗时多少,都没测,不得写成通过。 diff --git a/CHANGELOG.md b/CHANGELOG.md index cf35c376..ec5ff66b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-11-12 — 个人报告补充事实核对与依据清单 + +- 逐节核对宫主与落宫、标准 SAV 总和、阻碍宫及具体健康风险,并拦截没有依据的数值准确率说法。 +- 「依据与边界」未通过核对时,页面与 Markdown 显示由事实包生成的依据清单。沿用当前 Skill 版本(BUG-1860)。 + ## 2026-11-12 — 年运报告将 Narayana 列为条件参考 - 未决种子的具名分支独立列出,窗口来源数及把握等级依据独立证据。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e8459061..99a91df2 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -25112,3 +25112,18 @@ - 防复发:检查已算未用、缺真实结果、不适用理由及阻碍原因。断言宫主不在阻碍宫时不列入其宫内行星,落在该宫时列入。 - 关联记录:#216、BUG-1848、BUG-1852、BUG-1853。 - 修复版本:`codex/reading-status-axes-badhaka-20261112`。 + +## BUG-1860 | 个人报告事实复查漏拦错误结论,依据节退回后缺少说明 + +- 状态:fixed-pending-verify(合成回归通过,指定样稿待回放)。 +- 首次发现 / 最近更新:2026-11-12 / 2026-11-12。 +- 影响面:中文个人报告的逐节复查、已保存的依据与边界、Markdown 导出。 +- 用户现象:报告可能混用宫主与落宫、把标准 SAV 总和当成个人强弱、混写阻碍宫与宫主,或写入具体健康风险和数值准确率。合法的技法状态与预测边界说明可能被旧禁词误拦。依据节被拦时只剩占位说明。 +- 触发条件:模型草稿含上述表达并进入 `acceptReportReadingDraft`。 +- 根因:逐节复查未用事实包核对这些说法。「检测器」「死亡」按子串拒绝。页面和导出用同一通用占位,未保存可显示的分级依赖。 +- 修复:新增五项逐句复查,收窄两项禁词。依据节被拦时只从事实包分级生成审计清单,页面与导出共用。 +- 验证:合成用例覆盖触发与合法句式、新旧阻碍宫形状、依据节和其他节的渲染区别。已知验证缺口见 `docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md`。 +- 防复发:`report-reading-postcheck-facts-20261112.test.ts` 锁定逐节拒绝原因、存储回读与页面和 Markdown 的一致输出。 +- 相关记录:BUG-1854、BUG-1859、BUG-1848、#192。 +- 复发自:无。 +- 修复版本:`codex/reading-postcheck-facts-20261112`,待发布。 diff --git a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md new file mode 100644 index 00000000..a1c4203d --- /dev/null +++ b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md @@ -0,0 +1,14 @@ +# PROGRESS:个人报告事实复查与依据清单(#218) + +## 实施 + +- 按 Claude 放行评论 42317 接续 `codex/reading-postcheck-facts-20261112`。任务书提交 `fd1bd758895ba98908c193e1c3bb2ca2884425de` 建于 #207 合入提交 `2f374fff281622f342151a97fa6f6d758de74cc8`。开工时合入 `origin/staging` 的 #216(`718bc991368cd3eb33de60e93c22600e80a139f1`)。 +- 复查按「。;!?」分句。宫主用 `planets.<行星>.owns_houses`,落宫措辞保留。337 只作校验。阻碍宫读取 `badhaka_sthana.house` 或旧 `house`,保留独立宫主落点。新拒绝原因是 `house_lord_mismatch`、`sav_total_as_strength`、`badhaka_role_mix`、`health_outcome` 和 `accuracy_claim`。已有禁词或前置复查先命中时沿用原原因。 +- 「检测器」只放行同句同时说明「待核实」「未确认」或「候选」的「检测器检出」「检测器识别」。在「死亡」之前出现约定的否定措辞才放行。 +- 依据节被拦时在 v3 的该节保存可选审计行,页面和 Markdown 共用格式。缺失或临界依赖按 `report_evidence_grade_v1` 的状态判断。中文名通过已知 id 映射,未知 id 原样显示。历史报告没有审计数据时沿用已保存的占位内容。 +- 未更改既有断言。新增测试使用合成文字,覆盖规则触发、合法句式、只退回对应节、原有依据节通过、审计保存与两端渲染。 + +## 健康词表与样稿 + +- 健康和总评检查炎症、感染、外伤、受伤、损伤、骨折、手术、出血、发炎、结石、肝、肾、心脏、心血管、心律、肺、胃、血压、血糖、过敏、失眠、运动风险和运动损伤(由「损伤」命中)。「心理」「心情」「关心」照常通过。合成结构句「第六宫主所在位置已算出」通过。 +- #192 的可信交接将三份原样稿放在验收主机。本机检出目录、常用共享目录、本机 WSL 以及 #192、#207、#218 的 Gitea 附件均未找到该验收 ZIP。A-1、A-2、B-1 的原样回放和误伤列表待有权访问验收包的一方验证。合成测试覆盖任务书列出的例句,结果只用于本地回归。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index d3dd9b23..38590074 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -649,6 +649,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/ - **Export content:** 中文报告保存了解读时,导出列表在摘要之后列出「解读」,整篇和自选的默认正文均包含它。文件沿用页面的节标题、段落与「计算依据」标签。没有解读的报告沿用原正文。 Selected chart/table blocks append new content; selecting all groups is not byte-identical to body-only. D13 requires safe SVG in each chart, reusing the projected engine SVG or the existing hook-free Vedic drawing; offline fallback uses readable monochrome glyphs/legend and presentation attributes. No chart JSON fences or internal audit fields. Fact-table labels/precision/subtables match the reader. Full body uses the historical filename; excerpt adds `-节选`. The existing downloader appends `.md`. Chinese chart house lines use `SIGN_ZH`(白羊、金牛……,不带「座」). English chart house lines keep the English sign name. - **Structure:** inside the app shell, like every other secondary page — `/reports/[reportId]` was the last standalone full-screen route, and opening a report used to drop the reader out of the app entirely. 「个人报告」 sits in the 46px header; below it the reader owns its own scroll boundary, carrying the sticky screen chrome (back and the labelled 「导出」 button) and then the longform Markdown body beside a persistent TOC rail. Every phase — loading, generating, timed-out, and each error — renders inside the same shell, so the way back is always the rail. The five-chapter writer document is storage-only and is not rendered. Reports without Markdown show “旧版本报告,请重新生成”. +- **Reading basis fallback (BUG-1860):** when 「依据与边界」 fails the post-check, the same section shows one explanation and a list with one calculation-backed line per claim. Each line names its ceiling, required evidence that is missing or near a boundary, and disclosures. The Markdown reading carries the same list. Other withheld sections keep their one-line placeholder. - **Technique audit status (BUG-1849):** newly generated English reports use readable calculation and interpretation phrases together in the Status cell, with the evidence qualifier when present. The reader also renders the earlier status format from saved reports. - **TOC levels (2026-09-29):** chapters (`##`) are `--color-ink` at weight 600 with `--space-3` above each new chapter; sections (`###`) are indented `--space-5` and stay `--color-ink-secondary`. The two levels read apart without relying on the current-item pill. - **中文 / English (2026-09-29):** reports generated since the English edition carry both editions; the action bar shows segmented tabs (`.report-language-switch`, `aria-label="报告语言"`, 中文 / English, the same raised slot as the chart-type strip) before 「导出」. The selected segment keeps a neutral background; its text is `--color-action` and a 1px inset edge is `--color-action-strong`, matched on `[data-active]` or `[aria-selected="true"]`. Reports without an English edition show no switch at all rather than a disabled one. The edition lives in the URL (`?lang=en`, read by the server page, written with `history.replaceState`), so refresh and shared links keep it. Switching remounts the article, TOC and fact tables for that edition and sets `lang` on the reader body; chrome (header, rail, 到底部) stays Chinese. Charts draw the same glyphs; only the legend reads `Su Mo …` / 「bar = retrograde」. diff --git a/frontend/src/components/personal-report/report-reading-section.tsx b/frontend/src/components/personal-report/report-reading-section.tsx index 9313df41..be7116ca 100644 --- a/frontend/src/components/personal-report/report-reading-section.tsx +++ b/frontend/src/components/personal-report/report-reading-section.tsx @@ -1,8 +1,10 @@ import { + BASIS_AUDIT_INTRO, READING_SECTION_WITHHELD_TEXT, REPORT_READING_ANCHOR, REPORT_READING_BASIS_LABEL, REPORT_READING_HEADING, + basisAuditLine, evidenceGradeTag, readingSectionView, type ReportReading, @@ -38,7 +40,14 @@ export function ReportReadingSection({ reading }: { reading: ReportReading }) {

{section.title}

{view.status === "withheld" ? ( -

{READING_SECTION_WITHHELD_TEXT}

+ view.basisAudit ? ( + <> +

{BASIS_AUDIT_INTRO}

+
    + {view.basisAudit.map((row) =>
  • {basisAuditLine(row)}
  • )} +
+ + ) :

{READING_SECTION_WITHHELD_TEXT}

) : view.paragraphs.map((paragraph, paragraphIndex) => (

{paragraph.text}

diff --git a/frontend/src/lib/personal-report-reading-contract.ts b/frontend/src/lib/personal-report-reading-contract.ts index 491d7703..26b514be 100644 --- a/frontend/src/lib/personal-report-reading-contract.ts +++ b/frontend/src/lib/personal-report-reading-contract.ts @@ -85,6 +85,13 @@ const storedParagraphV3 = z.strictObject({ claimType: z.enum(REPORT_CLAIM_TYPES).optional(), grade: z.enum(EVIDENCE_GRADES).optional(), }); +const basisAuditRow = z.strictObject({ + claimType: z.string().min(1), + cap: z.enum(EVIDENCE_GRADES), + missingRequired: z.array(z.string()), + mustDisclose: z.array(z.string()), +}); +export type BasisAuditRow = z.infer; /** A section that failed the post-check is kept as a visible placeholder (owner 2026-11-12). */ const storedSectionV3 = z.union([ z.strictObject({ @@ -98,6 +105,7 @@ const storedSectionV3 = z.union([ title: z.string().min(1).max(12), status: z.literal("withheld"), reason: z.string().regex(/^[a-z_]{1,64}$/), + basisAudit: z.array(basisAuditRow).optional(), }), ]); /** Which of the astrologer's files the reading was written with (her §7.3: actually_loaded, not a path). */ @@ -134,6 +142,41 @@ export type ReportReadingParagraph = Readonly<{ text: string; grade?: EvidenceGr /** Shown in place of a section that did not pass the post-check (owner 2026-11-12). */ export const READING_SECTION_WITHHELD_TEXT = "这一节没有通过数据核对,暂不显示。"; +export const BASIS_AUDIT_INTRO = "这一节的模型文字没有通过核对,下面是由计算直接生成的依据清单。"; +const CLAIM_LABELS: Readonly> = { + character_structure: "性格结构", + career_promise: "事业倾向", + wealth_earning: "获得收入", + wealth_retention: "留住收入", + relationship_structure: "关系结构", + health_pressure: "健康压力", + health_obstruction: "健康阻碍", + timing_window: "时间窗口", +}; +const DEPENDENCY_LABELS: Readonly> = { + "v:D1": "本命盘上升边界", "v:D2": "财富分盘上升边界", "v:D6": "健康分盘上升边界", + "v:D8": "D8 上升边界", "v:D9": "九分盘上升边界", "v:D10": "事业分盘上升边界", "v:D30": "D30 上升边界", + "s:functional": "功能吉凶", "s:d9": "九分盘", "s:shadbala": "六力", "s:avastha": "星曜状态", + "s:dispositor_chain": "定位星链", "s:ashtakavarga": "八分法", "s:argala": "助阻关系", + "s:vimshottari": "Vimshottari 大运", "s:narayana": "Narayana 大运", "s:yogini": "Yogini 大运", "s:kp": "KP 判宫", + "p:career:D10": "事业分盘", "p:career:AmK": "事业指标星(AmK)", "p:career:AK": "自我指标星(AK)", + "p:career:Karakamsha": "Karakamsha", "p:career:A10": "事业映射点(A10)", "p:career:AL": "上升映射点(AL)", + "p:career:D1_D9_D10": "本命、九分与事业分盘合看", + "p:wealth:D2": "财富分盘(D2)", "p:wealth:D11": "财富参考分盘(D11)", "p:wealth:D10": "事业分盘(D10)", + "p:wealth:A2": "第二宫映射点(A2)", "p:wealth:A11": "第十一宫映射点(A11)", "p:wealth:A10": "事业映射点(A10)", + "p:marriage:D9_7": "九分盘第七宫", "p:marriage:UL": "婚姻映射点(UL)", "p:marriage:A7": "第七宫映射点(A7)", + "p:marriage:DK": "伴侣指标星(DK)", "p:marriage:Vivah": "婚姻参考点(Vivah)", + "p:health:D6": "健康分盘(D6)", "p:health:D8": "D8 分盘", "p:health:D30": "D30 分盘", + "p:health:drekkana22": "第 22 个三分盘", "p:health:Badhaka": "阻碍宫和宫主", + "t:transits": "行运", "t:annual": "年盘", "t:double_transit": "双行运", + purpose_stability_not_evaluated: "未评估出生时间对这类判断的影响", +}; + +export function basisAuditLine(row: BasisAuditRow): string { + const claim = Object.hasOwn(CLAIM_LABELS, row.claimType) ? CLAIM_LABELS[row.claimType as ReportClaimType] : row.claimType; + const labels = (ids: string[]) => ids.map((id) => Object.hasOwn(DEPENDENCY_LABELS, id) ? DEPENDENCY_LABELS[id] : id).join("、") || "无"; + return `${claim} · 上限 ${row.cap} · 缺失或临界的必需依据 ${labels(row.missingRequired)} · 必须说明 ${labels(row.mustDisclose)}`; +} export const EVIDENCE_GRADE_LABEL = "证据等级"; /** * The line under a graded paragraph (owner 2026-11-12, prototype option B): the @@ -150,9 +193,13 @@ export const EVIDENCE_GRADE_MEANING: Readonly> = { /** One reading section as the page and the export show it, for every stored version. */ export function readingSectionView(section: ReportReading["sections"][number]): | Readonly<{ status: "ok"; paragraphs: readonly ReportReadingParagraph[] }> - | Readonly<{ status: "withheld" }> { + | Readonly<{ status: "withheld"; basisAudit?: readonly BasisAuditRow[] }> { if (!("status" in section)) return { status: "ok", paragraphs: section.paragraphs.map((text) => ({ text })) }; - if (section.status === "withheld") return { status: "withheld" }; + if (section.status === "withheld") { + return section.id === "basis" && section.basisAudit + ? { status: "withheld", basisAudit: section.basisAudit } + : { status: "withheld" }; + } return { status: "ok", paragraphs: section.paragraphs.map(({ text, grade }) => (grade ? { text, grade } : { text })) }; } diff --git a/frontend/src/lib/personal-report-reading.ts b/frontend/src/lib/personal-report-reading.ts index c2c1d5ef..ad80ebd9 100644 --- a/frontend/src/lib/personal-report-reading.ts +++ b/frontend/src/lib/personal-report-reading.ts @@ -17,6 +17,7 @@ import { MIN_TOTAL_CHARS, REPORT_READING_SECTIONS, reportReadingDraftSchema, + type BasisAuditRow, type EvidenceGrade, type ReportReading, } from "./personal-report-reading-contract"; @@ -33,10 +34,10 @@ export const REPORT_READING_TIMEOUT_MS = 120_000; /** Never in the reading: deterministic fate, medical, investment (AGENTS Part B4). */ const BANNED_PHRASES = [ "注定", "先苦后甜", "宿命", "命中注定", - "癌", "肿瘤", "糖尿病", "高血压", "心脏病", "抑郁症", "焦虑症", "不孕", "流产", "早逝", "寿命", "死亡", "去世", + "癌", "肿瘤", "糖尿病", "高血压", "心脏病", "抑郁症", "焦虑症", "不孕", "流产", "早逝", "寿命", "去世", "股票", "基金", "炒股", "期货", "比特币", "加密货币", "买入", "卖出", "抄底", "投资建议", // Internal wording the engine report leaked (research §4.1). - "本仓", "检测器", "字段", "口径", + "本仓", "字段", "口径", // One name per thing, same as the chat (罗睺 / 计都). "北交点", "南交点", "克图", "凯图", // Web search and case calibration stay backstage (product 2026-11-08, BUG-1794); the page shows the grade line. @@ -87,9 +88,14 @@ export type ReadingRejection = | "current_advice_in_replay" | "double_transit_scope" | "yoga_status_mismatch" + | "house_lord_mismatch" + | "sav_total_as_strength" + | "badhaka_role_mix" + | "health_outcome" + | "accuracy_claim" | "all_sections_withheld"; -type Claims = Readonly>; +type Claims = Readonly>; function claimsOf(facts: Record | null): Claims { const grades = facts?.evidence_grades as { contract?: unknown; claims?: unknown } | undefined; @@ -246,6 +252,95 @@ function dignityMismatch(text: string, dignities: Map>): boo return false; } +const SENTENCE_END = /[。;!?]/; +const PLANET_KEYS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"] as const; +const HOUSE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"]; +const HOUSE_NUMBER = "(?:1[0-2]|[1-9]|十[一二]?|[一二三四五六七八九])"; +const HOUSE_LIST = `第\\s*${HOUSE_NUMBER}(?:\\s*宫?\\s*(?:[、,,]|与|和|及|以及|或)\\s*第?\\s*${HOUSE_NUMBER})*\\s*宫`; +const PLANET = `(${PLANETS_ZH.join("|")})`; +const LORD_FORWARD = new RegExp(`${PLANET}\\s*(?:(?:掌管|主管|管(?:辖)?)\\s*(${HOUSE_LIST})|(?:是|为)\\s*(${HOUSE_LIST})\\s*(?:的\\s*宫)?主)`, "g"); +const LORD_BACKWARD = new RegExp(`(${HOUSE_LIST})\\s*(?:的\\s*宫|宫)?主\\s*(?:是|为|由)?\\s*${PLANET}`, "g"); +const NUMBERED_HOUSE = new RegExp(`第\\s*(${HOUSE_NUMBER})\\s*宫`, "g"); +const HOUSE_VALUES = new RegExp(HOUSE_NUMBER, "g"); +const SAV_TOTAL = /(? | null): boolean { + const planets = facts?.planets as Record | undefined; + const conflicts = (name: string, houses: string) => { + const index = PLANETS_ZH.findIndex((planet) => planet === name); + const key = PLANET_KEYS[index]; + const owns = key ? planets?.[key]?.owns_houses : undefined; + return Array.isArray(owns) && [...houses.matchAll(HOUSE_VALUES)].some(([house]) => !owns.includes(houseNumber(house))); + }; + for (const [, planet, managedHouses, lordHouses] of sentence.matchAll(LORD_FORWARD)) { + if (conflicts(planet, managedHouses ?? lordHouses)) return true; + } + for (const [, houses, planet] of sentence.matchAll(LORD_BACKWARD)) { + if (conflicts(planet, houses)) return true; + } + return false; +} + +function badhakaHouse(facts: Record | null): unknown { + const domains = facts?.domains as { health?: { participation?: unknown } } | undefined; + const participation = domains?.health?.participation; + if (!Array.isArray(participation)) return undefined; + const value = participation.find((row) => row?.id === "Badhaka")?.value; + return value?.badhaka_sthana?.house ?? value?.house; +} + +function checkedFactClaim(text: string, section: string, facts: Record | null): ReadingRejection | undefined { + const obstructionHouse = badhakaHouse(facts); + for (const sentence of text.split(SENTENCE_END)) { + if (houseLordMismatch(sentence, facts)) return "house_lord_mismatch"; + if (SAV_TOTAL.test(sentence) && SAV_STRENGTH.test(sentence)) return "sav_total_as_strength"; + if (sentence.includes("阻碍星") && [...sentence.matchAll(NUMBERED_HOUSE)].some(([, house]) => houseNumber(house) === obstructionHouse)) { + return "badhaka_role_mix"; + } + if ((section === "health" || section === "overview") && HEALTH_OUTCOME.test(sentence)) return "health_outcome"; + if (ACCURACY_WORD.test(sentence) && PERCENTAGE.test(sentence)) return "accuracy_claim"; + } + return undefined; +} + +function basisAudit(claims: Claims): BasisAuditRow[] { + const rows: BasisAuditRow[] = []; + for (const [claimType, claim] of Object.entries(claims)) { + const cap = EVIDENCE_GRADES.find((grade) => grade === claim?.cap); + if (!claimType || !cap) continue; + const missingRequired = Array.isArray(claim.required) + ? claim.required.filter((dep) => typeof dep?.id === "string" && MISSING_DEPENDENCY.has(dep.state)).map((dep) => dep.id as string) + : []; + const mustDisclose = Array.isArray(claim.must_disclose) + ? claim.must_disclose.filter((id): id is string => typeof id === "string") + : []; + rows.push({ claimType, cap, missingRequired, mustDisclose }); + } + return rows; +} + type DraftSection = Readonly<{ id: string; paragraphs: ReadonlyArray<{ text: string; claim_type: string | null; grade: EvidenceGrade | null }>; @@ -264,7 +359,7 @@ function checkSection( facts: Record | null, ): { section: CheckedSection; chars: number } { const withheld = (reason: ReadingRejection) => ({ - section: { id: section.id, title, status: "withheld" as const, reason }, + section: { id: section.id, title, status: "withheld" as const, reason, ...(section.id === "basis" ? { basisAudit: basisAudit(claims) } : {}) }, chars: 0, }) as { section: CheckedSection; chars: number }; const graded = (GRADED_SECTION_IDS as readonly string[]).includes(section.id); @@ -277,12 +372,16 @@ function checkSection( if (!text) return withheld("too_short"); if (text.length > MAX_PARAGRAPH_CHARS) return withheld("too_long"); if (/[_={}<>#*|`]/.test(text)) return withheld("field_name"); - if (BANNED_PHRASES.some((phrase) => text.includes(phrase)) || CERTAINTY.test(text)) return withheld("banned_phrase"); + if (BANNED_PHRASES.some((phrase) => text.includes(phrase)) || CERTAINTY.test(text) || text.split(SENTENCE_END).some(bannedQualifiedPhrase)) { + return withheld("banned_phrase"); + } if (dignityMismatch(text, dignities)) return withheld("dignity_mismatch"); if (falseMissingLayer(text, computed)) return withheld("false_missing_layer"); if (currentAdviceInReplay(text, section.id, facts)) return withheld("current_advice_in_replay"); if (doubleTransitOutOfScope(text, section.id, facts)) return withheld("double_transit_scope"); if (yogaStatusMismatch(text, facts)) return withheld("yoga_status_mismatch"); + const factRejection = checkedFactClaim(text, section.id, facts); + if (factRejection) return withheld(factRejection); disclaimers += text.match(DISCLAIMER)?.length ?? 0; chars += text.length; if (!graded) { diff --git a/frontend/src/lib/report-export-blocks.ts b/frontend/src/lib/report-export-blocks.ts index 18df49e2..afb64fe0 100644 --- a/frontend/src/lib/report-export-blocks.ts +++ b/frontend/src/lib/report-export-blocks.ts @@ -9,9 +9,11 @@ import type { ReportLanguage } from "./report-language"; import { SIGN_ZH, type SignName } from "./chart-view-labels"; import { projectOrdinaryReportMarkdown } from "./report-public-projection"; import { + BASIS_AUDIT_INTRO, READING_SECTION_WITHHELD_TEXT, REPORT_READING_BASIS_LABEL, REPORT_READING_HEADING, + basisAuditLine, evidenceGradeTag, readingSectionView, type ReportReading, @@ -91,7 +93,11 @@ export function buildReportExportBlocks({ markdown, charts = [], factTables = [] `## ${REPORT_READING_HEADING}`, ...reading.sections.flatMap(section => { const view = readingSectionView(section); - // #192: the export carries the same grade tags and withheld line as the page. + if (view.status === "withheld" && view.basisAudit) { + const lines = view.basisAudit.map((row) => `- ${basisAuditLine(row).replace(/[\\`*_[\]<>&]/g, "\\$&").replace(/[\r\n]+/g, " ")}`); + return [`### ${section.title}`, BASIS_AUDIT_INTRO, lines.join("\n")].filter(Boolean); + } + // The export carries the same grade tags and fallback as the page. return [`### ${section.title}`, ...(view.status === "withheld" ? [READING_SECTION_WITHHELD_TEXT] : view.paragraphs.map(paragraph => paragraph.grade ? `${paragraph.text}\n\n${evidenceGradeTag(paragraph.grade)}` : paragraph.text))]; diff --git a/frontend/tests/report-reading-postcheck-facts-20261112.test.ts b/frontend/tests/report-reading-postcheck-facts-20261112.test.ts new file mode 100644 index 00000000..81837178 --- /dev/null +++ b/frontend/tests/report-reading-postcheck-facts-20261112.test.ts @@ -0,0 +1,120 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { ReportReadingSection } from "../src/components/personal-report/report-reading-section"; +import { + acceptReportReadingDraft, + REPORT_READING_SECTIONS, + reportReadingSchema, + type ReadingRejection, +} from "../src/lib/personal-report-reading"; +import { BASIS_AUDIT_INTRO, EVIDENCE_GRADE_CONTRACT, READING_SECTION_WITHHELD_TEXT } from "../src/lib/personal-report-reading-contract"; +import { buildReportExportBlocks } from "../src/lib/report-export-blocks"; + +type Section = (typeof REPORT_READING_SECTIONS)[number]["id"]; +const NEUTRAL = "这一段只说明虚构材料的记录顺序以及供比对的线索。叙述保留记录本身的范围和时间,也指出没有得到回答的问题。这里的细节用于检验逐节复查的行为。".repeat(2); +const CLAIMS: Partial> = { + character: "character_structure", career: "career_promise", wealth: "wealth_earning", + love: "relationship_structure", health: "health_pressure", now: "timing_window", +}; +const FACTS = { + planets: { Mercury: { owns_houses: [1, 10], d1_house: 5 }, Jupiter: { owns_houses: [7, 12], d1_house: 10 } }, + domains: { health: { participation: [{ id: "Badhaka", value: { badhaka_sthana: { house: 7 }, badhakesh: { planet: "Jupiter", house: 10 } } }] } }, + evidence_grades: { + contract: EVIDENCE_GRADE_CONTRACT, + claims: { + ...Object.fromEntries(Object.entries(CLAIMS).map(([section, claim]) => [claim, { cap: "C", sections: [section] }])), + wealth_earning: { + cap: "C", sections: ["wealth"], + required: [{ id: "v:D2", state: "boundary_sensitive" }, { id: "v:D1", state: "clear_at_tolerance" }], + must_disclose: ["s:shadbala", "unknown_dependency"], + }, + unknown_claim: { cap: "D", required: [{ id: "custom:missing", state: "not_computed" }], must_disclose: [] }, + }, + }, +}; + +function reading(overrides: Partial>, facts: Record = FACTS) { + const sections = REPORT_READING_SECTIONS.map(({ id }) => ({ + id, + paragraphs: [NEUTRAL, overrides[id] ?? NEUTRAL].map((text) => ({ + text, claim_type: CLAIMS[id] ?? null, grade: CLAIMS[id] ? "C" : null, + })), + })); + const result = acceptReportReadingDraft({ sections }, facts, { + name: "reading", version: "test", loaded: [{ path: "SKILL.md", sha256: "0".repeat(64) }], + }); + assert.ok(result.ok && result.reading.version === 3, JSON.stringify(result)); + return result.reading; +} + +function verify(section: Section, text: string, reason?: ReadingRejection, facts: Record = FACTS) { + const checked = reading({ [section]: text }, facts).sections; + assert.deepEqual(checked.filter((part) => part.status === "withheld").map((part) => [part.id, part.reason]), + reason ? [[section, reason]] : [], text); +} + +test("house lord checks every listed house using ownership and skips placement", () => { + for (const text of ["水星掌管第五宫与第十宫。", "第五宫的宫主水星。", "水星主管第1宫、5宫。", "水星为第五、十宫主。"]) { + verify("wealth", text, "house_lord_mismatch"); + } + for (const text of ["水星为第一、十宫主,落第五宫。", "第十宫主水星位于第五宫。", "水星坐第5宫。", "水星在这里,木星掌管第十二宫。", "第七宫主是木星,水星落在第五宫。", "水星是第五宫里的参考星。"] ) { + verify("wealth", text); + } +}); + +test("standard SAV total is a checksum and cannot stand in for individual strength", () => { + verify("wealth", "SAV 总分为337,显示不缺维持力。", "sav_total_as_strength"); + verify("wealth", "三百三十七说明力量充足。", "sav_total_as_strength"); + verify("wealth", "SAV 合计337,与标准值一致。另有一项力量记录。 "); +}); + +test("Badhaka separates the obstruction house from the lord's placement in both packet shapes", () => { + verify("health", "阻碍星落在第七宫。", "badhaka_role_mix"); + verify("health", "阻碍宫在第七宫。阻碍宫主木星在第十宫。"); + verify("health", "阻碍星落在第7宫。", "badhaka_role_mix", { + ...FACTS, + domains: { health: { participation: [{ id: "Badhaka", value: { house: 7, lord: "Jupiter" } }] } }, + }); +}); + +test("health and overview withhold specific risks while ordinary structural language stays", () => { + verify("health", "运动或炎症类风险。", "health_outcome"); + verify("overview", "这里预测心律问题。", "health_outcome"); + verify("health", "第六宫主所在位置已算出。心理、心情和关心是三种说法。"); + verify("wealth", "这个合成句用炎症作为排版词。"); +}); + +test("detector and death wording keep only sentence-local qualified mentions", () => { + verify("basis", "检测器检出,待核实。检测器识别的组合是候选。本报告不得预测死亡。"); + for (const text of ["检测器显示。", "检测器检出。另一条待核实。", "有死亡风险。", "死亡不预测。", "不得夸大。还有死亡风险。"]) { + verify("basis", text, "banned_phrase"); + } + verify("basis", "检测器识别的结果未确认,但本仓有对应记录。", "banned_phrase"); +}); + +test("percentages and fractions expressed as 成 cannot support accuracy claims", () => { + for (const text of ["成功率达85%。", "准确率接近九成。", "命中率为百分之九十。", "命中率有几成。"] ) { + verify("overview", text, "accuracy_claim"); + } + verify("overview", "没有计算准确率。另一项占九成。"); +}); + +test("withheld basis persists a fact-only audit for the page and Markdown", () => { + const checked = reading({ basis: "检测器显示。", health: "这里预测骨折。" }); + const stored = reportReadingSchema.parse(JSON.parse(JSON.stringify(checked))); + const rendered = renderToStaticMarkup(createElement(ReportReadingSection, { reading: stored })); + const markdown = buildReportExportBlocks({ markdown: "# 合成资料", reading: stored }).find((block) => block.id === "body:reading")!.text; + for (const output of [rendered, markdown]) { + assert.ok(output.includes(BASIS_AUDIT_INTRO)); + assert.ok(output.includes("获得收入 · 上限 C · 缺失或临界的必需依据 财富分盘上升边界 · 必须说明 六力")); + assert.ok(output.includes("custom:missing")); + assert.equal(output.split(READING_SECTION_WITHHELD_TEXT).length - 1, 1); + assert.ok(!output.includes("检测器显示") && !output.includes("本命盘上升边界")); + } + assert.match(rendered, /unknown_claim.*unknown_dependency|unknown_dependency.*unknown_claim/); + assert.ok(markdown.includes("unknown\\_claim") && markdown.includes("unknown\\_dependency")); + assert.ok(!JSON.stringify(reading({})).includes("basisAudit")); +}); From ad2773593d59bb41a2070efba7c1b7da846c3c59 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 13 Nov 2026 00:30:19 +0800 Subject: [PATCH 3/6] docs(report): record #218 checks and Claude replay handoff --- BLOCKED.md | 2 +- docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/BLOCKED.md b/BLOCKED.md index 5aa0a54f..ca7ce01d 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -3,7 +3,7 @@ ## TASK-reading-postcheck-facts-20261112:指定样稿待回放(#218) - 本机缺少任务书指定的三组草稿与事实包。合成用例已覆盖题例及页面、Markdown 依据清单。详见 `docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md`。 -- 解除:在持有验收包的受控环境对 PR head 回放 A-1、A-2、B-1,记录逐节结果及健康词表误伤。只记录脱敏结果。 +- 解除:按本轮用户安排,由 Claude 在持有验收包的验收主机对 PR head 回放 A-1、A-2、B-1,记录逐节结果及健康词表误伤。只记录脱敏结果。 ## TASK-voice-reading-20261027:真实模型输出没看(issue #113,2026-10-27) diff --git a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md index a1c4203d..06ba78be 100644 --- a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md +++ b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md @@ -11,4 +11,11 @@ ## 健康词表与样稿 - 健康和总评检查炎症、感染、外伤、受伤、损伤、骨折、手术、出血、发炎、结石、肝、肾、心脏、心血管、心律、肺、胃、血压、血糖、过敏、失眠、运动风险和运动损伤(由「损伤」命中)。「心理」「心情」「关心」照常通过。合成结构句「第六宫主所在位置已算出」通过。 -- #192 的可信交接将三份原样稿放在验收主机。本机检出目录、常用共享目录、本机 WSL 以及 #192、#207、#218 的 Gitea 附件均未找到该验收 ZIP。A-1、A-2、B-1 的原样回放和误伤列表待有权访问验收包的一方验证。合成测试覆盖任务书列出的例句,结果只用于本地回归。 +- #192 的可信交接将三份原样稿放在验收主机。本机检出目录、常用共享目录、本机 WSL 以及 #192、#207、#218 的 Gitea 附件均未找到该验收 ZIP。按本轮用户安排,由 Claude 在验收主机对 PR head 回放 A-1、A-2、B-1,记录逐节结果与健康词表误伤。合成测试覆盖任务书列出的例句,结果只用于本地回归。 + +## 验证 + +- 开工基线上的四份定向测试 38 项通过。合入 #217 后使用 staging `b1bb5c53fd19307caf7e4f2fd519bd7809daffe6` 为最终对照,保留两单 Bug 记录和变更日志。五份定向测试 40 项通过,含 #217 的提示与附录回归。 +- 合并后 `npm run lint` 为 0 错误、62 条既有警告。生产构建成功,首页 `/` 保持 `○ Static`。与 staging 相同的前端树逐项比较首页 39 个首屏资源,JS gzip 均为 515860 字节,CSS gzip 由 90886 变为 90906 字节(+0.022%)。 +- TypeScript `--noEmit` 通过。前端全量按 `npm test` 同一文件清单与同一并发数对照。staging 3665 项中 3638 通过,分支 3672 项中 3645 通过,两组均有相同的 27 项失败、0 取消、0 跳过,退出码均为 1。既有测试名完整保留,新增 7 项全部通过。 +- Windows 快速门统一设置 `PYTHONUTF8=1` 后重跑,1463 项通过、2 项跳过、29 个子测试通过,退出 1。失败的 3 项均为 `tests/test_upstream_technique_layers.py` 的既有基线失败(固定公开盘、大运注册表与 Kaala 对照)。第一次重试的 GBK 子进程解码失败在最终运行中消失。隐私守卫 62 项通过。 From bf41b9cd0008f88e2447427d7a1a58e8f685bc94 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Nov 2026 00:34:52 +0800 Subject: [PATCH 4/6] docs(tasks): #218 fix brief 1 (varga lord false positive, status/boundary phrases) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbBXozsiu43ob7B8UZCmDM --- docs/tasks/FIX-reading-postcheck-facts-1.md | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/tasks/FIX-reading-postcheck-facts-1.md diff --git a/docs/tasks/FIX-reading-postcheck-facts-1.md b/docs/tasks/FIX-reading-postcheck-facts-1.md new file mode 100644 index 00000000..777df047 --- /dev/null +++ b/docs/tasks/FIX-reading-postcheck-facts-1.md @@ -0,0 +1,59 @@ +# #218 验收退回:分盘宫主误拦、合法状态说明误拦(2026-11-13,Claude) + +本文件补充 `TASK-reading-postcheck-facts-20261112.md`,两者冲突时以本文件为准。 + +验收对象是 `ad277359`。其余部分已核对通过,不要改动:5 个新增退回原因、依据清单、页面与 MD 导出、健康词表。 + +## 原样稿回放结果(验收主机,脱敏) + +用上一版验收包里三份原始稿和当时的事实包回放: + +| 稿 | 结果 | +|---|---| +| A-1 | 健康节被拦:current_advice_in_replay。建议节被拦:double_transit_scope。依据节被拦:banned_phrase(误拦,见问题 2)。依据清单 8 行正常生成 | +| A-2 | 财富节被拦:house_lord_mismatch(应拦)。健康节被拦:badhaka_role_mix(应拦)。建议节被拦:current_advice_in_replay(应拦)。**事业节、感情节被拦:house_lord_mismatch(误拦,见问题 1)** | +| B-1 | 健康节被拦:health_outcome(应拦,「炎症」)。其余各节通过。三份稿里健康词表没有其他误伤 | + +## 问题 1:分盘里的宫主被当作本命宫主核对 + +误拦的两句: + +- 「D10 上升金牛,其第十宫主土星在 D10 第三宫」:在 D10 里,第十宫是水瓶,宫主是土星,这句是对的。 +- 「D9 婚姻分盘里……第七宫主月亮落在天蝎」:在 D9 里,第七宫是巨蟹,宫主是月亮,这句也是对的。 + +`houseLordMismatch` 一律用本命 `planets..owns_houses` 核对,所以把这两句误判了。 + +**改法:** + +- 同一分句里出现分盘标记时,不按本命核对。分盘标记包括:`D` 加数字(D2–D60)、「分盘」、「九分」、「事业盘」、「财富盘」、「健康盘」、「障碍盘」。 +- 如果事实包里有该分盘的上升星座,可以按「从该分盘上升数宫,再取宫主」来核对。没有就跳过。 +- 跳过的规则写进 PROGRESS。 + +## 问题 2:合法状态说明和边界说明被禁用词误拦 + +误拦的两句: + +- 「多条 Yoga 按形成状态仍是检测器命中/待核实」:这是合法的状态说明(她 §4)。现行规则只放行「检测器」后面紧跟「检出」或「识别」的写法。 +- 「不写疾病名、诊断、死亡、离婚……」:这是边界说明。否定词表里没有「不写」。 + +**改法:** + +- 「检测器」后面可以紧跟的词,加上「命中」。另外,同一分句里在「检测器」之后出现「待核实」「未确认」「候选」就算满足,中间允许有「/」「、」「,」。 +- 「死亡」前面的否定词,加上:不写、不谈、不涉及、不判断、不给、不讨论、不推断、避免。 + +## 验收标准 + +1. 新增测试(全部用合成文字,按上面四句的结构仿写): + - 分盘宫主的两句不触发; + - 本命里「水星掌管第五宫与第十宫」仍然触发; + - 「检测器命中/待核实」不触发; + - 「不写……死亡……」不触发; + - 「检测器显示」「有死亡风险」仍然触发。 +2. 原有测试全部通过。 +3. 我会在验收主机上再回放一遍。预期结果:A-2 的事业节、感情节通过;A-1 的依据节不再因 banned_phrase 被拦(如果因别的原因被拦,记下原因)。 + +## 不要碰 + +原任务书的「不要碰」清单继续有效。BLOCKED.md 中 #218 的条目在本轮删除,因为回放已由 Claude 完成。 + +BUG-1860:在原条目里补一行「验收退回:分盘宫主、合法状态说明误拦」,只改这一条。 From 1eee482aad431bec4ee0246239285a03867efaf2 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 13 Nov 2026 01:16:51 +0800 Subject: [PATCH 5/6] fix(report): preserve divisional lord and qualified basis wording (#218) --- BLOCKED.md | 5 ---- docs/BUG_HISTORY.md | 1 + ...OGRESS-reading-postcheck-facts-20261112.md | 8 +++++- frontend/src/lib/personal-report-reading.ts | 9 ++++--- ...t-reading-postcheck-facts-20261112.test.ts | 26 +++++++++++++++++++ 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/BLOCKED.md b/BLOCKED.md index ca7ce01d..12e85986 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,10 +1,5 @@ # BLOCKED -## TASK-reading-postcheck-facts-20261112:指定样稿待回放(#218) - -- 本机缺少任务书指定的三组草稿与事实包。合成用例已覆盖题例及页面、Markdown 依据清单。详见 `docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md`。 -- 解除:按本轮用户安排,由 Claude 在持有验收包的验收主机对 PR head 回放 A-1、A-2、B-1,记录逐节结果及健康词表误伤。只记录脱敏结果。 - ## TASK-voice-reading-20261027:真实模型输出没看(issue #113,2026-10-27) - **报告解读**:本机没有 `DS_KEY`、`BACKTEST_MODEL_KEY`、`OPENAI_API_KEY`、`ANTHROPIC_API_KEY`。解读的形状、校验、失败兜底、记账都用假模型测过;真实模型写出来的解读是什么样、几成能一次过校验、耗时多少,都没测,不得写成通过。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index bec59506..27aaeb54 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -25131,6 +25131,7 @@ ## BUG-1860 | 个人报告事实复查漏拦错误结论,依据节退回后缺少说明 - 状态:fixed-pending-verify(合成回归通过,指定样稿待回放)。 +- 验收退回:分盘宫主、合法状态说明误拦。 - 首次发现 / 最近更新:2026-11-12 / 2026-11-12。 - 影响面:中文个人报告的逐节复查、已保存的依据与边界、Markdown 导出。 - 用户现象:报告可能混用宫主与落宫、把标准 SAV 总和当成个人强弱、混写阻碍宫与宫主,或写入具体健康风险和数值准确率。合法的技法状态与预测边界说明可能被旧禁词误拦。依据节被拦时只剩占位说明。 diff --git a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md index 06ba78be..6f81d5fe 100644 --- a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md +++ b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md @@ -11,7 +11,7 @@ ## 健康词表与样稿 - 健康和总评检查炎症、感染、外伤、受伤、损伤、骨折、手术、出血、发炎、结石、肝、肾、心脏、心血管、心律、肺、胃、血压、血糖、过敏、失眠、运动风险和运动损伤(由「损伤」命中)。「心理」「心情」「关心」照常通过。合成结构句「第六宫主所在位置已算出」通过。 -- #192 的可信交接将三份原样稿放在验收主机。本机检出目录、常用共享目录、本机 WSL 以及 #192、#207、#218 的 Gitea 附件均未找到该验收 ZIP。按本轮用户安排,由 Claude 在验收主机对 PR head 回放 A-1、A-2、B-1,记录逐节结果与健康词表误伤。合成测试覆盖任务书列出的例句,结果只用于本地回归。 +- Claude 已在验收主机对首轮提交回放 A-1、A-2、B-1。A-2 财富、健康与 B-1 健康的预期拦截均生效,依据清单正常生成,健康词表没有其他误伤。A-2 的事业、感情及 A-1 的依据存在误拦,详见修复单。原样稿与事实包保留在验收主机,首轮本机缺包条目已解除。 ## 验证 @@ -19,3 +19,9 @@ - 合并后 `npm run lint` 为 0 错误、62 条既有警告。生产构建成功,首页 `/` 保持 `○ Static`。与 staging 相同的前端树逐项比较首页 39 个首屏资源,JS gzip 均为 515860 字节,CSS gzip 由 90886 变为 90906 字节(+0.022%)。 - TypeScript `--noEmit` 通过。前端全量按 `npm test` 同一文件清单与同一并发数对照。staging 3665 项中 3638 通过,分支 3672 项中 3645 通过,两组均有相同的 27 项失败、0 取消、0 跳过,退出码均为 1。既有测试名完整保留,新增 7 项全部通过。 - Windows 快速门统一设置 `PYTHONUTF8=1` 后重跑,1463 项通过、2 项跳过、29 个子测试通过,退出 1。失败的 3 项均为 `tests/test_upstream_technique_layers.py` 的既有基线失败(固定公开盘、大运注册表与 Kaala 对照)。第一次重试的 GBK 子进程解码失败在最终运行中消失。隐私守卫 62 项通过。 + +## 验收返工(2026-11-13) + +- 按 Claude 评论 42471 及 `docs/tasks/FIX-reading-postcheck-facts-1.md` 修复,沿用 PR #225。包含 D2–D60 或任务书列出的分盘称呼的分句按修复单跳过宫主核对。D1 与其他分句仍核对本命宫主。 +- 「检测器」后可跟「命中」。每处检测器措辞需要同一分句内随后出现待核实、未确认或候选,支持斜杠、顿号和逗号。「死亡」按修复单扩充同句前置边界词。合成回归保留原禁词和本命宫主触发断言。 +- 由 Claude 在验收主机对新提交再回放三份样稿,核对 A-2 的事业、感情及 A-1 依据节的误拦解除情况。 diff --git a/frontend/src/lib/personal-report-reading.ts b/frontend/src/lib/personal-report-reading.ts index ad80ebd9..75420396 100644 --- a/frontend/src/lib/personal-report-reading.ts +++ b/frontend/src/lib/personal-report-reading.ts @@ -253,6 +253,7 @@ function dignityMismatch(text: string, dignities: Map>): boo } const SENTENCE_END = /[。;!?]/; +const DIVISIONAL_CHART = /\bD(?:[2-9]|[1-5]\d|60)\b|分盘|九分|事业盘|财富盘|健康盘|障碍盘/i; const PLANET_KEYS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"] as const; const HOUSE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"]; const HOUSE_NUMBER = "(?:1[0-2]|[1-9]|十[一二]?|[一二三四五六七八九])"; @@ -272,12 +273,11 @@ const MISSING_DEPENDENCY = new Set([ ]); function bannedQualifiedPhrase(sentence: string): boolean { - const unconfirmed = /待核实|未确认|候选/.test(sentence); - for (const match of sentence.matchAll(/检测器/g)) { - if (!unconfirmed || !/^(?:检出|识别)/.test(sentence.slice(match.index + match[0].length))) return true; + for (const mention of sentence.split("检测器").slice(1)) { + if (!/^(?:检出|识别|命中)/.test(mention) || !/待核实|未确认|候选/.test(mention)) return true; } for (const match of sentence.matchAll(/死亡/g)) { - if (!/不得|不能|不会|不作|不做|不预测|禁止/.test(sentence.slice(0, match.index))) return true; + if (!/不得|不能|不会|不作|不做|不预测|禁止|不写|不谈|不涉及|不判断|不给|不讨论|不推断|避免/.test(sentence.slice(0, match.index))) return true; } return false; } @@ -287,6 +287,7 @@ function houseNumber(text: string): number { } function houseLordMismatch(sentence: string, facts: Record | null): boolean { + if (DIVISIONAL_CHART.test(sentence)) return false; const planets = facts?.planets as Record | undefined; const conflicts = (name: string, houses: string) => { const index = PLANETS_ZH.findIndex((planet) => planet === name); diff --git a/frontend/tests/report-reading-postcheck-facts-20261112.test.ts b/frontend/tests/report-reading-postcheck-facts-20261112.test.ts index 81837178..72a5a616 100644 --- a/frontend/tests/report-reading-postcheck-facts-20261112.test.ts +++ b/frontend/tests/report-reading-postcheck-facts-20261112.test.ts @@ -65,6 +65,19 @@ test("house lord checks every listed house using ownership and skips placement", } }); +test("divisional house lords skip natal ownership only within the marked sentence", () => { + const differentNatalLords = { ...FACTS, planets: { ...FACTS.planets, Saturn: { owns_houses: [2, 3] }, Moon: { owns_houses: [8] } } }; + verify("career", "D10 上升白羊,其第十宫主土星在 D10 第四宫。", undefined, differentNatalLords); + verify("love", "D9 婚姻分盘内,第七宫主月亮落在双鱼。", undefined, differentNatalLords); + for (const chart of ["D2", "D60", "分盘", "九分", "事业盘", "财富盘", "健康盘", "障碍盘"]) { + verify("wealth", `${chart}里,水星掌管第五宫与第十宫。`); + } + for (const text of ["本命里,水星掌管第五宫与第十宫。", "D1 里,水星掌管第五宫与第十宫。", + "上一句提到 D10。水星掌管第五宫与第十宫。"]) { + verify("wealth", text, "house_lord_mismatch"); + } +}); + test("standard SAV total is a checksum and cannot stand in for individual strength", () => { verify("wealth", "SAV 总分为337,显示不缺维持力。", "sav_total_as_strength"); verify("wealth", "三百三十七说明力量充足。", "sav_total_as_strength"); @@ -95,6 +108,19 @@ test("detector and death wording keep only sentence-local qualified mentions", ( verify("basis", "检测器识别的结果未确认,但本仓有对应记录。", "banned_phrase"); }); +test("qualified detector hits and explicit limits on discussing death pass the basis check", () => { + for (const separator of ["/", "、", ","]) { + verify("basis", `合成格局的形成状态仍是检测器命中${separator}待核实。`); + } + for (const boundary of ["不写", "不谈", "不涉及", "不判断", "不给", "不讨论", "不推断", "避免"]) { + verify("basis", `本段${boundary}疾病名、诊断、死亡或离婚的结论。`); + } + for (const text of ["检测器显示为候选。", "待核实的格局由检测器命中。", "检测器命中。其余格局待核实。", + "检测器命中,检测器识别的组合待核实。", "有死亡风险。", "不写疾病。仍然有死亡风险。"]) { + verify("basis", text, "banned_phrase"); + } +}); + test("percentages and fractions expressed as 成 cannot support accuracy claims", () => { for (const text of ["成功率达85%。", "准确率接近九成。", "命中率为百分之九十。", "命中率有几成。"] ) { verify("overview", text, "accuracy_claim"); From 9259dee34eb98360e8a190482978ff1940e0d211 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 13 Nov 2026 01:45:34 +0800 Subject: [PATCH 6/6] docs(report): record #218 repair validation --- docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md index 6f81d5fe..fbb1ec5c 100644 --- a/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md +++ b/docs/tasks/PROGRESS-reading-postcheck-facts-20261112.md @@ -25,3 +25,6 @@ - 按 Claude 评论 42471 及 `docs/tasks/FIX-reading-postcheck-facts-1.md` 修复,沿用 PR #225。包含 D2–D60 或任务书列出的分盘称呼的分句按修复单跳过宫主核对。D1 与其他分句仍核对本命宫主。 - 「检测器」后可跟「命中」。每处检测器措辞需要同一分句内随后出现待核实、未确认或候选,支持斜杠、顿号和逗号。「死亡」按修复单扩充同句前置边界词。合成回归保留原禁词和本命宫主触发断言。 - 由 Claude 在验收主机对新提交再回放三份样稿,核对 A-2 的事业、感情及 A-1 依据节的误拦解除情况。 +- 修复单分支的五份定向测试 42 项通过。合入 #224 后,以 staging `8eb54b18a29e11bbabb66e5967df5746b6a2b78c` 为对照,变更日志和 Bug 历史保留两单记录,合并后的六份定向测试 45 项通过。Lint 0 错误、62 条既有警告。生产构建通过,首页保持静态,首屏 39 个资源,JS gzip 515860 字节,CSS gzip 90906 字节,与 #218 首轮交付相同。 +- 合入 #224 前的 Python 快速门在构建结束后重跑,1463 项通过、2 项跳过、29 个子测试通过。退出 1,失败的 3 项均为 `tests/test_upstream_technique_layers.py` 的基线失败。合并树的 `scripts/`、`tests/` 和 `references/` 与 staging 一致,沿用 #224 对这些文件的验收结果。 +- 合并后 TypeScript `--noEmit` 通过。前端全量按 `npm test` 同一文件清单和并发数对照,staging 3668 项中 3641 通过,本分支 3677 项中 3650 通过。两边均为相同的 27 项失败、0 项取消和跳过。保留全部基线测试名,首轮 7 项加返工 2 项均通过。最终隐私守卫 62 项通过。