Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c5cf04440 | ||
|
|
cf4d28727e | ||
|
|
72179255c5 | ||
|
|
5183afcc2b |
@@ -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`。
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 个人报告解读复查 · 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 项均通过。
|
||||
- `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 相同。
|
||||
- 隐私检查通过。
|
||||
@@ -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` 里分不清哪些是形成类的状态。
|
||||
- 需要改提示词、事实包或冻结文件才能完成。
|
||||
@@ -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<Record<string, { cap?: unknown; sections?: unknown; required?: unknown }>>;
|
||||
@@ -154,6 +157,68 @@ function falseMissingLayer(text: string, computed: Set<string>): 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<string, unknown> | 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<Record<string, readonly number[]>> = {
|
||||
career: [10], wealth: [2, 11], love: [7], health: [1, 6, 8, 12], character: [1],
|
||||
};
|
||||
|
||||
function doubleTransitOutOfScope(text: string, section: string, facts: Record<string, unknown> | 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(`(?<![\\p{Script=Latin}\\p{N}])${escaped}(?![\\p{Script=Latin}\\p{N}])`, "iu");
|
||||
return name.test(sentence.replace(/\s+/g, " "));
|
||||
}
|
||||
|
||||
function yogaStatusMismatch(text: string, facts: Record<string, unknown> | 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<string, Set<string>>): boolean {
|
||||
if (dignities.size === 0) return false;
|
||||
@@ -196,6 +261,7 @@ function checkSection(
|
||||
claims: Claims,
|
||||
dignities: Map<string, Set<string>>,
|
||||
computed: Set<string>,
|
||||
facts: Record<string, unknown> | 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<string, u
|
||||
const dignities = dignitiesOf(facts);
|
||||
const computed = computedPointsOf(facts);
|
||||
const checked = sections.map((section, index) =>
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
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<string, unknown>,
|
||||
examples: Partial<Record<Section, string>>,
|
||||
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: "记录按「注意」技法归类。" });
|
||||
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}实际结果需现实验证。` });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user