From 5183afcc2b9ee5e7a0386d2d195b970c0cd5d4f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 19:11:06 +0800 Subject: [PATCH 1/4] docs(task): brief for reading post-check gaps (#207, BUG-1854, 2026-11-12) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8 --- .../TASK-reading-postcheck-gaps-20261112.md | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/tasks/TASK-reading-postcheck-gaps-20261112.md diff --git a/docs/tasks/TASK-reading-postcheck-gaps-20261112.md b/docs/tasks/TASK-reading-postcheck-gaps-20261112.md new file mode 100644 index 00000000..9ce39ac0 --- /dev/null +++ b/docs/tasks/TASK-reading-postcheck-gaps-20261112.md @@ -0,0 +1,107 @@ +# TASK:报告解读复查补三项机械检查(2026-11-12) + +- Gitea:#207 +- 分支:`codex/reading-postcheck-gaps-20261112`(基于 staging `1ba0364a7a8c0f964c3e21c21cb304aaa1899375`) +- Bug 编号:BUG-1854。开工时核对 `docs/BUG_HISTORY.md` 末尾;若已被占用,顺延,并在 PROGRESS 写明。 +- 负责验收:Claude(jyotisha-de) + +不得自行写“产品决定/裁定”;任务书没写的取舍一律选需拍板。 + +不得修改或删除既有断言来让测试通过;确需修改必须写原值/新值/原因,并在 PROGRESS 里逐条列出。 + +## 背景 + +个人报告的「解读」由模型写,写完后由 `frontend/src/lib/personal-report-reading.ts::acceptReportReadingDraft` 逐节复查。 + +- 复查不通过的那一节,页面上显示「这一节没有通过数据核对,暂不显示。」,其他节照常显示。 +- 每一节由 `checkSection` 检查,退回原因记在 `ReadingRejection` 类型里。 +- 模型的输入是事实包 `report-reading-facts-v3`,由 `scripts/report_render/reading_facts.py` 生成。 + +占星师 2026-11-12 审核了一份样稿(她的仓库 `8aaf6099`,`handoff/2026-11-12/preset_q3/README.md` 第六、七节),列出了一批必须覆盖的反例。其中三条目前只在提示词里要求,复查不拦。#192 的验收材料把这三条标成了「部分(只靠提示)」: + +1. **已故人物不给当前建议。** 事实包里 `report_mode = "historical_replay"`、`current_advice_allowed = false` 时,正文不应写对当事人现在的建议。一份样稿在健康节末尾写了「若有实际问题应依现实就医评估」。 +2. **Double Transit 不推到别的领域。** `transits.double_transit.checked_houses` 只列了被检测的宫位,例如 `[7]`。样稿曾把第 7 宫的检测结果写进健康和整体应期。 +3. **「形成」和「未验证」不混用。** 样稿曾写「Yoga 成立条件要等现实事件核验」,把“没有经过现实验证”写成了“没有形成”。 + +## 要改的文件 + +- `frontend/src/lib/personal-report-reading.ts`:只改复查部分,即 `checkSection`、`ReadingRejection`,以及新加的辅助函数。 +- 新建 `frontend/tests/report-reading-postcheck-gaps-20261112.test.ts` +- `docs/BUG_HISTORY.md`:只在末尾追加一条。 +- `docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md` + +## 要做的 + +每一条新检查不通过时,只退那一节,原因写进 `ReadingRejection`。 + +### 1. `current_advice_in_replay` + +只在事实包 `current_advice_allowed === false` 时启用。 + +- 「几条建议」「健康」「现在这几年」三节里,任何一段命中下面任一条,就退回这一节: + - 第二人称「你」「您」; + - 建议性措辞:「建议」「应当」「应该」「应依」「最好」「不妨」「可以考虑」「宜」「不宜」「就医」「多休息」「留意」「注意」。 +- 例外:「宜」「不宜」「注意」「留意」出现在引号内的书名、技法名里时不算。如果没法可靠识别这种情况,就在 PROGRESS 列出误伤的例子,选需拍板。 +- `current_advice_allowed` 为 `true` 或缺失时,这条检查不运行。 + +### 2. `double_transit_scope` + +- 先列出各节对应的宫位: + - 事业:10; + - 财富:2、11; + - 感情:7; + - 健康:1、6、8、12; + - 性格:1。 +- 某一节的段落提到「Double Transit」「双星过境」「双重行运」「双行运」中任何一个,而这一节的宫位和 `checked_houses` 没有交集,就退回这一节。 +- 总评、现在这几年、依据与边界三节不在上面的列表里,不做这项检查。 +- `checked_houses` 为空或缺失时,凡是提到双行运又不在这三节的段落,一律退回。 + +### 3. `yoga_status_mismatch` + +事实包 `yogas[]` 每一行有 `name`、`name_cn` 和 `state`。`state` 取 `candidate`、`formed` 等值,具体有哪些自己核对,列进 PROGRESS。 + +满足以下任一条件,就退回那一节: + +1. 段落里出现某一行的 `name_cn`,或者 `name` 去掉 `Yoga` 后的主体词,同一句里又出现「未形成」「没有形成」「不成立」「未成立」「尚未成立」,而这一行的 `state` 是形成类的状态。 +2. 段落里出现「等现实」「要等现实事件」「需现实验证」「经现实核验才」中任何一个,同一句里又出现「成立」或「形成」。 + +「句」以「。」「;」「!」「?」分割。 + +## 不要碰 + +- `frontend/src/lib/personal-report-reading.ts` 里的现有检查和 `writeReportReading`,只新增代码。 +- `references/**`,包括 `references/product_runtime_contract.md`:提示词不改。 +- `scripts/**`、冻结文件和其他前端文件。 + +## 验收标准(写进新测试) + +1. 三条检查各有一个能触发的合成段落和一个不触发的合成段落。 + - 第 1 条还要测:同一段文字在 `current_advice_allowed: true` 时不触发。 + - 第 2 条还要测:`checked_houses: [7]` 时,感情节可以提到双行运,健康节提到就退回。 + - 第 3 条还要测:`state` 为 `formed` 的瑜伽被写成「不成立」时退回;`candidate` 的瑜伽写成「候选、尚未确认」时不退回。 +2. 每条检查只退回所在的那一节,其他节保持 `status: "ok"`。 +3. 现有的 `frontend/tests/report-reading-20261027.test.tsx`、`report-md-export-reading-20261111.test.ts`、`report-no-web-wording-20261109.test.ts` 全部通过。 +4. 测试里的段落全部用合成文字,不使用真实模型的原文。 + +## 自测命令(在 frontend 目录) + +``` +npx tsx --test tests/report-reading-postcheck-gaps-20261112.test.ts tests/report-reading-20261027.test.tsx tests/report-md-export-reading-20261111.test.ts tests/report-no-web-wording-20261109.test.ts +npx tsc --noEmit -p . +npx eslint . +``` + +## 进度记录 + +写在 `docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md`,内容包括: + +- 每条检查的规则和词表; +- `yogas[].state` 实际有哪些取值; +- 误伤风险,以及你测过的边界例子; +- 测试结果。 + +## 何时停下来选“需拍板” + +- 第 1 条的词表误伤正常的历史回放写法,而且不放宽就无法避免。 +- `yogas[].state` 里分不清哪些是形成类的状态。 +- 需要改提示词、事实包或冻结文件才能完成。 -- 2.54.0 From 72179255c5d4eda1f83ec5a4a922c70726c73933 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 12 Nov 2026 19:42:43 +0800 Subject: [PATCH 2/4] fix(report): check replay advice, Double Transit scope, and Yoga wording (#207) --- docs/BUG_HISTORY.md | 14 ++ ...ROGRESS-reading-postcheck-gaps-20261112.md | 17 +++ frontend/src/lib/personal-report-reading.ts | 71 ++++++++++- ...rt-reading-postcheck-gaps-20261112.test.ts | 120 ++++++++++++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md create mode 100644 frontend/tests/report-reading-postcheck-gaps-20261112.test.ts diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 56e32bdc..e13fd7b9 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -25043,3 +25043,17 @@ - 防复发:同上。 - 关联记录:#192、BUG-1851、BUG-1852。 - 修复版本:`claude/report-reading-grade-20261112`。 + +## BUG-1854 | 个人报告解读缺少三项逐节复查 + +- 状态:fixed-pending-verify(待独立验收)。 +- 首次发现 / 最近更新:2026-11-12 / 2026-11-12。 +- 影响面:新生成的中文个人报告「解读」。 +- 现象:历史回放可能出现当前建议,双行运结果可能被用于未经检测的领域,瑜伽形成状态可能与现实结果核验混写。 +- 触发条件:模型草稿包含上述说法并进入 `acceptReportReadingDraft`。 +- 根因:这三项限制只在写作提示中声明,`checkSection` 缺少与事实包对照的复查。 +- 修复:逐段检查历史回放的建议词、双行运已检测宫位和瑜伽形成状态。对应原因记录在 `ReadingRejection`,不通过的节沿用现有退回显示。 +- 验证:新测试和任务书指定的既有测试共 35 项通过,核对每项触发只退回对应节。全量结果见本单进度。 +- 防复发:合成段落覆盖书名和普通引语、第 7 宫与空宫位、候选与形成、同句与分句,以及其他节继续显示。 +- 关联记录:#207、#192、BUG-1851、BUG-1852。 +- 修复版本:`codex/reading-postcheck-gaps-20261112`。 diff --git a/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md new file mode 100644 index 00000000..4e2c362b --- /dev/null +++ b/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md @@ -0,0 +1,17 @@ +# 个人报告解读复查 · 2026-11-12 + +Gitea #207,BUG-1854。按 Claude 评论 42178 认领,基线 `1ba0364a7a8c0f964c3e21c21cb304aaa1899375`。任务书在分支首提交 `5183afcc2`。 + +## 实施 + +- `current_advice_in_replay` 只在 `current_advice_allowed === false` 时扫描「几条建议」「健康」「现在这几年」的每段文字。词表包括「你」「您」「建议」「应当」「应该」「应依」「最好」「不妨」「可以考虑」「宜」「不宜」「就医」「多休息」「留意」「注意」。用中文分词识别单字「宜」,避免把「宜昌」「适宜」判成建议。书名号内或由书名、篇名、技法等语境明示名称的引文,仅豁免「宜」「不宜」「留意」「注意」。普通引用的「宜早作」和书名后的「最好」仍会触发。 +- `double_transit_scope` 识别 `Double Transit`、「双星过境」「双重行运」「双行运」。事业 10,财富 2、11,感情 7,健康 1、6、8、12,性格 1。总评、现在这几年、依据与边界免检。其余节须有一个本节宫位出现在 `transits.double_transit.checked_houses`,所以空或缺失时不能在这些节引用双行运。「几条建议」没有分配宫位。 +- `yoga_status_mismatch` 用「。」「;」「!」「?」划句。同句点名已形成的瑜伽又写「未形成」「没有形成」「不成立」「未成立」「尚未成立」时退回。名字使用中文名或去掉 `Yoga` 后的英文主体。另一条规则识别同句内的「等现实」「要等现实事件」「需现实验证」「经现实核验才」与「成立」「形成」。结果核验与形成分句时可以并存。 + +`reading_facts.py::_yogas` 通过 `yoga_status_words.py::yoga_status` 写出的 `state` 有 `candidate`、`computed`、`formed_under_source`、`conditions_not_met`、`blocked`。形成状态为 `formed_under_source`,兼容任务书验收样例中的 `formed`。候选和已计算的状态均不当作已形成。 + +## 验证 + +- 新测试用合成段落验证节级退回原因,其余节均为 `ok`。覆盖明确引用名称、普通引语、地名、历史和在世标志、第 7 宫和空宫位、句界,以及候选瑜伽的谨慎说法。词表按机械规则检查,普通引用的历史指令仍会命中。 +- 任务书指定的四个测试文件共 35 项通过。沿用既有断言。 +- 将新增测试接到未修复的 staging 实现,6 项拦截回归失败,2 项允许场景通过。接回任务分支后 8 项均通过。 diff --git a/frontend/src/lib/personal-report-reading.ts b/frontend/src/lib/personal-report-reading.ts index 12b21560..16345671 100644 --- a/frontend/src/lib/personal-report-reading.ts +++ b/frontend/src/lib/personal-report-reading.ts @@ -84,6 +84,9 @@ export type ReadingRejection = | "false_no_blocker" | "dignity_mismatch" | "false_missing_layer" + | "current_advice_in_replay" + | "double_transit_scope" + | "yoga_status_mismatch" | "all_sections_withheld"; type Claims = Readonly>; @@ -154,6 +157,68 @@ function falseMissingLayer(text: string, computed: Set): boolean { return false; } +const REPLAY_ADVICE_SECTIONS = new Set(["advice", "health", "now"]); +const REPLAY_ADVICE = /你|您|建议|应当|应该|应依|最好|不妨|可以考虑|不宜|就医|多休息|留意|注意/; +const TITLE_ADVICE_WORDS = /不宜|宜|注意|留意/g; +const QUOTED_NAME = /《[^《》\n]+》|〈[^〈〉\n]+〉|「[^「」\n]+」|『[^『』\n]+』|“[^“”\n]+”|‘[^‘’\n]+’|"[^"\n]+"/g; +const NAME_INTRO = /(?:书名|篇名|著作|技法(?:名|名称)?)[:: \t]*$/; +const NAME_SUFFIX = /^[ \t]*(?:一书|这本书|这一技法|这项技法|这门技法)/; +const CHINESE_WORDS = new Intl.Segmenter("zh", { granularity: "word" }); + +function currentAdviceInReplay(text: string, section: string, facts: Record | null): boolean { + if (facts?.current_advice_allowed !== false || !REPLAY_ADVICE_SECTIONS.has(section)) return false; + const prose = text.replace(QUOTED_NAME, (quote: string, start: number) => { + const title = quote.startsWith("《") || quote.startsWith("〈") || + NAME_INTRO.test(text.slice(0, start)) || NAME_SUFFIX.test(text.slice(start + quote.length)); + return title ? quote.replace(TITLE_ADVICE_WORDS, " ") : quote; + }); + if (REPLAY_ADVICE.test(prose)) return true; + // Match the word 宜 without treating 宜昌 or 适宜 as advice. + if (prose.includes("宜")) { + for (const { segment } of CHINESE_WORDS.segment(prose)) { + if (segment === "宜") return true; + } + } + return false; +} + +const DOUBLE_TRANSIT = /\bDouble\s+Transit\b|双星过境|双重行运|双行运/i; +const TRANSIT_SCOPE_EXEMPT = new Set(["overview", "now", "basis"]); +const SECTION_HOUSES: Readonly> = { + career: [10], wealth: [2, 11], love: [7], health: [1, 6, 8, 12], character: [1], +}; + +function doubleTransitOutOfScope(text: string, section: string, facts: Record | null): boolean { + if (TRANSIT_SCOPE_EXEMPT.has(section) || !DOUBLE_TRANSIT.test(text)) return false; + const transits = facts?.transits as { double_transit?: { checked_houses?: unknown } } | undefined; + const checked = transits?.double_transit?.checked_houses; + return !Array.isArray(checked) || !SECTION_HOUSES[section]?.some((house) => checked.includes(house)); +} + +const YOGA_FORMATION_NEGATED = /未形成|没有形成|不成立|未成立|尚未成立/; +const REAL_WORLD_FORMATION_CONDITION = /等现实|要等现实事件|需现实验证|经现实核验才/; +const YOGA_FORMATION = /成立|形成/; + +function mentionsYoga(sentence: string, yoga: { name?: unknown; name_cn?: unknown }): boolean { + const chinese = typeof yoga.name_cn === "string" ? yoga.name_cn.trim() : ""; + if (chinese && sentence.includes(chinese)) return true; + if (typeof yoga.name !== "string") return false; + const english = yoga.name.replace(/\bYoga\b/gi, " ").trim().replace(/\s+/g, " "); + if (!english) return false; + const escaped = english.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const name = new RegExp(`(? | null): boolean { + const yogas = facts?.yogas; + return text.split(/[。;!?]/).some((sentence) => { + if (REAL_WORLD_FORMATION_CONDITION.test(sentence) && YOGA_FORMATION.test(sentence)) return true; + return YOGA_FORMATION_NEGATED.test(sentence) && Array.isArray(yogas) && yogas.some((yoga) => + (yoga?.state === "formed_under_source" || yoga?.state === "formed") && mentionsYoga(sentence, yoga)); + }); +} + /** A planet name followed closely by a dignity word the facts never give that planet. */ function dignityMismatch(text: string, dignities: Map>): boolean { if (dignities.size === 0) return false; @@ -196,6 +261,7 @@ function checkSection( claims: Claims, dignities: Map>, computed: Set, + facts: Record | null, ): { section: CheckedSection; chars: number } { const withheld = (reason: ReadingRejection) => ({ section: { id: section.id, title, status: "withheld" as const, reason }, @@ -214,6 +280,9 @@ function checkSection( if (BANNED_PHRASES.some((phrase) => text.includes(phrase)) || CERTAINTY.test(text)) 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"); disclaimers += text.match(DISCLAIMER)?.length ?? 0; chars += text.length; if (!graded) { @@ -261,7 +330,7 @@ export function acceptReportReadingDraft(value: unknown, facts: Record - checkSection(section, REPORT_READING_SECTIONS[index]!.title, claims, dignities, computed)); + checkSection(section, REPORT_READING_SECTIONS[index]!.title, claims, dignities, computed, facts)); const withheld = checked.filter(({ section }) => section.status === "withheld").length; if (withheld === checked.length) return { ok: false, reason: "all_sections_withheld" }; const total = checked.reduce((sum, { chars }) => sum + chars, 0); diff --git a/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts b/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts new file mode 100644 index 00000000..416c8696 --- /dev/null +++ b/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts @@ -0,0 +1,120 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + acceptReportReadingDraft, + REPORT_READING_SECTIONS, + type ReadingRejection, + type ReadingSkillMeta, +} from "../src/lib/personal-report-reading"; +import { EVIDENCE_GRADE_CONTRACT } from "../src/lib/personal-report-reading-contract"; + +const SKILL: ReadingSkillMeta = { + name: "jyotish-vedic-astrology", version: "test", + loaded: [{ path: "SKILL.md", sha256: "0".repeat(64) }], +}; +const NEUTRAL = "这里仅复述一个虚构场景中的既有记录。叙述把记载的顺序和可核对的线索分开,也说明当时的材料有些空白。相关细节均用于这个例子的文本核对。".repeat(2); +const CLAIMS = { + character: "character_structure", career: "career_promise", wealth: "wealth_earning", + love: "relationship_structure", health: "health_pressure", now: "timing_window", +} as const; +const GRADES = { + contract: EVIDENCE_GRADE_CONTRACT, + claims: Object.fromEntries(Object.entries(CLAIMS).map(([section, claim]) => [claim, { cap: "C", sections: [section] }])), +}; +type Section = (typeof REPORT_READING_SECTIONS)[number]["id"]; + +function expectPostcheck( + facts: Record, + examples: Partial>, + rejected?: readonly [Section, ReadingRejection], +): void { + const draft = { + sections: REPORT_READING_SECTIONS.map(({ id }) => { + const claim = id in CLAIMS ? CLAIMS[id as keyof typeof CLAIMS] : null; + return { + id, + paragraphs: [NEUTRAL, examples[id] ?? NEUTRAL].map((text) => ({ + text, claim_type: claim, grade: claim ? "C" : null, + })), + }; + }), + }; + const verdict = acceptReportReadingDraft(draft, { ...facts, evidence_grades: GRADES }, SKILL); + assert.ok(verdict.ok && verdict.reading.version === 3, JSON.stringify(verdict)); + assert.equal(verdict.withheld, rejected ? 1 : 0); + for (const section of verdict.reading.sections) { + if (section.id === rejected?.[0]) { + assert.equal(section.status, "withheld", section.id); + assert.equal(section.reason, rejected[1], section.id); + } else { + assert.equal(section.status, "ok", section.id); + } + } +} + +test("the three historical replay sections withhold direct address or current advice", () => { + const replay = { report_mode: "historical_replay", current_advice_allowed: false }; + for (const section of ["advice", "health", "now"] as const) { + expectPostcheck(replay, { [section]: "这里建议先放慢节奏。" }, [section, "current_advice_in_replay"]); + } + for (const phrase of ["你", "您", "建议", "应当", "应该", "应依", "最好", "不妨", "可以考虑", "宜", "不宜", "就医", "多休息", "留意", "注意"]) { + expectPostcheck(replay, { health: `本节使用该措辞:${phrase}。` }, ["health", "current_advice_in_replay"]); + } + expectPostcheck(replay, { career: "这里以第二人称转述这段记录:你核对了这一点。" }); +}); + +test("the same text passes when current advice is allowed or the flag is absent", () => { + const example = { health: "这里建议先放慢节奏。" }; + expectPostcheck({ current_advice_allowed: true }, example); + expectPostcheck({}, example); + expectPostcheck({ current_advice_allowed: false }, { health: "当年途经宜昌,留下的文字只有一条纪事。" }); +}); + +test("title quoting exempts only the four title words and ordinary quotes still count", () => { + const replay = { current_advice_allowed: false }; + expectPostcheck(replay, { health: "这里引用《宜忌》与〈注意〉,并抄录技法「不宜」和“留意”这项技法的名称。" }); + expectPostcheck(replay, { health: "记录曾出现「宜早作」这句话。" }, ["health", "current_advice_in_replay"]); + expectPostcheck(replay, { health: "历史笔记提到《建议录》。" }, ["health", "current_advice_in_replay"]); + expectPostcheck(replay, { health: "文中引用《宜忌》。另外,最好先核对。" }, ["health", "current_advice_in_replay"]); +}); + +test("a checked seventh house covers relationships and withholds Double Transit in health", () => { + const facts = { transits: { double_transit: { checked_houses: [7] } } }; + expectPostcheck(facts, { love: "双行运仅讨论第七宫。", health: "健康这一段引用 Double Transit。" }, ["health", "double_transit_scope"]); +}); + +test("Double Transit accepts an intersecting house for each mapped section", () => { + for (const [section, house] of [["career", 10], ["wealth", 2], ["wealth", 11], ["character", 1], ["health", 1], ["health", 6], ["health", 8], ["health", 12]] as const) { + expectPostcheck({ transits: { double_transit: { checked_houses: [house, 7] } } }, { [section]: "这里核对双星过境。" }); + } +}); + +test("missing and empty checked houses withhold other sections and preserve the three exemptions", () => { + const phrase = "这里提到双重行运。"; + for (const facts of [{}, { transits: { double_transit: { checked_houses: [] } } }]) { + expectPostcheck(facts, { overview: phrase, now: phrase, basis: phrase, career: phrase }, ["career", "double_transit_scope"]); + expectPostcheck(facts, { advice: phrase }, ["advice", "double_transit_scope"]); + } +}); + +test("a formed Yoga cannot be negated in the sentence that names it", () => { + const row = { name: "Dhana Yoga", name_cn: "财富瑜伽" }; + for (const state of ["formed", "formed_under_source"]) { + const facts = { yogas: [{ ...row, state }] }; + expectPostcheck(facts, { wealth: "财富瑜伽不成立。" }, ["wealth", "yoga_status_mismatch"]); + expectPostcheck(facts, { wealth: "这里的 dhana 尚未成立。" }, ["wealth", "yoga_status_mismatch"]); + expectPostcheck(facts, { wealth: "财富瑜伽成立。另一项组合未形成。" }); + expectPostcheck(facts, { wealth: "另一项称为 MahaDhana 的组合未形成。" }); + } + expectPostcheck({ yogas: [{ ...row, state: "candidate" }] }, { wealth: "财富瑜伽是候选、尚未确认。" }); +}); + +test("reality checks can qualify outcomes but cannot be required for Yoga formation in the same sentence", () => { + for (const condition of ["等现实", "要等现实事件", "需现实验证", "经现实核验才"]) { + expectPostcheck({}, { basis: `瑜伽是否成立,${condition}核对。` }, ["basis", "yoga_status_mismatch"]); + } + for (const punctuation of ["。", ";", "!", "?"]) { + expectPostcheck({}, { basis: `瑜伽按这组条件形成${punctuation}实际结果需现实验证。` }); + } +}); -- 2.54.0 From cf4d28727ec810e5d817a0667f2f73579e30c20e Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 12 Nov 2026 19:44:37 +0800 Subject: [PATCH 3/4] test(report): cover a quoted technique name before its noun (#207) --- frontend/src/lib/personal-report-reading.ts | 2 +- frontend/tests/report-reading-postcheck-gaps-20261112.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/personal-report-reading.ts b/frontend/src/lib/personal-report-reading.ts index 16345671..c2c1d5ef 100644 --- a/frontend/src/lib/personal-report-reading.ts +++ b/frontend/src/lib/personal-report-reading.ts @@ -162,7 +162,7 @@ const REPLAY_ADVICE = /你|您|建议|应当|应该|应依|最好|不妨|可以 const TITLE_ADVICE_WORDS = /不宜|宜|注意|留意/g; const QUOTED_NAME = /《[^《》\n]+》|〈[^〈〉\n]+〉|「[^「」\n]+」|『[^『』\n]+』|“[^“”\n]+”|‘[^‘’\n]+’|"[^"\n]+"/g; const NAME_INTRO = /(?:书名|篇名|著作|技法(?:名|名称)?)[:: \t]*$/; -const NAME_SUFFIX = /^[ \t]*(?:一书|这本书|这一技法|这项技法|这门技法)/; +const NAME_SUFFIX = /^[ \t]*(?:一书|这本书|(?:这[一项门])?技法)/; const CHINESE_WORDS = new Intl.Segmenter("zh", { granularity: "word" }); function currentAdviceInReplay(text: string, section: string, facts: Record | null): boolean { diff --git a/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts b/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts index 416c8696..4b1f5f49 100644 --- a/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts +++ b/frontend/tests/report-reading-postcheck-gaps-20261112.test.ts @@ -74,6 +74,7 @@ test("the same text passes when current advice is allowed or the flag is absent" test("title quoting exempts only the four title words and ordinary quotes still count", () => { const replay = { current_advice_allowed: false }; expectPostcheck(replay, { health: "这里引用《宜忌》与〈注意〉,并抄录技法「不宜」和“留意”这项技法的名称。" }); + expectPostcheck(replay, { health: "记录按「注意」技法归类。" }); expectPostcheck(replay, { health: "记录曾出现「宜早作」这句话。" }, ["health", "current_advice_in_replay"]); expectPostcheck(replay, { health: "历史笔记提到《建议录》。" }, ["health", "current_advice_in_replay"]); expectPostcheck(replay, { health: "文中引用《宜忌》。另外,最好先核对。" }, ["health", "current_advice_in_replay"]); -- 2.54.0 From 9c5cf04440e7b59115a286892dad6dee1238afd6 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 12 Nov 2026 20:05:41 +0800 Subject: [PATCH 4/4] docs(report): record #207 verification and baseline results --- docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md b/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md index 4e2c362b..690d737c 100644 --- a/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md +++ b/docs/tasks/PROGRESS-reading-postcheck-gaps-20261112.md @@ -15,3 +15,8 @@ Gitea #207,BUG-1854。按 Claude 评论 42178 认领,基线 `1ba0364a7a8c0f9 - 新测试用合成段落验证节级退回原因,其余节均为 `ok`。覆盖明确引用名称、普通引语、地名、历史和在世标志、第 7 宫和空宫位、句界,以及候选瑜伽的谨慎说法。词表按机械规则检查,普通引用的历史指令仍会命中。 - 任务书指定的四个测试文件共 35 项通过。沿用既有断言。 - 将新增测试接到未修复的 staging 实现,6 项拦截回归失败,2 项允许场景通过。接回任务分支后 8 项均通过。 +- `tsc --noEmit -p .` 通过,ESLint 为 0 错误、62 警告。 +- 用仓库的 Node/tsx 全量文件清单和 4 个测试进程比对。staging 共 3,653 项,3,623 通过、30 失败、0 取消。任务分支共 3,661 项,3,633 通过、28 失败、0 取消。顶层 TAP 名称新增 8 项、缺失 0 项。两边有 27 个同名失败,另有 4 个数据库测试在其中一边的并行运行中失败。 +- 分别串行复跑涉及两边差异的 4 个数据库测试文件。两边各 7 项通过,含任务分支并行时失败的并发档案修正迁移,以及 staging 并行时失败的管理员恢复和两项计费测试。 +- `npm run build` 通过,`/` 为静态页面。首页引用的 39 个资源逐文件 gzip -9 后,JS 为 515,860 字节,CSS 为 90,886 字节,两项均与 staging 相同。 +- 隐私检查通过。 -- 2.54.0