fix(rectification): equal-weight collect, year-cue questions, classifier no/unsure (BUG-641–643)
Ask finance and health like other domains, put year-cued collect questions first, and let the classifier distinguish decline vs skip. Skill 10.0.22. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-10 — 财务和健康也会主动问;采集题可带年份线索;「没有 / 记不清」改由分类器判断
|
||||
|
||||
生时校正里,财务和健康与其它经历同权:服务器可以主动问,你说了就记下并计分。采集题如果有年份线索,会先问「某年前后有没有…别的年份也行」,不再又指定年份又让你报年份。打「没有」和「不记得了」由分类器区分成「这方面先跳过」和「这题先放着」,不再只认整句这两个词。Skill 10.0.22。
|
||||
|
||||
|
||||
## 2026-09-10 — 采集题被系统顶替后仍会再问;没有可采用结果时助手不再用交付口吻收尾
|
||||
|
||||
生时校正里,如果系统先为某一领域落下采集题、随后又被别的题顶替,原来的题号仍会挡住再问。现在撞号会改用带后缀的题号继续问;没有可采用结果时,助手不再说「这次给出的范围」却不出卡。Skill 版本不变。
|
||||
|
||||
+51
-3
@@ -8329,15 +8329,15 @@
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-04
|
||||
- 最近更新:2026-09-04
|
||||
- 最近更新:2026-09-10
|
||||
- 影响面:`collectionYearFields`、`spokenFollowupForUser` 对 `collect_method_evidence`
|
||||
- 用户现象:家庭采集题写成「某年前后,家里如果有结婚、添丁或住院这类事,记得大概哪年就行。」既指定了年份又让用户报年份。
|
||||
- 触发条件:家庭轮转把 dated 探针的 `year_label` 拼进 `USER_COLLECT_QUESTION.family`。
|
||||
- 根因:`68f0759e` 为 BUG-414/418「带年份的口语问、不出无年份分盘选择卡」把 dated 探针年份塞进采集口语。`USER_COLLECT_QUESTION.family` 本身以「记得大概哪年就行」结尾,拼在一起自相矛盾。本单只去年份前缀。家人拒答后若还有未问的财务/搬家/健康/职业采集,不得直接给结果——见 BUG-546(推翻本条原先「出采用卡不是缺陷」的收口)。
|
||||
- 修复:采集口语不再拼「某年前后,」。`probe_year` / `semantic_key` 仍留给计分与去重。不出无年份分盘选择卡的半边保留。
|
||||
- 验证:家庭采集口语等于 `USER_COLLECT_QUESTION.family`,同时 `probe_year > 0`。答完最后一道区分题后仍落家庭采集焦点,旁白不含探针年份。BUG-418 无年份分盘卡测试仍过。
|
||||
- 防复发:`collect_method_evidence` 口语不得加年份前缀。不得为去前缀而丢掉 `probe_year` 或放回无年份分盘选择卡。
|
||||
- 相关记录:BUG-414、BUG-418、BUG-546
|
||||
- 防复发:采集口语不得既指定年份又说「记得大概哪年就行」。**口径被 BUG-642 改为带线索模板**:有 `probe_year` 时用「某年前后有没有…别的年份也行」;无线索模板仍用「记得大概哪年就行」。不得为去前缀而丢掉 `probe_year` 或放回无年份分盘选择卡抢在采集前面。
|
||||
- 相关记录:BUG-414、BUG-418、BUG-546、BUG-642
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -9957,6 +9957,54 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-641 | 财务与健康被写成「只有主动说才问」,服务器却会主动采集
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-10
|
||||
- 最近更新:2026-09-10
|
||||
- 影响面:`skills/jyotish-birth-time-rectification/SKILL.md`、`conversation-strategy.md`、`scripts/rectification/event_probes.py`、Skill 10.0.22
|
||||
- 用户现象:Skill 写财务/健康只有用户主动说才问,实际对话里服务器仍会问钱和身体;Python 采集探针又不给这两域年份线索。
|
||||
- 触发条件:新校正走到方法覆盖采集;账本只有学业/感情时看 `evidence_collection_probes`。
|
||||
- 根因:三处口径各自为政。Skill L84 与 conversation-strategy 写 volunteer-only;TS `DATED_COLLECT_ORDER` 自 BUG-462 起含 finance;Python `VOLUNTEER_ONLY` 把 finance / health_pressure 挡在 `missing_collection_domains` 和 `evidence_collection_probes` 外。
|
||||
- 修复:Skill 10.0.22 改为财务、健康与其他领域同权,服务器按 `method_followup_plan.next_followup` 主动问。删除 `VOLUNTEER_ONLY`。`MAX_COLLECTION_PROBES` 3→7,避免学业+感情账本被前三个领域占满后发不出财务/健康。历史 10.0.21 包冻结,`resolveExactSkillPackage` 仍能打开。
|
||||
- 验证:`tests/test_rectification_event_probes.py` 学业+感情账本含 finance 与 health_pressure;`frontend/tests/skill-registry.test.ts` 10.0.21 精确解析;live SKILL.md 与 `versions/10.0.22` 逐字节相等。
|
||||
- 防复发:现行 Skill / 10.0.22 不得再写「主动说才问」。不得把冻结历史包改写成新口径。不得把 `.workbuddy` 镜像当运行主仓。
|
||||
- 相关记录:BUG-462、BUG-621、BUG-642
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-642 | 带年份线索的采集题被整句去前缀,无年份性格卡还能抢在采集前面
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-10
|
||||
- 最近更新:2026-09-10
|
||||
- 影响面:`user-copy.ts`、`spokenFollowupForUser`、`nextDatedCollectFollowup`、方法覆盖链
|
||||
- 用户现象:家人采集题号带 2021,题干却没有年份;同领域无年份性格卡可以抢在带线索采集题前面。
|
||||
- 触发条件:`evidence_collection_probes` 给出 `age_band` 年份;同域还有 D12 等无年份对照。
|
||||
- 根因:BUG-539 因「某年前后…记得大概哪年就行」自相矛盾而把年份前缀整句删掉。`sameDomainYearlessCard` 在感情/事业/家人未覆盖时优先于采集。
|
||||
- 修复:采集题拆成无线索模板与带线索模板。有 `probe_year` 时写成「家里在 2021 年前后有没有…有就说个大概年月,别的年份也行。」不得同时出现「记得大概哪年就行」。未覆盖的感情/事业/家人先采集;`nextDatedCollectFollowup` 在 `DATED_COLLECT_ORDER` 内先排有年份线索的领域。带年月区分点选题仍在一切采集之前。
|
||||
- 验证:`rectification-spoken-collect` / `rectification-eight-method` / `rectification-collect-stall`:家人 2021 采集口语含「2021 年前后」、不含「记得大概哪年就行」;感情/事业/家人未覆盖时 `next_followup` 是采集而不是无年份性格卡;existence 类 D12 仍记 `yearless_ungrounded_contrast`,没有单独的「拒答后出 D12 风格卡」用例。无 `probe_year` 用无线索模板。
|
||||
- 防复发:有线索的采集口语必须带「某年前后」且允许别的年份。无线索模板不得带年份。不得让同域无年份性格卡抢在该域采集之前。
|
||||
- 相关记录:BUG-539、BUG-418、BUG-629、BUG-641
|
||||
- 复发自:BUG-539(整句去前缀把回忆线索一并删掉)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-643 | 「没有 / 记不清」靠原字匹配,分类器分不出「没有」和「先放着」
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-10
|
||||
- 最近更新:2026-09-10
|
||||
- 影响面:`turn-intent-classifier.ts`、`POST /api/rectification/agent`、`expectedWriteFromCollectIntent`
|
||||
- 用户现象:只有整句等于「没有」或「记不清」才走跳过;「不记得了」「忘了」落到 Agent。采集题分类器只教了 `no`。用户用带年月的事回答采集题时,`expectedWrite` 仍是 none,BUG-635 守卫只剩正文「记下了」。
|
||||
- 触发条件:采集焦点下打「没有」「不记得了」「2018 年 3 月开始欠债」。
|
||||
- 根因:`isCollectDeclineUtterance` / `isCollectSkipUtterance` 去掉句末标点后要求整句相等。`shouldDeclineCollectFocus` 只认 `no`。`expectedWriteFromCollectIntent` 不把 collect 焦点上的 yes/weak_yes 当写入。
|
||||
- 修复:删除两个原字匹配函数和路由短路。分类器:没有→`no`(declined),记不清→`unsure`(skipped),带年月直接回答采集题→`yes` 且 `expectedWrite=evidence`。`collectFocusCloseStatus` 走既有 `applyCollectFocusDenial`。分类器失败记 `collectIntent=unclassified`,不做关键词兜底。
|
||||
- 验证:`rectification-turn-intent-classifier.test.ts`、`rectification-spoken-collect.test.ts`、`rectification-unwritten-evidence.test.ts`(collect + yes + 无写入 → 重试)。源码合同:route 与 classifier 不再引用那两个函数。
|
||||
- 防复发:采集跳过/没有不得用正则或词表。collect 焦点 yes/weak_yes 必须 `expectedWrite=evidence`。分类器 null 不得回退到关键词。
|
||||
- 相关记录:BUG-605、BUG-626、BUG-635
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-644 | 被顶替的采集题号永久占位,落库撞号后当成已问过并断流
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# PROGRESS · 采集语义三改(2026-09-10)
|
||||
|
||||
工作树:`.worktrees/rectification-collect-semantics-20260910`
|
||||
分支:`codex/rectification-collect-semantics-20260910`
|
||||
任务书:`docs/tasks/TASK-rectification-collect-semantics-20260910.md`
|
||||
基线:任务书写 `82eb3b79`;开工接到 `origin/staging` @ `71eaeb2e`(BUG-635~640、BUG-644/645 已在其上)。
|
||||
|
||||
## 开工回执
|
||||
|
||||
- 编号 **BUG-641 / 642 / 643**。当时 `docs/BUG_HISTORY.md` 最大号是 645,但 641~643 未占用,按任务书使用。
|
||||
- 顺序:T1 Skill 10.0.22 + 去掉 `VOLUNTEER_ONLY` → T2 带线索模板与采集优先 → T3 分类器 no/unsure + collect yes 的 `expectedWrite` → T4 记录。
|
||||
|
||||
## 已完成
|
||||
|
||||
- **T1 / BUG-641**:Skill L84 与 `conversation-strategy.md` 改为同权采集。删除 `VOLUNTEER_ONLY`。`MAX_COLLECTION_PROBES` 3→7。live `SKILL.md` 与 `versions/10.0.22/SKILL.md` 逐字节相等。sha256 `e3765b20ce6b49a5c7c9ed6b0a8caacc6fc4b8a16a127b93bd92c537330a54e2`。10.0.21 仍 deprecated,`resolveExactSkillPackage` 可打开。
|
||||
- **T2 / BUG-642**:`COLLECT_QUESTION_YEAR_CUE` / `collectQuestionForDomain`。`nextDatedCollectFollowup` 先排有 `evidence_collection_probes` 年份线索的领域。未覆盖感情/事业/家人先采集,不再 `sameDomainYearlessCard` 抢跑。existence 类 D12 仍 `yearless_ungrounded_contrast`。带年月区分点选题顺序未改。
|
||||
- **T3 / BUG-643**:删除 `isCollectDeclineUtterance` / `isCollectSkipUtterance`。`collectFocusCloseStatus`:`no`→declined,`unsure`→skipped。分类器失败记 `collectIntent=unclassified`。collect 焦点 `yes`/`weak_yes` → `expectedWrite="evidence"`。
|
||||
- **T4**:BUG-641~643、BUG-539 补口、CHANGELOG、本文件、任务表待验收、走查第 16~18 条、`VOICE.md` 带线索对照行。
|
||||
|
||||
## 三栏(既有断言)
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| Skill 当前钉 | 10.0.21 / sha256 `3ced1073…` | 10.0.22 / `e3765b20…` | 改 SKILL.md 必须升版 |
|
||||
| Skill L84 / conversation-strategy | 「财务与健康只有用户主动说才问,仍可计分。」 | 「财务、健康与其他领域同权:服务器按 next_followup 主动问,用户说了就记、就计分。」 | 决策 1 |
|
||||
| Python `VOLUNTEER_ONLY` | `{finance, health_pressure}` | 删除 | 决策 1 |
|
||||
| `MAX_COLLECTION_PROBES` | 3 | 7 | 学业+感情账本仍要发出 finance / health_pressure |
|
||||
| 家人采集口语(有 2021 线索) | `USER_COLLECT_QUESTION.family`(「记得大概哪年就行」) | 「家里在 2021 年前后有没有…别的年份也行。」 | 推翻 BUG-539 整句去前缀,保留矛盾禁令 |
|
||||
| 家人未覆盖 + D12 | `sameDomainYearlessCard("family")` 可抢采集 | 感情/事业/家人未覆盖先采集;existence 类 D12 仍记 `yearless_ungrounded_contrast`,没有「拒答后立刻出 D12 风格卡」断言 | 决策 2 |
|
||||
| 采集「没有 / 记不清」 | `isCollectDeclineUtterance` / `isCollectSkipUtterance` 整句相等即短路 | 分类器 `no`/`unsure` → `collectFocusCloseStatus` | 决策 3 |
|
||||
| collect 焦点 yes/weak_yes 的 `expectedWrite` | `"none"`(只靠「记下了」文字) | `"evidence"` | BUG-635 验收补漏 |
|
||||
|
||||
## 验收命令(实测)
|
||||
|
||||
工作树 `codex/rectification-collect-semantics-20260910`,相对 `origin/staging` @ `71eaeb2e`。
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `.venv/bin/python -m pytest tests -k "event_probes or collection" -q` | **45 passed / 0 fail**(collect-only:event_probes 35 + oracle collection 8 + discriminator 1 + tajika 1) |
|
||||
| `cd frontend && ls tests/rectification-*.test.ts tests/skill-registry.test.ts tests/agent-voice-copy-contract.test.ts \| grep -v database \| xargs ./node_modules/.bin/tsx --test` | **1156 pass / 0 fail**(17.7s)。同类 glob 在 yearless 单曾记 1087;本 SHA 含 BUG-635~640、644/645 及本单新增用例。 |
|
||||
| `cd frontend && ./node_modules/.bin/tsc --noEmit` | **0 error** |
|
||||
| `cd frontend && npm run lint` | **0 error**,113 warning(既有债务;本单未做无关清理) |
|
||||
| `git diff --check` | 干净 |
|
||||
| `python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime` | **Quality gate passed.** 核心 pytest **731 passed / 1 skipped / 0 fail**(312s)。未带全量 `npm test`。 |
|
||||
| `cd frontend && npm run build` | **blocked**:Turbopack `node_modules` 软链指到 worktree 外(`../../../frontend/node_modules`)。未核首页 Static / gzip。 |
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 无 Docker:未跑 `npm run test:db`。
|
||||
- 无登录态、无 Chrome:走查第 16~18 条未做。部署后须先打开 10.0.21 历史校正。
|
||||
- `next build` 在本 worktree 因 `node_modules` 软链被 Turbopack 拒绝;未核首页 Static / gzip。
|
||||
- `grep -n "主动说才问" skills/` 在冻结历史包(≤10.0.21)仍有命中;live + `versions/10.0.22` 为空。不得改历史包哈希。
|
||||
|
||||
## 未做
|
||||
|
||||
- 未提升 `main`。
|
||||
- 未改点选题四选项合同、采用门、holdout 计分。
|
||||
@@ -97,7 +97,7 @@
|
||||
| `TASK-rectification-evidence-turn-empty-answer-20260910.md` | `PROGRESS-rectification-evidence-turn-empty-answer-20260910.md` | 证据轮模型无正文被判整轮失败:证据、评分、下一问都已落库却只剩『没有拿到下一个问题』(BUG-633);答题旁白只说『范围没变』、时间线写死『还在收窄』(BUG-634) | 待验收 |
|
||||
| `TASK-rectification-unwritten-evidence-claim-20260910.md` | `PROGRESS-rectification-unwritten-evidence-claim-20260910.md` | 证据轮模型只说『记下了』却没调 batch、没设下一问,财务一件静默丢失、流程停在原题且照常扣点(BUG-635);校正流不识别 Mastra schema 拒绝信封,会把被拒的 batch 报成 completed(BUG-636) | 已验收通过(2026-09-10,Claude;环境缺口:`next build` / 真人走查) | `codex/rectification-unwritten-evidence-claim-20260910` |
|
||||
| `TASK-rectification-latent-audit-20260910.md` | `PROGRESS-rectification-latent-audit-20260910.md` | 顺带审计:账本派生同年键误杀 `known_event_quality`(BUG-637,BUG-389 复发);双轨一致性按 31 分钟逐分判冲突并降置信度、与报告两套口径(BUG-638);不可分宽度按簇代表分钟少算(BUG-639);引擎与推断层两套代表分钟、回执宫位表不是卡片那一分钟(BUG-640);另 8 条观察项 | 已验收通过(2026-09-10,Claude;环境缺口:`next build` / 真人走查) | `codex/rectification-unwritten-evidence-claim-20260910` |
|
||||
| `TASK-rectification-collect-semantics-20260910.md` | `PROGRESS-rectification-collect-semantics-20260910.md` | 产品决策三改:Skill 去掉「财务/健康只有主动说才问」并清空 Python `VOLUNTEER_ONLY`,升 10.0.22(BUG-641);带年份线索的采集题优先、题干带线索不矛盾,推翻 BUG-539 的整句去前缀(BUG-642);删掉「没有/记不清」原字匹配,分类器分 no/unsure 两类(BUG-643) | 待执行 | `codex/rectification-collect-semantics-20260910` |
|
||||
| `TASK-rectification-collect-semantics-20260910.md` | `PROGRESS-rectification-collect-semantics-20260910.md` | 产品决策三改:Skill 去掉「财务/健康只有主动说才问」并清空 Python `VOLUNTEER_ONLY`,升 10.0.22(BUG-641);带年份线索的采集题优先、题干带线索不矛盾,推翻 BUG-539 的整句去前缀(BUG-642);删掉「没有/记不清」原字匹配,分类器分 no/unsure 两类(BUG-643) | 待验收 | `codex/rectification-collect-semantics-20260910` |
|
||||
| `TASK-rectification-superseded-focus-duplicate-20260910.md` | `PROGRESS-rectification-superseded-focus-duplicate-20260910.md` | P0:一轮两次 batch 让感情采集题被顶替成 superseded 行;题号唯一约束含已结束行、汇总只聚合 declined/skipped,计划层看不见→撞号 `duplicate_focus` 被当「已问过」→出口误走交付旁白无卡(BUG-644);交付口吻与 `can_adopt=false` 矛盾(BUG-645) | 待验收(环境缺口:`next build` / 真人走查) | `a3a51c32`(BUG-644/645) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -221,3 +221,35 @@
|
||||
- 要么助手说「这件我还没记上。请再说一次大概年月和发生的事。」并再显示财务题
|
||||
- **不得出现**:助手只说「记下了:…」而证据数不变、下面没有问题
|
||||
|
||||
## 16. 财务采集题「没有」与「不记得了」回执不同
|
||||
|
||||
资料与开场同第 0 条。虚构经历。新 Case 走到财务采集题。
|
||||
|
||||
先回「没有」,记下回执后如有需要可新开一次,再走到财务题回「不记得了」。
|
||||
|
||||
期望:
|
||||
|
||||
- 「没有」看到「记下了,这方面先跳过。」
|
||||
- 「不记得了」看到「记下了,这题先放着。」
|
||||
- 不得只在整句恰好等于「没有」或「记不清」时才有回执
|
||||
|
||||
## 17. 家人采集题带年份线索,别的年份也能答
|
||||
|
||||
资料与开场同第 0 条。虚构经历。走到家人采集题。
|
||||
|
||||
期望:
|
||||
|
||||
- 题干含「某年前后有没有…」,例如「家里在 2021 年前后有没有结婚、添丁或住院?有就说个大概年月,别的年份也行。」
|
||||
- 不得同时出现「记得大概哪年就行」
|
||||
- 用另一个年份回答(例如 2018 年家里有人住院)可以记上,不得被题干年份挡掉
|
||||
|
||||
## 18. 打开绑定 10.0.21 的历史校正
|
||||
|
||||
从历史对话打开一个创建时绑定 Skill 10.0.21 的生时校正。
|
||||
|
||||
期望:
|
||||
|
||||
- 能打开,不要求等于当前 10.0.22
|
||||
- 错误若出现,画在被点的那一行下面,不像没点到
|
||||
- **部署后先做这一条**(BUG-621)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
| (服务端探针 2016 升学)2018 年你是哪年上的大学? | 2016 年前后,你是哪年上的大学? | 不得改年份。 |
|
||||
| (服务端探针 2019 入职)2019 年前后,你有没有换过工作?A. 明确发生且时间吻合 | 2019 年前后,你有没有换过工作? | 题干不写选项字面;选项由服务端画在同一条消息里。 |
|
||||
| 有没有记得住时间的收入变化、大笔支出或欠债? | 钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗? | 去掉审讯口吻,先给领域引子。 |
|
||||
| 家里如果有结婚、添丁或住院这类事,记得大概哪年就行。 | 家里在 2021 年前后有没有结婚、添丁或住院?有就说个大概年月,别的年份也行。 | 年份是回忆线索,不是限定;不得同时出现「记得大概哪年就行」。无线索时仍用「记得大概哪年就行」那句。 |
|
||||
| 请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。 | 直接点下面的选项就行,打字回答也一样算数。 | 内部计分规则不进聊天。 |
|
||||
| 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 | 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 | 承认不确定,不训人。 |
|
||||
| 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 | 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。代表分钟是 05:00,这只是代表性候选,不是已确认的唯一出生分钟。 | 同一边界语义,带进度,少法务腔。 |
|
||||
|
||||
@@ -36,10 +36,8 @@ import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inf
|
||||
import { CHOICE_STOP_LABEL, parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import {
|
||||
classifyRectificationTurnIntent,
|
||||
isCollectDeclineUtterance,
|
||||
isCollectSkipUtterance,
|
||||
optionIdForAnswerClass,
|
||||
shouldDeclineCollectFocus,
|
||||
collectFocusCloseStatus,
|
||||
shouldContinueAgentForDatedEvent,
|
||||
classifyTurnIntentWithRetry,
|
||||
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
|
||||
@@ -309,6 +307,7 @@ export async function POST(request: Request) {
|
||||
),
|
||||
});
|
||||
let expectedWrite: "evidence" | "none" | "unknown" = "none";
|
||||
let collectIntent: "classified" | "unclassified" | null = null;
|
||||
let writeClassified = false;
|
||||
const immediateResponse = await (async (): Promise<Response | null> => {
|
||||
if (isStructuredChoice) {
|
||||
@@ -522,24 +521,6 @@ export async function POST(request: Request) {
|
||||
}
|
||||
} else if (focus && isCollectFocusSchema(focus.expectedAnswerSchema)) {
|
||||
const userMessage = parsed.data.message ?? "";
|
||||
if (isCollectDeclineUtterance(userMessage) || isCollectSkipUtterance(userMessage)) {
|
||||
const applied = await applyCollectFocusDenial(accounting, {
|
||||
userId,
|
||||
caseId,
|
||||
focusId: focus.id,
|
||||
status: isCollectSkipUtterance(userMessage) ? "skipped" : "declined",
|
||||
narrateAdopt,
|
||||
});
|
||||
const finished = await persistCollectDenialTurn({
|
||||
accounting,
|
||||
userId,
|
||||
caseId,
|
||||
requestId,
|
||||
userMessage,
|
||||
applied,
|
||||
});
|
||||
return completedMessageResponse(finished.streamText, requestId, caseId, finished.turnId);
|
||||
}
|
||||
let classified = null;
|
||||
const intent = await classifyTurnIntentWithRetry(resolvedModel, {
|
||||
focus,
|
||||
@@ -550,6 +531,7 @@ export async function POST(request: Request) {
|
||||
classified = intent.classified;
|
||||
expectedWrite = intent.expectedWrite;
|
||||
writeClassified = true;
|
||||
collectIntent = classified ? "classified" : "unclassified";
|
||||
if (classified?.intent === "stop_rectification" || classified?.intent === "ask_about_result") {
|
||||
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const applied = await applyRectificationChoice(accounting, {
|
||||
@@ -570,12 +552,14 @@ export async function POST(request: Request) {
|
||||
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
|
||||
return completedMessageResponse(applied.narration, requestId, caseId, applied.turnId);
|
||||
}
|
||||
if (shouldDeclineCollectFocus(classified)) {
|
||||
const closeStatus = collectFocusCloseStatus(classified);
|
||||
if (closeStatus) {
|
||||
const continueToAgent = shouldContinueAgentForDatedEvent(classified);
|
||||
const applied = await applyCollectFocusDenial(accounting, {
|
||||
userId,
|
||||
caseId,
|
||||
focusId: focus.id,
|
||||
status: closeStatus,
|
||||
deferFollowup: continueToAgent,
|
||||
narrateAdopt,
|
||||
});
|
||||
@@ -882,6 +866,7 @@ export async function POST(request: Request) {
|
||||
timeContext,
|
||||
generationModel: selectedModel.model,
|
||||
expectedWrite,
|
||||
collectIntent,
|
||||
buildAgent: async (turnId, skillPackage, attemptId) => {
|
||||
let decision;
|
||||
try {
|
||||
|
||||
@@ -93,6 +93,27 @@ export const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
|
||||
health_pressure: "身体或压力这边,还记得哪年生病、受伤,或特别难熬的一段时间吗?",
|
||||
};
|
||||
|
||||
/** Year is a recall cue, not a limiter. Must not also say “记得大概哪年就行”. */
|
||||
export const COLLECT_QUESTION_YEAR_CUE: Readonly<Record<string, (year: number) => string>> = {
|
||||
relationship: (year) => `感情这边,${year} 年前后有没有认真在一起、分开,或结婚?有就说个大概年月,别的年份也行。`,
|
||||
career: (year) => `工作上呢,${year} 年前后有没有入职、换工作,或职责一下子变重?有就说个大概年月,别的年份也行。`,
|
||||
family: (year) => `家里在 ${year} 年前后有没有结婚、添丁或住院?有就说个大概年月,别的年份也行。`,
|
||||
education: (year) => `上学这边,${year} 年前后有没有升学、转学或大考?有就说个大概年月,别的年份也行。`,
|
||||
relocation: (year) => `${year} 年前后有没有搬家,或开始长期住在外地?有就说个大概年月,别的年份也行。`,
|
||||
finance: (year) => `钱的方面,${year} 年前后有没有收入明显变过、有过大笔支出,或欠过债?有就说个大概年月,别的年份也行。`,
|
||||
health_pressure: (year) => `身体或压力这边,${year} 年前后有没有生病、受伤,或特别难熬的一段时间?有就说个大概年月,别的年份也行。`,
|
||||
};
|
||||
|
||||
export function collectQuestionForDomain(
|
||||
domain: string,
|
||||
probeYear?: number | null,
|
||||
): string | undefined {
|
||||
if (typeof probeYear === "number" && probeYear > 0) {
|
||||
return COLLECT_QUESTION_YEAR_CUE[domain]?.(probeYear) ?? USER_COLLECT_QUESTION[domain];
|
||||
}
|
||||
return USER_COLLECT_QUESTION[domain];
|
||||
}
|
||||
|
||||
/** Second phrasing when that domain's collect focus was already established and not declined. */
|
||||
export const USER_COLLECT_QUESTION_RETRY: Readonly<Record<string, string>> = {
|
||||
relationship: "回到感情这边——刚才说的工作我记下了,哪年认真在一起或分开还记得吗?",
|
||||
@@ -544,6 +565,7 @@ export function listUserVisibleCopy(): string[] {
|
||||
"性格底色",
|
||||
...Object.values(USER_COLLECT_QUESTION),
|
||||
...Object.values(USER_COLLECT_QUESTION_RETRY),
|
||||
...Object.keys(COLLECT_QUESTION_YEAR_CUE).map((domain) => COLLECT_QUESTION_YEAR_CUE[domain](2021)),
|
||||
];
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export type V9AgentRunOptions = Readonly<{
|
||||
generationModel?: unknown;
|
||||
attemptTimeoutMs?: number;
|
||||
expectedWrite?: "evidence" | "none" | "unknown";
|
||||
collectIntent?: "classified" | "unclassified" | null;
|
||||
}>;
|
||||
|
||||
export const RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS = 210_000;
|
||||
@@ -1163,6 +1164,7 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
lastCompletedTool: lastCompletedPublicTool(toolTerminalStatus),
|
||||
stateMutationCommitted: publicWriteToolCompleted(toolTerminalStatus) || hostFallbackUsed,
|
||||
expectedWrite: options.expectedWrite ?? null,
|
||||
collectIntent: options.collectIntent ?? null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
|
||||
export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.21";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.22";
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION_RETRY,
|
||||
collectQuestionForDomain,
|
||||
withFirstDatedCollectInvite,
|
||||
} from "../user-copy.ts";
|
||||
|
||||
@@ -1242,7 +1243,7 @@ export function spokenFollowupForUser(
|
||||
}
|
||||
const mapped = followup.collect_retry === true
|
||||
? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain])
|
||||
: USER_COLLECT_QUESTION[domain];
|
||||
: collectQuestionForDomain(domain, followup.probe_year);
|
||||
if (!mapped) return null;
|
||||
const dated = (DATED_COLLECT_ORDER as readonly string[]).includes(domain);
|
||||
if (followup.invite_more_once && dated && followup.collect_retry !== true) {
|
||||
@@ -1309,14 +1310,23 @@ export function nextDatedCollectFollowup(
|
||||
evidence: readonly MethodFollowupEvidence[],
|
||||
declined: ReadonlySet<string>,
|
||||
answeredYes: ReadonlySet<string> = new Set(),
|
||||
options?: { askedDatedCollect?: boolean },
|
||||
options?: {
|
||||
askedDatedCollect?: boolean;
|
||||
collectionProbes?: readonly DiscriminatingEventProbe[];
|
||||
},
|
||||
): MethodFollowup | null {
|
||||
for (const domain of DATED_COLLECT_ORDER) {
|
||||
if (datedCollectDomainBlocked(domain, evidence, declined, answeredYes)) continue;
|
||||
const probes = options?.collectionProbes ?? [];
|
||||
const remaining = DATED_COLLECT_ORDER.filter(
|
||||
(domain) => !datedCollectDomainBlocked(domain, evidence, declined, answeredYes),
|
||||
);
|
||||
const withYearCue = remaining.filter((domain) => datedCollectionProbe(probes, domain));
|
||||
const withoutCue = remaining.filter((domain) => !datedCollectionProbe(probes, domain));
|
||||
for (const domain of [...withYearCue, ...withoutCue]) {
|
||||
const next = datedCollectFollowup(domain, evidence);
|
||||
if (next) {
|
||||
return {
|
||||
...next,
|
||||
...collectionYearFields(datedCollectionProbe(probes, domain)),
|
||||
invite_more_once: options?.askedDatedCollect !== true,
|
||||
};
|
||||
}
|
||||
@@ -1410,6 +1420,7 @@ export function exhaustionSpokenCollectFollowup(input: {
|
||||
const answeredYes = domainsAnsweredYes(input.answeredProbes, input.eventProbes);
|
||||
const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes, {
|
||||
askedDatedCollect: datedCollectAlreadyAsked(declined, input.declinedTopics ?? []),
|
||||
collectionProbes: input.eventProbes,
|
||||
});
|
||||
if (dated) return dated;
|
||||
if (
|
||||
@@ -2447,10 +2458,6 @@ export function buildMethodFollowupPlan(input: {
|
||||
};
|
||||
const firstRenderableYearlessFollowup = (): MethodFollowup | null => {
|
||||
for (const ranked of yearlessDiscriminators) {
|
||||
const domain = contrastFollowupDomain(
|
||||
ranked.eventProbe?.domain ?? ranked.contrastProbe?.domain ?? null,
|
||||
);
|
||||
if (declined.has(domain)) continue;
|
||||
const key = rankedDiscriminatorKey(ranked);
|
||||
if (key && askedKeys.has(key)) continue;
|
||||
const candidate = followupFromRanked(ranked);
|
||||
@@ -2551,7 +2558,8 @@ export function buildMethodFollowupPlan(input: {
|
||||
if (!next) {
|
||||
const precisionCard = takeRenderableDistinguish(precisionStageFollowup());
|
||||
if (!relationshipCovered && !declined.has("relationship")) {
|
||||
next = sameDomainYearlessCard("relationship") ?? makeFollowup({
|
||||
const relationshipCollect = datedCollectionProbe(input.evidenceCollectionProbes, "relationship");
|
||||
next = makeFollowup({
|
||||
method_id: "d9_relationship",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "relationship_style",
|
||||
@@ -2563,9 +2571,11 @@ export function buildMethodFollowupPlan(input: {
|
||||
"对照 D9 上升类型表(白羊主动热情、天蝎深刻占有等)只作校时方法,不是命运承诺。",
|
||||
),
|
||||
source: "method_coverage",
|
||||
...collectionYearFields(relationshipCollect),
|
||||
});
|
||||
} else if (!careerCovered && !declined.has("career")) {
|
||||
next = sameDomainYearlessCard("career") ?? makeFollowup({
|
||||
const careerCollect = datedCollectionProbe(input.evidenceCollectionProbes, "career");
|
||||
next = makeFollowup({
|
||||
method_id: "d10_career",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "career_style",
|
||||
@@ -2577,10 +2587,11 @@ export function buildMethodFollowupPlan(input: {
|
||||
"可用 D10 事业类型表作校时对照。",
|
||||
),
|
||||
source: "method_coverage",
|
||||
...collectionYearFields(careerCollect),
|
||||
});
|
||||
} else if (!familyCovered && !declined.has("family")) {
|
||||
const familyCollect = datedCollectionProbe(input.evidenceCollectionProbes, "family");
|
||||
next = sameDomainYearlessCard("family") ?? makeFollowup({
|
||||
next = makeFollowup({
|
||||
method_id: "relatives",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "family_event",
|
||||
@@ -2609,6 +2620,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
...(input.closedCollectFocuses ?? []),
|
||||
...(input.declinedTopics ?? []),
|
||||
]),
|
||||
collectionProbes: input.evidenceCollectionProbes,
|
||||
}))) {
|
||||
next = makeFollowup(datedCollect);
|
||||
} else if (!occupationCovered) {
|
||||
@@ -2790,6 +2802,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
...(input.closedCollectFocuses ?? []),
|
||||
...(input.declinedTopics ?? []),
|
||||
]),
|
||||
collectionProbes: input.evidenceCollectionProbes,
|
||||
});
|
||||
if (leftoverDated) {
|
||||
next = makeFollowup(leftoverDated);
|
||||
|
||||
@@ -33,6 +33,7 @@ export type RectificationRunDiagnostic = Readonly<{
|
||||
lastCompletedTool: string | null;
|
||||
stateMutationCommitted: boolean;
|
||||
expectedWrite: "evidence" | "none" | "unknown" | null;
|
||||
collectIntent: "classified" | "unclassified" | null;
|
||||
}>;
|
||||
|
||||
const USER_COPY: Readonly<Record<string, string>> = {
|
||||
|
||||
@@ -30,18 +30,19 @@ export function parseRectificationTurnIntent(value: unknown): RectificationTurnI
|
||||
return parsed.data.answer_class === null ? parsed.data : null;
|
||||
}
|
||||
|
||||
export function collectFocusCloseStatus(
|
||||
classified: RectificationTurnIntent | null,
|
||||
): "declined" | "skipped" | null {
|
||||
if (classified?.intent !== "answer_current_focus") return null;
|
||||
if (classified.answer_class === "no") return "declined";
|
||||
if (classified.answer_class === "unsure") return "skipped";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldDeclineCollectFocus(
|
||||
classified: RectificationTurnIntent | null,
|
||||
): boolean {
|
||||
return classified?.intent === "answer_current_focus" && classified.answer_class === "no";
|
||||
}
|
||||
|
||||
export function isCollectDeclineUtterance(message: string): boolean {
|
||||
return message.trim().replace(/[。!?.!?]+$/u, "") === "没有";
|
||||
}
|
||||
|
||||
export function isCollectSkipUtterance(message: string): boolean {
|
||||
return message.trim().replace(/[。!?.!?]+$/u, "") === "记不清";
|
||||
return collectFocusCloseStatus(classified) === "declined";
|
||||
}
|
||||
|
||||
export function shouldContinueAgentForDatedEvent(
|
||||
@@ -54,11 +55,20 @@ export type ExpectedWriteSignal = "evidence" | "none" | "unknown";
|
||||
|
||||
export function expectedWriteFromCollectIntent(
|
||||
classified: RectificationTurnIntent | null,
|
||||
focus?: ConversationFocus | null,
|
||||
): "evidence" | "none" {
|
||||
if (classified?.intent === "provide_new_evidence") return "evidence";
|
||||
if (classified?.intent === "answer_current_focus" && classified.has_new_dated_event === true) {
|
||||
return "evidence";
|
||||
}
|
||||
if (
|
||||
focus
|
||||
&& isCollectFocusSchema(focus.expectedAnswerSchema)
|
||||
&& classified?.intent === "answer_current_focus"
|
||||
&& (classified.answer_class === "yes" || classified.answer_class === "weak_yes")
|
||||
) {
|
||||
return "evidence";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
@@ -75,7 +85,10 @@ export async function classifyTurnIntentWithRetry(
|
||||
try {
|
||||
const classified = await classifyRectificationTurnIntent(model, input);
|
||||
if (classified) {
|
||||
return { classified, expectedWrite: expectedWriteFromCollectIntent(classified) };
|
||||
return {
|
||||
classified,
|
||||
expectedWrite: expectedWriteFromCollectIntent(classified, input.focus),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// One retry, then fail-open as unknown.
|
||||
@@ -120,9 +133,12 @@ has_new_dated_event 仅在用户同一句里除了回答当前问题之外,还
|
||||
不要按 A/B/C/D 的位置猜语义,只按选项 label 与 answer_class 判断。`
|
||||
: `你只做当前生时校正采集题的意图分类,不回答用户,也不修改任何状态。
|
||||
当前问题没有点选选项。判断用户是在回答当前采集题、提供新的带时间经历、要求停止整个校正、询问结果,还是语义不清。
|
||||
若用户明确表示这个方面没有发生过、没有这类事,intent 为 answer_current_focus,answer_class 为 no。
|
||||
「没有、没发生过、这方面没什么」→ intent 为 answer_current_focus,answer_class 为 no(该方面没有事,本次不再问)。
|
||||
「记不清、不记得、忘了、想不起来、以后再说」→ intent 为 answer_current_focus,answer_class 为 unsure(先放着,以后可补)。
|
||||
用户用带大概年月的经历直接回答当前采集题 → intent 为 answer_current_focus,answer_class 为 yes(程度较弱时为 weak_yes),has_new_dated_event 为 false。
|
||||
不要把「没有」或「记不清」标成 yes。若既没有否定、也没有说记不清、也没有给出带年月经历,intent 为 unclear,answer_class 必须为 null。
|
||||
若用户只在补充带时间的经历、并没有回答当前采集题,intent 为 provide_new_evidence,answer_class 必须为 null。
|
||||
has_new_dated_event 仅在用户同一句里除了回答当前采集题之外,还提供了新的、带大概时间的经历时为 true;单纯的否定必须为 false。
|
||||
has_new_dated_event 仅在用户同一句里除了回答当前采集题之外,还提供了新的、带大概时间的经历时为 true;单纯的否定或记不清必须为 false。
|
||||
若同一句话既明确否定当前采集题又补充了新的带时间经历,intent 仍为 answer_current_focus 且 answer_class 为 no,不要改成 provide_new_evidence。
|
||||
“当前方面没有”通常是回答当前采集题,不是停止整个流程;只有用户明确要求停止整个校正时才分类为 stop_rectification。
|
||||
不要按关键词表或正则猜测,只根据当前问题与用户这句话的语义分类。`,
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
evidenceSubjectForDomain,
|
||||
applyOccupationCollectLedgerNorm,
|
||||
} from "@/lib/rectification-agentic/v9/evidence-model";
|
||||
import { USER_COLLECT_QUESTION } from "@/lib/rectification-agentic/user-copy";
|
||||
import { collectQuestionForDomain } from "@/lib/rectification-agentic/user-copy";
|
||||
import {
|
||||
isHoldoutVerificationQuote,
|
||||
isPersistedFocusId,
|
||||
@@ -1165,7 +1165,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
});
|
||||
const fallbackDomain = nextFollowup.domain ?? "";
|
||||
const fallbackPrompt = fallbackDomain && fallbackDomain !== "other"
|
||||
? USER_COLLECT_QUESTION[fallbackDomain]
|
||||
? collectQuestionForDomain(fallbackDomain, nextFollowup.probe_year)
|
||||
: undefined;
|
||||
if (
|
||||
spokenPromptFailures < 2
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
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, ADOPT_OUTCOMES } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY, collectQuestionForDomain, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
evidenceLedgerFingerprint,
|
||||
parseV9CaseDossier,
|
||||
@@ -978,14 +978,15 @@ test("answering the last discriminator persists a year-locked family collect foc
|
||||
const schema = setFocus.args.p_expected_answer_schema as { choice?: unknown; prompt?: string; collect?: boolean };
|
||||
assert.equal(schema.choice, undefined);
|
||||
assert.equal(schema.collect, true);
|
||||
// 旧:prompt 等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604;年份仍不进采集题干
|
||||
// 原值: USER_COLLECT_QUESTION.family,年份不进题干
|
||||
// 新值: 带 2021 年前后线索
|
||||
// 原因: BUG-642
|
||||
assert.equal(
|
||||
schema.prompt,
|
||||
withFirstDatedCollectInvite(RECTIFICATION_USER_COPY.collectQuestionByDomain.family),
|
||||
withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""),
|
||||
);
|
||||
assert.match(schema.prompt ?? "", /家人|结婚|添丁|住院/);
|
||||
assert.match(schema.prompt ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(schema.prompt ?? "", /记得大概哪年就行/);
|
||||
assert.equal(applied.nextInterviewPersisted, true);
|
||||
assert.equal(applied.nextChoiceReady, false);
|
||||
assert.equal(shouldContinueAfterStructuredChoice(applied.nextAction, applied), false);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
parseV9CaseDossier,
|
||||
RectificationToolServiceError,
|
||||
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, collectQuestionForDomain, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { turnQuestionKind } from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
||||
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import {
|
||||
@@ -165,7 +165,7 @@ test("two scoreable events do not open OOS even when holdout prompts exist", ()
|
||||
assert.match(spokenFollowupForUser(plan.next_followup) ?? "", /上学|升学/);
|
||||
});
|
||||
|
||||
test("family collect spoken stem has no year prefix while probe_year stays dated", () => {
|
||||
test("family collect spoken stem uses a year cue without the contradictory prefix", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: TWO_SCOREABLE,
|
||||
evidenceCollectionProbes: [{
|
||||
@@ -186,10 +186,13 @@ test("family collect spoken stem has no year prefix while probe_year stays dated
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
// 原值: USER_COLLECT_QUESTION.family(不带年份)
|
||||
// 新值: 带 2021 年前后线索,且不含「记得大概哪年就行」
|
||||
// 原因: BUG-642 年份是回忆线索不是限定
|
||||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""));
|
||||
assert.match(spoken ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(spoken ?? "", /记得大概哪年就行/);
|
||||
});
|
||||
|
||||
test("four scoreable events skip holdout for domains already in the ledger", () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { persistServerOwnedFocus } from "../src/lib/rectification-agentic/v9/ser
|
||||
import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
||||
import {
|
||||
shouldContinueAgentForDatedEvent,
|
||||
shouldDeclineCollectFocus,
|
||||
collectFocusCloseStatus,
|
||||
} from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
|
||||
import {
|
||||
applyCollectFocusDenial,
|
||||
@@ -33,6 +33,7 @@ import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9
|
||||
import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
USER_COLLECT_QUESTION,
|
||||
collectQuestionForDomain,
|
||||
withFirstDatedCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
@@ -463,8 +464,8 @@ function rpcDossier(decision: DecisionDossier, activeFocus?: Record<string, unkn
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
test("skill version is 10.0.22 after the collect-semantics bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("revision 5 with uncovered relatives asks the dated family collect, not a yearless D12 card", () => {
|
||||
@@ -487,10 +488,10 @@ test("revision 5 with uncovered relatives asks the dated family collect, not a y
|
||||
// 旧:year_label 含 2021 → 新:采集口语不带年份前缀,year_label 不进 followup
|
||||
// 原因:决策 7;probe_year 仍锁定 dated 家庭采集,不出无年份 D12 卡
|
||||
assert.equal(plan.next_followup?.year_label, undefined);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 单事件开场不插追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
// 原值: USER_COLLECT_QUESTION.family
|
||||
// 新值: 带 2021 年前后线索
|
||||
// 原因: BUG-642
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""));
|
||||
assert.equal(plan.dropped_probes.some((item) => (
|
||||
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
|
||||
)), true);
|
||||
@@ -554,18 +555,18 @@ test("persistServerOwnedFocus writes family as target_domain so a no answer beco
|
||||
});
|
||||
|
||||
test("occupation collect denial declines the focus and advances coverage to horary", async () => {
|
||||
assert.equal(shouldDeclineCollectFocus({
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "no",
|
||||
}), true);
|
||||
assert.equal(shouldDeclineCollectFocus({
|
||||
}), "declined");
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "provide_new_evidence",
|
||||
answer_class: null,
|
||||
}), false);
|
||||
assert.equal(shouldDeclineCollectFocus({
|
||||
}), null);
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "unsure",
|
||||
}), false);
|
||||
}), "skipped");
|
||||
|
||||
const askedYearless = [D24, D12, D7, D4, D5].map((item) => item.semantic_key);
|
||||
const occupationPlan = planFrom(revision5Dossier(revision5State(), {
|
||||
@@ -1248,7 +1249,7 @@ test("collect-focus classifier and resolve-focus copy do not treat explicit no a
|
||||
route.indexOf("const requestTime"),
|
||||
);
|
||||
assert.match(fastPath, /isCollectFocusSchema/);
|
||||
assert.match(fastPath, /shouldDeclineCollectFocus/);
|
||||
assert.match(fastPath, /collectFocusCloseStatus/);
|
||||
assert.match(fastPath, /applyCollectFocusDenial/);
|
||||
assert.match(classifier, /collect === true|isCollectFocusSchema/);
|
||||
assert.doesNotMatch(classifier + fastPath, /USER_STOP_PATTERN|parseChoiceKeyFromUserMessage/);
|
||||
@@ -1285,7 +1286,7 @@ test("has_new_dated_event continues into the agent after applying the answer", (
|
||||
assert.match(choiceApply, /return completedMessageResponse\(applied\.narration/);
|
||||
assert.ok(choiceApply.indexOf("if (!continueToAgent)") < choiceApply.indexOf("return completedMessageResponse(applied.narration"));
|
||||
const collectApply = fastPath.slice(
|
||||
fastPath.indexOf("if (shouldDeclineCollectFocus(classified))"),
|
||||
fastPath.indexOf("if (closeStatus)"),
|
||||
fastPath.indexOf("} else {"),
|
||||
);
|
||||
assert.ok(collectApply.indexOf("applyCollectFocusDenial") < collectApply.indexOf("if (!continueToAgent)"));
|
||||
|
||||
@@ -360,7 +360,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(agentSource, /不得宣称唯一出生分钟/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -249,12 +249,12 @@ test("public house table follows the inference representative minute", () => {
|
||||
assert.match(source, /natal_recast_pre_inference: _preNatal/);
|
||||
});
|
||||
|
||||
test("skill 10.0.21 forbids computing varga signs from transition times", () => {
|
||||
test("skill 10.0.22 forbids computing varga signs from transition times", () => {
|
||||
const skillDir = fileURLToPath(new URL("../../skills/jyotish-birth-time-rectification", import.meta.url));
|
||||
const skill = readFileSync(`${skillDir}/SKILL.md`, "utf8");
|
||||
const comparison = readFileSync(`${skillDir}/references/candidate-comparison.md`, "utf8");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.match(skill, /^version: 10\.0\.21$/m);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
assert.match(skill, /^version: 10\.0\.22$/m);
|
||||
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
parseWindowScan,
|
||||
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
|
||||
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, collectQuestionForDomain, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
@@ -1454,9 +1454,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.21", () => {
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.22", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -2386,15 +2386,15 @@ test("family collect attaches the collection-probe year instead of a yearless D2
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
// 旧:year_label / spoken 带「2021 年前后,」前缀
|
||||
// 新:probe_year 仍是 2021,口语等于 USER_COLLECT_QUESTION.family,不填 year_label
|
||||
// 原因:决策 7,年份只用于计分与去重,不进采集题干
|
||||
// 原值: USER_COLLECT_QUESTION.family,不进题干
|
||||
// 新值: 带 2021 年前后线索,不含「记得大概哪年就行」
|
||||
// 原因: BUG-642
|
||||
assert.equal(plan.next_followup?.year_label, undefined);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 单事件开场不插追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""));
|
||||
assert.match(spoken ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(spoken ?? "", /记得大概哪年就行/);
|
||||
});
|
||||
|
||||
test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => {
|
||||
|
||||
@@ -584,8 +584,8 @@ function warnLines(run: () => Promise<unknown> | unknown) {
|
||||
}).then((result) => ({ result, lines }));
|
||||
}
|
||||
|
||||
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
test("skill version is 10.0.22 after the collect-semantics bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("USER_COLLECT_QUESTION no longer has an other fallback", () => {
|
||||
|
||||
@@ -213,9 +213,9 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||||
});
|
||||
|
||||
test("new-case skill identity is 10.0.21 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.match(skill, /^version: 10\.0\.21$/m);
|
||||
test("new-case skill identity is 10.0.22 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
assert.match(skill, /^version: 10\.0\.22$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /新事件走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
|
||||
@@ -184,8 +184,8 @@ const CANDIDATE_IDS = [
|
||||
"88888888-8888-4888-8888-888888888882",
|
||||
] as const;
|
||||
|
||||
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
test("skill version is 10.0.22 after the collect-semantics bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
spokenFollowupForUser,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import {
|
||||
USER_COLLECT_QUESTION,
|
||||
collectQuestionForDomain,
|
||||
withFirstDatedCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
@@ -181,7 +181,7 @@ test("verified_idle UI is a closing line without reload, gap copy, or consult ha
|
||||
assert.doesNotMatch(chat, /之后新建对话即按此时间排盘/);
|
||||
});
|
||||
|
||||
test("family collect spoken stem equals USER_COLLECT_QUESTION.family while probe_year stays dated", () => {
|
||||
test("family collect spoken stem uses a year cue while probe_year stays dated", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2011-01-01", occurredTo: null },
|
||||
@@ -207,9 +207,10 @@ test("family collect spoken stem equals USER_COLLECT_QUESTION.family while probe
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
assert.doesNotMatch(spoken ?? "", /^\d{4} 年前后,/);
|
||||
// 原值: USER_COLLECT_QUESTION.family
|
||||
// 新值: 带 2021 年前后线索,不含「记得大概哪年就行」
|
||||
// 原因: BUG-642
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""));
|
||||
assert.match(spoken ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(spoken ?? "", /记得大概哪年就行/);
|
||||
});
|
||||
|
||||
@@ -426,8 +426,8 @@ function rpcDossier(decision: DecisionDossier) {
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
test("skill version is 10.0.22 after the collect-semantics bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("pre-fix dual-exit constant is gone; range narration carries numbers and the disclaimer", () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION_RETRY,
|
||||
collectQuestionForDomain,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import type { EventProbeStyleOption } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { fakeAccounting, CASE_ID, FOCUS_ID, USER_ID, activeFocusFixture } from "./rectification-v9-test-support.ts";
|
||||
@@ -499,13 +500,15 @@ test("spoken collect followup persists a collect focus without a choice card", a
|
||||
assert.equal(result.focus?.intent, "collect_method_evidence");
|
||||
assert.equal(result.focus?.expectedAnswerSchema.collect, true);
|
||||
assert.equal(result.focus?.expectedAnswerSchema.choice, undefined);
|
||||
// 旧:prompt 含 2021 → 新:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 原因:决策 7,采集题不拼年份前缀;fixture 仍带 probe_year 供计分
|
||||
assert.equal(result.focus?.expectedAnswerSchema.prompt, USER_COLLECT_QUESTION.family);
|
||||
// 原值: USER_COLLECT_QUESTION.family,题干不含 2021
|
||||
// 新值: 带 2021 年前后线索,不含「记得大概哪年就行」
|
||||
// 原因: BUG-642
|
||||
assert.equal(result.focus?.expectedAnswerSchema.prompt, collectQuestionForDomain("family", 2021));
|
||||
const openCollect = openQuestionFromPersistedFocus(result);
|
||||
assert.equal(openCollect?.kind, "collect_spoken");
|
||||
assert.equal(openCollect?.prompt, USER_COLLECT_QUESTION.family);
|
||||
assert.doesNotMatch(openCollect?.prompt ?? "", /2021/);
|
||||
assert.equal(openCollect?.prompt, collectQuestionForDomain("family", 2021));
|
||||
assert.match(openCollect?.prompt ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(openCollect?.prompt ?? "", /记得大概哪年就行/);
|
||||
assert.notEqual(openCollect?.unrenderable, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ import {
|
||||
} from "./rectification-v9-test-support.ts";
|
||||
import { parseV9CaseDossier as parseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
|
||||
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { isCollectDeclineUtterance, isCollectSkipUtterance } from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
|
||||
import { collectFocusCloseStatus } from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
|
||||
|
||||
const RELATIONSHIP_PROMPT = USER_COLLECT_QUESTION.relationship;
|
||||
|
||||
@@ -94,8 +94,8 @@ function collectPersistResult(overrides: {
|
||||
};
|
||||
}
|
||||
|
||||
test("skill version is 10.0.21 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
test("skill version is 10.0.22 after the collect-semantics bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("cases current_question remains the submit contract, not a visual slot", () => {
|
||||
@@ -448,22 +448,28 @@ test("agent route keeps question ownership in the server Case projection", () =>
|
||||
assert.doesNotMatch(agentRun, /当前可询问范围/);
|
||||
});
|
||||
|
||||
test("没有 and 记不清 short-circuit collect_spoken without a model run", () => {
|
||||
assert.equal(isCollectDeclineUtterance("没有"), true);
|
||||
assert.equal(isCollectDeclineUtterance("没有。"), true);
|
||||
assert.equal(isCollectSkipUtterance("记不清"), true);
|
||||
assert.equal(isCollectSkipUtterance("记不清!"), true);
|
||||
assert.equal(isCollectDeclineUtterance("记不清"), false);
|
||||
assert.equal(isCollectSkipUtterance("没有"), false);
|
||||
test("classifier close-status maps no and unsure onto collect denial", () => {
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "no",
|
||||
}), "declined");
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "unsure",
|
||||
}), "skipped");
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "yes",
|
||||
}), null);
|
||||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||||
const classifier = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-intent-classifier.ts", import.meta.url), "utf8");
|
||||
const collectStart = route.lastIndexOf("} else if (focus && isCollectFocusSchema(focus.expectedAnswerSchema))");
|
||||
const collectBranch = route.slice(collectStart);
|
||||
const collectFocus = collectBranch.slice(0, collectBranch.indexOf("classifyRectificationTurnIntent"));
|
||||
assert.match(collectFocus, /isCollectDeclineUtterance/);
|
||||
assert.match(collectFocus, /isCollectSkipUtterance/);
|
||||
assert.match(collectFocus, /status: isCollectSkipUtterance\(userMessage\) \? "skipped" : "declined"/);
|
||||
assert.match(collectFocus, /persistCollectDenialTurn/);
|
||||
assert.doesNotMatch(collectFocus, /runV9AgentTurn/);
|
||||
assert.doesNotMatch(collectBranch + classifier, /isCollectDeclineUtterance|isCollectSkipUtterance/);
|
||||
assert.match(collectBranch, /collectFocusCloseStatus/);
|
||||
assert.match(collectBranch, /status: closeStatus/);
|
||||
assert.match(collectBranch, /persistCollectDenialTurn/);
|
||||
assert.doesNotMatch(collectBranch.slice(0, collectBranch.indexOf("classifyTurnIntentWithRetry") + 40), /runV9AgentTurn/);
|
||||
});
|
||||
|
||||
test("skipped collect domains are not re-asked this session; reverse-verify may still touch them", () => {
|
||||
@@ -547,3 +553,33 @@ test("three confirmed domains from one message are not re-asked by dated collect
|
||||
assert.match(tools, /recordV10EvidenceBatch/);
|
||||
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
|
||||
});
|
||||
|
||||
test("nextDatedCollectFollowup prefers year-cued domains and cue-less templates stay without a year", () => {
|
||||
const financeProbe: DiscriminatingEventProbe = {
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "finance",
|
||||
event_family: "收入明显变化、大笔支出或欠债",
|
||||
source: "age_band",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2018 年前后;领域锁定 finance。",
|
||||
role: "collect",
|
||||
phase: "evidence_collection",
|
||||
information_gain: 0,
|
||||
semantic_key: "finance.2018",
|
||||
};
|
||||
const cued = nextDatedCollectFollowup([], new Set(), new Set(), {
|
||||
collectionProbes: [financeProbe],
|
||||
});
|
||||
assert.equal(cued?.domain, "finance");
|
||||
assert.equal(cued?.probe_year, 2018);
|
||||
const cuedSpoken = spokenFollowupForUser(cued);
|
||||
assert.match(cuedSpoken ?? "", /2018 年前后/);
|
||||
assert.doesNotMatch(cuedSpoken ?? "", /记得大概哪年就行/);
|
||||
const cueLess = nextDatedCollectFollowup([], new Set());
|
||||
assert.equal(cueLess?.domain, "family");
|
||||
assert.equal(spokenFollowupForUser(cueLess), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
assert.doesNotMatch(spokenFollowupForUser(cueLess) ?? "", /年前后/);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseRectificationTurnIntent,
|
||||
shouldContinueAgentForDatedEvent,
|
||||
shouldDeclineCollectFocus,
|
||||
collectFocusCloseStatus,
|
||||
expectedWriteFromCollectIntent,
|
||||
} from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
|
||||
import type { ConversationFocus } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
@@ -159,6 +160,63 @@ test("production intent handling contains no semantic regex or positional text p
|
||||
answer_class: "no",
|
||||
}), "none");
|
||||
assert.equal(expectedWriteFromCollectIntent(null), "none");
|
||||
assert.doesNotMatch(route, /isCollectDeclineUtterance|isCollectSkipUtterance/);
|
||||
assert.doesNotMatch(classifier, /isCollectDeclineUtterance|isCollectSkipUtterance/);
|
||||
assert.match(classifier, /没有、没发生过、这方面没什么/);
|
||||
assert.match(classifier, /记不清、不记得、忘了、想不起来、以后再说/);
|
||||
assert.match(classifier, /answer_class 为 unsure/);
|
||||
assert.match(classifier, /answer_class 为 yes/);
|
||||
assert.match(route, /collectFocusCloseStatus/);
|
||||
assert.match(route, /collectIntent/);
|
||||
});
|
||||
|
||||
test("collect focus close-status and expectedWrite map classifier classes", () => {
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "no",
|
||||
}), "declined");
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "unsure",
|
||||
}), "skipped");
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "yes",
|
||||
}), null);
|
||||
assert.equal(collectFocusCloseStatus({
|
||||
intent: "provide_new_evidence",
|
||||
answer_class: null,
|
||||
}), null);
|
||||
const collectFocus = focusWithOptions({
|
||||
collect: true,
|
||||
prompt: "钱的方面,还记得哪年收入明显变过吗?",
|
||||
});
|
||||
assert.equal(expectedWriteFromCollectIntent({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "yes",
|
||||
}, collectFocus), "evidence");
|
||||
assert.equal(expectedWriteFromCollectIntent({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "weak_yes",
|
||||
}, collectFocus), "evidence");
|
||||
assert.equal(expectedWriteFromCollectIntent({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "yes",
|
||||
}), "none");
|
||||
assert.deepEqual(parseRectificationTurnIntent({
|
||||
intent: "provide_new_evidence",
|
||||
answer_class: null,
|
||||
}), {
|
||||
intent: "provide_new_evidence",
|
||||
answer_class: null,
|
||||
});
|
||||
const noPlusEvent = parseRectificationTurnIntent({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "no",
|
||||
has_new_dated_event: true,
|
||||
});
|
||||
assert.equal(noPlusEvent?.answer_class, "no");
|
||||
assert.equal(noPlusEvent?.has_new_dated_event, true);
|
||||
});
|
||||
|
||||
test("classifyTurnIntentWithRetry fails open as unknown after two misses", () => {
|
||||
|
||||
@@ -135,6 +135,13 @@ test("collect intent maps provide_new_evidence and dated current-focus to expect
|
||||
answer_class: "no",
|
||||
}), "none");
|
||||
assert.equal(expectedWriteFromCollectIntent(null), "none");
|
||||
const collectFocus = {
|
||||
expectedAnswerSchema: { collect: true, prompt: "钱的方面,还记得哪年收入明显变过吗?" },
|
||||
};
|
||||
assert.equal(expectedWriteFromCollectIntent({
|
||||
intent: "answer_current_focus",
|
||||
answer_class: "yes",
|
||||
}, collectFocus as never), "evidence");
|
||||
});
|
||||
|
||||
test("runner does not use a year-utterance fallback", () => {
|
||||
@@ -203,6 +210,23 @@ test("unwritten 记下了 retries once then host-falls-back without billing", as
|
||||
assert.match(retryBootstrap, /记下了/);
|
||||
});
|
||||
|
||||
test("collect focus yes without 记下了 still retries when nothing was written", async () => {
|
||||
let buildCount = 0;
|
||||
const { options } = runOptions({
|
||||
expectedWrite: "evidence",
|
||||
buildAgent: async () => {
|
||||
buildCount += 1;
|
||||
return fakeAgentStream(readCaseThen([
|
||||
chunk("text-delta", { text: "2018 年 3 月开始欠债。" }),
|
||||
chunk("finish"),
|
||||
])) as never;
|
||||
},
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(buildCount, 2);
|
||||
assert.equal(result.answerText, RECTIFICATION_USER_COPY.evidenceNotRecorded);
|
||||
});
|
||||
|
||||
test("read-case then batch then set-focus then 记下了 stays on the baseline path", async () => {
|
||||
const { options, billing } = runOptions({
|
||||
expectedWrite: "evidence",
|
||||
|
||||
@@ -96,11 +96,11 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.21"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.22"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
|
||||
@@ -95,9 +95,9 @@ test("terminal transitions are one-way and evidence writes stop at terminal", ()
|
||||
|
||||
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.21$/m);
|
||||
assert.match(skill, /^version: 10\.0\.22$/m);
|
||||
assert.match(skill, /至多一个主问题且唯一来源:[\s\S]*不得自行提出、复述、改写或预告问题/);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
|
||||
@@ -209,7 +209,7 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
session_id: SESSION_ID,
|
||||
status: "draft",
|
||||
should_start_opening: true,
|
||||
skill_version: "10.0.21",
|
||||
skill_version: "10.0.22",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -247,11 +247,11 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
});
|
||||
assert.equal(response.disposition, "created");
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
assert.equal(response.skillVersion, "10.0.21");
|
||||
assert.equal(response.skillVersion, "10.0.22");
|
||||
const openCall = accounting.calls.find((call) => call.fn === "open_agentic_rectification_case_v2");
|
||||
assert.ok(openCall);
|
||||
assert.equal(openCall.args.p_skill_name, "jyotish-birth-time-rectification");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.21");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.22");
|
||||
assert.equal(openCall.args.p_user_id, "user-1");
|
||||
// The server derives the baseline; the request never carries it from the browser.
|
||||
assert.equal("birth_date" in openCall.args, false);
|
||||
|
||||
@@ -83,5 +83,5 @@ test("agent body cannot verbally accept a spoken birth window", () => {
|
||||
assert.equal(stripVerbalWindowChange("以你说的为准。"), "");
|
||||
assert.equal(stripVerbalWindowChange("明白了,以你说的为准。"), "明白了。");
|
||||
assert.match(SKILL, /不得回答『以你说的为准』或改写搜索窗口/);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ test("SCORE_DELTA stays ±2/±1 and yearless weight is half", () => {
|
||||
assert.equal(PROBE_WEIGHT.dated, 1);
|
||||
assert.equal(PROBE_WEIGHT.yearless, 0.5);
|
||||
assert.equal(STRONG_CONFLICT_ELIMINATION_COUNT, 3);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.21");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
});
|
||||
|
||||
test("D9 answer B moves scores by ±1 and does not count toward elimination", () => {
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.21",
|
||||
sha256: "3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||||
version: "10.0.22",
|
||||
sha256: "e3765b20ce6b49a5c7c9ed6b0a8caacc6fc4b8a16a127b93bd92c537330a54e2",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
@@ -162,6 +162,17 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
deprecated1017.sha256,
|
||||
"f77b06de392feaa7c23e64beb05a90bcc0734db73747eb7915cba338b8eff1e6",
|
||||
);
|
||||
const deprecated1021 = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.21",
|
||||
"3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||||
{ projectRoot },
|
||||
);
|
||||
assert.equal(deprecated1021.status, "deprecated");
|
||||
assert.equal(
|
||||
deprecated1021.sha256,
|
||||
"3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||||
);
|
||||
});
|
||||
|
||||
test("exact resolution of an old deprecated package is independent of active switches", (t) => {
|
||||
|
||||
@@ -49,8 +49,8 @@ from scripts.rectification.refinement_packet import match_level
|
||||
# three domains keep two years plus a couple of activation fallbacks without
|
||||
# flooding the ask layer, which still ranks globally by information_gain.
|
||||
MAX_PROBES = 8
|
||||
# Collection still asks at most three missing-domain age-band questions.
|
||||
MAX_COLLECTION_PROBES = 3
|
||||
# Collection asks an age-band cue for every missing catalog domain.
|
||||
MAX_COLLECTION_PROBES = 7
|
||||
# N: keep the top scored probes per domain (boundary years, plus at most one
|
||||
# activation if the domain is still under this cap).
|
||||
MAX_PROBES_PER_DOMAIN = 3
|
||||
@@ -142,7 +142,6 @@ LAYER_DOMAIN = {
|
||||
"d11": "finance",
|
||||
"d30": "health_pressure",
|
||||
}
|
||||
VOLUNTEER_ONLY = frozenset({"finance", "health_pressure"})
|
||||
LAYER_VARGA = {
|
||||
"d9": "D9",
|
||||
"d10": "D10",
|
||||
@@ -334,15 +333,10 @@ def _differing_layers(contexts: Sequence[dict[str, Any]]) -> set[str]:
|
||||
|
||||
def _probe_domains(
|
||||
remaining_layers: set[str],
|
||||
events: Sequence[dict[str, Any]],
|
||||
_events: Sequence[dict[str, Any]],
|
||||
*,
|
||||
d1_differs: bool = False,
|
||||
) -> list[str]:
|
||||
volunteered = {
|
||||
str(event.get("domain"))
|
||||
for event in events
|
||||
if isinstance(event, dict) and event.get("domain")
|
||||
}
|
||||
ordered: list[str] = []
|
||||
for layer in SCORING_LAYERS:
|
||||
domain = LAYER_DOMAIN.get(layer)
|
||||
@@ -350,8 +344,6 @@ def _probe_domains(
|
||||
continue
|
||||
if layer not in remaining_layers:
|
||||
continue
|
||||
if domain in VOLUNTEER_ONLY and domain not in volunteered:
|
||||
continue
|
||||
ordered.append(domain)
|
||||
if not ordered and d1_differs:
|
||||
ordered.append("education")
|
||||
@@ -1440,7 +1432,7 @@ def evidence_collection_probes(
|
||||
return []
|
||||
now = today or date.today()
|
||||
events = [item for item in (request.get("events") or []) if isinstance(item, dict)]
|
||||
missing = missing_collection_domains(events, list(DOMAIN_CATALOG), VOLUNTEER_ONLY)
|
||||
missing = missing_collection_domains(events, list(DOMAIN_CATALOG), frozenset())
|
||||
rows: list[dict[str, Any]] = []
|
||||
for domain in missing:
|
||||
year = _age_band_year(birth_year, domain, now)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: jyotish-birth-time-rectification
|
||||
version: 10.0.21
|
||||
version: 10.0.22
|
||||
description: "生时校正专用 Skill(V10)。以服务器权威 Case、ConversationFocus 与 CaseConversationSummary 驱动低负担访谈;批量证据逐项判定,candidate / accepted / confirmed 严格分离,全部计算与持久化只走服务端工具。触发词:生时校正、出生时间校正、校正出生时间、rectification、birth time correction。"
|
||||
---
|
||||
|
||||
@@ -81,7 +81,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv
|
||||
`CaseConversationSummary` 是长会话的权威记忆,至少投影:confirmed evidence summary、pending revisions、active focus、declined/skipped topics、candidate divergence summary、missing evidence categories、`method_followup_plan`、last result policy。
|
||||
|
||||
- 选择下一动作、识别已确认事实、避免重复追问、理解候选差异与结果政策时,优先依据服务器提供的 `CaseConversationSummary` 与 `method_followup_plan`。
|
||||
- 不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,仍可计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup` 带 `choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份,不得改写时间范围;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
|
||||
- 不要按 `missing_evidence_categories` 轮询迁居。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup` 带 `choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份,不得改写时间范围;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
|
||||
- recent turns 只是有界的原文引用窗口,用于核对当前措辞、quote 和局部承接;不得把 recent turns 当作唯一记忆,也不得用截断历史覆盖 summary。
|
||||
- summary 与 recent turns 看似冲突时,不自行裁决或默默改写事实:以服务器状态为准;需要用户确认时围绕 active focus 只澄清一个关键点。
|
||||
- 超过长会话窗口后仍不得忘记已确认证据、pending revision、拒答主题或 active focus。
|
||||
@@ -135,7 +135,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv
|
||||
- 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。
|
||||
- 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。
|
||||
- 不得询问外貌、体质、胎记或疤痕。D9/D10 类型表是校时方法,写「该分钟下 D9/D10 升 X,与用户所述特质的对应/冲突」,不是咨询命运承诺。职业对照本命第 10 宫和 D10,允许类型表。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 可用于选题,类型对照写入验证报告。若用户消息以「盘外核对(不计分)」开头,不得写入可评分证据。
|
||||
- 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5/D24 成就收窄;家人走 D12/D7/D3 方法覆盖。财务走 D2/D11、健康走 D30,仅在用户主动说时计分,均不得混进 D4。Pada / Hora / Ghati / Bhava / Pranapada / KP 子主只展示换升,不确认唯一分钟。
|
||||
- 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5/D24 成就收窄;家人走 D12/D7/D3 方法覆盖。财务走 D2/D11、健康走 D30,与其他领域同权计分,均不得混进 D4。Pada / Hora / Ghati / Bhava / Pranapada / KP 子主只展示换升,不确认唯一分钟。
|
||||
- 采用后按采用分钟核最多两件服务器探针前事;对得上写入并重算,对不上可改选其他候选。不得声称唯一分钟,也不自动进入咨询 Agent。
|
||||
- 采用候选后自然说明 accepted 与 confirmed 边界;`verify_adopted_time` 时必须核一件前事,核对结束或用户先这样才请看盘。不主动关闭 Case,Session 会保留并可日后继续。
|
||||
- 不再有固定 10–15 个事件长表、外貌/体型/疤痕主评分、或“稳定确定到精确分钟”的承诺。A/B/C/D 主题问卷只在候选已经分不开或采用后核对前事时使用。80%/60% 只描述事件吻合率。
|
||||
|
||||
@@ -75,12 +75,12 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服
|
||||
追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级:
|
||||
|
||||
1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。
|
||||
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;时间范围和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 大运/副运起点的年或月差,没有可问边界时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份,不得改写时间范围。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
|
||||
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;时间范围和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 大运/副运起点的年或月差,没有可问边界时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份,不得改写时间范围。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
|
||||
3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察用于选题,并在出牌轮写入类型对照(校时方法,不是命运承诺)。
|
||||
4. pending revision 的一个关键歧义。
|
||||
5. 已有证据的必要稳定性补强。
|
||||
|
||||
不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,不是 SQL 类别轮询。`stop_domain_rotation=true` 时停止领域清单。一轮最多一个主要问题。用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,直接说明目的、当前状态和边界,不绕开问题继续索取证据。
|
||||
不要按 `missing_evidence_categories` 轮询迁居。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。不是 SQL 类别轮询。`stop_domain_rotation=true` 时停止领域清单。一轮最多一个主要问题。用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,直接说明目的、当前状态和边界,不绕开问题继续索取证据。
|
||||
|
||||
## 8. 日期精度
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: jyotish-birth-time-rectification
|
||||
version: 10.0.22
|
||||
description: "生时校正专用 Skill(V10)。以服务器权威 Case、ConversationFocus 与 CaseConversationSummary 驱动低负担访谈;批量证据逐项判定,candidate / accepted / confirmed 严格分离,全部计算与持久化只走服务端工具。触发词:生时校正、出生时间校正、校正出生时间、rectification、birth time correction。"
|
||||
---
|
||||
|
||||
# Jyotish 生时校正(V10)
|
||||
|
||||
## 1. 触发条件与方法学归属
|
||||
|
||||
本 Skill 只服务 `agentic_rectification_cases` 绑定的生时校正会话:
|
||||
|
||||
- 服务端 Case 存在且 `skill_name = 'jyotish-birth-time-rectification'`。
|
||||
- 用户话题是出生时间 / 出生分钟 / 事件发生时间能否定位到某几分钟,而不是普通解盘或推运。
|
||||
- 普通咨询、推运、合盘、补救问题交给 `jyotish-vedic-astrology`,不要在这里处理。
|
||||
|
||||
生时校正的方法学、访谈策略、证据边界与候选表达规则只定义在本 Skill 及其 references。system prompt 只保留安全、权限、隐私、工具和运行边界,不得复制、压缩或另写一套校时方法学,也不得用 system prompt 覆盖本版本政策。
|
||||
|
||||
## 2. 必须先读与服务器权威
|
||||
|
||||
进入任何一轮实质工作前读取(服务器会随 Dossier 提供投影,缺文件时以服务器 Dossier 为准):
|
||||
|
||||
1. `references/evidence-model.md`:证据种类、日期精度、原文引用、修订链、服务器持有 ID。
|
||||
2. `references/conversation-strategy.md`:OpeningPolicy、ConversationFocus、长会话记忆、批量证据与追问策略。
|
||||
3. `references/candidate-comparison.md`:candidate / accepted / confirmed 三层语义与表达边界。
|
||||
4. `references/technique-routing.md`:技法按主题调用,D9/D10 核心,不一次性调用所有分盘。
|
||||
5. `references/truth-consent-boundaries.md`:真实性、同意与选择政策。
|
||||
|
||||
服务器是下列信息的唯一权威:Skill 绑定版本、Case/Session 身份与状态、`ConversationFocus`、`CaseConversationSummary`、evidence/focus ID、事件状态与修订链、候选范围与评分、采用/确认权限、工具执行、持久化和计费。Agent 只能解释服务器投影并选择自然表达,不得从对话文本、上一条 assistant 消息或 recent turns 重建权威状态。
|
||||
|
||||
每次 attempt 必须先完成真实 Skill 绑定和 Case 加载,之后才能执行 action。失败或重试 attempt 的部分文本、工具结果与推断不得当作已提交事实;只依据服务器提交成功的 attempt 与 receipt。
|
||||
|
||||
## 3. Case 状态与只读边界
|
||||
|
||||
服务器 Dossier 会给出当前 `status`。按表行动:
|
||||
|
||||
| status | 允许动作 |
|
||||
|---|---|
|
||||
| `draft` / `collecting_evidence` | 继续收集/修订带日期事件;可读取诊断。`next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间,**不得**同时追问;仍有挡住出牌的 `next_followup` 时继续收集,**不得**提供候选。`selection_allowed` 不够作为出示卡片的理由;提出门看 `propose_allowed` 且访谈已停或用户喊停 |
|
||||
| `candidate_ready` | 可比较候选、说明当前边界;仍可继续补证据 |
|
||||
| `candidate_accepted` | 已采用代表性时间。采用后先按该分钟核最多两件前事,对不上可改选其他候选;核对结束再用这个时间看盘。`unique_minute_path=closed_at_representative` 时本会话以此收口,**不得**进入唯一分钟确认 |
|
||||
| `needs_rebaseline` | 出生资料基线已变化,候选失效;只允许重新收集/修订事件,禁止引用旧候选 |
|
||||
| `paused` | 可继续访谈;不要声称结束 |
|
||||
| `confirmed` / `closed` / `abandoned` / `superseded` | terminal Case,只读历史;不得追加/修订/确认证据,不得采用/确认候选,不得关闭第二次 |
|
||||
|
||||
- terminal Case 的只读限制由服务器强制;Agent 不得用换工具、换措辞、重试或旧 focus 绕过。用户要继续校正时,说明需要走显式新建 Case 的入口。
|
||||
- 同一用户可以保留多个可恢复 Case;首页显式新建与历史 Session 精确恢复是两条不同入口,不得因存在旧 Case 强制回到旧 Session。
|
||||
- 历史 Session 必须恢复对应的精确 Case/Session;不得把另一个 resumable Case 的上下文混入当前会话。
|
||||
|
||||
## 4. OpeningPolicy
|
||||
|
||||
服务端首次提供 opening brief:Case 状态、当前搜索窗口(`candidate_range`)与来源(intake 声明的不确定档)、做法三句要点、六类领域清单(升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤)。Agent 按下列三句模板自然开场,不得要求先准备一套材料,也不得写具体年份:
|
||||
|
||||
1. 一句当前搜索窗口与核对做法。
|
||||
2. 一句「最后给区间和代表分钟,不给精确到秒」。
|
||||
3. 一句「想到几件说几件,有大概年月就行」并点出上述六类。
|
||||
|
||||
开场必须满足:
|
||||
|
||||
- 一条消息可以报多件;想到几件说几件,有大概年月即可。只报一件也继续既有逐领域采集,不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
|
||||
- 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。
|
||||
- 首题保持采集题身份(`collect:other:*`),题干写成「先说你最容易想起的一两件,年月大概就行」。
|
||||
- 至多一个主问题且唯一来源:每轮当前问题只能由服务端建立 `ConversationFocus` 并通过界面问题槽呈现。Agent 回复正文只做承接与解释,不得自行提出、复述、改写或预告问题;正文内容不参与问题槽判定。
|
||||
- 不机械复述 opening brief,不泄露服务器字段、内部状态对象或出生资料明文。
|
||||
- 用户说出出生时间或时段时,不得回答『以你说的为准』或改写搜索窗口;服务端会固定回复范围在开始时已定、过程中不改。
|
||||
|
||||
## 5. ConversationFocus 与意图承接
|
||||
|
||||
`ConversationFocus` 是服务器持久化的当前对话目标,至少包含 `id`(即 `focusId`)、`questionId`、`intent`、`targetEvidenceId`、目标领域/类型、预期回答结构、状态与时间。Agent 可做意图分类,但服务器必须验证目标仍为 `active`。
|
||||
|
||||
- “是的 / 不是 / 大概那年 / 后来改了 / 不记得 / 不想回答 / 换个方向”等承接、拒答、确认和修订,必须依赖服务器给出的 active focus。
|
||||
- 拒绝、跳过、解决或修订既有目标时,工具调用必须引用服务器提供的 `focusId`;涉及既有证据时还必须引用对应 `evidenceId`。用户对已有 pending 说“对/是”时,`rectification-confirm-evidence` 可以省略 `focusId`,尤其当 active focus 是无 `target_evidence_id` 的 opening focus 时,不得用它烧掉后续事件确认。
|
||||
- 不得从 assistant 上一句倒推拒答目标,不得仅靠 pending revision 或中文正则构造 active focus,也不得把脱离上下文的承接词保存成新事件。
|
||||
- 没有 active focus、focus 已 resolved/declined/skipped/superseded、或当前表达可能指向多个目标时,只做一句简短澄清;不得猜测或写 evidence。
|
||||
- 当前轮用户主动、明确、无歧义地提出全新事件时,可按新事件处理;若需要后续问题,由服务器建立新的 focus。
|
||||
- 用户已拒绝或跳过的目标不得换词重问;只有用户主动重开该主题或服务器建立新的有效 focus 才可继续。
|
||||
- 性格类点选题只在带年月题问完仍分不开时出现,分值减半、不淘汰。
|
||||
|
||||
## 6. CaseConversationSummary 与长会话记忆
|
||||
|
||||
`CaseConversationSummary` 是长会话的权威记忆,至少投影:confirmed evidence summary、pending revisions、active focus、declined/skipped topics、candidate divergence summary、missing evidence categories、`method_followup_plan`、last result policy。
|
||||
|
||||
- 选择下一动作、识别已确认事实、避免重复追问、理解候选差异与结果政策时,优先依据服务器提供的 `CaseConversationSummary` 与 `method_followup_plan`。
|
||||
- 不要按 `missing_evidence_categories` 轮询迁居。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup` 带 `choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份,不得改写时间范围;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
|
||||
- recent turns 只是有界的原文引用窗口,用于核对当前措辞、quote 和局部承接;不得把 recent turns 当作唯一记忆,也不得用截断历史覆盖 summary。
|
||||
- summary 与 recent turns 看似冲突时,不自行裁决或默默改写事实:以服务器状态为准;需要用户确认时围绕 active focus 只澄清一个关键点。
|
||||
- 超过长会话窗口后仍不得忘记已确认证据、pending revision、拒答主题或 active focus。
|
||||
|
||||
## 7. 批量证据与日期真实性
|
||||
|
||||
一次用户消息可包含多件事件。优先使用服务器提供的批量 proposal/confirmation 服务,并遵守逐项原子语义:
|
||||
|
||||
- 每件事件独立保留用户原话 `quote`、`kind`、`domain` 和真实 `date precision`;不得合并、拆错主体或要求用户逐条重发。
|
||||
- 服务器逐项返回 `accepted` / `needs_clarification` / `rejected`;Agent 按每项结果分别处理,不得让一条模糊或拒绝项阻塞同批清晰项。
|
||||
- 清晰且 quote grounding 通过的新事件必须走批量服务写入;不要对同一句用户消息里的多件事件逐条 propose+confirm。`rectification-confirm-evidence` 只用于用户对已有 pending 明确说“对/是”。
|
||||
- 证据有效写入后,服务器会按当前账本重算候选。不要等用户说“没有更多了”才 compare;同一证据指纹不要再 compare。不要调用新的扫描工具。
|
||||
- 证据轮正文只写一句复述,格式「记下了:年 月 事件短语(、…)。」例如「记下了:2016 年 9 月入学、2020 年 6 月毕业。」不得加评价句,不得写「很有帮助 / 很有价值 / 很有分量 / 特别有用」。范围变化由服务器接到正文后面。
|
||||
- 批量结果中的 evidence item `accepted` 只是该项被服务接纳处理,不等于候选 `accepted`;清晰项在批量路径上可由服务器直接 `confirmed`。
|
||||
- 复述任何事件日期必须使用服务器 `display_date_label`。日级不得说成“年份已确定为 YYYY”。用户确认“是/对”不得改 `date_precision`。
|
||||
- `needs_clarification` 不得猜补日期、主体、事件身份、主动/被动、原因或人物关系;`rejected` 不得伪装成已记录。
|
||||
- 修订必须生成 superseding revision,引用 active `focusId` 与目标 `evidenceId`,不得覆盖历史;pending revision 不自动确认。
|
||||
- 日期精度真实保留:`year` / `month` / `quarter` / `day` / `range` / `unknown` 按用户原话保存,范围不得取中点,只有服务器目标已明确年份时才可把用户补充的月份/季度并入修订。
|
||||
- 批量服务与单项工具都必须依赖服务器幂等键;重试不得重复创建或确认 evidence。Agent 不自行生成 evidence/focus ID。
|
||||
|
||||
## 8. 可调用工具与输入边界
|
||||
|
||||
只调用服务器提供的 `rectification-*` 工具,包括 read-case、set/resolve-focus、批量 evidence、单项 proposal/confirmation/revision、candidate comparison/offer/accept/confirm 与 close-case。工具 input 只含服务端合同要求的最小引用(如 caseId、focusId、evidenceId、quote、proposedKind),**绝不**传:
|
||||
|
||||
- userId、出生日期/时间/地点/时区、candidate range、完整 events 数组、分数与阈值、confirmationAllowed/selectionAllowed、profile 写入目标。
|
||||
|
||||
工具结果只读取;事实、ID、评分、范围、状态、持久化、幂等与权限一律以服务器为准。工具执行对用户保持静默:不得叙述读取 Skill、Case 已加载、调用工具、建立草稿、读取诊断或呈现快照,也不得自行生成“本轮做了什么”“执行步骤”“使用技法”或 Activity 状态文案;运行状态和实际方法 receipt 只由服务器公开凭证展示。
|
||||
|
||||
## 9. candidate / accepted / confirmed 语言边界
|
||||
|
||||
- `candidate`:引擎对当前证据的归一化比较结果,称“当前候选 / 相对支持度”,**不得**称概率、置信度或确定性。
|
||||
- `accepted`:用户明确选择的当前排盘时间,称“校正采用时间”,**不得**称“已确认唯一出生时间”。
|
||||
- `confirmed`:通过服务器确认门且用户明确同意,称“已确认校正时间”。
|
||||
- `session_outcome=adopt_representative` / `next_user_action.id=adopt_representative`:本轮**有结果**,结果是采用代表性时间作当前排盘。正文应自然说明代表性候选可用于当前排盘,但它不是已确认的唯一出生分钟;不要使用固定收口句式。不要调用 confirm。只有这时才调用 `rectification-offer-candidates`。服务器会拒绝访谈未停且用户未喊停的 offer。`collecting_evidence` 且仍有挡住出牌的 `next_followup` 时不得 offer/accept。`propose_allowed` 需要可评分事件≥4、领域≥3、诊断稳定,或事件吻合率≥80%;唯一领先和宽度≤5只挡确认门,不挡出示代表性时间卡。精度阶段追问在方法覆盖完成后才问,且不挡出牌。KP 观察不计分、不挡提出门。
|
||||
- 确认门以 `latest_result.confirmation_gate` 为准。`unique_minute_path=closed_at_representative` 或任一 blocker 未通过时,不得把唯一分钟确认当下一步;用户仍可 accepted 代表性候选。
|
||||
- `vedastro_minute_sensitive` 为 `not_evaluated` 表示尚未跑通,不等于 fail,但缺它不能写 confirmed。
|
||||
- 若 `vedastro_minute_sensitive` 为 `passed` 但 `public_aa_holdout` 为 `not_ready`,可以说官方分钟层已区分相邻分钟,仍必须说公开密封集尚未达标,不能确认唯一分钟。
|
||||
- `public_aa_holdout` 为 `not_ready` 时 `unique_minute_path` 必须是 `closed_at_representative`:不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。
|
||||
- 未达到唯一分钟确认门时,任何“就用 HH:MM”都只能进入 accepted;只有 `confirmation_allowed=true` 且用户同意才可写 confirmed。
|
||||
- 若不可分 blocker 为 `blocked`、宽度大于 5、top `tied_minute_count` > 1,或 `confirmation_allowed=false`,正文必须说这是一段不可分区间,把代表分钟称为代表性候选,不得说已定位到唯一分钟。
|
||||
- 分钟窗口扫描只在服务端。即使高吻合、宽度 ≤5、`can_apply`/`propose_allowed`,仍写 `candidate_range_not_birth_time_truth`。
|
||||
- 出牌/采用轮正文只写三句:这次给出的范围与排盘用代表分钟、对照了几件经历与事件吻合率、边界句「这只是代表性候选,不是已确认的唯一出生分钟」。八法验证报告(筛选窗、方法1–8、Technique Audit Table)由服务端 `skill_verification_report.markdown` 渲染在卡片下方折叠块「查看验证报告」,**不得**写入助手气泡。宽度、双轨只抄 `skill_verification_report` 的 `width_minutes` / `dasha_agreement`。分盘上升只抄 `skill_verification_report.sign_by_candidate`,不得自行按换升时刻推算。
|
||||
- 80%/60% 只描述**事件吻合率**(高度/中度/低度拟合),**不得**写成“已确认唯一出生分钟”。
|
||||
- 不得在同一回复中一边要求继续补证据、一边提供采用候选。
|
||||
- 不得伪造出生分钟、分数、权重、事件 ID、分盘事实或确认门结果。
|
||||
|
||||
## 10. 输出与停止条件
|
||||
|
||||
- 简体中文。访谈按 skill 路径 C:先用自然语言收集带大概年份的经历;只有候选已经分不开时才生成可点选的 A/B/C/D 主题问卷。允许模糊日期、允许分多轮。**不得**一进场就出点选卡,也不得先逼 10–15 条事件长表。
|
||||
- 每轮最多一个主要问题;完整回复可以零问题,不为了延续对话强行追问,不生成三条推荐问题。
|
||||
- 用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,基于服务器状态直接回答,不把问题当作事件。
|
||||
- 用户说“不知道 / 记不清 / 不想回答 / 换个方向”时,按 active focus 关闭或跳过该目标;用户说“目前没有 / 没有更多事件”时,不再轮换证据领域,也不要求结束、暂停或保存进度。
|
||||
- 不得询问外貌、体质、胎记或疤痕。D9/D10 类型表是校时方法,写「该分钟下 D9/D10 升 X,与用户所述特质的对应/冲突」,不是咨询命运承诺。职业对照本命第 10 宫和 D10,允许类型表。占问只问一次;有问起时间则观察,没有也不挡出牌。`internal_observations` 可用于选题,类型对照写入验证报告。若用户消息以「盘外核对(不计分)」开头,不得写入可评分证据。
|
||||
- 精度阶段按本命上升 → D9 → D10 → D4 居所 → D5/D24 成就收窄;家人走 D12/D7/D3 方法覆盖。财务走 D2/D11、健康走 D30,与其他领域同权计分,均不得混进 D4。Pada / Hora / Ghati / Bhava / Pranapada / KP 子主只展示换升,不确认唯一分钟。
|
||||
- 采用后按采用分钟核最多两件服务器探针前事;对得上写入并重算,对不上可改选其他候选。不得声称唯一分钟,也不自动进入咨询 Agent。
|
||||
- 采用候选后自然说明 accepted 与 confirmed 边界;`verify_adopted_time` 时必须核一件前事,核对结束或用户先这样才请看盘。不主动关闭 Case,Session 会保留并可日后继续。
|
||||
- 不再有固定 10–15 个事件长表、外貌/体型/疤痕主评分、或“稳定确定到精确分钟”的承诺。A/B/C/D 主题问卷只在候选已经分不开或采用后核对前事时使用。80%/60% 只描述事件吻合率。
|
||||
- 无法验证时如实降级并说明受限,不得把内部一致性伪装成全球顶级精度。
|
||||
|
||||
## 11. 上游同步边界
|
||||
|
||||
方法源只在本 Skill 与 references。不得把本 Skill 内容反向写回 `yinduzhanxing` 上游快照,也不得在同步时自动覆盖商业 Skill。
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Candidate Comparison(V9)
|
||||
|
||||
候选比较是服务器计算产物,Agent 只负责解释与引导,不负责产生候选、分数或范围。
|
||||
|
||||
## 1. 三层语义
|
||||
|
||||
| 层 | 含义 | 表达 |
|
||||
|---|---|---|
|
||||
| `candidate` | 引擎对当前证据的归一化比较结果 | “当前候选”“相对支持度” |
|
||||
| `accepted` | 用户明确选择的当前排盘时间 | “校正采用时间” |
|
||||
| `confirmed` | 通过服务器确认门且用户明确同意 | “已确认校正时间” |
|
||||
|
||||
- `candidate_accepted` 不是“唯一出生分钟已确认”,默认仍可继续补充证据。
|
||||
- accepted 后用户仍可在同一批有效候选中改选(幂等 RPC 支持)。
|
||||
- confirmed 只能由服务器确认门 + 用户明确同意触发,同时写 `completed_at`。
|
||||
|
||||
## 2. 何时提供候选
|
||||
|
||||
- 只有本轮完成 `rectification-offer-candidates` 且返回 `selection_allowed=true` 时,界面才展示候选卡。
|
||||
- `selection_allowed` 只表示可以采用代表性时间,**不是**本轮必须出示卡片。提出门看 `latest_result.propose_allowed`,并且没有挡住出牌的 `method_followup_plan.next_followup`(占问和精度阶段追问不挡;职业挡出牌)。唯一领先和宽度≤5只挡确认门。
|
||||
- `next_user_action.id=adopt_representative`,或用户停止且 `on_user_stop` 为 adopt 时,本轮才 offer/accept。服务器会拒绝访谈未停的 offer。这是采用代表性时间,不是 confirmed。
|
||||
- 继续收集证据时不得边追问边提供采用。
|
||||
- 候选卡内容来自持久化 Candidate Snapshot(`agentic_rectification_results`),不是 Agent 文本解析。
|
||||
- 候选卡按一行至多三列并排:每列一个候选分钟,写相对可能性、性格处事、经历对照、往后 12 个月事件窗;「更像这个」即采用。不预标「排盘用」。Agent 正文在出牌轮**不得**复述八法表格或 Technique Audit。
|
||||
|
||||
## 3. 表达边界
|
||||
|
||||
- 相对支持度是候选间归一化比较,**不是**概率、统计置信度或确定性。卡片上的「相对可能性」是答题后的后验百分比,同样不是引擎置信度。80%/60% 只描述事件吻合率。
|
||||
- 出牌轮正文不写事件–Dasha–Gochara 表、D9/D10 类型对照和技法审计;那些只出现在折叠的验证报告里。不暴露隐藏分钟证据或把分数说成唯一分钟概率。分盘上升只抄 `skill_verification_report.sign_by_candidate`,不得自行按换升时刻推算。
|
||||
- 候选范围必须说明“待核对边界”,不得表述为已确认出生分钟。
|
||||
- 外部验证状态按服务器字面读取:`not_evaluated` 表示未调用(入口门未就绪),不是“调用了但失败”。
|
||||
|
||||
## 4. 证据变化与重算
|
||||
|
||||
- 证据有效变化时由服务器重算候选;Agent 不必等用户说“没有更多了”才 compare。
|
||||
- 相同 evidence 指纹 + 引擎版本复用缓存;不要对同一指纹再 compare。
|
||||
- 分钟窗口扫描只在服务端,结果进入候选卡 / 不可分平台语言。不得把若干事件说成已确定到 ±5 分钟。
|
||||
- 普通澄清轮若不改变账本指纹,不重复播报。
|
||||
- 出生资料基线变化 → `needs_rebaseline`,旧候选失效;不得静默继续用旧结果。
|
||||
- `needs_rebaseline` 下不引用旧候选、不提供采用。
|
||||
|
||||
## 5. 不可分平台与确认门(必须说出来)
|
||||
|
||||
服务器 `latest_result` 含 `confirmation_gate`、`engine_indistinguishable_width_minutes`、`confirmation_allowed`、`selection_allowed` 与 `margin_percent`(若有)。`confirmation_gate` 是确认门权威,不是让 Agent 另算一分钟。折叠验证报告的宽度、双轨、分盘星座只抄 `skill_verification_report`(`width_minutes` / `dasha_agreement` / `sign_by_candidate`),不得用引擎原跨度或已淘汰分钟。Agent 正文不得再写这些表。
|
||||
|
||||
- 宽度大于 `maxConfirmationWidthMinutes`(5),或 top 候选 `tied_minute_count` > 1,或 `confirmation_allowed=false` 时:正文必须说这是**一段不可分区间**,必须把代表分钟说成**代表性候选**,不得说已定位到唯一分钟,也不得学本地扫分钟后的 1 分钟尖峰。
|
||||
- `vedastro_minute_sensitive` 为 `not_evaluated` 表示官方分钟敏感校验尚未跑通,不是 fail;缺它不能写 confirmed。
|
||||
- 若官方分钟层已 `passed` 但 `public_aa_holdout` 为 `not_ready`:可以说已区分相邻分钟,仍不得确认唯一分钟或发布准确率。
|
||||
- `public_aa_holdout` 为 `not_ready` 时不得声称已校准到精确分钟,也不得把确认门放到更细宽度或发布准确率。
|
||||
- 用户仍可 accepted 代表性候选;accepted ≠ confirmed。`session_outcome=adopt_representative` 时自然说明代表性候选可用于当前排盘、但不是已确认的唯一出生分钟,不要使用固定收口句式。`unique_minute_path=closed_at_representative` 时不得把确认当下一步。
|
||||
- `confirmation_allowed=true` 才允许进入唯一分钟确认门;平台结果禁止把 `confirmation_allowed` 说成已确认。
|
||||
- 候选卡仍可展示代表性时间;Agent 不得把该时间写成“已校正到 HH:MM”。
|
||||
|
||||
## 6. 保存边界
|
||||
|
||||
- accepted 写入 `active_birth_time`,保留 `reported_birth_time` 原填报,不写兼容 `birth_time`。
|
||||
- 采用后界面按采用分钟重算本命宫位表,并折叠展示本轮技法审计。这不是唯一分钟确认,也不自动进入咨询 Agent。
|
||||
- confirmed 同样保留原填报;不自动写入,需要用户明确同意。
|
||||
- 失败、空流、Skill 未加载或未完成必要工具链时不保存、不扣费。
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# Conversation Strategy(V10)
|
||||
|
||||
生时校正访谈按 skill 路径 C:先用自然语言收集带大概年份的经历,再在候选已经分不开时由服务器锁定时间范围和事件家族,由你写成一句具体生平题干(某年或某月是否搬过家、高考是否发挥失常),用 A/B/C/D 点选卡回答同一件事的吻合程度;不是 10–15 条事件长表,也不是无结构闲聊,更不是让用户给两套盘排序。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。
|
||||
|
||||
## 1. 每轮上下文优先级
|
||||
|
||||
每轮先按以下优先级理解会话:
|
||||
|
||||
1. 当前 Case 的服务器状态与读写权限。
|
||||
2. `CaseConversationSummary`:confirmed evidence、pending revisions、active focus、declined/skipped topics、candidate divergence、`method_followup_plan`、last result policy。不要把 `missing_evidence_categories` 当下一问。
|
||||
3. 当前用户消息。
|
||||
4. recent turns:只作为有界原文引用窗口,辅助 quote grounding 和局部措辞理解。
|
||||
|
||||
recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什么”的倒推、正则匹配或被截断的聊天记录重建 Case 状态。summary 与局部文本不一致时,以服务器状态为准;若用户意图仍不唯一,只澄清一个关键点。
|
||||
|
||||
## 2. OpeningPolicy
|
||||
|
||||
首次开场只使用服务器 opening brief 中的 Case 状态、当前搜索窗口(intake 不确定档)、做法三句要点与六类领域清单,并自然满足:
|
||||
|
||||
- 三句模板:当前窗口与核对做法;「最后给区间和代表分钟,不给精确到秒」;「想到几件说几件,有大概年月就行」并点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤。
|
||||
- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。只报一件也走既有逐领域采集;不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
|
||||
- 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。
|
||||
- 不得写具体年份,不得要求先准备材料。
|
||||
- 首题 `collect:other:*` 题干写成「先说你最容易想起的一两件,年月大概就行」。
|
||||
- 至多一个主问题;开场可以零问题。
|
||||
- 不固定复述身份、opening brief 原文或服务器字段。
|
||||
|
||||
区分阶段的题干由你写成自然语言;时间范围和事件家族以服务器探针为准,不得发明年份,不得改写时间范围。例如把锁定的 2015 年和搬家写成“2015 年前后你是否搬过家?”,把锁定的 2018 年 3 月写成“2018 年 3 月前后你是否入职或职责加重?”,把已有高考经历写成“高考的时候是否发挥失常?”
|
||||
|
||||
## 3. 一轮的基本形态
|
||||
|
||||
1. 先判断用户意图:新事件、批量事件、补日期、修正旧事实、回答上一问、确认/否认、询问进度或原因、拒答/换方向、查看或采用候选。
|
||||
2. 先读取服务器 Case、summary 与 active focus;静默完成必要的工具调用后再输出答案。正文不叙述内部执行步骤,也不生成 Activity/技法凭证文案。
|
||||
3. 自然回应本轮内容。证据轮正文只写一句复述:「记下了:年 月 事件短语(、…)。」不评价价值,不写「很有帮助 / 很有价值 / 很有分量 / 特别有用」。范围变化由服务器接在后面。
|
||||
4. 清晰项先处理;若仍需追问,只保留一个最有信息增益的主问题。完整回复可以没有问题。
|
||||
5. 不允许在同一回复中既要求补证据、又提供采用候选;不生成三条推荐问题。
|
||||
6. `next_user_action.id=adopt_representative` 时本轮只解释结果并邀请采用,零追问(除非有 active focus)。`id=verify_adopted_time` 时本轮只核一件前事,不要 offer,不要看盘。仍有挡住出牌的 `next_followup` 时不得出示采用卡。提出门看 `propose_allowed`。精度阶段追问和占问不挡出牌;职业仍挡。不得询问外貌、体质、胎记或疤痕。宽度大于 5 仍可出示代表性时间卡,不得为把不可分区间问到 5 分钟以内而继续 A/B/C/D。`unique_minute_path=closed_at_representative` 时不得把唯一分钟确认当下一步。
|
||||
|
||||
## 4. ConversationFocus
|
||||
|
||||
active `ConversationFocus` 是承接型意图的唯一目标来源。它由服务器持久化并提供 `focusId`、目标 `evidenceId`(如有)、intent、预期回答结构和状态。
|
||||
|
||||
- “是的 / 不是 / 对 / 不对 / 大概那年 / 后来改了 / 不记得 / 不想回答 / 换个方向”只有在存在唯一 active focus 时才能解释为回答、拒答、确认或修订。
|
||||
- 拒绝、跳过、解决 focus 时,工具调用必须引用 active `focusId`;修订既有 evidence 时同时引用目标 `evidenceId`。用户对已有 pending 说“对/是”时,确认工具可以省略 `focusId`;opening focus(无 `target_evidence_id`)不得因第一条确认被 resolve。
|
||||
- 无 active focus、focus 已非 active、目标已被 supersede、或一句话可能指向多个问题时,简短问清“你指的是哪一件/哪一个时间点”;不得猜测,不调用 evidence 写工具。
|
||||
- 脱离 active focus 的“是的 / 不是”不是新事件。不得从 assistant 上一句倒推目标,不得只用 pending revision 构造 `active_followup`。
|
||||
- 当前消息若主动、明确陈述全新事件,可独立进入 evidence 流程;需要追问时由服务器建立新 focus。
|
||||
- 服务器验证 focus 已失效时,停止该动作并基于最新 summary 重新回应,不沿用旧目标。
|
||||
|
||||
## 5. 自然叙述与批量 evidence
|
||||
|
||||
用户一段话中可以包含多件事件。应优先走服务器批量服务:
|
||||
|
||||
- 每件事件分别保留原话 `quote`、`kind`、`domain`、主体和日期精度,不合并,不要求逐条重发。
|
||||
- 服务器对每项独立返回 `accepted`、`needs_clarification` 或 `rejected`。一项失败不改变其他项结果。
|
||||
- 新事件优先走批量服务;一句里两件及以上事件时只允许批量。清晰项在批量路径上可由服务器直接 `confirmed`,不要再逐条 propose+confirm。不要让模糊项阻塞清晰项。
|
||||
- 多个模糊项同时存在时,只选择信息增益最高的一项追问一个关键点,其余维持待澄清,不连续抛出问题清单。
|
||||
- `needs_clarification` 只问缺失的关键事实;不猜日期、主体、事件身份、动机、因果、主动/被动或人物关系。
|
||||
- `rejected` 如需解释,只说明用户可理解的边界,不伪装成已记录。
|
||||
- 批量 evidence item 的 `accepted` 是服务处理结果,不是候选采用状态;清晰项的最终 `status` 以服务器返回为准,批量路径上可以为 `confirmed`。
|
||||
- 询问进度/原因、拒答、查看结果、采用候选,以及无唯一 active focus 的承接词,都不是新事件。
|
||||
|
||||
## 6. 确认、修订、拒答与换方向
|
||||
|
||||
- 确认既有事实:必须有对应 `evidenceId`;确认词本身不创建新 evidence。无匹配 pending-target 的 focus 时可省略 `focusId`。
|
||||
- 修订既有事实:必须有 active `focusId` 和目标 `evidenceId`,生成 superseding revision,不覆盖历史;pending revision 不自动确认。
|
||||
- 用户明确“不知道 / 记不清”:将 active focus 解决为 skipped,本会话不再问该领域采集;采用后核对仍可碰。回执「记下了,这题先放着。」
|
||||
- 用户明确“没有 / 不想回答 / 换个方向”:decline/skip active focus;不得换词重开同一目标。采集题「没有」走 declined,回执「记下了,这方面先跳过。」
|
||||
- 用户主动重新打开曾拒绝主题时,可让服务器建立新 focus;否则 declined/skipped topics 以 `CaseConversationSummary` 为准。
|
||||
- 用户说“目前没有 / 没有更多事件”时,停止轮换证据领域;不要求结束、暂停或保存进度。
|
||||
- 若没有其他具备信息增益的问题,可以直接说明当前边界或自然结束本轮。
|
||||
|
||||
## 7. 追问策略
|
||||
|
||||
追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级:
|
||||
|
||||
1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。
|
||||
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;时间范围和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 大运/副运起点的年或月差,没有可问边界时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份,不得改写时间范围。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative` 时 `next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
|
||||
3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察用于选题,并在出牌轮写入类型对照(校时方法,不是命运承诺)。
|
||||
4. pending revision 的一个关键歧义。
|
||||
5. 已有证据的必要稳定性补强。
|
||||
|
||||
不要按 `missing_evidence_categories` 轮询迁居。财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。不是 SQL 类别轮询。`stop_domain_rotation=true` 时停止领域清单。一轮最多一个主要问题。用户询问“为什么问这个 / 现在到哪一步 / 还需要多少信息”时,直接说明目的、当前状态和边界,不绕开问题继续索取证据。
|
||||
|
||||
## 8. 日期精度
|
||||
|
||||
- `year`:只说年份;复述用 `display_date_label`(如 `2024年`)。
|
||||
- `month`:明确到月份;复述如 `2024-05`。
|
||||
- `quarter`:明确到季度。
|
||||
- `day`:明确到日期;复述必须是 `YYYY-MM-DD`,禁止说成“年份已确定为 YYYY”。
|
||||
- `range`:只有范围,不得擅自取中点当事实;复述用 `from–to`。
|
||||
- `unknown`:日期不明;可保留背景,但不得当作高权重校正证据。
|
||||
- 用户确认“是 / 对”不得改 `date_precision`。
|
||||
- 用户只补月份/季度时,只有 active focus 与目标 evidence 已由服务器明确年份,才可合并为 revision;不得猜年份。
|
||||
- “大概 3 月”仍按用户真实表达保存,不升级成某一天。
|
||||
|
||||
## 9. 候选输出与终态
|
||||
|
||||
- 候选卡负责呈现时间、排名、相对支持度、采用动作与选中状态。
|
||||
- 出牌/采用轮正文写入 skill 八法验证报告:候选窗、代表分钟、相对支持、事件–Dasha–Gochara 表、D9/D10 类型对照、技法审计表。卡片仍作 adopt 控件。
|
||||
- `relative_support` 不是概率,不能写“准确率 70%”。80%/60% 只描述事件吻合率。
|
||||
- candidate、accepted、confirmed 严格分离;accepted 不是 confirmed。
|
||||
- `next_user_action.id=adopt_representative` 时本轮结果是采用代表性时间;正文自然说明代表性候选可用于当前排盘、但不是已确认的唯一出生分钟,不要使用固定收口句式。仍有 `next_followup` 时不得出示采用卡。
|
||||
- 确认门以 `confirmation_gate` 为准。`not_evaluated` 不是 fail;holdout `not_ready` 时 `unique_minute_path=closed_at_representative`,不得声称精确分钟或发布准确率,也不得把唯一分钟确认当下一步。官方分钟层 `passed` 仍不能单独打开确认门。
|
||||
- 若确认门 `confirmation_allowed=false`,或 `confirmation_gate` 的不可分 blocker 为 blocked,必须说不可分区间 / 代表性候选,不得说已定位到唯一分钟。交付轮宽度只抄 `skill_verification_report.width_minutes`。accepted ≠ confirmed。
|
||||
- accepted 后按采用分钟核最多两件前事;对得上写入并重算,对不上可改选。不强制看盘,不要求用户结束、暂停或保存进度。核对结束或用户先这样才 `start_consultation`。
|
||||
- terminal Case(confirmed / closed / abandoned / superseded)只读:不得新增/修订/确认 evidence,不得采用/确认候选;若用户要继续,指向显式新建 Case。
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# Evidence Model(V9)
|
||||
|
||||
证据是生时校正的唯一事实账本。本文件定义证据如何进入、校验、修订与关闭。服务器是证据账本的唯一写入者;Agent 只能提出 proposal。
|
||||
|
||||
## 1. 证据最小单元
|
||||
|
||||
一条证据(`agentic_rectification_evidence` 一行)至少包含:
|
||||
|
||||
- `case_id`:所属 Case,由服务器生成。
|
||||
- `source_turn_id`:用户消息所在轮次;`source_message_id` 可选。
|
||||
- `user_quote`:用户原话的规范化子串。
|
||||
- `subject`:主体(`self` 或亲属关系;家庭事件必须显式 `related_person`)。
|
||||
- `event_kind`:语义种类(见 §2),不再只保留粗领域。
|
||||
- `domain`:评分/路由领域。
|
||||
- `occurred_from` / `occurred_to`:真实日期边界,可空。
|
||||
- `date_precision`:`year | month | quarter | day | range | unknown`。
|
||||
- `summary`:服务器从已验证引用中生成的安全摘要。
|
||||
- `status`:`draft | pending_confirmation | confirmed | superseded | rejected`。
|
||||
- `supersedes_evidence_id`:修订链指针。
|
||||
|
||||
## 2. 事件种类(event_kind)
|
||||
|
||||
```text
|
||||
education_start
|
||||
education_completion
|
||||
education_interruption
|
||||
education_change
|
||||
education_milestone
|
||||
career_entry
|
||||
career_change
|
||||
promotion
|
||||
career_pressure
|
||||
career_exit
|
||||
business_start
|
||||
relationship_start
|
||||
relationship_commitment
|
||||
relationship_separation
|
||||
relationship_end
|
||||
relationship_change
|
||||
relocation
|
||||
foreign_move
|
||||
return
|
||||
home_change
|
||||
finance_gain
|
||||
finance_loss
|
||||
income_change
|
||||
asset_change
|
||||
finance_change
|
||||
self_health_event
|
||||
pressure_period
|
||||
family_event
|
||||
appearance_note
|
||||
birthmark_or_scar
|
||||
occupation_note
|
||||
horary_query
|
||||
other
|
||||
```
|
||||
|
||||
语义不折叠:`career_entry / career_pressure / career_exit` 不同;`relationship_start / relationship_commitment / relationship_separation` 不同;不得把“开始关系”与“关系变化”混成同一事件。`education_milestone`、`relationship_end`、`return`、`home_change`、`health_pressure` 等与 TypeScript `EVIDENCE_KINDS` / `EVIDENCE_DOMAINS` 对齐,不得再因枚举缺口导致写入失败。
|
||||
|
||||
领域(`domain`):
|
||||
|
||||
```text
|
||||
education
|
||||
career
|
||||
relationship
|
||||
relocation
|
||||
finance
|
||||
health
|
||||
health_pressure
|
||||
family
|
||||
appearance
|
||||
marks
|
||||
occupation
|
||||
horary
|
||||
other
|
||||
```
|
||||
|
||||
## 3. 日期精度
|
||||
|
||||
- 用户只给年份 → `date_precision = 'year'`,`occurred_from = YYYY-01-01`(边界),不得诱导编造月份。
|
||||
- 用户给年月 → `month`;给季度 → `quarter`;给年月日 → `day`;给区间 → `range`。
|
||||
- 相对表达(“刚毕业那年”)必须由服务器结合权威当前时间解析,Agent 不得自行假设年份。
|
||||
- 跨午夜、未知时间不伪造具体分钟;`unknown` 精度允许保留。
|
||||
- 服务器投影只读字段 `display_date_label`:日级用 `YYYY-MM-DD`,月级用 `YYYY-MM`,年级用 `YYYY年`,range 用 `from–to`。复述必须用该标签;禁止把日级格式化成“年份已确定为 YYYY”。用户确认“是/对”不得改 `date_precision`。更粗的修订若 quote 并没有更粗的日期表达,服务器拒绝 `precision_downgrade`。
|
||||
|
||||
## 4. 原文引用(quote grounding)
|
||||
|
||||
- `user_quote` 必须能在对应 `source_turn.user_message` 中找到规范化匹配(去空白、去标点后子串命中)。
|
||||
- 服务器确认路径必须校验:引用来自本轮用户消息、kind 属于枚举、日期与原文一致。
|
||||
- 模型不得凭空补充月份、日期、原因、主动/被动、人物关系。
|
||||
|
||||
## 5. 修订链(append-only)
|
||||
|
||||
- 事实变化 = 新增 superseding row,旧行标记 `superseded`,永不覆盖/删除。
|
||||
- 合法修订:日期更正、日期补全(如“2016 年 + 9 月”合并为 `2016-09`)、事件重分类(同身份)。
|
||||
- 非法修订:跨事件覆盖既有 ID(如把“大学入学”改成“搬家”);服务器拒绝并降级为新的 pending proposal。
|
||||
- 证据 ID 只能由服务器生成;模型不得提供或覆盖。
|
||||
|
||||
## 6. 状态迁移
|
||||
|
||||
```text
|
||||
draft -> confirmed (当前轮明确事件:proposal 通过原文绑定后,同轮走服务器确认路径)
|
||||
draft -> pending_confirmation (事实模糊、冲突或需要用户补充)
|
||||
pending_confirmation -> confirmed (用户明确确认 + 服务器确认路径)
|
||||
pending_confirmation -> superseded(用户更正,产生修订)
|
||||
confirmed -> superseded (后续修订使旧事实失效)
|
||||
draft / pending_confirmation -> rejected (用户否认,保留只读历史)
|
||||
```
|
||||
|
||||
- Agent 只能先产生 `draft`;`confirmed` 只能由服务器确认路径产生。服务器确认路径不等于必须额外等待一轮用户回复。
|
||||
- 终态 Case(confirmed/closed/abandoned/superseded)禁止新增或修订证据。
|
||||
- 同一请求重放不得重复写证据(幂等键 = case + source_turn + quote + kind + summary)。
|
||||
|
||||
## 7. 评分输入边界
|
||||
|
||||
- 只有 `confirmed` 证据进入评分账本;`draft` 与 `pending_confirmation` 都不参与评分。
|
||||
- `family_event` 进入评分(D12 + D7 + D3 + 六亲宫位)。`other` 只作背景,不推进评分覆盖计数。
|
||||
- `appearance_note` / `birthmark_or_scar`:无日期只覆盖访谈;有日期才进上升/一宫辅助评分,不得当主公式。
|
||||
- `occupation_note`:与带日期事业事件独立。无日期只覆盖访谈;有日期按 D10 + 本命 10 宫辅助评分,允许事业类型表作校时方法。
|
||||
- `horary_query` 只作背景观察,不推进评分覆盖计数,也不计入 4 事件 / 3 领域。
|
||||
- 证据变化才触发重算;相同证据指纹复用缓存,不重复评分。
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Technique Routing(V9)
|
||||
|
||||
生时校正是“有日期事件 + Dasha 为主要证据”的校准任务,分盘按主题调用,不一次性调用所有分盘。所有计算只能通过服务端工具;本文件只决定读哪些技法证据,不复制任何引擎实现。
|
||||
|
||||
## 1. 主证据
|
||||
|
||||
- 有明确日期(年月级或更精确)的人生事件 + 对应 Dasha 边界是主要证据。
|
||||
- 事件原文是用户原话;日期精度按用户真实提供保留。
|
||||
- 不把“支持某技法”误当作已完成独立验证;内部一致性不得伪装成全球顶级精度。
|
||||
|
||||
## 2. 分盘调用层级
|
||||
|
||||
| 层级 | 分盘 | 用途 |
|
||||
|---|---|---|
|
||||
| 核心 | D1(本命) | 全局框架 |
|
||||
| 核心辅助 | D9、D10 | 关系与事业的主要主题 |
|
||||
| 主题 | D2/D11(财富)、D3(兄弟姐妹)、D7(子女/伴侣细节)、D12(父母)、D24(教育)、D4(居所/不动产)、D5(成就)、D30(健康压力) | 按主题补充 |
|
||||
| 仅参考 | D60 | 只作参考,不驱动结论 |
|
||||
|
||||
- 同一轮最多调用 2–3 个相关分盘;D9/D10 之外的分盘必须由当前主题驱动。
|
||||
- 未执行、不可用或仅供参考的技法不得显示为已执行。
|
||||
|
||||
## 3. 按问题域强制调取
|
||||
|
||||
- 事业:同一件带日期的事业事件必须同时计算 `D10` **和** D1 第 10 宫 / 10 宫主(A10 为事业 Arudha,服务器可用时)。职业说明与带日期事业事件独立,同样对照 D10 与本命 10 宫,**允许**事业类型表作校时方法;无日期只覆盖访谈。
|
||||
- 财富:用户主动提供带日期的收入、资产或财务变化时计分 `D2 / D11`。不要主动追问。窗口扫描记录 D2/D11 换升,但不新增精度阶段。
|
||||
- 婚恋:`D9 + UL`(UL 为 Upapada Lagna,服务器可用时)。
|
||||
- 六亲/家人:`D12` 加 `D7`(子女/伴侣细节)加 `D3`(兄弟姐妹)加 D1 三/四/五/九宫。家人事件进入评分,不只作背景。D3 不另开精度阶段。
|
||||
- 外貌/体质/胎记疤痕:本轮访谈不追问。若用户主动提到带日期的外貌或受伤变化,只对照 D1 上升/一宫作辅助降权,不得当主评分。
|
||||
- 健康:用户主动提供带日期的健康、事故或压力变化时计分 D1 + D30。不要主动追问。不是医学判断。窗口扫描记录 D30 换升,但不新增精度阶段。
|
||||
- 迁居:精度阶段 `d4_refine` 问带日期的搬家/住处变化;这不是领域轮询。计分 D4 + D1 四/十二宫。
|
||||
- 教育/成就:精度阶段 `d5_refine` 在 D5 **或 D24** 换升时问带日期的学业、考试或被委以责任的变化。计分 D24 + D5 + D1 四/五/九宫。D24 窗口扫描并入 `d5_refine`,不新增阶段 id。
|
||||
- 占问:只问一次第一次认真问起这件事的时间。有日期则按该时点重算观察盘(出生地经纬,除非另给地点),可附 1/4/7/10 KP 子主。失败写成 blocked 观察,不计分,不挡提出门或确认门。没有时间或拒绝则 `skipped_by_policy`。
|
||||
- 精度阶段顺序:有日期事件 → 本命上升 → 方法覆盖(感情 → 事业 → 家人 → 职业 → 占问)→ 再对已覆盖领域做 D9 / D10 / D4 / D5(含 D24 换升)。家人不得混进 D4,也不另开 `d11_refine` / `d30_refine`。未走完挡住出牌的方法覆盖(含职业)时,不得因为关系盘仍会换升就提前出示时间卡。
|
||||
- Nakshatra pada、Hora Lagna、Ghati Lagna、Bhava Lagna、Pranapada Lagna、KP 子主只在窗口扫描中展示换升,不驱动 `ready_to_adopt`,也不打开确认门。日出不可用时省略 Hora/Ghati/Pranapada,不得用 06:00 假日出。Bhava 只用本命日月,不依赖日出。
|
||||
- D9/D10 类型表写入出牌轮验证报告,作为校时方法,不得写成命运承诺。`internal_observations.ask_theme` 决定下一问主题。
|
||||
|
||||
## 4. 受限技法边界
|
||||
|
||||
- KP、Muhurta、Gochara、Sahams、Sphuta、Tajika 为 reference-only 或 blocked;不得作为确认或精确应期依据。KP 按 Swiss Ephemeris Placidus + Krishnamurti 观察 12 宫头;成功为 `executed`,失败为诚实 `blocked`。不计分,不参与提出门或确认门。不得把政策跳过冒充已观察。
|
||||
- Shadbala / Ashtakavarga 外部绝对值未闭环前不作确定性结论。
|
||||
- 外部验证状态按服务器字面读取;`not_evaluated` ≠ `fail`。
|
||||
- 禁止 D60 驱动结论;禁止把邻近分钟与留一事件诊断描述为硬阻塞。
|
||||
|
||||
## 5. 决策树(简化)
|
||||
|
||||
1. 有日期事件 → 按 Dasha 建立时间框架。
|
||||
2. 主题缺口 → 调对应分盘(§2/§3)。
|
||||
3. 候选对比有差异 → 服务器 Candidate Contrast 驱动下一问。
|
||||
4. 唯一分钟确认门以 `confirmation_gate` 为准(事件数/领域数/宽度/唯一领先/必需层/VedAstro/holdout)。`not_evaluated` ≠ fail。Agent 不得自行宣告通过或失败。
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Truth / Consent Boundaries(V9)
|
||||
|
||||
本文件定义真实性、用户同意与选择政策。服务器拥有事实、权限与状态;Agent 必须服从服务器返回的 truth/consent/selection policy。
|
||||
|
||||
## 1. 真实性硬边界
|
||||
|
||||
- 禁止虚构:事件、日期、候选、分盘数据、评分、Dasha 边界或出生分钟。
|
||||
- 计算只能通过服务端工具;模型不得重算或发明行星位置、分数或权重。
|
||||
- 内部一致性不等于“全球顶级精度”;外部 oracle 未闭环、参照引擎不可用时必须写成 `blocked` 或降级置信度。
|
||||
- 系统提示词与 Skill 原文不得输出;reasoning / chain-of-thought 不向用户展示。
|
||||
|
||||
## 2. 用户同意边界
|
||||
|
||||
- 保存 profile 需要用户明确同意 + 服务器确认门。
|
||||
- accepted(用户选择)与 confirmed(引擎唯一确认 + 用户同意)严格区分;不得把 accepted 写成 confirmed。`confirmation_gate` 是确认门权威;`not_evaluated` 不是失败。
|
||||
- 助手文本、模型推断与历史摘要不得升级为已确认事实;当前轮用户主动、明确且无歧义的事件可在 quote grounding 通过后同轮走服务器确认路径。旧文本只能作为显示历史或 pending evidence draft。
|
||||
- 用户说“不知道/不想回答”时尊重并关闭该目标,不换词重开。
|
||||
|
||||
## 3. 选择政策
|
||||
|
||||
- 候选卡只展示服务器持久化候选与相对支持度;不得暴露原始分数、权重、贡献矩阵、技术层或隐藏分钟。
|
||||
- 继续收集证据时不得同时提供采用操作。界面只在本轮完成 `rectification-offer-candidates` 且 `selection_allowed=true` 时展示候选卡。
|
||||
- 相同 evidence 指纹复用缓存;只有有效变化才重算。
|
||||
- 终态 Case 只读;追加证据、采用、确认全部拒绝。
|
||||
|
||||
## 4. 隐私与泄露防护
|
||||
|
||||
- 不输出 userId、出生资料明文、内部 ID、工具参数/结果、数据库错误原文、密钥或内部 URL。
|
||||
- 每轮持久化公开执行回执(phase/tool 白名单、状态、时间),不含 reasoning 与 payload。
|
||||
- 家庭健康事件不得投射为本人生成评分证据;亲属主体必须显式标记。
|
||||
|
||||
## 5. 受限技法降级
|
||||
|
||||
| 状态 | 表达 |
|
||||
|---|---|
|
||||
| `blocked` | 明确写 blocked,不得包装成通过 |
|
||||
| `partial` | 说明部分边界,降级置信度 |
|
||||
| `reference_only` | 只作参考,不驱动结论 |
|
||||
| `not_evaluated`(外部验证) | 未调用,不等于失败 |
|
||||
|
||||
## 6. 功能吉凶层(高严谨模式)
|
||||
|
||||
进入高严谨模式(事业/财富/婚恋/应期/技法可靠性)时,除自然吉凶星外必须叠加当前 Lagna 下的 Functional Benefic/Malefic 判定;自然与功能属性冲突时必须说明冲突来源并降级或标记 blocked。未完成该判定不得声称高严谨解读完成。
|
||||
@@ -183,6 +183,14 @@
|
||||
"sha256": "3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||||
"sourceCommit": null,
|
||||
"packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.21",
|
||||
"status": "deprecated"
|
||||
},
|
||||
{
|
||||
"name": "jyotish-birth-time-rectification",
|
||||
"version": "10.0.22",
|
||||
"sha256": "e3765b20ce6b49a5c7c9ed6b0a8caacc6fc4b8a16a127b93bd92c537330a54e2",
|
||||
"sourceCommit": null,
|
||||
"packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.22",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -491,7 +491,33 @@ class EventProbesTest(unittest.TestCase):
|
||||
self.assertFalse(any(item["domain"] == "relationship" for item in probes))
|
||||
self.assertFalse(any(item["domain"] == "finance" for item in probes))
|
||||
|
||||
def test_finance_layer_stays_volunteer_only(self) -> None:
|
||||
def test_finance_and_health_collection_probes_when_ledger_has_education_and_relationship(self) -> None:
|
||||
request = {
|
||||
"birth_date": "1997-08-08",
|
||||
"events": [
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000001",
|
||||
"domain": "education",
|
||||
"summary": "2015年升学",
|
||||
"date": "2015-06-01",
|
||||
"precision": "year",
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000002",
|
||||
"domain": "relationship",
|
||||
"summary": "2019年交往",
|
||||
"date": "2019-05-01",
|
||||
"precision": "year",
|
||||
},
|
||||
],
|
||||
}
|
||||
collection = evidence_collection_probes(request, today=date(2026, 8, 22))
|
||||
domains = {item["domain"] for item in collection}
|
||||
self.assertIn("finance", domains)
|
||||
self.assertIn("health_pressure", domains)
|
||||
self.assertTrue(all(item["source"] == "age_band" for item in collection))
|
||||
|
||||
def test_finance_layer_can_enter_discriminators_when_d2_differs(self) -> None:
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("05:13", d4_asc=1, sun_house=10, sun_varga_sign=9),
|
||||
|
||||
Reference in New Issue
Block a user