fix(rectification): narrate the credible range and stop scoring periods by length (BUG-569–570)
Choice copy was comparing the search window, so every answer said the range had not changed. Block-scan summed raw scores, so longer afternoon windows won before any evidence difference. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+17
@@ -83,3 +83,20 @@
|
||||
- 无法获取任务书要求的上游 `interview_playbook.md`、`evidence_thresholds.md`:任务书所指的 `~/.workbuddy/skills/jyotish-birth-time-rectification/` 在当前执行环境不存在,仓库内只有测试对该外部路径的引用;未伪造文件,也没有可验证的上游来源可供导入。
|
||||
- 无法获取一次真实本地校正会话完整记录:当前仓库没有可证明为真实线上会话的完整原始记录,执行环境也没有受控会话/上游维护者提供的记录。因此无法可靠回答轮数、最终区间宽度、`confidence` 与 `can_apply`。
|
||||
- 该信息收集缺口不阻塞任务 1–3,按 v2 任务书继续实现并在进度文件中标记为未验证;不得据此声称已验证“固定题数后停止”的上游机制。
|
||||
|
||||
## BLK-001 · 长对话本地收窄后 VedAstro 调用窗口与断言不一致(2026-09-06)
|
||||
|
||||
- 状态:blocked(基线即失败,非 BUG-565~567 引入)
|
||||
- 首次记录:2026-09-06
|
||||
- 测试:`tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow`
|
||||
- 复现:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest -q tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow --tb=line
|
||||
```
|
||||
|
||||
- 失败断言:`result["winning_segment"]` 期望 `start_time=05:07`、`end_time=05:08`、`representative_time=05:07`、`width_minutes=2`;实测 `04:16` / `04:16` / `04:16` / `width_minutes=1`。
|
||||
- 已知同样失败的 SHA:`e2f4b55c`(父任务书验收段已复跑);工作树 `43a26a3c`(文档头,Python 与 `origin/staging` 一致)同样失败。测试首次出现于 `3ca30ed7`。未做完整二分:父任务已证明早于 BUG-558~560。
|
||||
- 本单范围:只记录。不改引擎、不改门槛、不改断言来让它变绿。
|
||||
- 相关:BUG-560 根因升级(分钟级原始分区分力≈随机);`docs/tasks/TASK-rectification-convergence-exit-20260906.md` 验收段。
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-07 — 答完题后的范围跟顶部一致,不清楚出生时间时不再偏爱下午
|
||||
|
||||
生时校正点完选择题后,旁白里的范围跟输入框上方那一行走,不会再把开工搜索窗口说成「范围没变」。完全不清楚出生时间时,清晨、上午、下午、傍晚、夜里五段按段内平均分来比,不会因为下午更长就先天占优。Skill 版本仍是 10.0.14。
|
||||
|
||||
## 2026-09-06 — 完全不知道出生时间时先比时段
|
||||
|
||||
选「完全不清楚」后可以直接开始生时校正:先用记得的经历比出清晨、上午、下午、傍晚或夜里哪一段更像,选定后再按分钟收窄。不会再劝退成「以后再做」,也不会在全日窗口上给出分钟或采用卡。家人记得的时段只作旁白建议,改不了搜索窗口。Skill 版本仍是 10.0.14。
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Blocked work
|
||||
|
||||
本文件记录当前无法在本仓内闭环、也不得标成通过的验证。新条目追加在表后。不写姓名、出生资料或完整请求体。
|
||||
|
||||
## 条目
|
||||
|
||||
### BLK-001 · 长对话本地收窄后 VedAstro 调用窗口与断言不一致
|
||||
|
||||
- 状态:blocked(基线即失败,非 BUG-565~567 引入)
|
||||
- 首次记录:2026-09-06
|
||||
- 测试:`tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow`
|
||||
- 复现:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest -q tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow --tb=line
|
||||
```
|
||||
|
||||
- 失败断言:`result["winning_segment"]` 期望 `start_time=05:07`、`end_time=05:08`、`representative_time=05:07`、`width_minutes=2`;实测 `04:16` / `04:16` / `04:16` / `width_minutes=1`。
|
||||
- 已知同样失败的 SHA:`e2f4b55c`(父任务书验收段已复跑);本工作树 `43a26a3c`(文档头,Python 与 `origin/staging` 一致)同样失败。测试首次出现于 `3ca30ed7`。未做完整二分:父任务已证明早于 BUG-558~560。
|
||||
- 本单范围:只记录。不改引擎、不改门槛、不改断言来让它变绿。
|
||||
- 相关:BUG-560 根因升级(分钟级原始分区分力≈随机);`docs/tasks/TASK-rectification-convergence-exit-20260906.md` 验收段。
|
||||
@@ -8806,4 +8806,36 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待合入 `origin/staging`(`codex/rectification-convergence-exit-fix-20260906`)
|
||||
|
||||
## BUG-569 | 答后旁白把搜索窗口当成可信区间,每题都说「范围没变」
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-07
|
||||
- 最近更新:2026-09-07
|
||||
- 影响面:`composeChoiceNarration`、`applyRectificationChoice`、`probe-explain.explainRangeChange`
|
||||
- 用户现象:顶部只读范围已经随答题收窄,助手旁白仍说「范围没变」。
|
||||
- 触发条件:点选区分卡 A/B/C,搜索窗口 `range_start/range_end` 不变,而 `credible_range` 变了。
|
||||
- 根因:`applyRectificationChoice` 把 `InferenceState.range_start/range_end`(开工搜索窗口)传给旁白。答题改的是 `credible_range`。直喂 `composeChoiceNarration` 的单测没走这条路径。
|
||||
- 修复:旁白改比 `previous.credible_range` 与 `applied.state.credible_range`。入参改名 `credibleBefore/credibleAfter`。任一为空则不说范围句。
|
||||
- 验证:`frontend/tests/rectification-answer-choice.test.ts`:搜索窗口固定、`credible_range` 收窄时旁白含「范围从 X 收到 Y」;`credible_range` 不变时仍说「范围没变」。源扫描锁定不再传 `range_start`。
|
||||
- 防复发:不得把 `range_start/range_end` 再传给答后范围句。新增端到端用例必须走 `applyRectificationChoice`,不能只直喂纯函数。
|
||||
- 相关记录:BUG-568
|
||||
- 复发自:过程解释层(`814c924e`)把搜索窗口误当可信区间
|
||||
- 修复版本:待合入 `origin/staging`(`codex/rectification-capability-fix-20260907`)
|
||||
|
||||
## BUG-570 | 时段支持度按段内原始分求和,长时段先天占优
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-07
|
||||
- 最近更新:2026-09-07
|
||||
- 影响面:`scripts/rectification/api_service.py::block_scan`
|
||||
- 用户现象:完全不清楚出生时间时,第一张时段卡下午段支持度偏高,即使各分钟分数相同。
|
||||
- 触发条件:`stage=block_scan`、10 分钟步长扫 24 小时。五段候选数 24/24/36/30/30。
|
||||
- 根因:`relative_support` 用段内 `sum(score)`。下午 36 个候选,清晨 24 个;引擎原始分底座远大于事件差,于是在没有证据差异时也是在比时段长度。
|
||||
- 修复:`raw_support = max(mean(score in block) − min(all scores), 0)`,再归一。全部为零时五段各 20%。不进采用门,不改分钟级 `_relative_support`。
|
||||
- 验证:`tests/test_rectification_v5_services.py`:144 行同分 → 五段各 20.0;只抬高清晨 → 清晨最高,下午不再因长度领先。`minute_step=1` 指纹用例未改。
|
||||
- 防复发:`block_scan` 不得再对段内原始分求和。分钟级 prior 仍走原来的正比例归一(BUG-560)。
|
||||
- 相关记录:BUG-560
|
||||
- 复发自:unknown-time `block_scan`(`814c924e`)按时段长度偏置
|
||||
- 修复版本:待合入 `origin/staging`(`codex/rectification-capability-fix-20260907`)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# PROGRESS · 校正能力补齐修复单(2026-09-07)
|
||||
|
||||
工作树:`.worktrees/rectification-capability-fix-20260907`
|
||||
分支:`codex/rectification-capability-fix-20260907`(跟踪 `origin/staging`)
|
||||
基线:`origin/staging` @ `d6ee8cbb`(含 `814c924e` BUG-565~568)
|
||||
|
||||
未改 `SKILL.md`(仍 10.0.14)、未改采用门 / 确认门 / `MIN_SEPARATION_LEAD` / 分钟级 `_relative_support`、未改 `minute_step=1` 路径、未改 `page.tsx`。未把 `.venv` 加入提交。
|
||||
|
||||
本单状态:**待验收**。BUG-569 / BUG-570 已实现;P2 文案与 P3 BLOCKED 同提交。
|
||||
|
||||
| 任务 | 状态 | BUG |
|
||||
| --- | --- | --- |
|
||||
| 4.1 旁白比 credible_range | 完成 | BUG-569 |
|
||||
| 4.2 时段支持度按段内均值 | 完成 | BUG-570 |
|
||||
| 4.3 TRACK_LABEL + BLOCKED 归位 | 完成 | P2 / P3 |
|
||||
| 4.4 记录 | 完成 | BUG_HISTORY / CHANGELOG / 本文件 / testing 第 2 条 |
|
||||
|
||||
## 实现要点
|
||||
|
||||
- `composeChoiceNarration` 入参改为 `credibleBefore/credibleAfter`。`applyRectificationChoice` 传入 `previous.credible_range` 与 `applied.state.credible_range`。搜索窗口 `range_start/range_end` 不再进旁白。
|
||||
- `block_scan`:`raw_support = max(mean(score in block) − min(all scores), 0)`,全零时五段各 20%。`candidate_count` 保留。
|
||||
- `probe-explain.ts` 的大运标签改走 `publicRectificationMethodLabel`(`rectification-varga-sentence.ts`),不再维护第二份 TRACK_LABEL。
|
||||
- `docs/BLOCKED.md` 的 BLK-001 搬回根目录 `BLOCKED.md` 并删除前者。
|
||||
|
||||
### 既有断言改动
|
||||
|
||||
| 文件 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `rectification-answer-choice` 纯函数旁白 | `rangeBefore/rangeAfter` 直喂 | 入参改名 `credibleBefore/credibleAfter`,仍测拼句;新增 `applyRectificationChoice` 端到端 | 直喂拦不住把搜索窗口当可信区间 |
|
||||
| `rectification-choice-card` why_user | 只锁「分成两组」 | 并锁 Vimshottari / Narayana,禁止音译 | P2 |
|
||||
|
||||
## 测试
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| 非 DB `rectification-*` + `consultation-*` + `report-*` + `personal-report-*` + `agent-voice-copy-contract` | **1521 pass / 0 fail**(≥ 1518) |
|
||||
| `.venv/bin/python -m pytest tests/test_rectification_v5_services.py -k 'not seven_events'` | **31 pass / 0 fail**(含 BUG-570 等分网格与清晨抬高) |
|
||||
| `test_block_scan_seven_events_finishes_within_fifteen_seconds` 单独跑 | 通过。整文件连跑时偶发 16.0s / 24s / 34.8s,与本次均值公式无关;机器有负载时 15s 预算偏紧 |
|
||||
| `npx tsc --noEmit` | 0 错 |
|
||||
| `./node_modules/.bin/eslint --quiet src tests` | 0 error |
|
||||
|
||||
排除 `rectification-*-database.test.ts`。ERR-011:验收命令用 `.venv/bin/python -m pytest`。未改 `page.tsx`。未改 `minute_step=1` 指纹。
|
||||
@@ -13,7 +13,7 @@
|
||||
| 5.1 单一写入方 | 完成 | BUG-565 |
|
||||
| 5.2 穷尽态视为已交付 | 完成 | BUG-565 |
|
||||
| 5.3 穷尽分支不吞题 | 完成 | BUG-566 |
|
||||
| 5.4 范围小字还原 + 记录 | 完成 | BUG-567;BUG-560 根因升级;`docs/BLOCKED.md` BLK-001 |
|
||||
| 5.4 范围小字还原 + 记录 | 完成 | BUG-567;BUG-560 根因升级;`BLOCKED.md` BLK-001 |
|
||||
|
||||
## 实现要点
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
| 非 DB `tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` | **910 pass / 0 fail**(解释层叠上后 917) |
|
||||
| `npx tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | 0 error / 81 warning(既有 warning,未清) |
|
||||
| `.venv/bin/python -m pytest tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` | 失败:`winning_segment` 04:16 / 宽 1 vs 期望 05:07–05:08 / 宽 2。基线 `e2f4b55c` 同样失败。见 `docs/BLOCKED.md` BLK-001。 |
|
||||
| `.venv/bin/python -m pytest tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` | 失败:`winning_segment` 04:16 / 宽 1 vs 期望 05:07–05:08 / 宽 2。基线 `e2f4b55c` 同样失败。见 `BLOCKED.md` BLK-001。 |
|
||||
|
||||
排除 `rectification-*-database.test.ts`。ERR-011:验收命令用 `.venv/bin/python -m pytest`。未改 `page.tsx`。
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
| `TASK-rectification-explain-layer-20260906.md` | `PROGRESS-rectification-explain-layer-20260906.md` | 过程解释层(对照上游 yinduzhanxing 旧工作台唯一领先的业务层):每张卡服务端生成「为什么问这题」与 A/B/C/D「答了会怎样」、答后旁白改成「哪段升降 + 范围从 X 收到 Y」、每轮步骤条「第 N 步 / 为什么 / 下一步」;不做双视图 | 已验收(带修复单:答后旁白比的是搜索窗口,永远「范围没变」,P1) | `814c924e`;修复单 `TASK-rectification-capability-fix-20260907.md`(BUG-569) |
|
||||
| `TASK-rectification-range-reading-20260906.md` | `PROGRESS-rectification-range-reading-20260906.md` | 可信区间成为一等公民:采用时落库 `adopted_credible_range`,报告 `read_report_candidate_range` 与聊天 `verified_chart(accepted)` 都改读它并接同一份 `birth_time_sensitivity`(现在报告读的是开工窗口,BUG-568);引擎 >15 分钟只取 3 样本改为 ≤31 逐分钟;采用旁白加「稳定 / 随分钟变」两句;`declared_birth_window` 复用 | 已验收通过(test:db 37/0 为执行方数字,本机无 Docker) | `814c924e`(BUG-568);两条迁移待应用 |
|
||||
| `TASK-rectification-unknown-time-20260906.md` | `PROGRESS-rectification-unknown-time-20260906.md` | 完全不知道出生时间的两段式路线:`stage=block_scan` 以 10 分钟步长扫 24 小时只做事件计分、出五时段四选卡(不写账本不采用),选定后进现有分钟流程;引擎加 `minute_step`;开场读 `birth_time_clue`;删 intake 劝退文案 | 已验收(带修复单:时段支持度按段长偏置,下午段先天 25%,P1) | `814c924e`;修复单 `TASK-rectification-capability-fix-20260907.md`(BUG-570) |
|
||||
| `TASK-rectification-capability-fix-20260907.md` | `PROGRESS-rectification-capability-fix-20260907.md` | 能力补齐修复单:答后旁白把 `range_start/range_end`(搜索窗口)当范围比较,每题都说「范围没变」;`block_scan` 五段支持度按段内原始分求和,长时段先天占优(24/24/36/30/30 个候选);`TRACK_LABEL` 音译与产品 Vimshottari/Narayana 口径不一;BLK-001 写到 `docs/BLOCKED.md` 应回根目录 | 待领取 | `codex/rectification-capability-fix-20260907`(BUG-569~570) |
|
||||
| `TASK-rectification-capability-fix-20260907.md` | `PROGRESS-rectification-capability-fix-20260907.md` | 能力补齐修复单:答后旁白把 `range_start/range_end`(搜索窗口)当范围比较,每题都说「范围没变」;`block_scan` 五段支持度按段内原始分求和,长时段先天占优(24/24/36/30/30 个候选);`TRACK_LABEL` 音译与产品 Vimshottari/Narayana 口径不一;BLK-001 写到 `docs/BLOCKED.md` 应回根目录 | 待验收 | `codex/rectification-capability-fix-20260907`(BUG-569~570) |
|
||||
| `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 已验收 | `5483649b`(BUG-542);2 条子进程测试留 CI Node 22 复核 |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
- ✅ 预期:题干下方有可点开的「为什么问这题」。展开后能看到年月或 D9/D10,以及「分成两组」或「类型差异」。没有「概率 / 置信度 / 确定」。
|
||||
- ❌ 失败:没有折叠说明;或说明仍是给模型看的「时间范围锁定…语义目标是…」。
|
||||
|
||||
## 2. 答完一题能看见哪段升降和范围变化
|
||||
## 2. 答完一题旁白里的范围与顶部范围一致
|
||||
|
||||
点 A 或 B。
|
||||
|
||||
- ✅ 预期:助手旁白仍以「已记录你的选择」开头,并写哪段领先/落后;范围变了就写「范围从 … 收到 …」,没变就写「范围没变」。点 D 仍是「这题先不计分,换一件事问」。
|
||||
- ❌ 失败:仍只有「并更新了候选比较」;或范围没变却写成收窄。
|
||||
- ✅ 预期:助手旁白仍以「已记录你的选择」开头,并写哪段领先/落后;旁白里的范围与输入框上方那一行相同,变了就写「范围从 … 收到 …」,没变就写「范围没变」。点 D 仍是「这题先不计分,换一件事问」。
|
||||
- ❌ 失败:仍只有「并更新了候选比较」;或顶部范围已经收窄,旁白却写「范围没变」。
|
||||
|
||||
## 3. 每轮顶部有第 N 步 / 原因 / 下一步
|
||||
|
||||
|
||||
@@ -592,8 +592,8 @@ export async function applyRectificationChoice(
|
||||
appliedInference: persistable && scoring,
|
||||
answerClass,
|
||||
deltasByCluster: clusterScoreDeltas(previous.candidates, appliedScoreDeltas),
|
||||
rangeBefore: [previous.range_start, previous.range_end],
|
||||
rangeAfter: [applied.state.range_start, applied.state.range_end],
|
||||
credibleBefore: previous.credible_range,
|
||||
credibleAfter: applied.state.credible_range,
|
||||
});
|
||||
const evidenceFp = dossier.latestResult?.evidenceLedgerFingerprint
|
||||
?? evidenceLedgerFingerprint(dossier.evidence);
|
||||
|
||||
@@ -102,8 +102,8 @@ export function composeChoiceNarration(input: {
|
||||
appliedInference: boolean;
|
||||
answerClass?: AnswerClass | null;
|
||||
deltasByCluster?: readonly ClusterScoreDelta[];
|
||||
rangeBefore?: readonly [string, string] | null;
|
||||
rangeAfter?: readonly [string, string] | null;
|
||||
credibleBefore?: readonly [string, string] | null;
|
||||
credibleAfter?: readonly [string, string] | null;
|
||||
}): string {
|
||||
if (input.optionId === "stop") {
|
||||
return `已记录你的选择,并结束本次校正,交付当前可信区间和代表性工作时间。${RECTIFICATION_TERMINATION_COPY}`;
|
||||
@@ -119,7 +119,7 @@ export function composeChoiceNarration(input: {
|
||||
}
|
||||
if (input.appliedInference) {
|
||||
const movement = explainScoreMovement(input.deltasByCluster ?? []);
|
||||
const range = explainRangeChange(input.rangeBefore, input.rangeAfter);
|
||||
const range = explainRangeChange(input.credibleBefore, input.credibleAfter);
|
||||
if (movement || range) {
|
||||
return ["已记录你的选择", movement, range].filter(Boolean).join("。") + "。";
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* The model writes the stem; this module writes "why" and "what answering does".
|
||||
*/
|
||||
|
||||
import { publicRectificationMethodLabel } from "../../rectification-varga-sentence.ts";
|
||||
import type { AnswerClass } from "../core/types.ts";
|
||||
import type { DiscriminatingEventProbe, ProbeExpectedOutcome } from "./refinement-packet.ts";
|
||||
|
||||
@@ -31,11 +32,6 @@ export type ProbeUserExplain = Readonly<{
|
||||
answer_impact: ProbeAnswerImpact;
|
||||
}>;
|
||||
|
||||
const TRACK_LABEL: Readonly<Record<string, string>> = {
|
||||
vimshottari: "毗湿奴多利",
|
||||
narayana: "那罗延",
|
||||
};
|
||||
|
||||
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
|
||||
export const PROBE_EXPLAIN_COPY = {
|
||||
@@ -91,7 +87,10 @@ function rangesForTimes(
|
||||
}
|
||||
|
||||
function trackLabels(tracks: readonly string[] | undefined): string {
|
||||
const labels = [...new Set((tracks ?? []).map((track) => TRACK_LABEL[track]).filter(Boolean))];
|
||||
const labels = [...new Set((tracks ?? []).flatMap((track) => {
|
||||
const label = publicRectificationMethodLabel(track);
|
||||
return label ? [label] : [];
|
||||
}))];
|
||||
return labels.join("、");
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,17 @@ export const PUBLIC_RECTIFICATION_METHOD_LABELS: Readonly<Record<PublicRectifica
|
||||
"functional-benefic-malefic": "本命功能吉凶星",
|
||||
};
|
||||
|
||||
export function publicRectificationMethodLabel(methodOrTrack: string): string | null {
|
||||
const direct = PUBLIC_RECTIFICATION_METHOD_LABELS[methodOrTrack as PublicRectificationMethod];
|
||||
if (direct) return direct;
|
||||
return PUBLIC_RECTIFICATION_METHOD_LABELS[`${methodOrTrack}-dasha` as PublicRectificationMethod] ?? null;
|
||||
}
|
||||
|
||||
export function vargaSentenceFromMethods(
|
||||
methods: readonly string[] | null | undefined,
|
||||
): string | null {
|
||||
const labels = [...new Set((methods ?? []).flatMap((method) => {
|
||||
const label = PUBLIC_RECTIFICATION_METHOD_LABELS[method as PublicRectificationMethod];
|
||||
const label = publicRectificationMethodLabel(method);
|
||||
return label ? [label] : [];
|
||||
}))];
|
||||
if (labels.length === 0) return null;
|
||||
|
||||
@@ -150,6 +150,13 @@ test("collect prompt skip no longer writes a second assistant turn", () => {
|
||||
assert.doesNotMatch(exit, /shouldPersistFocusPromptTurn/);
|
||||
});
|
||||
|
||||
test("probe explain tracks use the shared Vimshottari / Narayana labels", () => {
|
||||
const explain = readFileSync(new URL("../src/lib/rectification-agentic/v9/probe-explain.ts", import.meta.url), "utf8");
|
||||
const visible = listUserVisibleCopy().join("\n");
|
||||
assert.doesNotMatch(explain, /毗湿奴多利|那罗延/);
|
||||
assert.doesNotMatch(visible, /毗湿奴多利|那罗延/);
|
||||
});
|
||||
|
||||
test("E-2 display copy keeps engine meaning out of instruction tone", () => {
|
||||
const dasha = engineMeaningToDisplayCopy(
|
||||
"主限更偏向 05:00,分盘大运更偏向 04:54。冲突时不能按更高把握收口。",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
isIncompleteRunBanner,
|
||||
} from "../src/lib/rectification-agentic/v9/run-diagnostic.ts";
|
||||
import { applyHoldoutAnswer, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { applyChoiceWithoutEvidence } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { RECTIFICATION_TERMINATION_COPY } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { parseV9CaseDossier, RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
@@ -193,6 +194,68 @@ function inferenceState() {
|
||||
});
|
||||
}
|
||||
|
||||
function rangeNarrationInference(leadSupport: number, trailSupport: number) {
|
||||
return buildInferenceState({
|
||||
range_start: "04:31",
|
||||
range_end: "05:07",
|
||||
candidates: [
|
||||
{ id: "04:31", time: "04:31", relative_support: leadSupport },
|
||||
{ id: "05:07", time: "05:07", relative_support: trailSupport },
|
||||
],
|
||||
events: [
|
||||
{ id: "e1", domain: "education", year: 2016, precision: "month" },
|
||||
{ id: "e2", domain: "career", year: 2018, precision: "year" },
|
||||
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
|
||||
{ id: "e4", domain: "family", year: 2023, precision: "year" },
|
||||
],
|
||||
probes: [{
|
||||
id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
candidate_split_hash: "04:31|05:07",
|
||||
domain: "career",
|
||||
year: 2015,
|
||||
question: "2016 年前后有没有高考或重要考试发挥失常?",
|
||||
candidate_ids: ["04:31", "05:07"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["04:31"], conflicts: ["05:07"] },
|
||||
{ answer_class: "no", supports: ["05:07"], conflicts: ["04:31"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.4,
|
||||
source: "dasha_boundary",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function rangeNarrationDossier(inference: ReturnType<typeof buildInferenceState>) {
|
||||
return dossierFixture({
|
||||
latestResult: candidateSnapshotFixture({
|
||||
decisionReceipt: { inference_state: inference },
|
||||
}),
|
||||
conversationSummary: conversationSummaryFixture({
|
||||
activeFocus: activeFocusFixture({
|
||||
expectedAnswerSchema: {
|
||||
choice: {
|
||||
prompt: "2016 年前后,有没有明显高考或重要考试发挥失常?",
|
||||
option_a: "是,大概就在那段时间",
|
||||
option_b: "有类似,但年份不对或不够重大",
|
||||
option_c: "没有明显发生",
|
||||
option_d: "不记得 / 不确定",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
{ key: "B", label: "有类似,但年份不对或不够重大", answer_class: "weak_yes" },
|
||||
{ key: "C", label: "没有明显发生", answer_class: "no" },
|
||||
{ key: "D", label: "不记得 / 不确定", answer_class: "unsure" },
|
||||
],
|
||||
},
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function choiceDossier() {
|
||||
const inference = inferenceState();
|
||||
const snapshot = candidateSnapshotFixture({
|
||||
@@ -1207,8 +1270,10 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
{ range: ["04:31", "04:39"], delta: 2 },
|
||||
{ range: ["05:00", "05:07"], delta: -2 },
|
||||
],
|
||||
rangeBefore: ["04:31", "05:07"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
// 旧:rangeBefore/rangeAfter 直喂两个不同搜索窗口 → 新:入参改名 credibleBefore/credibleAfter,仍测纯函数拼句
|
||||
// 原因:直喂拦不住 applyRectificationChoice 把 range_start/range_end 当成范围
|
||||
credibleBefore: ["04:31", "05:07"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(narrowed, /领先/);
|
||||
assert.match(narrowed, /落后/);
|
||||
@@ -1218,8 +1283,8 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
deltasByCluster: [{ range: ["04:31", "04:39"], delta: 1 }],
|
||||
rangeBefore: ["04:31", "04:39"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
credibleBefore: ["04:31", "04:39"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(unchanged, /范围没变/);
|
||||
const unsure = composeChoiceNarration({
|
||||
@@ -1227,12 +1292,80 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
answerClass: "unsure",
|
||||
rangeBefore: ["04:31", "04:39"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
credibleBefore: ["04:31", "04:39"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.equal(unsure, "已记录。这题先不计分,换一件事问。");
|
||||
});
|
||||
|
||||
test("answering A narrates the credible range even when the search window stays put", async () => {
|
||||
const previous = rangeNarrationInference(10, 3);
|
||||
assert.deepEqual([previous.range_start, previous.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(previous.credible_range, ["04:31", "05:07"]);
|
||||
const reduced = applyChoiceWithoutEvidence(previous, {
|
||||
choiceKey: "A",
|
||||
schema: {
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(reduced.applied, true);
|
||||
assert.deepEqual([reduced.state.range_start, reduced.state.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(reduced.state.credible_range, ["04:31", "04:31"]);
|
||||
const applied = await applyRectificationChoice(persistChoiceAccounting(rangeNarrationDossier(previous)).client, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
actionId: ACTION_ID,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: FOCUS_ID,
|
||||
questionId: QUESTION_ID,
|
||||
probeId: "p-cd",
|
||||
optionId: "A",
|
||||
expectedRevision: previous.revision,
|
||||
});
|
||||
assert.match(applied.narration, /范围从 04:31–05:07 收到 04:31–04:31/);
|
||||
assert.doesNotMatch(applied.narration, /范围没变/);
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
|
||||
assert.match(source, /credibleBefore:\s*previous\.credible_range/);
|
||||
assert.match(source, /credibleAfter:\s*applied\.state\.credible_range/);
|
||||
assert.doesNotMatch(source, /rangeBefore:\s*\[previous\.range_start/);
|
||||
});
|
||||
|
||||
test("answering A says the range did not change when the credible range stays put", async () => {
|
||||
const previous = rangeNarrationInference(10, 10);
|
||||
assert.deepEqual(previous.credible_range, ["04:31", "05:07"]);
|
||||
const reduced = applyChoiceWithoutEvidence(previous, {
|
||||
choiceKey: "A",
|
||||
schema: {
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.deepEqual([reduced.state.range_start, reduced.state.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(reduced.state.credible_range, ["04:31", "05:07"]);
|
||||
const applied = await applyRectificationChoice(persistChoiceAccounting(rangeNarrationDossier(previous)).client, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
actionId: ACTION_ID,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: FOCUS_ID,
|
||||
questionId: QUESTION_ID,
|
||||
probeId: "p-cd",
|
||||
optionId: "A",
|
||||
expectedRevision: previous.revision,
|
||||
});
|
||||
assert.match(applied.narration, /范围没变/);
|
||||
assert.doesNotMatch(applied.narration, /范围从 /);
|
||||
});
|
||||
|
||||
test("the public agent route treats structured choice as a non-model command", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||||
const start = route.indexOf("if (isStructuredChoice)");
|
||||
|
||||
@@ -130,6 +130,9 @@ test("choice frames ask one biographical event from a server probe, not competin
|
||||
assert.equal(mergeChoiceCard(frame, null), null);
|
||||
assert.match(frame.why_user, /2016 年前后/);
|
||||
assert.match(frame.why_user, /分成两组/);
|
||||
assert.match(frame.why_user, /Vimshottari/);
|
||||
assert.match(frame.why_user, /Narayana/);
|
||||
assert.doesNotMatch(frame.why_user, /毗湿奴多利|那罗延/);
|
||||
assert.match(frame.answer_impact.A, /05:00/);
|
||||
assert.match(frame.answer_impact.A, /领先/);
|
||||
assert.equal(frame.answer_impact.D, "不计分,换一题");
|
||||
|
||||
@@ -427,6 +427,8 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
row for row in scored.get("candidate_scores") or []
|
||||
if isinstance(row, dict) and str(row.get("time") or "")[:5]
|
||||
]
|
||||
day_scores = [float(row.get("score") or 0) for row in rows]
|
||||
min_day = min(day_scores) if day_scores else 0.0
|
||||
raw_support: list[float] = []
|
||||
blocks: list[dict[str, Any]] = []
|
||||
for period, start_time, end_time in BLOCK_SCAN_PERIODS:
|
||||
@@ -448,7 +450,9 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
"domain": event.get("domain"),
|
||||
"summary": event.get("summary"),
|
||||
})
|
||||
raw_support.append(sum(float(row.get("score") or 0) for row in members))
|
||||
member_scores = [float(row.get("score") or 0) for row in members]
|
||||
mean = (sum(member_scores) / len(member_scores)) if member_scores else 0.0
|
||||
raw_support.append(max(mean - min_day, 0.0))
|
||||
blocks.append({
|
||||
"period": period,
|
||||
"start_time": start_time,
|
||||
|
||||
@@ -908,6 +908,46 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
self.assertEqual(result["candidate_count"], 144)
|
||||
self.assertLessEqual(elapsed, 15, f"block_scan took {elapsed:.1f}s")
|
||||
|
||||
def test_block_scan_equal_scores_share_evenly_and_length_does_not_win(self):
|
||||
from scripts.rectification.api_service import block_scan
|
||||
|
||||
def grid(score_for):
|
||||
return [
|
||||
{"time": f"{hour:02d}:{minute:02d}", "score": score_for(hour), "supporting_event_ids": []}
|
||||
for hour in range(24)
|
||||
for minute in range(0, 60, 10)
|
||||
]
|
||||
|
||||
equal_rows = grid(lambda _hour: 12.0)
|
||||
morning_rows = grid(lambda hour: 20.0 if 4 <= hour <= 7 else 10.0)
|
||||
dummy = {
|
||||
"result_id": "00000000-0000-4000-8000-000000000099",
|
||||
"algorithm_version": "test",
|
||||
"calculation_spec": {},
|
||||
"calculation_spec_hash": "abc",
|
||||
"decision_receipt": {},
|
||||
}
|
||||
request_body = {
|
||||
"birth_date": "1998-03-15",
|
||||
"start_time": "00:00",
|
||||
"end_time": "23:59",
|
||||
"lat": 39.9042,
|
||||
"lon": 116.4074,
|
||||
"tz": 8.0,
|
||||
"minute_step": 10,
|
||||
"events": [],
|
||||
}
|
||||
with patch("scripts.rectification.api_service.score_candidates", return_value={**dummy, "candidate_scores": equal_rows}):
|
||||
equal = block_scan(request_body)
|
||||
self.assertEqual([row["relative_support"] for row in equal["blocks"]], [20.0, 20.0, 20.0, 20.0, 20.0])
|
||||
self.assertEqual([row["candidate_count"] for row in equal["blocks"]], [24, 24, 36, 30, 30])
|
||||
with patch("scripts.rectification.api_service.score_candidates", return_value={**dummy, "candidate_scores": morning_rows}):
|
||||
morning = block_scan(request_body)
|
||||
shares = {row["period"]: row["relative_support"] for row in morning["blocks"]}
|
||||
self.assertEqual(max(shares, key=shares.get), "early_morning")
|
||||
self.assertGreater(shares["early_morning"], shares["afternoon"])
|
||||
self.assertEqual(shares["afternoon"], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user