feat(rectification): an answered post-adopt check says whether it fits the adopted time (BUG-1170)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
691ee440e3
commit
42ff82916e
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-02 — 采用后答完核对题,会说这件事和采用的时间对不对得上
|
||||
|
||||
- 采用后答核对题,回复先说结果:「这件事和 04:55 对得上」「对不上;如果别的经历也对不上,可以回到上面的卡片改选」或「分不出」;盘型口径下再说「盘型结论不变」或给出变化后的各盘上升(BUG-1170)。
|
||||
- 题数规则不变(至多 2 道)。Skill 版本不变;不改数据库结构。
|
||||
|
||||
## 2026-10-01 — 普通对话的数据卡看得到「谁照谁、谁和谁同宫、燃烧」,瑜伽按领域分
|
||||
|
||||
- 数据卡每颗星带上被哪些星照到、和谁同宫、是否燃烧;各领域选中的宫带上被哪些星照到(BUG-1154、BUG-1155)。
|
||||
|
||||
@@ -15604,3 +15604,20 @@
|
||||
- 相关记录:BUG-1157
|
||||
- 复发自:无
|
||||
- 修复版本:缓解随 `codex/consult-card-affliction-data-20261001`;其余无
|
||||
|
||||
## BUG-1170 | 采用后答完核对题只说「前事核对到这里」,不说对不对得上
|
||||
|
||||
- 状态:resolved(2026-10-02 Claude 直接执行;产品 10-01 选 A)
|
||||
- 首次发现 / 最近更新:2026-10-01 / 2026-10-02
|
||||
- 影响面:`v9/answer-choice.ts::persistApplied`(已采用且所答焦点是 reverse_verify / out_of_sample_check)、新增 `v9/post-adopt-verdict.ts`。
|
||||
- 用户现象:采用后答了「2016 年 9 月那次升学」核对题,回复只有「前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。」用户不知道答案说明了什么,只问一道就结束。
|
||||
- 触发条件:已采用,答一道核对题(跳过不算)。
|
||||
- 根因:收尾文案只看「还有没有下一道」,不看刚答的这道对采用时间是支持还是冲突。只问一道是规则所致(采用后核对至多 2 道,只取采用前没问过、不在用户已报年份/领域、未拒答的探针;该案只剩 1 道),不是本条要改的。
|
||||
- 决策记录:产品 10-01 选 A——收尾先说结果,题数规则不变。
|
||||
- 修复:`postAdoptCheckVerdict` 用该探针按所答类别的 supports / conflicts,对照「采用分钟所在候选」(簇含该分钟,否则最近):支持→「这件事和 HH:MM 对得上。」;冲突→「这件事和 HH:MM 对不上;如果别的经历也对不上,可以回到上面的卡片改选。」;都不涉及→「这件事分不出 HH:MM 和别的时间。」;记不清→「这件事记不清,不算。」盘型口径再比对答题前后的分盘摘要:没变→「盘型结论不变。」,变了→「盘型结论有变化:<各目标盘上升与档位>」。结果句放在收尾句或下一道题之前。只用服务端事实,不写「确认 / 精确」。
|
||||
- 验证:`frontend/tests/rectification-post-adopt-verdict-20261002.test.ts`(候选映射、四种结果、盘型变/不变);`frontend/tests/rectification-answer-choice.test.ts` 新增作答最后一道核对题用例(去掉接线即红);本机 PG17 真库重放虚构案例:回复为「这件事和 15:26 对得上。盘型结论不变。」+ 收尾句。
|
||||
- 防复发:采用后作答的回复必须带结果句;跳过保持原收尾句(既有用例锁定)。
|
||||
- 相关记录:BUG-536~538、BUG-1146、BUG-1150。
|
||||
- 复发自:无
|
||||
- 修复版本:`codex/rectification-post-adopt-verify-20261001`。
|
||||
|
||||
|
||||
@@ -63,3 +63,7 @@ Python 快速门:本单不改 `scripts/**`、`tests/**`,未跑。`test:db`
|
||||
## 第四轮:交付正文重复(BUG-1153,我在 BUG-1149 引入)
|
||||
|
||||
真机:交付时「D1 上升…用了 5 道选择题」连出两条。gate 收尾轮次在 BUG-1149 后首次真的落库,与答题回复里的同一段重复。修:写前比对最近一条助手消息。真机另报两点:①采用消息里题干「出现两次」——服务端与单测均确认正文已剥题干,粘贴里多出的一遍来自选项卡给读屏用的隐藏标题(sr-only legend)被鼠标选中复制,屏幕上只显示一次;②答一道核对题就结束——按现行规则:采用后核对题至多 2 道,且只取采用前没问过、不在用户已报年份/领域、未拒答的探针,该案只剩 1 道,属设计,已向产品提出改进选项。
|
||||
|
||||
## 第五轮:核对题答完说结果(BUG-1170,产品选 A)
|
||||
|
||||
新增 `v9/post-adopt-verdict.ts`;`persistApplied` 已采用且所答为核对题时,在收尾句或下一题前加结果句。真库重放:「这件事和 15:26 对得上。盘型结论不变。」+ 收尾句。题数规则不变。
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
| `TASK-rectification-chart-tier-caveats-20261001.md` | — | **报告与聊天标注判断不了的分盘**:采用后读 `active_birth_provenance`(segment-v1)→ 结果 `segment_summary` 档位,blocked/indistinct 的 D9/D10 在聊天 answer_policy 与一句说明、报告章节顶部提示 + 主题降级;旧资料逐字节不变;不改库、不改 consult/route.ts | 待领取 | BUG-1138~1139 |
|
||||
| `TASK-rectification-angle-timing-research-20261001.md` | `PROGRESS-rectification-angle-timing-research-20261001.md` | **角度类时间技法研究(离线)**:行运土木罗压本命上升 / 天顶度数、次限推运角、太阳弧角(1° ≈ 4 分钟出生时间),77 例 v5;乱序日期对照 ≥ 50 次必做、留一法、不得挤出真值;显著才叠加到六题后验比头段命中与区间宽度 | 已实现待验收(0/9 预登记检验显著,BUG-1141 closed_by_design;分支 codex/rectification-angle-timing-research-20261001,未推送) | BUG-1141 |
|
||||
| (直接执行,无任务书) | `PROGRESS-rectification-segment-order-20261001.md` | **健康区分题写入失败(BUG-1143)+ 按盘型挑题默认开启(BUG-1144)**:区分类焦点把 `health_pressure` 原样写入、违反表约束、写入被吞后提前交付(BUG-672 同族);修后 77 例真库回放平均问题数 ±10 2.8→4.6、±30 3.6→5.4,D10 头段 ±30 39→52;按段选题 ≤61 分钟默认开,±30 D9 46→51、D10 52→55,真值段 76/76 | 已验收(Claude 10-01 直接执行) | 分支 codex/rectification-segment-order-20261001 |
|
||||
| (直接执行,无任务书) | `PROGRESS-rectification-post-adopt-card-20261001.md` | **采用后核对题无按钮(BUG-1146)+ 盘型口径回复念分钟排名(BUG-1147)**:keep 分支丢探针身份致 GET 空卡(BUG-498 复发);采用接口不写轮次、只读运行被 60 秒交付窗口挡掉,题挂到上一题回复下;改为采用接口写「已采用 HH:MM。再核对一件过去的事。」轮次并挂题。盘型口径下去掉「领先 / 落后」、交付旁白不再说「分不开」、边界句只说一次 | 已验收(Claude 10-01 直接执行);真机回报后补修 BUG-1149(请求号非 uuid 被真库拒写)、BUG-1150(核对题作答 stale_probe),本机真库重放通过;BUG-1151 盘型报告去分钟排名(产品选 A);BUG-1153 交付正文连出两条(BUG-1149 引入的回归);真机清单 `docs/testing/rectification-post-adopt-card-20261001.md` | 分支 codex/rectification-post-adopt-verify-20261001 |
|
||||
| (直接执行,无任务书) | `PROGRESS-rectification-post-adopt-card-20261001.md` | **采用后核对题无按钮(BUG-1146)+ 盘型口径回复念分钟排名(BUG-1147)**:keep 分支丢探针身份致 GET 空卡(BUG-498 复发);采用接口不写轮次、只读运行被 60 秒交付窗口挡掉,题挂到上一题回复下;改为采用接口写「已采用 HH:MM。再核对一件过去的事。」轮次并挂题。盘型口径下去掉「领先 / 落后」、交付旁白不再说「分不开」、边界句只说一次 | 已验收(Claude 10-01 直接执行);真机回报后补修 BUG-1149(请求号非 uuid 被真库拒写)、BUG-1150(核对题作答 stale_probe),本机真库重放通过;BUG-1151 盘型报告去分钟排名(产品选 A);BUG-1153 交付正文连出两条(BUG-1149 引入的回归);BUG-1170 核对题答完说对不对得上(产品选 A);真机清单 `docs/testing/rectification-post-adopt-card-20261001.md` | 分支 codex/rectification-post-adopt-verify-20261001 |
|
||||
| `TASK-rectification-typed-event-scoring-research-20260929.md` | `PROGRESS-rectification-typed-event-research-20260929.md` | **打字经历按选择题规则计分(离线研究,不上线)**:计分通道不对称 + 已入账年份挡题;R0 学业质量题措辞 / 年精度显示成 1 月(冻结文件,需重新冻结) | 已验收关单(Claude 10-01:三类技法 9 项检验 0 显著,真值平均排位 0.49~0.59;Claude 补正对照——埋入信号排位 0.077、挪年 0.502——流程有效;BUG-1141 closed_by_design) | BUG-1088、1089 |
|
||||
| `TASK-report-reader-polish-20260929.md` | `PROGRESS-report-reader-polish-20260929.md` | **报告页打磨**:生成入口挪进页面主体(删标题栏按钮)、详情页到底部按钮(懒渲染一次到底)、导出按钮带文字、目录一级/二级分层、去掉「字段」与 RL/NL 缩写表头、状态列同义重复去重、报告表格淡底色、分块导出显示真实文件大小 | Claude 直接执行并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +7 B、快速门 Python 1000 passed),已部署 staging `d7772011`,真机欠 | BUG-1092~1094 |
|
||||
| `TASK-report-english-edition-20260929.md` | `PROGRESS-report-english-edition-20260929.md` | **报告中英两版**:同一次引擎计算渲染 zh/en 两遍(不用模型翻译),瑜伽库 477 条补英文、模板与前端表头英文化;中文逐字节不变、英文零汉字、两版数字序列一致;阅读页 `?lang=en` 切换,导出当前语言 + 「问 AI 建议导出英文版」提示;旧报告不补英文 | Claude 直接执行(含两个 fork 子代理)并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +0.06%、Python 定向全绿),已部署 staging `d7772011`,真机欠 | — |
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 出现一条新的助手消息「已采用 HH:MM。再核对一件过去的事。」,不是挂在上一题「已记录…」下面;
|
||||
- 如果有核对题,题目下有**四个按钮**,可以点。
|
||||
4. **刷新**:刷新页面,核对题还在、四个按钮还在、仍挂在那条采用消息下面。
|
||||
5. **答核对题**:点一个按钮,不出现「这次没提交上」,能进入下一题或看到「前事核对到这里…」。
|
||||
5. **答核对题**:点一个按钮,不出现「这次没提交上」;回复先说「这件事和 HH:MM 对得上 / 对不上 / 分不出」,再说「盘型结论不变」或给出变化,然后进入下一题或看到「前事核对到这里…」。
|
||||
5a. **选中时**:点「更像这个」后,上面的旁白和卡片不应消失;如果仍消失一下,请录屏发给 Claude。
|
||||
5b. **验证报告**:交付卡点「查看验证报告」,里面没有「当前候选基本并列」「本命上升(该分钟)」和「排名 / 时间 / 相对支持」表。
|
||||
6. **旧校正会话**:从会话列表打开一个以前的校正会话,能打开、不报错。
|
||||
|
||||
@@ -275,7 +275,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
| 已答6轮,再问下去也分不开04:53和05:00,没有年份的分盘题也不再问……两者相对支持度都是16。 | 再问下去也分不开 04:53 和 05:00。这两个时间按现有信息分不开。范围和代表分钟照常说。 | 旁白不得念内部计分词。卡上的数字按上一行的 5 个百分点规则显示,旁白不念百分比。 |
|
||||
| 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 每列用服务器拼好的一行时段;没有窗就写「未来一年没有明显的时段」;没算出来写「这一分钟还没对照」。 | 窗口按候选分钟各算,模型不写。不得把「没算」说成「没有窗」。 |
|
||||
| 「再补一件经历」后卡片消失、输入框上方空白 | 需要补经历时继续在输入框说下一件;交付卡不再提供「再补一件经历」按钮。 | 交付卡只负责选分钟。 |
|
||||
| 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,新建对话即按这个时间排盘。有核对题:前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 |
|
||||
| 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,新建对话即按这个时间排盘。有核对题:前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。答完核对题先说结果(BUG-1170):「这件事和 04:53 对得上。」/「这件事和 04:53 对不上;如果别的经历也对不上,可以回到上面的卡片改选。」/「这件事分不出 04:53 和别的时间。」/「这件事记不清,不算。」;盘型口径再接「盘型结论不变。」或「盘型结论有变化:…」。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 |
|
||||
| (采用后只念代表分钟)代表分钟是 05:12。 | 这 27 分钟里,事业方向、性格底色的判断是稳定的;婚恋(D9)会随分钟变,看盘时按范围读。这不是已确认的唯一出生分钟。 | 采用后先讲区间里什么稳、什么随分钟变;两句来自服务端,不是模型编的。 |
|
||||
| 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 |
|
||||
| 选择题处理失败,请稍后重试。 / 这道题已经过期,请回答当前问题(点选没提交上,下面又画出同一张卡) | 这次没提交上,请再点一次。 | 点选没到服务器时撤回已选,只留一张可点的卡,错误行就这一句。不说「稍后」,不把服务端状态码翻成话。 |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { postAdoptCheckVerdict } from "./post-adopt-verdict.ts";
|
||||
import { posteriorMap, scoreDeltas } from "../core/decision-fingerprint";
|
||||
import { productDeliveryForDomain } from "./product-delivery.ts";
|
||||
import { nextProbe } from "../core/build-state";
|
||||
@@ -2415,6 +2416,26 @@ ${nextInterview.hostNarration}`;
|
||||
hostNarration = adoptionNarration ?? completedRangeNarration ?? hostNarration;
|
||||
}
|
||||
|
||||
// BUG-1170: an answered post-adopt check says what it meant for the adopted time.
|
||||
const answeredIntent = input.dossier.conversationSummary.activeFocus?.intent;
|
||||
if (accepted && !skipThisProbe && input.userStopped !== true
|
||||
&& (answeredIntent === "reverse_verify" || answeredIntent === "out_of_sample_check")) {
|
||||
const before = inferenceForPersistedAnswer(input.dossier.latestResult?.decisionReceipt ?? null);
|
||||
const after = input.decisionState ?? before;
|
||||
const segmentProduct = input.dossier.case.rectificationDomain !== null && input.dossier.case.rectificationDomain !== undefined;
|
||||
const verdict = postAdoptCheckVerdict({
|
||||
acceptedTime: input.dossier.case.acceptedTime,
|
||||
probe: input.probeId
|
||||
? (after?.probes.find((probe) => probe.id === input.probeId) ?? before?.probes.find((probe) => probe.id === input.probeId) ?? null)
|
||||
: null,
|
||||
answerClass: input.answerClass,
|
||||
candidates: after?.candidates ?? [],
|
||||
segmentBefore: segmentProduct ? before?.segment_summary ?? null : null,
|
||||
segmentAfter: segmentProduct ? after?.segment_summary ?? null : null,
|
||||
});
|
||||
if (verdict) hostNarration = hostNarration ? `${verdict}\n\n${hostNarration}` : verdict;
|
||||
}
|
||||
|
||||
const choiceStem = nextChoiceReady ? renderableChoiceStem(nextFocus) : null;
|
||||
if (choiceStem) {
|
||||
const body = skipThisProbe
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* What a post-adopt check said about the adopted time (BUG-1170, product 10-01 option A).
|
||||
*
|
||||
* The closing line used to be only 「前事核对到这里」: the reader answered and learned nothing.
|
||||
* The verdict reads the answered probe's own expected outcomes for the candidate that holds the
|
||||
* adopted minute, and — in the segment product — whether any target chart's ascendant or tier
|
||||
* moved with this answer. Server facts only; no model text.
|
||||
*/
|
||||
import type { AnswerClass, ConflictProbe, InferenceCandidate } from "../core/types.ts";
|
||||
import { chartSummaryCopy, type SegmentSummary } from "../core/segment-summary.ts";
|
||||
|
||||
export type PostAdoptVerdictInput = Readonly<{
|
||||
acceptedTime: string | null | undefined;
|
||||
probe: ConflictProbe | null | undefined;
|
||||
answerClass: AnswerClass | null | undefined;
|
||||
candidates: readonly InferenceCandidate[];
|
||||
segmentBefore?: SegmentSummary | null;
|
||||
segmentAfter?: SegmentSummary | null;
|
||||
}>;
|
||||
|
||||
function minutes(clock: string): number | null {
|
||||
const match = /^(\d{2}):(\d{2})/.exec(clock);
|
||||
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
|
||||
}
|
||||
|
||||
function inRange(clock: number, range: readonly [string, string]): boolean {
|
||||
const start = minutes(range[0]);
|
||||
const end = minutes(range[1]);
|
||||
if (start === null || end === null) return false;
|
||||
return start <= end ? clock >= start && clock <= end : clock >= start || clock <= end;
|
||||
}
|
||||
|
||||
/** The candidate that stands for the adopted minute: its cluster holds it, else the nearest. */
|
||||
export function candidateForAdoptedMinute(
|
||||
acceptedTime: string,
|
||||
candidates: readonly InferenceCandidate[],
|
||||
): InferenceCandidate | null {
|
||||
const clock = acceptedTime.slice(0, 5);
|
||||
const at = minutes(clock);
|
||||
if (at === null || candidates.length === 0) return null;
|
||||
const holding = candidates.find((candidate) => candidate.time === clock || candidate.cluster_times?.includes(clock))
|
||||
?? candidates.find((candidate) => inRange(at, candidate.cluster_range));
|
||||
if (holding) return holding;
|
||||
const distance = (candidate: InferenceCandidate) => {
|
||||
const value = minutes(candidate.time);
|
||||
if (value === null) return Number.POSITIVE_INFINITY;
|
||||
const raw = Math.abs(value - at);
|
||||
return Math.min(raw, 1440 - raw);
|
||||
};
|
||||
return [...candidates].sort((left, right) => distance(left) - distance(right))[0] ?? null;
|
||||
}
|
||||
|
||||
function chartsMoved(before: SegmentSummary, after: SegmentSummary): boolean {
|
||||
return after.charts.some((chart) => {
|
||||
const prior = before.charts.find((row) => row.chart === chart.chart);
|
||||
return !prior || prior.sign !== chart.sign || prior.tier !== chart.tier;
|
||||
});
|
||||
}
|
||||
|
||||
export function postAdoptCheckVerdict(input: PostAdoptVerdictInput): string | null {
|
||||
const clock = input.acceptedTime?.trim().slice(0, 5) ?? "";
|
||||
if (!clock || !input.answerClass) return null;
|
||||
if (input.answerClass === "unsure") return "这件事记不清,不算。";
|
||||
if (!input.probe) return null;
|
||||
const candidate = candidateForAdoptedMinute(clock, input.candidates);
|
||||
const outcome = input.probe.expected_outcomes.find((row) => row.answer_class === input.answerClass) ?? null;
|
||||
const ids = candidate ? new Set([candidate.id, candidate.time]) : new Set<string>();
|
||||
const supports = Boolean(outcome?.supports.some((id) => ids.has(id)));
|
||||
const conflicts = Boolean(outcome?.conflicts.some((id) => ids.has(id)));
|
||||
const fit = supports && !conflicts
|
||||
? `这件事和 ${clock} 对得上。`
|
||||
: conflicts && !supports
|
||||
? `这件事和 ${clock} 对不上;如果别的经历也对不上,可以回到上面的卡片改选。`
|
||||
: `这件事分不出 ${clock} 和别的时间。`;
|
||||
const before = input.segmentBefore ?? null;
|
||||
const after = input.segmentAfter ?? null;
|
||||
if (!before || !after) return fit;
|
||||
return chartsMoved(before, after)
|
||||
? `${fit}盘型结论有变化:${chartSummaryCopy(after)}`
|
||||
: `${fit}盘型结论不变。`;
|
||||
}
|
||||
@@ -1795,6 +1795,30 @@ function lastVerifyDossier() {
|
||||
}));
|
||||
}
|
||||
|
||||
test("answering the last post-adopt verify question says what it meant for the adopted time (BUG-1170)", async () => {
|
||||
const dossier = withAcceptedTime(lastVerifyDossier());
|
||||
const accounting = persistChoiceAccounting(dossier, nextFocusHandler());
|
||||
const applied = await applyRectificationChoice(accounting.client, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
actionId: ACTION_ID,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: FOCUS_ID,
|
||||
questionId: QUESTION_ID,
|
||||
probeId: EDUCATION_2014_PROBE.id,
|
||||
optionId: "A",
|
||||
expectedRevision: (
|
||||
dossier.latest_result as {
|
||||
decision_receipt: { inference_state: { revision: number } };
|
||||
}
|
||||
).decision_receipt.inference_state.revision,
|
||||
});
|
||||
assert.match(applied.narration, /^这件事(和 05:07 (对得上|对不上|分不出)|记不清)/);
|
||||
assert.ok(applied.narration.endsWith(RECTIFICATION_USER_COPY.postAdoptVerifyDone), applied.narration);
|
||||
assert.doesNotMatch(applied.narration, /确认|精确|领先|落后/);
|
||||
});
|
||||
|
||||
test("skipping the last post-adopt verify question closes with start_consultation", async () => {
|
||||
const dossier = withAcceptedTime(lastVerifyDossier());
|
||||
const accounting = persistChoiceAccounting(dossier, nextFocusHandler());
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { candidateForAdoptedMinute, postAdoptCheckVerdict } from "../src/lib/rectification-agentic/v9/post-adopt-verdict.ts";
|
||||
import type { SegmentSummary } from "../src/lib/rectification-agentic/core/segment-summary.ts";
|
||||
|
||||
// BUG-1170 (product 10-01 option A): after a post-adopt check the reply says whether the event
|
||||
// fits the adopted time, and whether the chart verdicts moved — not only 「前事核对到这里」.
|
||||
const candidate = (time: string, range: [string, string], clusterTimes?: string[]) => ({
|
||||
id: time, time, cluster_range: range, ...(clusterTimes ? { cluster_times: clusterTimes } : {}),
|
||||
prior_score: 10, posterior_score: 10, probability: 0.5, status: "active", rank: 1, strong_conflict_count: 0,
|
||||
}) as never;
|
||||
const CANDIDATES = [candidate("04:53", ["04:48", "04:57"], ["04:53", "04:55"]), candidate("05:03", ["05:00", "05:07"])];
|
||||
const probe = {
|
||||
id: "probe:education.2016.09.known_event_quality:x", semantic_key: "education.2016.09.known_event_quality",
|
||||
candidate_split_hash: "x", domain: "education", year: 2016, question: "2016", candidate_ids: ["04:53", "05:03"],
|
||||
information_gain: 0.4, source: "known_event_quality",
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:03"] },
|
||||
{ answer_class: "no", supports: ["05:03"], conflicts: ["04:53"] },
|
||||
{ answer_class: "weak_yes", supports: [], conflicts: [] },
|
||||
],
|
||||
} as never;
|
||||
const summary = (tier: string, sign: number): SegmentSummary => ({
|
||||
targets: ["D1", "D9"], window_minutes: 21, scan_complete: true, no_rectification_needed: false, adoption_minute: null,
|
||||
charts: [
|
||||
{ chart: "D1", sign: 3, tier: "certain", alternatives: [], tied: false },
|
||||
{ chart: "D9", sign, tier, alternatives: [], tied: false },
|
||||
],
|
||||
}) as never;
|
||||
|
||||
test("the adopted minute maps to the candidate whose cluster holds it, else the nearest", () => {
|
||||
assert.equal(candidateForAdoptedMinute("04:55:00", CANDIDATES)?.time, "04:53");
|
||||
assert.equal(candidateForAdoptedMinute("05:01", CANDIDATES)?.time, "05:03");
|
||||
assert.equal(candidateForAdoptedMinute("05:30", CANDIDATES)?.time, "05:03", "outside every cluster: nearest");
|
||||
});
|
||||
|
||||
test("verdict: fits, does not fit, cannot tell, and unsure", () => {
|
||||
const base = { acceptedTime: "04:55:00", probe, candidates: CANDIDATES };
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "yes" }), "这件事和 04:55 对得上。");
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "no" }),
|
||||
"这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。");
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "weak_yes" }), "这件事分不出 04:55 和别的时间。");
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件事记不清,不算。");
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: null }), null);
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, acceptedTime: null, answerClass: "yes" }), null);
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, probe: null, answerClass: "yes" }), null, "no probe, no claim");
|
||||
});
|
||||
|
||||
test("segment product says whether the chart verdicts moved", () => {
|
||||
const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "yes" as const };
|
||||
assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }),
|
||||
"这件事和 04:55 对得上。盘型结论不变。");
|
||||
const moved = postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("tentative", 6) });
|
||||
assert.match(moved!, /^这件事和 04:55 对得上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。$/);
|
||||
for (const text of [moved!]) assert.doesNotMatch(text, /确认|精确|相对支持|领先|落后/);
|
||||
});
|
||||
Reference in New Issue
Block a user